From 0c6516978d54027509f747d5412c6ea7dda5fd6a Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Tue, 24 Mar 2026 22:36:49 +0000 Subject: [PATCH 1/2] test(api): add signJWT issuer fallback coverage Preserve payload iss claim in signJWT when present and add regression tests. --- packages/api/src/services/jwks.test.ts | 65 ++++++++++++++++++++++++++ packages/api/src/services/jwks.ts | 4 +- 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 packages/api/src/services/jwks.test.ts diff --git a/packages/api/src/services/jwks.test.ts b/packages/api/src/services/jwks.test.ts new file mode 100644 index 0000000..3467cc8 --- /dev/null +++ b/packages/api/src/services/jwks.test.ts @@ -0,0 +1,65 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { decodeJwt, exportJWK, generateKeyPair } from "jose"; +import type { Context } from "../types.ts"; +import { signJWT } from "./jwks.ts"; + +function createContextWithKey(issuer: string) { + return async () => { + const { publicKey, privateKey } = await generateKeyPair("EdDSA"); + const publicJwk = await exportJWK(publicKey); + const privateJwk = await exportJWK(privateKey); + + const context = { + config: { + issuer, + }, + db: { + query: { + jwks: { + findFirst: async () => ({ + kid: "test-kid", + publicJwk, + privateJwk, + privateJwkEnc: null, + createdAt: new Date(), + }), + }, + }, + }, + } as unknown as Context; + + return context; + }; +} + +test("signJWT preserves payload issuer when provided", async () => { + const context = await createContextWithKey("http://localhost:9080")(); + + const token = await signJWT( + context, + { + sub: "user-sub", + iss: "https://auth.puzed.com", + }, + "5m" + ); + + const claims = decodeJwt(token); + assert.equal(claims.iss, "https://auth.puzed.com"); +}); + +test("signJWT falls back to context issuer when payload issuer is absent", async () => { + const context = await createContextWithKey("https://auth.puzed.com")(); + + const token = await signJWT( + context, + { + sub: "user-sub", + }, + "5m" + ); + + const claims = decodeJwt(token); + assert.equal(claims.iss, "https://auth.puzed.com"); +}); diff --git a/packages/api/src/services/jwks.ts b/packages/api/src/services/jwks.ts index 07631bd..1e5b756 100644 --- a/packages/api/src/services/jwks.ts +++ b/packages/api/src/services/jwks.ts @@ -118,13 +118,15 @@ export async function signJWT( expiresIn = "5m" ): Promise { const { kid, privateKey } = await getLatestSigningKey(context); + const issuer = + typeof payload.iss === "string" && payload.iss.length > 0 ? payload.iss : context.config.issuer; const jwt = new SignJWT(payload) .setProtectedHeader({ alg: "EdDSA", kid }) .setIssuedAt() .setJti(generateRandomString(32)) .setExpirationTime(expiresIn) - .setIssuer(context.config.issuer); + .setIssuer(issuer); // Set audience if provided in payload if (payload.aud) { From dd727a5811da5f50fa120ca3007730f7389274c7 Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Tue, 24 Mar 2026 22:40:02 +0000 Subject: [PATCH 2/2] fix(api): align jwks tests with encrypted key flow Use generateEdDSAKeyPair in the issuer tests and mock KEK-backed private key retrieval so the test setup matches signJWT runtime behavior. --- packages/api/src/services/jwks.test.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/api/src/services/jwks.test.ts b/packages/api/src/services/jwks.test.ts index 3467cc8..f20548f 100644 --- a/packages/api/src/services/jwks.test.ts +++ b/packages/api/src/services/jwks.test.ts @@ -1,14 +1,13 @@ import assert from "node:assert/strict"; import { test } from "node:test"; -import { decodeJwt, exportJWK, generateKeyPair } from "jose"; +import { decodeJwt } from "jose"; import type { Context } from "../types.ts"; -import { signJWT } from "./jwks.ts"; +import { generateEdDSAKeyPair, signJWT } from "./jwks.ts"; function createContextWithKey(issuer: string) { return async () => { - const { publicKey, privateKey } = await generateKeyPair("EdDSA"); - const publicJwk = await exportJWK(publicKey); - const privateJwk = await exportJWK(privateKey); + const { publicJwk, privateJwk } = await generateEdDSAKeyPair(); + const privateJwkEnc = Buffer.from(JSON.stringify(privateJwk)); const context = { config: { @@ -20,13 +19,19 @@ function createContextWithKey(issuer: string) { findFirst: async () => ({ kid: "test-kid", publicJwk, - privateJwk, - privateJwkEnc: null, + privateJwkEnc, createdAt: new Date(), }), }, }, }, + services: { + kek: { + decrypt: async (data: Buffer) => data, + encrypt: async (data: Buffer) => data, + isAvailable: () => true, + }, + }, } as unknown as Context; return context;