Summary
PrivateKeyJwtProviderOptions.claims currently documents overlapping custom claims as taking precedence over the SDK's standard JWT claims, but the implementation keeps the reserved claims authoritative.
This looks like a docs / contract mismatch rather than a runtime bug or security issue.
Current docs
packages/client/src/client/authExtensions.ts says:
These are merged with the standard claims (iss, sub, aud, exp, iat, jti), with custom claims taking precedence for any overlapping keys.
Actual behavior
createPrivateKeyJwtAuth() constructs claims from { ...baseClaims, ...options.claims }, but then immediately calls:
.setIssuer(options.issuer)
.setSubject(options.subject)
.setAudience(audience)
.setIssuedAt(now)
.setExpirationTime(now + lifetimeSeconds)
.setJti(jti)
Those setters overwrite overlapping values from options.claims, so the SDK's reserved claims remain authoritative.
Minimal reproduction
const addClientAuth = createPrivateKeyJwtAuth({
issuer: 'client-id',
subject: 'client-id',
privateKey: 'a-string-secret-at-least-256-bits-long',
alg: 'HS256',
audience: 'https://aud.example.com',
claims: {
iss: 'override-issuer',
sub: 'override-subject',
aud: 'https://override.example.com',
tenant_id: 'org-123'
}
});
Decoding the resulting JWT shows:
iss === 'client-id'
sub === 'client-id'
aud === 'https://aud.example.com'
tenant_id === 'org-123'
So additional custom claims are included, but overlapping reserved claims are not overridden.
Why this matters
This can mislead users into thinking they can customize reserved JWT claims through claims, when in practice only non-overlapping claims are honored.
Suggested resolution
I think the smallest fix is to align the docs/tests with the current runtime behavior:
- clarify that additional custom claims are included
- clarify that reserved standard claims are still set explicitly by the SDK and are not overridden
- add a regression test showing the current behavior
If maintainers prefer the opposite behavior, that would likely deserve a separate design discussion because it changes runtime semantics.
Summary
PrivateKeyJwtProviderOptions.claimscurrently documents overlapping custom claims as taking precedence over the SDK's standard JWT claims, but the implementation keeps the reserved claims authoritative.This looks like a docs / contract mismatch rather than a runtime bug or security issue.
Current docs
packages/client/src/client/authExtensions.tssays:Actual behavior
createPrivateKeyJwtAuth()constructsclaimsfrom{ ...baseClaims, ...options.claims }, but then immediately calls:.setIssuer(options.issuer).setSubject(options.subject).setAudience(audience).setIssuedAt(now).setExpirationTime(now + lifetimeSeconds).setJti(jti)Those setters overwrite overlapping values from
options.claims, so the SDK's reserved claims remain authoritative.Minimal reproduction
Decoding the resulting JWT shows:
iss === 'client-id'sub === 'client-id'aud === 'https://aud.example.com'tenant_id === 'org-123'So additional custom claims are included, but overlapping reserved claims are not overridden.
Why this matters
This can mislead users into thinking they can customize reserved JWT claims through
claims, when in practice only non-overlapping claims are honored.Suggested resolution
I think the smallest fix is to align the docs/tests with the current runtime behavior:
If maintainers prefer the opposite behavior, that would likely deserve a separate design discussion because it changes runtime semantics.