Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/admin-sso/state.server.test.ts
T

186 lines
7.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const { testWebConfig } = vi.hoisted(() => ({
testWebConfig: {
nodeEnv: "test" as string,
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_CHALLENGE_COOKIE,
ADMIN_SSO_CHALLENGE_TTL_MS,
consumeAdminSsoChallenge,
createAdminSsoChallenge,
LOCAL_ADMIN_SSO_CHALLENGE_COOKIE,
serializeAdminSsoChallengeCookie,
} from "./state.server";
const SECRET = "test-secret-do-not-use-in-prod";
function cookieValue(cookie: string): string {
const head = cookie.split(";")[0] ?? "";
return head.slice(`${ADMIN_SSO_CHALLENGE_COOKIE}=`.length);
}
function requestWithCookie(value: string, name = ADMIN_SSO_CHALLENGE_COOKIE): Request {
return new Request("https://admin.example.test/auth/admin/callback", {
headers: { cookie: `${name}=${value}` },
});
}
beforeEach(() => {
testWebConfig.nodeEnv = "test";
testWebConfig.routing.protocol = "https";
testWebConfig.routing.appBaseDomain = "example.test";
});
describe("createAdminSsoChallenge", () => {
it("produces distinct state, nonce and codeVerifier for each invocation", () => {
const a = createAdminSsoChallenge({ secret: SECRET });
const b = createAdminSsoChallenge({ secret: SECRET });
expect(a.payload.state).not.toBe(b.payload.state);
expect(a.payload.nonce).not.toBe(b.payload.nonce);
expect(a.payload.codeVerifier).not.toBe(b.payload.codeVerifier);
// state should be 32 random bytes hex-encoded -> 64 chars
expect(a.payload.state).toMatch(/^[0-9a-f]{64}$/);
expect(a.payload.nonce).toMatch(/^[0-9a-f]{64}$/);
});
it("derives codeChallenge from codeVerifier (PKCE S256)", () => {
const { payload } = createAdminSsoChallenge({ secret: SECRET });
expect(payload.codeChallenge).toMatch(/^[A-Za-z0-9_-]+$/);
expect(payload.codeChallenge).not.toBe(payload.codeVerifier);
});
it("emits a host-prefixed cookie on HTTPS without a Domain attribute", () => {
const { cookie } = createAdminSsoChallenge({ secret: SECRET });
expect(cookie).toContain(`${ADMIN_SSO_CHALLENGE_COOKIE}=`);
expect(cookie).toMatch(/HttpOnly/);
expect(cookie).toMatch(/Path=\//);
expect(cookie).toMatch(/SameSite=Lax/);
expect(cookie).toMatch(/Secure/);
expect(cookie).not.toMatch(/Domain=/);
});
it("uses a distinct localhost-compatible name on local HTTP", () => {
testWebConfig.nodeEnv = "development";
testWebConfig.routing.protocol = "http";
testWebConfig.routing.appBaseDomain = "localhost:3000";
const { cookie } = createAdminSsoChallenge({ secret: SECRET });
expect(cookie).toContain(`${LOCAL_ADMIN_SSO_CHALLENGE_COOKIE}=`);
expect(cookie).not.toContain(ADMIN_SSO_CHALLENGE_COOKIE);
expect(cookie).not.toContain("Secure");
expect(cookie).not.toContain("Domain=");
});
});
describe("consumeAdminSsoChallenge", () => {
it("round-trips a fresh challenge and returns a clear-cookie header", () => {
const { payload, cookie } = createAdminSsoChallenge({ secret: SECRET, returnTo: "/admin/dashboard" });
const value = cookieValue(cookie);
const req = requestWithCookie(value);
const result = consumeAdminSsoChallenge(req, { secret: SECRET });
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.payload.state).toBe(payload.state);
expect(result.payload.nonce).toBe(payload.nonce);
expect(result.payload.codeVerifier).toBe(payload.codeVerifier);
expect(result.payload.returnTo).toBe("/admin/dashboard");
expect(result.clearCookie).toContain(`${ADMIN_SSO_CHALLENGE_COOKIE}=`);
expect(result.clearCookie).toContain("Max-Age=0");
}
});
it("reports `missing` when no cookie is attached", () => {
const req = new Request("https://admin.example.test/auth/admin/callback");
const result = consumeAdminSsoChallenge(req, { secret: SECRET });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.reason).toBe("missing");
});
it("ignores a legacy parent-domain cookie on HTTPS", () => {
const { cookie } = createAdminSsoChallenge({ secret: SECRET });
const result = consumeAdminSsoChallenge(requestWithCookie(cookieValue(cookie), LOCAL_ADMIN_SSO_CHALLENGE_COOKIE), { secret: SECRET });
expect(result).toEqual({ ok: false, reason: "missing" });
});
it("does not let a legacy cookie shadow the host-prefixed challenge", () => {
const { payload, cookie } = createAdminSsoChallenge({ secret: SECRET });
const value = cookieValue(cookie);
const request = new Request("https://admin.example.test/auth/admin/callback", {
headers: { cookie: `${LOCAL_ADMIN_SSO_CHALLENGE_COOKIE}=attacker; ${ADMIN_SSO_CHALLENGE_COOKIE}=${value}` },
});
const result = consumeAdminSsoChallenge(request, { secret: SECRET });
expect(result.ok).toBe(true);
if (result.ok) expect(result.payload.state).toBe(payload.state);
});
it("rejects a cookie with a tampered signature", () => {
const { cookie } = createAdminSsoChallenge({ secret: SECRET });
const value = cookieValue(cookie);
const dot = value.lastIndexOf(".");
const tampered = value.slice(0, dot) + ".AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
const result = consumeAdminSsoChallenge(requestWithCookie(tampered), { secret: SECRET });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.reason).toBe("bad-signature");
});
it("rejects a cookie signed under a different secret", () => {
const { cookie } = createAdminSsoChallenge({ secret: SECRET });
const result = consumeAdminSsoChallenge(requestWithCookie(cookieValue(cookie)), { secret: "different-secret" });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.reason).toBe("bad-signature");
});
it("rejects an expired cookie (older than TTL)", () => {
const past = Date.now() - ADMIN_SSO_CHALLENGE_TTL_MS - 1000;
const { payload } = createAdminSsoChallenge({ secret: SECRET, now: () => past });
const cookie = serializeAdminSsoChallengeCookie(payload, SECRET);
const result = consumeAdminSsoChallenge(requestWithCookie(cookieValue(cookie)), { secret: SECRET });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.reason).toBe("expired");
});
it("rejects a challenge dated implausibly far in the future", () => {
const now = Date.now();
const { payload } = createAdminSsoChallenge({ secret: SECRET, now: () => now + 5 * 60 * 1000 });
const cookie = serializeAdminSsoChallengeCookie(payload, SECRET);
const result = consumeAdminSsoChallenge(requestWithCookie(cookieValue(cookie)), { secret: SECRET, now: () => now });
expect(result).toEqual({ ok: false, reason: "malformed" });
});
it("rejects a signed payload whose PKCE verifier and challenge do not match", () => {
const { payload } = createAdminSsoChallenge({ secret: SECRET });
const inconsistent = { ...payload, codeChallenge: "A".repeat(43) };
const cookie = serializeAdminSsoChallengeCookie(inconsistent, SECRET);
const result = consumeAdminSsoChallenge(requestWithCookie(cookieValue(cookie)), { secret: SECRET });
expect(result).toEqual({ ok: false, reason: "malformed" });
});
it("rejects a malformed cookie value", () => {
const result = consumeAdminSsoChallenge(requestWithCookie("garbage"), { secret: SECRET });
expect(result.ok).toBe(false);
if (!result.ok) expect(["malformed", "bad-signature"]).toContain(result.reason);
});
});