feat(webui): parameterize session TTL and cookie maxAge
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { AUTH_COOKIE_NAME, isAuthenticationEnabled } from "@/lib/auth";
|
||||
import {
|
||||
AUTH_COOKIE_NAME,
|
||||
isAuthenticationEnabled,
|
||||
sessionTtlSeconds,
|
||||
} from "@/lib/auth";
|
||||
import {
|
||||
AuthConfigurationError,
|
||||
authCookieOptions,
|
||||
@@ -96,10 +100,17 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
await updateAuthPassword(session.username, newPassword);
|
||||
const role = roleForUsername(session.username) ?? session.role;
|
||||
const { token, payload } = createSession(session.username, role);
|
||||
const { token, payload } = createSession(
|
||||
session.username,
|
||||
role,
|
||||
sessionTtlSeconds()
|
||||
);
|
||||
await recordLogin(payload);
|
||||
const response = NextResponse.json({ ok: true }, { headers: NO_STORE });
|
||||
response.cookies.set({ ...authCookieOptions(), value: token });
|
||||
response.cookies.set({
|
||||
...authCookieOptions(sessionTtlSeconds()),
|
||||
value: token,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
const message =
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { afterAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("server-only", () => ({}));
|
||||
|
||||
const ORIGINAL_ENABLED = process.env.WEBUI_AUTH_ENABLED;
|
||||
const ORIGINAL_SECRET = process.env.WEBUI_AUTH_SECRET;
|
||||
|
||||
process.env.WEBUI_AUTH_ENABLED = "true";
|
||||
process.env.WEBUI_AUTH_SECRET = "test-secret-with-at-least-32-characters";
|
||||
|
||||
const { authCookieOptions, createSession, decodeSessionPayload } =
|
||||
await import("./auth");
|
||||
|
||||
afterAll(() => {
|
||||
if (ORIGINAL_ENABLED === undefined) delete process.env.WEBUI_AUTH_ENABLED;
|
||||
else process.env.WEBUI_AUTH_ENABLED = ORIGINAL_ENABLED;
|
||||
if (ORIGINAL_SECRET === undefined) delete process.env.WEBUI_AUTH_SECRET;
|
||||
else process.env.WEBUI_AUTH_SECRET = ORIGINAL_SECRET;
|
||||
});
|
||||
|
||||
describe("createSession", () => {
|
||||
it("sets expiresAt from the given TTL and round-trips", () => {
|
||||
const before = Math.floor(Date.now() / 1000);
|
||||
const { token, payload } = createSession("alice", "admin", 60);
|
||||
expect(payload.expiresAt).toBeGreaterThanOrEqual(before + 60);
|
||||
expect(payload.expiresAt).toBeLessThanOrEqual(before + 65);
|
||||
expect(decodeSessionPayload(token)).toEqual(payload);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authCookieOptions", () => {
|
||||
it("omits maxAge for a session-scoped cookie", () => {
|
||||
const options = authCookieOptions(null);
|
||||
expect(options).not.toHaveProperty("maxAge");
|
||||
expect(options.name).toBe("evoscientist_webui_session");
|
||||
expect(options.httpOnly).toBe(true);
|
||||
expect(options.path).toBe("/");
|
||||
});
|
||||
|
||||
it("sets maxAge when a TTL is given", () => {
|
||||
expect(authCookieOptions(3600).maxAge).toBe(3600);
|
||||
});
|
||||
});
|
||||
@@ -117,7 +117,8 @@ function sign(payload: string, secret: string): string {
|
||||
|
||||
export function createSession(
|
||||
username: string,
|
||||
role: UserRole
|
||||
role: UserRole,
|
||||
ttlSeconds: number
|
||||
): {
|
||||
token: string;
|
||||
payload: SessionPayload;
|
||||
@@ -128,7 +129,7 @@ export function createSession(
|
||||
username,
|
||||
role,
|
||||
loggedInAt: now.toISOString(),
|
||||
expiresAt: Math.floor(now.getTime() / 1000) + sessionTtlSeconds(),
|
||||
expiresAt: Math.floor(now.getTime() / 1000) + ttlSeconds,
|
||||
};
|
||||
const encoded = encode(JSON.stringify(payload));
|
||||
return { token: `${encoded}.${sign(encoded, secret)}`, payload };
|
||||
@@ -170,14 +171,14 @@ export async function recordLogin(payload: SessionPayload): Promise<void> {
|
||||
await fs.rename(temp, target);
|
||||
}
|
||||
|
||||
export function authCookieOptions() {
|
||||
export function authCookieOptions(ttlSeconds: number | null) {
|
||||
return {
|
||||
name: AUTH_COOKIE_NAME,
|
||||
httpOnly: true,
|
||||
sameSite: "lax" as const,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
path: "/",
|
||||
maxAge: sessionTtlSeconds(),
|
||||
...(ttlSeconds === null ? {} : { maxAge: ttlSeconds }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user