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', () => {