Skip to content

fix(auth): allow public CIMD clients to use the jwt-bearer grant - #3606

Closed
seidnerj wants to merge 1 commit into
modelcontextprotocol:mainfrom
seidnerj:cimd-public-client-jwt-bearer
Closed

seidnerj wants to merge 1 commit into
modelcontextprotocol:mainfrom
seidnerj:cimd-public-client-jwt-bearer

Conversation

@seidnerj

@seidnerj seidnerj commented Oct 1, 2026

Copy link
Copy Markdown

Fixes #3598

Summary

The identity-assertion branch of TokenHandler rejects every client without a stored client_secret. The enterprise-managed authorization extension (section 5) lets a client that is not pre-registered use its Client ID Metadata Document URL as client_id and authenticate optionally with private_key_jwt, so a public CIMD client presenting only the ID-JAG is a valid request.

This change relaxes the check for that one case only:

  • Pre-registered clients still need a stored secret, exactly as today.
  • A new provider hook, is_metadata_document_client(client), decides what counts as a CIMD client. It defaults to False, so servers that do not support CIMD see no behavior change.
  • For a secretless client the hook accepts, the handler requires the ID-JAG's client_id claim to equal the requesting client's client_id, and answers invalid_grant otherwise (including a malformed assertion or a missing claim), before the provider hook runs.
  • DCR still refuses jwt-bearer in grant_types (unchanged), and a CIMD client must still list the grant type in its metadata.

Why this is safe

The authority for this grant is the ID-JAG, not the client credential. exchange_identity_assertion already has to verify the assertion's signature, iss, aud, resource, exp and jti, and bind its client_id claim to the client. The handler's extra check reads client_id unverified, which is safe because a mismatch can only cause rejection; the provider still verifies the signature before issuing anything. It guarantees an ID-JAG issued to one client cannot be redeemed by another. A stolen ID-JAG gives at most a single, short-lived redemption for the named client and resource, similar to a bearer authorization code for a public PKCE client. Confidential clients keep proving possession of their secret.

Changes

  • server/auth/provider.py: add is_metadata_document_client (default False); update the exchange_identity_assertion docstring, which no longer promises a confidential client.
  • server/auth/handlers/token.py: allow a secretless client when the provider reports it is a CIMD client and the assertion's client_id claim names it; otherwise keep unauthorized_client.
  • server/auth/routes.py: advertise none in token_endpoint_auth_methods_supported only when identity assertion is enabled. This touches the same line as fix: include "none" in token_endpoint_auth_methods_supported metadata #2261, which adds none unconditionally; whichever lands second needs a trivial rebase.
  • docs/client/identity-assertion.md: one sentence on the exception.

Tests (tests/server/auth/test_identity_assertion.py)

  • CIMD public client with an ID-JAG naming it: 200, and the provider hook receives that client.
  • CIMD public client with an ID-JAG naming another client, lacking a client_id claim, or malformed: invalid_grant, provider hook not called.
  • CIMD public client without jwt-bearer in grant_types: unsupported_grant_type.
  • Non-CIMD public client with an ID-JAG naming it: still unauthorized_client (existing test, now presenting a matching assertion so only the CIMD check can reject it).
  • Confidential client with wrong or missing secret: 401 invalid_client. Correct secret: 200 (existing test).
  • identity_assertion_enabled=False: CIMD client gets unsupported_grant_type.
  • DCR with jwt-bearer in grant_types: still rejected (existing test).
  • Metadata lists none only when identity assertion is enabled; the default provider's hook returns False.

The new-behavior tests fail on main and pass with this change. ./scripts/test passes with 100% coverage; pyright and ruff are clean.

AI disclosure: I used an AI coding assistant to draft this change and its tests; I have reviewed them and can answer for them.

The enterprise-managed authorization extension (section 5) lets a client
that is not pre-registered use its Client ID Metadata Document URL as
client_id and present an ID-JAG without client authentication. The token
handler rejected every secretless client for this grant.

Add OAuthAuthorizationServerProvider.is_metadata_document_client
(default False). When it returns True for a secretless client, the
handler accepts the grant if the ID-JAG's client_id claim names the
requesting client, and otherwise answers invalid_grant. Advertise none
in token_endpoint_auth_methods_supported when identity assertion is
enabled.

Fixes modelcontextprotocol#3598
@github-actions github-actions Bot added the missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md) label Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

This PR has been closed automatically. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and you aren't currently assigned to #3598.

If a maintainer assigns you to #3598, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take.

You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way.

CONTRIBUTING.md has the full reasoning, but in short:

  • We're a small team with very little capacity to review community PRs right now.
  • Many recent PRs are AI-generated with little human review, and reviewing one carefully still costs a maintainer as much time as it ever did. A well-described issue is usually more useful to us than the code.

Maintainers: reopen, remove missing-issue-link, or add bypass-issue-check to override.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Server TokenHandler rejects the jwt-bearer (ID-JAG) grant from public CIMD clients, which the enterprise-managed authorization extension permits

1 participant