diff --git a/src/services/management/__tests__/management-auth.test.ts b/src/services/management/__tests__/management-auth.test.ts new file mode 100644 index 0000000..f5b5282 --- /dev/null +++ b/src/services/management/__tests__/management-auth.test.ts @@ -0,0 +1,53 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const cookieSet = vi.hoisted(() => vi.fn()); + +vi.mock("next/headers", () => ({ + cookies: vi.fn(async () => ({ set: cookieSet })), +})); + +import { createManagerSession } from "../management-auth"; + +describe("Manager session cookie", () => { + const originalAppUrl = process.env.NEXT_PUBLIC_APP_URL; + const originalAdminEmail = process.env.MANAGER_ADMIN_EMAIL; + const originalAuthSecret = process.env.MANAGER_AUTH_SECRET; + + afterEach(() => { + if (originalAppUrl === undefined) delete process.env.NEXT_PUBLIC_APP_URL; + else process.env.NEXT_PUBLIC_APP_URL = originalAppUrl; + if (originalAdminEmail === undefined) delete process.env.MANAGER_ADMIN_EMAIL; + else process.env.MANAGER_ADMIN_EMAIL = originalAdminEmail; + if (originalAuthSecret === undefined) delete process.env.MANAGER_AUTH_SECRET; + else process.env.MANAGER_AUTH_SECRET = originalAuthSecret; + cookieSet.mockReset(); + }); + + it("does not mark the cookie Secure for an HTTP deployment", async () => { + process.env.NEXT_PUBLIC_APP_URL = "http://manager.example.com"; + process.env.MANAGER_ADMIN_EMAIL = "admin@example.com"; + process.env.MANAGER_AUTH_SECRET = "test-secret"; + + await createManagerSession(); + + expect(cookieSet).toHaveBeenCalledWith( + "nextchair_manager_session", + expect.any(String), + expect.objectContaining({ secure: false, path: "/" }), + ); + }); + + it("marks the cookie Secure for an HTTPS deployment", async () => { + process.env.NEXT_PUBLIC_APP_URL = "https://manager.example.com"; + process.env.MANAGER_ADMIN_EMAIL = "admin@example.com"; + process.env.MANAGER_AUTH_SECRET = "test-secret"; + + await createManagerSession(); + + expect(cookieSet).toHaveBeenCalledWith( + "nextchair_manager_session", + expect.any(String), + expect.objectContaining({ secure: true, path: "/" }), + ); + }); +}); diff --git a/src/services/management/management-auth.ts b/src/services/management/management-auth.ts index fada80a..c3960e7 100644 --- a/src/services/management/management-auth.ts +++ b/src/services/management/management-auth.ts @@ -1,5 +1,6 @@ import { createHmac, timingSafeEqual } from "node:crypto"; import { cookies } from "next/headers"; +import { isSecureDeployment } from "@/lib/security/cookie-security"; import { getManagerConfig } from "./manager-config"; const COOKIE_NAME = "nextchair_manager_session"; @@ -27,8 +28,10 @@ export async function createManagerSession() { (await cookies()).set(COOKIE_NAME, `${value}.${signature(value, config.authSecret)}`, { httpOnly: true, sameSite: "strict", - secure: process.env.NODE_ENV === "production", - path: "/manager", + secure: isSecureDeployment(), + // The Manager UI is under /manager, but its authenticated API is under + // /api/manager, so the cookie must cover both paths. + path: "/", maxAge: 60 * 60 * 8, }); }