From 2c37dfb258dd34907d0a0fbb3bb318777ac1c647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusufhan=20Sa=C3=A7ak?= Date: Fri, 10 Apr 2026 13:45:27 +0300 Subject: [PATCH] feat(client): support custom claims in PrivateKeyJwtProvider Expose the existing `claims` parameter from `createPrivateKeyJwtAuth` in the `PrivateKeyJwtProviderOptions` interface, allowing custom claims to be included in the JWT client assertion. This enables enterprise scenarios where additional claims (e.g., tenant_id) help scope the access token with finer granularity than scopes alone, without requiring clients to manage JWT signing directly. Closes #1477 --- packages/client/src/client/authExtensions.ts | 13 +++++- .../client/test/client/authExtensions.test.ts | 43 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/client/src/client/authExtensions.ts b/packages/client/src/client/authExtensions.ts index 80efc0a12..cb476c12f 100644 --- a/packages/client/src/client/authExtensions.ts +++ b/packages/client/src/client/authExtensions.ts @@ -233,6 +233,16 @@ export interface PrivateKeyJwtProviderOptions { * Space-separated scopes values requested by the client. */ scope?: string; + + /** + * Optional custom claims to include in the JWT assertion. + * These are merged with the standard claims (`iss`, `sub`, `aud`, `exp`, `iat`, `jti`), + * with custom claims taking precedence for any overlapping keys. + * + * Useful for including additional claims that help scope the access token + * with finer granularity than what scopes alone allow. + */ + claims?: Record; } /** @@ -277,7 +287,8 @@ export class PrivateKeyJwtProvider implements OAuthClientProvider { subject: options.clientId, privateKey: options.privateKey, alg: options.algorithm, - lifetimeSeconds: options.jwtLifetimeSeconds + lifetimeSeconds: options.jwtLifetimeSeconds, + claims: options.claims }); } diff --git a/packages/client/test/client/authExtensions.test.ts b/packages/client/test/client/authExtensions.test.ts index f7737077e..16c3ea33e 100644 --- a/packages/client/test/client/authExtensions.test.ts +++ b/packages/client/test/client/authExtensions.test.ts @@ -424,6 +424,49 @@ describe('createPrivateKeyJwtAuth', () => { /Key for the RS256 algorithm must be one of type CryptoKey, KeyObject, or JSON Web Key/ ); }); + + it('includes custom claims in the signed JWT assertion', async () => { + const addClientAuth = createPrivateKeyJwtAuth({ + issuer: 'client-id', + subject: 'client-id', + privateKey: 'a-string-secret-at-least-256-bits-long', + alg: 'HS256', + claims: { tenant_id: 'org-123', role: 'admin' } + }); + + const params = new URLSearchParams(); + await addClientAuth(new Headers(), params, 'https://auth.example.com/token', undefined); + + const assertion = params.get('client_assertion'); + expect(assertion).toBeTruthy(); + + const jose = await import('jose'); + const decoded = jose.decodeJwt(assertion!); + expect(decoded.tenant_id).toBe('org-123'); + expect(decoded.role).toBe('admin'); + expect(decoded.iss).toBe('client-id'); + expect(decoded.sub).toBe('client-id'); + }); + + it('passes custom claims through PrivateKeyJwtProvider', async () => { + const provider = new PrivateKeyJwtProvider({ + clientId: 'client-id', + privateKey: 'a-string-secret-at-least-256-bits-long', + algorithm: 'HS256', + claims: { tenant_id: 'org-456' } + }); + + const params = new URLSearchParams(); + await provider.addClientAuthentication(new Headers(), params, 'https://auth.example.com/token', undefined); + + const assertion = params.get('client_assertion'); + expect(assertion).toBeTruthy(); + + const jose = await import('jose'); + const decoded = jose.decodeJwt(assertion!); + expect(decoded.tenant_id).toBe('org-456'); + expect(decoded.iss).toBe('client-id'); + }); }); describe('CrossAppAccessProvider', () => {