Skip to content

fix(auth): use the discovered issuer as the ID-JAG token-exchange audience - #1890

Open
mauri0686 wants to merge 1 commit into
modelcontextprotocol:mainfrom
mauri0686:fix/1617-jag-audience-issuer
Open

mauri0686 wants to merge 1 commit into
modelcontextprotocol:mainfrom
mauri0686:fix/1617-jag-audience-issuer

Conversation

@mauri0686

@mauri0686 mauri0686 commented Sep 24, 2026 •

Copy link
Copy Markdown

Fixes #1617

Problem

IdentityAssertionGrantProvider sends authorizationServerUrl.ToString() as the audience of the RFC 8693 token exchange at the IdP. That value isn't guaranteed to match the MCP authorization server's issuer identifier, and in the most common setup it doesn't: new Uri("https://auth.example.com").ToString() returns "https://auth.example.com/", while the metadata advertises "issuer": "https://auth.example.com". An IdP that compares the audience against the issuer exactly will reject the exchange.

Fix

Use the issuer from the authorization server metadata that step 1 of the flow already fetches (mcpAuthMetadata). Issuer.OriginalString is used so the value is sent exactly as published, since Uri.ToString() would reintroduce the same trailing-slash normalization. This matches how ClientOAuthProvider compares issuers, and the direction taken by the Python SDK (override_audience_with_issuer, modelcontextprotocol/python-sdk#1721).

If the metadata doesn't include an issuer, it falls back to the previous behavior, so nothing that works today changes.

Scope is limited to the audience, as described in the issue. resource is left as is.

Tests

Added IdentityAssertionGrantProvider_UsesDiscoveredIssuerAsJagAudience (theory), which reads the audience form field sent to the IdP:

configured URL advertised issuer expected audience
https://auth.example.com https://auth.example.com https://auth.example.com
https://auth.example.com/ https://auth.example.com/tenant https://auth.example.com/tenant
https://auth.example.com (missing) https://auth.example.com/ (fallback)

The first two cases fail on main (actual: https://auth.example.com/) and pass with this change. IdentityAssertionGrantTests and the AspNetCore OAuth integration tests pass locally on net10.0, net9.0, net8.0 and net472. The full ModelContextProtocol.Tests suite also passes on net10.0 (2399 tests, 6 skipped because they need an OpenAI key).

🤖 Generated with Claude Code

…ience

IdentityAssertionGrantProvider passed authorizationServerUrl.ToString() as the
RFC 8693 audience. Uri.ToString() appends a trailing slash to an empty path, so
the common case (https://auth.example.com, whose metadata advertises the issuer
without a slash) sent an audience that did not match the issuer. Use the issuer
from the authorization server metadata already fetched in step 1, falling back
to the configured URL when the metadata omits it.

Fixes modelcontextprotocol#1617

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copy link
Copy Markdown

Before making the discovered issuer authoritative for the JAG audience, I think this flow needs the RFC 8414 issuer-identity validation that the SDK's regular OAuth path already performs.

IdentityAssertionGrant.DiscoverAuthServerMetadataAsync() currently returns the metadata without checking that metadata.Issuer is identical to the issuer URL used to construct the well-known request. This PR then uses that unvalidated field as the token-exchange audience.

The new theory test actually makes the mismatch observable: for an authorization server URL of https://auth.example.com/, an advertised issuer of https://auth.example.com/tenant is accepted and becomes the audience. RFC 8414 §3.3 requires the returned issuer to be identical to the issuer identifier used for discovery; the main ClientOAuthProvider has an explicit check for this.

Because this PR changes the audience from the caller-selected AS URL to metadata-controlled input, I would validate the issuer first and make the mismatched-issuer case fail rather than assert it as supported behavior.

AI-assisted review; checked the current head and both metadata-discovery implementations before posting.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Identity Assertion Grant uses raw authorization server URL as token-exchange audience

2 participants