import { expect, test } from "bun:test"; import { createRemoteJwks, verifyJwtWithJwks } from "../src/index.ts"; function base64url(value: Uint8Array): string { let binary = ""; for (const byte of value) binary += String.fromCharCode(byte); return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } async function rsaKey(kid: string) { const pair = (await crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256", }, true, ["sign", "verify"], )) as CryptoKeyPair; const jwk = await crypto.subtle.exportKey("jwk", pair.publicKey); return { pair, jwk: { ...jwk, kid, alg: "RS256", use: "sig" } }; } async function sign(privateKey: CryptoKey, kid: string, claims: Record) { const encoder = new TextEncoder(); const header = base64url(encoder.encode(JSON.stringify({ alg: "RS256", typ: "JWT", kid }))); const payload = base64url(encoder.encode(JSON.stringify(claims))); const input = `${header}.${payload}`; const signature = await crypto.subtle.sign( "RSASSA-PKCS1-v1_5", privateKey, encoder.encode(input), ); return `${input}.${base64url(new Uint8Array(signature))}`; } test("remote JWKS verifies RS256 claims and refreshes immediately for key rotation", async () => { const first = await rsaKey("key-1"); const second = await rsaKey("key-2"); const documents = [{ keys: [first.jwk] }, { keys: [second.jwk] }]; let fetches = 0; const jwks = createRemoteJwks("https://issuer.example/jwks", { now: () => 0, cacheTtlMs: 60_000, fetch: (async () => Response.json( documents[Math.min(fetches++, documents.length - 1)], )) as unknown as typeof fetch, }); const tokenOne = await sign(first.pair.privateKey, "key-1", { sub: "user-1", iss: "https://issuer.example", aud: "client-1", iat: 100, exp: 200, }); expect( ( await verifyJwtWithJwks(tokenOne, jwks, { issuer: "https://issuer.example", audience: "client-1", now: 150, }) ).sub, ).toBe("user-1"); expect(fetches).toBe(1); const tokenTwo = await sign(second.pair.privateKey, "key-2", { sub: "user-2", iss: "https://issuer.example", aud: "client-1", exp: 200, }); expect((await verifyJwtWithJwks(tokenTwo, jwks, { now: 150 })).sub).toBe("user-2"); expect(fetches).toBe(2); expect(jwks.stats()).toMatchObject({ fetches: 2, hits: 2, keys: 1 }); await expect(verifyJwtWithJwks(tokenOne, jwks, { now: 150 })).rejects.toThrow("Unknown key"); }); test("remote JWKS rejects insecure, oversized, and incompatible key sets", async () => { expect(() => createRemoteJwks("http://issuer.example/jwks")).toThrow("HTTPS"); const oversized = createRemoteJwks("https://issuer.example/jwks", { maxBytes: 10, fetch: (async () => Response.json({ keys: [] })) as unknown as typeof fetch, }); await expect(oversized.refresh()).rejects.toThrow("too large"); const incompatible = createRemoteJwks("https://issuer.example/jwks", { fetch: (async () => Response.json({ keys: [{ kid: "ec", kty: "EC", alg: "ES256" }], })) as unknown as typeof fetch, }); await incompatible.refresh(); await expect(incompatible.resolve("ec", "RS256")).rejects.toThrow("Unknown key"); });