112 lines
4.0 KiB
TypeScript
112 lines
4.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { testWebConfig } = vi.hoisted(() => ({
|
|
testWebConfig: {
|
|
nodeEnv: "test" as string,
|
|
cryptoSecret: "test-crypto-secret-do-not-use-in-prod",
|
|
sessionSecret: "test-session-secret-do-not-use-in-prod",
|
|
routing: {
|
|
appBaseDomain: "example.test",
|
|
webAppHost: "app.example.test",
|
|
adminAppHost: "admin.example.test",
|
|
apiHost: "api.example.test",
|
|
protocol: "https" as "http" | "https",
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.mock("@hivegrid/config", async () => {
|
|
const actual = await vi.importActual<typeof import("@hivegrid/config")>("@hivegrid/config");
|
|
return {
|
|
...actual,
|
|
loadRequiredWebConfig: () => testWebConfig,
|
|
};
|
|
});
|
|
|
|
import {
|
|
ADMIN_SSO_ID_TOKEN_COOKIE,
|
|
clearIdTokenCookie,
|
|
LOCAL_ADMIN_SSO_ID_TOKEN_COOKIE,
|
|
readIdTokenFromRequest,
|
|
serializeIdTokenCookie,
|
|
} from "./logout.server";
|
|
|
|
const SECRET = "test-secret-do-not-use-in-prod";
|
|
|
|
function requestWithCookie(pair: string): Request {
|
|
return new Request("https://admin.example.test/auth/admin/logout", { headers: { cookie: pair } });
|
|
}
|
|
|
|
beforeEach(() => {
|
|
testWebConfig.nodeEnv = "test";
|
|
testWebConfig.routing.protocol = "https";
|
|
testWebConfig.routing.appBaseDomain = "example.test";
|
|
});
|
|
|
|
describe("admin SSO id-token cookie", () => {
|
|
it("uses a host-prefixed HTTPS cookie without a Domain attribute", () => {
|
|
const cookie = serializeIdTokenCookie("id-token-value", { secret: SECRET });
|
|
|
|
expect(cookie).toContain(`${ADMIN_SSO_ID_TOKEN_COOKIE}=`);
|
|
expect(cookie).toContain("Path=/");
|
|
expect(cookie).toContain("HttpOnly");
|
|
expect(cookie).toContain("SameSite=Lax");
|
|
expect(cookie).toContain("Secure");
|
|
expect(cookie).not.toContain("Domain=");
|
|
});
|
|
|
|
it("round-trips a valid signed id token", () => {
|
|
const pair = serializeIdTokenCookie("id-token-value", { secret: SECRET }).split(";")[0] ?? "";
|
|
|
|
expect(readIdTokenFromRequest(requestWithCookie(pair), { secret: SECRET })).toBe("id-token-value");
|
|
});
|
|
|
|
it("rejects tampering", () => {
|
|
const pair = serializeIdTokenCookie("id-token-value", { secret: SECRET }).split(";")[0] ?? "";
|
|
|
|
expect(readIdTokenFromRequest(requestWithCookie(`${pair}tampered`), { secret: SECRET })).toBeNull();
|
|
});
|
|
|
|
it("ignores a legacy parent-domain cookie on HTTPS", () => {
|
|
const pair = serializeIdTokenCookie("id-token-value", { secret: SECRET })
|
|
.split(";")[0]
|
|
?.replace(ADMIN_SSO_ID_TOKEN_COOKIE, LOCAL_ADMIN_SSO_ID_TOKEN_COOKIE);
|
|
|
|
expect(readIdTokenFromRequest(requestWithCookie(pair ?? ""), { secret: SECRET })).toBeNull();
|
|
});
|
|
|
|
it("does not let a legacy cookie shadow the host-prefixed id token", () => {
|
|
const securePair = serializeIdTokenCookie("id-token-value", { secret: SECRET }).split(";")[0] ?? "";
|
|
const request = requestWithCookie(`${LOCAL_ADMIN_SSO_ID_TOKEN_COOKIE}=attacker; ${securePair}`);
|
|
|
|
expect(readIdTokenFromRequest(request, { secret: SECRET })).toBe("id-token-value");
|
|
});
|
|
|
|
it("clears the exact secure cookie scope", () => {
|
|
const cookie = clearIdTokenCookie();
|
|
|
|
expect(cookie).toContain(`${ADMIN_SSO_ID_TOKEN_COOKIE}=`);
|
|
expect(cookie).toContain("Path=/");
|
|
expect(cookie).toContain("Max-Age=0");
|
|
expect(cookie).toContain("Secure");
|
|
expect(cookie).not.toContain("Domain=");
|
|
});
|
|
|
|
it("uses a distinct localhost-compatible fallback on local HTTP", () => {
|
|
testWebConfig.nodeEnv = "development";
|
|
testWebConfig.routing.protocol = "http";
|
|
testWebConfig.routing.appBaseDomain = "localhost:3000";
|
|
|
|
const cookie = serializeIdTokenCookie("id-token-value", { secret: SECRET });
|
|
const pair = cookie.split(";")[0] ?? "";
|
|
|
|
expect(cookie).toContain(`${LOCAL_ADMIN_SSO_ID_TOKEN_COOKIE}=`);
|
|
expect(cookie).not.toContain(ADMIN_SSO_ID_TOKEN_COOKIE);
|
|
expect(cookie).not.toContain("Secure");
|
|
expect(cookie).not.toContain("Domain=");
|
|
expect(readIdTokenFromRequest(new Request("http://localhost:3000/auth/admin/logout", { headers: { cookie: pair } }), { secret: SECRET })).toBe(
|
|
"id-token-value"
|
|
);
|
|
});
|
|
});
|