signSession + verifySession run on Edge runtime (Web Crypto only). Verifier checks signature (constant-time compare), expiry, clock-skew on iat (60s tolerance), token version vs OPERATOR_TOKEN_VERSION env, and role-shape sanity. 11 unit tests cover round-trip plus every rejection path attackers could probe.
98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
import { describe, it, expect, beforeAll } from "vitest";
|
|
import {
|
|
signSession,
|
|
verifySession,
|
|
COOKIE_NAME,
|
|
DEFAULT_TTL_SECONDS,
|
|
type SessionPayload,
|
|
} from "./auth-cookie";
|
|
|
|
const SECRET = "test-secret-not-used-anywhere-real";
|
|
const NOW = 1_700_000_000; // 2023-11-14 — fixed clock for determinism
|
|
|
|
beforeAll(() => {
|
|
process.env.AUTH_SECRET = SECRET;
|
|
process.env.OPERATOR_TOKEN_VERSION = "1";
|
|
});
|
|
|
|
const validPayload = (): SessionPayload => ({
|
|
userId: "11111111-1111-1111-1111-111111111111",
|
|
role: "admin",
|
|
iat: NOW,
|
|
exp: NOW + DEFAULT_TTL_SECONDS,
|
|
v: 1,
|
|
});
|
|
|
|
describe("auth-cookie", () => {
|
|
it("signSession + verifySession round-trips a valid payload", async () => {
|
|
const cookie = await signSession(validPayload(), SECRET);
|
|
const verified = await verifySession(cookie, SECRET, NOW);
|
|
expect(verified).toEqual(validPayload());
|
|
});
|
|
|
|
it("rejects when the payload portion has been tampered with", async () => {
|
|
const cookie = await signSession(validPayload(), SECRET);
|
|
// Flip the role to admin → user in the payload, keep the same signature.
|
|
const [, sig] = cookie.split(".");
|
|
const tampered = btoa(JSON.stringify({ ...validPayload(), role: "user" }))
|
|
.replace(/\+/g, "-")
|
|
.replace(/\//g, "_")
|
|
.replace(/=+$/, "") + "." + sig;
|
|
expect(await verifySession(tampered, SECRET, NOW)).toBeNull();
|
|
});
|
|
|
|
it("rejects when the signature has been tampered with", async () => {
|
|
const cookie = await signSession(validPayload(), SECRET);
|
|
const [payload] = cookie.split(".");
|
|
const tampered = payload + ".AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
expect(await verifySession(tampered, SECRET, NOW)).toBeNull();
|
|
});
|
|
|
|
it("rejects when verified with a different secret", async () => {
|
|
const cookie = await signSession(validPayload(), SECRET);
|
|
expect(await verifySession(cookie, "different-secret", NOW)).toBeNull();
|
|
});
|
|
|
|
it("rejects an expired cookie (exp <= now)", async () => {
|
|
const expired = { ...validPayload(), exp: NOW - 1 };
|
|
const cookie = await signSession(expired, SECRET);
|
|
expect(await verifySession(cookie, SECRET, NOW)).toBeNull();
|
|
});
|
|
|
|
it("rejects a cookie issued in the future beyond clock-skew tolerance", async () => {
|
|
const future = { ...validPayload(), iat: NOW + 120 };
|
|
const cookie = await signSession(future, SECRET);
|
|
expect(await verifySession(cookie, SECRET, NOW)).toBeNull();
|
|
});
|
|
|
|
it("accepts a cookie issued slightly in the future (within 60s skew)", async () => {
|
|
const future = { ...validPayload(), iat: NOW + 30 };
|
|
const cookie = await signSession(future, SECRET);
|
|
expect(await verifySession(cookie, SECRET, NOW)).not.toBeNull();
|
|
});
|
|
|
|
it("rejects a cookie whose v is stale (token-version bumped)", async () => {
|
|
const cookie = await signSession({ ...validPayload(), v: 1 }, SECRET);
|
|
process.env.OPERATOR_TOKEN_VERSION = "2";
|
|
expect(await verifySession(cookie, SECRET, NOW)).toBeNull();
|
|
process.env.OPERATOR_TOKEN_VERSION = "1";
|
|
});
|
|
|
|
it("rejects a cookie with an unknown role string", async () => {
|
|
const cookie = await signSession(
|
|
{ ...validPayload(), role: "superadmin" as never },
|
|
SECRET,
|
|
);
|
|
expect(await verifySession(cookie, SECRET, NOW)).toBeNull();
|
|
});
|
|
|
|
it("rejects a cookie that doesn't have a '.' separator", async () => {
|
|
expect(await verifySession("not-a-cookie", SECRET, NOW)).toBeNull();
|
|
expect(await verifySession("", SECRET, NOW)).toBeNull();
|
|
});
|
|
|
|
it("exposes COOKIE_NAME as 'session'", () => {
|
|
expect(COOKIE_NAME).toBe("session");
|
|
});
|
|
});
|