From a134af5403fa7afd6a01bde1d18d4de33b3723e2 Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Mon, 27 Jul 2026 11:58:37 -0400 Subject: [PATCH] test: cover oidc key generation on a fresh install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guard the fix in 4b85d24 (extractable signing keys) with tests that run the full generate → store → JWKS → sign round-trip against an empty oidc_keys table, so a future change can't silently break first-run key generation again. --- test/oidc-keys.test.ts | 52 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 test/oidc-keys.test.ts diff --git a/test/oidc-keys.test.ts b/test/oidc-keys.test.ts new file mode 100644 index 0000000..3e3d4ad --- /dev/null +++ b/test/oidc-keys.test.ts @@ -0,0 +1,52 @@ +import { beforeEach, describe, expect, test } from "bun:test"; +import { getActiveKey, getJWKS, signIDToken } from "../src/oidc"; +import { db } from "./helpers/db"; + +beforeEach(() => { + db.query("DELETE FROM oidc_keys").run(); +}); + +describe("oidc key generation (fresh install)", () => { + test("generates and stores an RS256 key on an empty table", async () => { + // Regression: generateKeyPair must be extractable or exportKeyToPem + // throws InvalidAccessError on first-ever key generation. + const key = await getActiveKey(); + + expect(key.kid).toStartWith("indiko-oidc-key-"); + expect(key.private_key).toContain("BEGIN PRIVATE KEY"); + expect(key.public_key).toContain("BEGIN PUBLIC KEY"); + expect(key.is_active).toBe(1); + }); + + test("reuses the stored key on subsequent calls", async () => { + const first = await getActiveKey(); + const second = await getActiveKey(); + expect(second.kid).toBe(first.kid); + }); + + test("jwks exposes the public key with a matching kid", async () => { + const key = await getActiveKey(); + const jwks = await getJWKS(); + + expect(jwks.keys.length).toBeGreaterThan(0); + const jwk = jwks.keys.find((k) => k.kid === key.kid); + expect(jwk).toBeDefined(); + expect(jwk?.kty).toBe("RSA"); + expect(jwk?.alg).toBe("RS256"); + expect(jwk?.use).toBe("sig"); + }); + + test("signs an id token that references the key id", async () => { + const key = await getActiveKey(); + const jwt = await signIDToken("https://issuer.example", { + sub: "https://issuer.example/u/kieran", + aud: "ikc_test", + }); + + const [headerB64] = jwt.split("."); + const header = JSON.parse(Buffer.from(headerB64, "base64url").toString()); + expect(header.alg).toBe("RS256"); + expect(header.kid).toBe(key.kid); + expect(jwt.split(".").length).toBe(3); + }); +}); -- 2.51.2