Skip to content

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

Description

@seidnerj

Problem

With identity_assertion_enabled=True, TokenHandler rejects every urn:ietf:params:oauth:grant-type:jwt-bearer request from a client that has no stored client_secret:

# mcp/server/auth/handlers/token.py
if not client_info.client_secret:
    return self.response(
        TokenErrorResponse(
            error="unauthorized_client",
            error_description="The JWT bearer grant requires a confidential client",
        )
    )

A client identified by a Client ID Metadata Document (CIMD) cannot hold a shared secret (CIMD forbids client_secret_*), so the only methods open to it are none and private_key_jwt. Since ClientAuthenticator does not implement private_key_jwt, a CIMD client can never complete the ID-JAG exchange against an SDK-based authorization server.

This blocks real clients. At least one widely deployed MCP client documents its enterprise-managed authorization token request as carrying only grant_type, assertion, client_id (a CIMD URL), scope and resource, with no client secret and no client assertion.

What the extension spec says

modelcontextprotocol/ext-auth, specification/stable/enterprise-managed-authorization.mdx, section 5 (Access Token Request):

The MCP Client authenticates with its credentials as registered with the Resource Authorization Server.

If the MCP Client is not pre-registered with the Resource Authorization Server, then it can use its Client ID Metadata Document as its client ID, and optionally authenticate using private_key_jwt.

Its example request has no client authentication at all:

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGciOiJIUzI1NiIsI...
&client_id=https://client.example.com/client.json

So for a CIMD client, client authentication at this step is optional. The current check is stricter than the spec and turns away the case the spec calls out by name.

Expected

A CIMD client with token_endpoint_auth_method="none" can exchange a valid ID-JAG issued for its client_id. Pre-registered clients keep today's behavior (they must authenticate as registered).

Related

AI disclosure: I used an AI coding assistant to draft this issue; I have reviewed it and can answer for it.

Activity

  1. added
    v2Affects the v2 line (2.x on main)
    on Sep 30, 2026
  2. Sanjay-jat commented on Oct 1, 2026

    @Sanjay-jat

    @seidnerj j are you planning to fix this yourself? If not, I've been looking at it.

    The only blocker I found is the if not client_info.client_secret check in TokenHandler. ClientAuthenticator already lets none clients through, so the check is all that stops a CIMD client.

    The approach I tried: allow a secretless client when its client_id is a CIMD URL (https with a path), and keep refusing every other public client. The grant is still gated by the registered grant_types, and DCR still refuses to register it.

    One question on intended behavior: is a URL-shape check on client_id the right way to detect a CIMD client here, or would you rather the provider decide?

    I have a working version with tests on a branch if it's useful.

    AI disclosure: I used an AI assistant while working on this and reviewed the change myself.

  3. seidnerj commented on Oct 1, 2026

    @seidnerj
    Author

    Hey Sanjay,

    Thx for digging into this! I already have a fix on a branch and will open a PR shortly.

    On your question, I went with a provider hook (is_metadata_document_client) rather than a URL-shape check on client_id. I think whether a client came from a metadata document is the provider's call - a provider may resolve CIMD clients differently, or register https-URL client_ids some other way, and a shape check can't tell those apart.

    I also added a check I think a secretless client needs: the ID-JAG's client_id claim has to match the requesting client, otherwise a public client could redeem an assertion that was issued to someone else.

    Would be happy to have your review on the PR.

    AI disclosure: I used an AI assistant while working on this and reviewed the change myself.

  4. seidnerj commented on Oct 1, 2026

    @seidnerj
    Author

    Some context on why this matters for me, in case it helps with triage.

    I'm building MCP servers that support enterprise-managed authorization, where clients identify themselves with a Client ID Metadata Document and never get a client secret. With the current TokenHandler, every one of those clients is refused on the jwt-bearer grant, so the ID-JAG flow from the extension can't work for them unless the server works around the SDK's handler.

    Repro is short: a CIMD client (https client_id, jwt-bearer in its grant_types, token_endpoint_auth_method none) posts grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer with a valid ID-JAG and no client_secret. It gets 400 unauthorized_client, "The JWT bearer grant requires a confidential client", before the provider ever sees the assertion.

    The fix is in #3606, which the bot closed since I'm not assigned here. Happy to take this if a maintainer assigns it to me.

    AI disclosure: I used an AI assistant while working on this and reviewed the change myself.

  5. maxisbey commented on Oct 2, 2026

    @maxisbey
    Contributor

    The built-in authorization server authenticates clients by shared secret only and doesn't currently support clients identified by a Client ID Metadata Document, so a client with no secret can't use the ID-JAG grant against it. You were right that this is the SDK's own policy rather than something SEP-990 requires, so #3622 clarifies the docs, the code comments and the client's error message to say so. Server-side support for those clients is tracked in the older issue #1801, which is where this use case belongs. Until then, a deployment that needs a different policy can swap the /token route that create_auth_routes returns for its own.

    AI Disclaimer

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    v2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions