import { describe, expect, it } from "@effect/vitest"; import { SignJWT, exportJWK, generateKeyPair, jwtVerify, type JSONWebKeySet, type JWK, type KeyLike, } from "jose"; import { createCachedRemoteJWKSet, type JwksEdgeCache } from "./jwks-cache"; const issuer = "https://test-authkit.example.com"; const audience = "client_test_fixture"; const jwksUrl = new URL("https://test-authkit.example.com/oauth2/jwks"); interface Keypair { readonly kid: string; readonly publicJwk: JWK; readonly privateKey: KeyLike; } const generateRotatableKeypair = async (kid: string): Promise => { const { publicKey, privateKey } = await generateKeyPair("RS256"); const jwk = await exportJWK(publicKey); return { kid, publicJwk: { ...jwk, kid, alg: "RS256" }, privateKey }; }; const sign = (keypair: Keypair) => new SignJWT({}) .setProtectedHeader({ alg: "RS256", kid: keypair.kid }) .setIssuer(issuer) .setSubject("user_test") .setAudience(audience) .setIssuedAt() .setExpirationTime("5m") .sign(keypair.privateKey); interface FetchHarness { readonly fetch: typeof globalThis.fetch; readonly callCount: () => number; readonly setKeys: (keys: ReadonlyArray) => void; } const makeFetchHarness = (initialKeys: ReadonlyArray): FetchHarness => { let keys: ReadonlyArray = initialKeys; let calls = 0; const fetch: typeof globalThis.fetch = async () => { calls++; const body: JSONWebKeySet = { keys: keys.map((k) => ({ ...k })) }; return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" }, }); }; return { fetch, callCount: () => calls, setKeys: (next) => { keys = next; }, }; }; describe("createCachedRemoteJWKSet", () => { it("FAILING-WITHOUT-CACHE: N verifications hit JWKS endpoint only once within TTL", async () => { const kp = await generateRotatableKeypair("k1"); const harness = makeFetchHarness([kp.publicJwk]); const jwks = createCachedRemoteJWKSet(jwksUrl, { fetch: harness.fetch }); for (let i = 0; i < 5; i++) { const token = await sign(kp); const { payload } = await jwtVerify(token, jwks, { issuer, audience }); expect(payload.sub).toBe("user_test"); } expect(harness.callCount()).toBe(1); }); it("single-flights concurrent cache misses into one fetch", async () => { const kp = await generateRotatableKeypair("k1"); let resolveFetch!: () => void; const gate = new Promise((r) => { resolveFetch = r; }); let calls = 0; const fetch: typeof globalThis.fetch = async () => { calls++; await gate; const body: JSONWebKeySet = { keys: [{ ...kp.publicJwk }] }; return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" }, }); }; const jwks = createCachedRemoteJWKSet(jwksUrl, { fetch }); const token = await sign(kp); const verifies = Array.from({ length: 10 }, () => jwtVerify(token, jwks, { issuer, audience })); // Let microtasks settle so all 10 calls hit the cache miss path. await new Promise((r) => setTimeout(r, 10)); resolveFetch(); const results = await Promise.all(verifies); expect(results).toHaveLength(10); expect(calls).toBe(1); }); it("forces a refresh when verification fails with a no-matching-key error (key rotation)", async () => { const oldKey = await generateRotatableKeypair("k_old"); const newKey = await generateRotatableKeypair("k_new"); const harness = makeFetchHarness([oldKey.publicJwk]); const jwks = createCachedRemoteJWKSet(jwksUrl, { fetch: harness.fetch }); // Warm the cache with the old key. const t1 = await sign(oldKey); const ok = await jwtVerify(t1, jwks, { issuer, audience }); expect(ok.payload.sub).toBe("user_test"); expect(harness.callCount()).toBe(1); // Upstream rotates: only the new key remains in the JWKS endpoint. harness.setKeys([newKey.publicJwk]); // A token signed with the new key must verify even though our cache // still has the old one — the resolver must refetch on miss. const t2 = await sign(newKey); const ok2 = await jwtVerify(t2, jwks, { issuer, audience }); expect(ok2.payload.sub).toBe("user_test"); expect(harness.callCount()).toBe(2); }); it("re-fetches after the TTL window elapses", async () => { const kp = await generateRotatableKeypair("k1"); const harness = makeFetchHarness([kp.publicJwk]); const jwks = createCachedRemoteJWKSet(jwksUrl, { fetch: harness.fetch, ttlMs: 10, }); const t1 = await sign(kp); await jwtVerify(t1, jwks, { issuer, audience }); expect(harness.callCount()).toBe(1); await new Promise((r) => setTimeout(r, 20)); const t2 = await sign(kp); await jwtVerify(t2, jwks, { issuer, audience }); expect(harness.callCount()).toBe(2); }); it("forceRefresh() invalidates the cache so the next call refetches", async () => { const kp = await generateRotatableKeypair("k1"); const harness = makeFetchHarness([kp.publicJwk]); const jwks = createCachedRemoteJWKSet(jwksUrl, { fetch: harness.fetch }); const t1 = await sign(kp); await jwtVerify(t1, jwks, { issuer, audience }); expect(harness.callCount()).toBe(1); jwks.forceRefresh(); await jwtVerify(t1, jwks, { issuer, audience }); expect(harness.callCount()).toBe(2); }); }); interface EdgeCacheHarness { readonly cache: JwksEdgeCache; readonly matchCount: () => number; readonly putCount: () => number; readonly stored: () => Response | null; readonly seed: (keys: ReadonlyArray) => void; } const makeEdgeCacheHarness = (): EdgeCacheHarness => { let stored: Response | null = null; let matches = 0; let puts = 0; return { cache: { match: async () => { matches++; return stored ? stored.clone() : undefined; }, put: async (_url, response) => { puts++; stored = response; }, }, matchCount: () => matches, putCount: () => puts, stored: () => stored, seed: (keys) => { const body: JSONWebKeySet = { keys: keys.map((k) => ({ ...k })) }; stored = new Response(JSON.stringify(body), { headers: { "content-type": "application/json" }, }); }, }; }; describe("createCachedRemoteJWKSet edge cache", () => { it("FAILING-WITHOUT-EDGE-CACHE: a cold isolate with a warm edge cache never fetches upstream", async () => { const kp = await generateRotatableKeypair("k1"); const upstream = makeFetchHarness([kp.publicJwk]); const edge = makeEdgeCacheHarness(); edge.seed([kp.publicJwk]); // Fresh resolver = fresh isolate: empty in-memory cache. const jwks = createCachedRemoteJWKSet(jwksUrl, { fetch: upstream.fetch, edgeCache: edge.cache, }); const token = await sign(kp); const { payload } = await jwtVerify(token, jwks, { issuer, audience }); expect(payload.sub).toBe("user_test"); expect(edge.matchCount()).toBe(1); expect(upstream.callCount()).toBe(0); }); it("an edge-cache miss falls through to upstream and populates the edge copy", async () => { const kp = await generateRotatableKeypair("k1"); const upstream = makeFetchHarness([kp.publicJwk]); const edge = makeEdgeCacheHarness(); const jwks = createCachedRemoteJWKSet(jwksUrl, { fetch: upstream.fetch, edgeCache: edge.cache, }); const token = await sign(kp); await jwtVerify(token, jwks, { issuer, audience }); expect(upstream.callCount()).toBe(1); expect(edge.putCount()).toBe(1); // A second cold isolate now reads the populated edge copy. const jwks2 = createCachedRemoteJWKSet(jwksUrl, { fetch: upstream.fetch, edgeCache: edge.cache, }); await jwtVerify(token, jwks2, { issuer, audience }); expect(upstream.callCount()).toBe(1); }); it("a malformed edge-cache entry is a miss, not a failure", async () => { const kp = await generateRotatableKeypair("k1"); const upstream = makeFetchHarness([kp.publicJwk]); const edge = makeEdgeCacheHarness(); await edge.cache.put(jwksUrl.toString(), new Response("not json")); const jwks = createCachedRemoteJWKSet(jwksUrl, { fetch: upstream.fetch, edgeCache: edge.cache, }); const token = await sign(kp); const { payload } = await jwtVerify(token, jwks, { issuer, audience }); expect(payload.sub).toBe("user_test"); expect(upstream.callCount()).toBe(1); }); it("key rotation bypasses the stale edge copy and overwrites it", async () => { const oldKey = await generateRotatableKeypair("k_old"); const newKey = await generateRotatableKeypair("k_new"); const upstream = makeFetchHarness([newKey.publicJwk]); const edge = makeEdgeCacheHarness(); // The edge copy predates the rotation: it only has the old key. edge.seed([oldKey.publicJwk]); const jwks = createCachedRemoteJWKSet(jwksUrl, { fetch: upstream.fetch, edgeCache: edge.cache, }); // First verify warms in-memory from the (stale) edge copy; the new-key // token then misses, which must force an UPSTREAM refetch — re-reading // the same stale edge copy would loop the miss forever. const token = await sign(newKey); const { payload } = await jwtVerify(token, jwks, { issuer, audience }); expect(payload.sub).toBe("user_test"); expect(upstream.callCount()).toBe(1); // And the rotated set replaced the stale edge copy for other isolates. expect(edge.putCount()).toBe(1); }); it("degrades to upstream-only when the edge cache itself fails", async () => { const kp = await generateRotatableKeypair("k1"); const upstream = makeFetchHarness([kp.publicJwk]); // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: fakes the Cache API's own failure mode, a rejected promise const rejectingCacheCall = () => Promise.reject(new TypeError("cache backend unavailable")); const broken: JwksEdgeCache = { match: rejectingCacheCall, put: rejectingCacheCall, }; const jwks = createCachedRemoteJWKSet(jwksUrl, { fetch: upstream.fetch, edgeCache: broken, }); const token = await sign(kp); const { payload } = await jwtVerify(token, jwks, { issuer, audience }); expect(payload.sub).toBe("user_test"); expect(upstream.callCount()).toBe(1); }); });