Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/fix-custom-claims-override.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@modelcontextprotocol/client": patch
---

Fix custom claims not overriding reserved JWT claims in createPrivateKeyJwtAuth

Remove redundant jose setter calls (setIssuer, setSubject, setAudience, setIssuedAt,
setExpirationTime, setJti) that silently overwrote user-provided custom claims, making
the runtime behavior match the documented contract that custom claims take precedence
for overlapping keys.
11 changes: 5 additions & 6 deletions packages/client/src/client/authExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,13 @@ export function createPrivateKeyJwtAuth(options: {
}

// Sign JWT
// All six reserved claims (iss, sub, aud, iat, exp, jti) are already
// present in `claims` via `baseClaims`, and custom overrides from
// `options.claims` are merged on top. We intentionally do NOT call
// the jose setter helpers here so the documented "custom claims take
// precedence" contract is honored.
const assertion = await new jose.SignJWT(claims)
.setProtectedHeader({ alg, typ: 'JWT' })
.setIssuer(options.issuer)
.setSubject(options.subject)
.setAudience(audience)
.setIssuedAt(now)
.setExpirationTime(now + lifetimeSeconds)
.setJti(jti)
.sign(key as unknown as Uint8Array | CryptoKey);

params.set('client_assertion', assertion);
Expand Down
29 changes: 29 additions & 0 deletions packages/client/test/client/authExtensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,35 @@ describe('createPrivateKeyJwtAuth', () => {
expect(decoded.sub).toBe('client-id');
});

it('allows custom claims to override reserved JWT claims', async () => {
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'
}
});

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.iss).toBe('override-issuer');
expect(decoded.sub).toBe('override-subject');
expect(decoded.aud).toBe('https://override.example.com');
expect(decoded.tenant_id).toBe('org-123');
});

it('passes custom claims through PrivateKeyJwtProvider', async () => {
const provider = new PrivateKeyJwtProvider({
clientId: 'client-id',
Expand Down
Loading