The redirect loop was caused by the Manager cookie configuration: 1. secure was based on NODE_ENV=production, so HTTP deployments silently dropped the cookie. 2. Path=/manager excluded /api/manager/*, preventing authenticated Manager API requests.
This commit is contained in:
2 files changed
+58
-2
No files matched your search
@@ -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: "/" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
import { isSecureDeployment } from "@/lib/security/cookie-security";
|
||||||
import { getManagerConfig } from "./manager-config";
|
import { getManagerConfig } from "./manager-config";
|
||||||
|
|
||||||
const COOKIE_NAME = "nextchair_manager_session";
|
const COOKIE_NAME = "nextchair_manager_session";
|
||||||
@@ -27,8 +28,10 @@ export async function createManagerSession() {
|
|||||||
(await cookies()).set(COOKIE_NAME, `${value}.${signature(value, config.authSecret)}`, {
|
(await cookies()).set(COOKIE_NAME, `${value}.${signature(value, config.authSecret)}`, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
secure: process.env.NODE_ENV === "production",
|
secure: isSecureDeployment(),
|
||||||
path: "/manager",
|
// 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,
|
maxAge: 60 * 60 * 8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user