authExtensions docs say custom claims override reserved JWT claims, but implementation keeps reserved claims authoritative

Open Beginner friendly
#1,914 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
74/100
Issue type
Documentation
Clarity
Clearly specified
Activity status
Quiet
Tech stack
typescript

Research direction

Start in packages/client/src/client/authExtensions.ts and read the PrivateKeyJwtProviderOptions.claims documentation alongside createPrivateKeyJwtAuth(). Locate the related auth extension tests, then update the wording to explain that additional claims are included while reserved standard claims remain SDK-controlled. Done means the documentation matches runtime behavior and a regression test demonstrates the reserved-claim behavior.

Written by the indexing model from the issue text.

Description

bug documentation fix proposed P3 ready for work

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.

Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 12h
Merged PRs (30d)
3

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from modelcontextprotocol/typescript-sdk

All issues in modelcontextprotocol/typescript-sdk

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.