105 lines
4.4 KiB
TypeScript
105 lines
4.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
ADMIN_SSO_CHALLENGE_COOKIE,
|
|
ADMIN_SSO_CHALLENGE_TTL_MS,
|
|
consumeAdminSsoChallenge,
|
|
createAdminSsoChallenge,
|
|
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): Request {
|
|
return new Request("https://admin.example.test/auth/admin/callback", {
|
|
headers: { cookie: `${ADMIN_SSO_CHALLENGE_COOKIE}=${value}` },
|
|
});
|
|
}
|
|
|
|
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 an HttpOnly, Path-scoped cookie", () => {
|
|
const { cookie } = createAdminSsoChallenge({ secret: SECRET });
|
|
expect(cookie).toMatch(/HttpOnly/);
|
|
expect(cookie).toMatch(/Path=\/auth\/admin/);
|
|
expect(cookie).toMatch(/SameSite=Lax/);
|
|
});
|
|
});
|
|
|
|
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("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 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);
|
|
});
|
|
});
|