import { describe, expect, it } from "vitest"; import { getOidcTransactionCookie, getSessionCookie, oidcTransactionTtlMs, setOidcTransactionCookie, setSessionCookie, } from "@/worker/auth/cookies"; import { isValidHostLabel, generateHostLabel } from "@/worker/auth/host-labels"; import { HOST_WORDS } from "@/worker/auth/host-wordlist"; import { digestPat, generatePat, normalizeScopes, parseBasicAuth, parsePat } from "@/worker/auth/pats"; import { hasDavScope } from "@/worker/auth/scopes"; import type { AppContext } from "@/worker/types"; function contextWithEnv(cookie?: string): { c: AppContext; responseHeaders: Headers } { const headers = new Headers(); if (cookie) headers.set("cookie", cookie); const c = { env: { TESSERA_OIDC_CLIENT_SECRET: "oidc-test-secret", DAB_SESSION_SECRET: "session-test-secret", }, req: { raw: new Request("https://dav.example.com/", { headers }), }, header(name: string, value: string, options?: { append?: boolean }) { if (options?.append) headers.append(name, value); else headers.set(name, value); }, } as unknown as AppContext; return { c, responseHeaders: headers }; } function setCookie(responseHeaders: Headers): string { const value = responseHeaders.get("set-cookie") ?? ""; if (value) return value; throw new Error("missing Set-Cookie"); } function cookiePair(header: string): string { return header.split(";")[0] ?? header; } describe("auth primitives", () => { it("signs and verifies OIDC transaction cookies with host cookie attributes", async () => { const ctx = contextWithEnv(); const nowMs = Date.now(); await setOidcTransactionCookie(ctx.c, { state: "state", nonce: "nonce", codeVerifier: "verifier", returnTo: "/after", createdAtMs: nowMs, expiresAtMs: nowMs + oidcTransactionTtlMs, }); const header = setCookie(ctx.responseHeaders); expect(header).toContain("__Host-dab_oidc_tx="); expect(header).toContain("HttpOnly"); expect(header).toContain("Secure"); expect(header).toContain("SameSite=Lax"); expect(header).toContain("Path=/"); expect(header).not.toContain("Domain="); await expect(getOidcTransactionCookie(contextWithEnv(cookiePair(header)).c)).resolves.toMatchObject({ state: "state", nonce: "nonce", codeVerifier: "verifier", returnTo: "/after", }); }); it("signs and verifies API session cookies with a distinct key context", async () => { const ctx = contextWithEnv(); const nowMs = Date.now(); await setSessionCookie(ctx.c, { subjectId: "sub", storageId: "stg_123", sessionId: "ses_123", createdAtMs: nowMs, expiresAtMs: nowMs + 1000, }); const header = setCookie(ctx.responseHeaders); expect(header).toContain("__Host-dab_session="); await expect(getSessionCookie(contextWithEnv(cookiePair(header)).c)).resolves.toMatchObject({ subjectId: "sub", storageId: "stg_123", sessionId: "ses_123", }); await expect(getOidcTransactionCookie(contextWithEnv(cookiePair(header)).c)).resolves.toBeNull(); }); it("generates and validates five-word host labels from the checked-in wordlist", () => { expect(HOST_WORDS).toHaveLength(8192); expect(new Set(HOST_WORDS).size).toBe(8192); expect(HOST_WORDS.every((word) => /^[a-z]{3,8}$/.test(word))).toBe(true); for (let i = 0; i < 50; i += 1) { const label = generateHostLabel(); expect(label).toMatch(/^[a-z]+(-[a-z]+){4}$/); expect(label.length).toBeLessThanOrEqual(63); expect(isValidHostLabel(label)).toBe(true); } expect(isValidHostLabel("river-copper-lantern-velvet-maple")).toBe(true); expect(isValidHostLabel("not-enough-words")).toBe(false); expect(isValidHostLabel("river-copper-lantern-velvet-unknownword")).toBe(false); }); it("parses PAT grammar, digests tokens, normalizes scopes, and parses Basic auth", async () => { const generated = await generatePat(); const parsed = parsePat(generated.token); expect(parsed?.id).toBe(generated.id); expect(generated.id).toMatch(/^dab_pat_[0-9a-f]{24}$/); expect(generated.token).toMatch(/^dab_pat_[0-9a-f]{24}_[a-z2-7]{52}$/); await expect(digestPat(generated.token)).resolves.toBe(generated.tokenDigest); expect(parsePat("dab_pat_short_bad")).toBeNull(); expect(normalizeScopes(["files.full", "dav:carddav:read"])).toEqual([ "dav:carddav:read", "dav:files:read", "dav:files:write", ]); expect(normalizeScopes(["unknown.scope"])).toBeNull(); const auth = `Basic ${btoa(`user:${generated.token}`)}`; expect(parseBasicAuth(auth)).toEqual({ username: "user", password: generated.token }); }); it("treats write DAV scopes as implying read for authorization checks", () => { expect(hasDavScope(["dav:files:write"], "dav:files:read")).toBe(true); expect(hasDavScope(["dav:caldav:write"], "dav:caldav:read")).toBe(true); expect(hasDavScope(["dav:carddav:write"], "dav:carddav:read")).toBe(true); expect(hasDavScope(["dav:files:read"], "dav:files:write")).toBe(false); }); });