diff --git a/packages/api/src/services/jwks.test.ts b/packages/api/src/services/jwks.test.ts new file mode 100644 index 0000000..f20548f --- /dev/null +++ b/packages/api/src/services/jwks.test.ts @@ -0,0 +1,70 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { decodeJwt } from "jose"; +import type { Context } from "../types.ts"; +import { generateEdDSAKeyPair, signJWT } from "./jwks.ts"; + +function createContextWithKey(issuer: string) { + return async () => { + const { publicJwk, privateJwk } = await generateEdDSAKeyPair(); + const privateJwkEnc = Buffer.from(JSON.stringify(privateJwk)); + + const context = { + config: { + issuer, + }, + db: { + query: { + jwks: { + findFirst: async () => ({ + kid: "test-kid", + publicJwk, + privateJwkEnc, + createdAt: new Date(), + }), + }, + }, + }, + services: { + kek: { + decrypt: async (data: Buffer) => data, + encrypt: async (data: Buffer) => data, + isAvailable: () => true, + }, + }, + } 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) {