Skip to content

Validate the issuer of legacy authorization server metadata - #556

Merged
koic merged 1 commit into
modelcontextprotocol:mainfrom
koic:validate_the_issuer_of_legacy_authorization_server_metadata
Sep 18, 2026
Merged

koic merged 1 commit into
modelcontextprotocol:mainfrom
koic:validate_the_issuer_of_legacy_authorization_server_metadata

Conversation

@koic

@koic koic commented Sep 17, 2026

Copy link
Copy Markdown
Member

Motivation and Context

On the legacy 2025-03-26 path, taken when a server publishes no Protected Resource Metadata, the authorization server metadata is fetched from the MCP server's origin, which that spec names as the authorization base URL, but its issuer was never compared with that origin. RFC 8414 Section 3.3 requires the comparison, the 2025-03-26 spec makes RFC 8414 a MUST for clients, and the TypeScript and Python SDKs both compare on this path, tolerating only a trailing slash. The flow nevertheless used the unverified issuer as the authorization server's identity everywhere: tokens were stamped with it, stored client information was matched against it, the private_key_jwt and ID-JAG assertions took it as their audience, and authorization_request_validator was shown it.

A server on that path could therefore claim any issuer. Claiming the one that minted the stored tokens made refresh! send the refresh token and the stored client secret to the server's own token endpoint, claiming the one that issued stored client information made the flow reuse those credentials there, and a validator allow-listing the claimed issuer approved the request.

The legacy path now applies the same check as the modern one: the document's issuer must be the origin, rendered with or without a trailing slash, and any other document is refused before registration, redirect, or token request. A server without a metadata document still gets the spec's default endpoints.

How Has This Been Tested?

New tests in test/mcp/client/oauth/flow_test.rb serve legacy metadata at the MCP server origin that claims https://auth.example.com and check that the flow, a refresh of tokens issued by that server, and a flow holding client information bound to it are all refused before any request, with the validator never asked and the credentials never presented. Metadata naming the origin, with or without a trailing slash and with endpoints under a path prefix, completes the flow, including the RFC 9207 iss check. The former test of a path-prefix issuer now expects the refusal. The refusal tests fail against the previous library.

Breaking Changes

A server without Protected Resource Metadata whose authorization server metadata names an issuer other than its origin is refused instead of used. The TypeScript and Python SDKs already refuse such a server, and the 2025-03-26 spec places the metadata at the origin.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

## Motivation and Context

On the legacy 2025-03-26 path, taken when a server publishes no Protected Resource Metadata,
the authorization server metadata is fetched from the MCP server's origin, which that spec names
as the authorization base URL, but its `issuer` was never compared with that origin.
RFC 8414 Section 3.3 requires the comparison, the 2025-03-26 spec makes RFC 8414 a MUST for clients,
and the TypeScript and Python SDKs both compare on this path, tolerating only a trailing slash.
The flow nevertheless used the unverified `issuer` as the authorization server's identity everywhere:
tokens were stamped with it, stored client information was matched against it, the `private_key_jwt`
and ID-JAG assertions took it as their audience, and `authorization_request_validator` was shown it.

A server on that path could therefore claim any issuer. Claiming the one that minted the stored tokens made
`refresh!` send the refresh token and the stored client secret to the server's own token endpoint, claiming
the one that issued stored client information made the flow reuse those credentials there, and a validator
allow-listing the claimed issuer approved the request.

The legacy path now applies the same check as the modern one: the document's `issuer` must be the origin,
rendered with or without a trailing slash, and any other document is refused before registration, redirect,
or token request. A server without a metadata document still gets the spec's default endpoints.

## How Has This Been Tested?

New tests in `test/mcp/client/oauth/flow_test.rb` serve legacy metadata at the MCP server origin that claims
`https://auth.example.com` and check that the flow, a refresh of tokens issued by that server,
and a flow holding client information bound to it are all refused before any request, with the validator never asked
and the credentials never presented. Metadata naming the origin, with or without a trailing slash and with
endpoints under a path prefix, completes the flow, including the RFC 9207 `iss` check. The former test
of a path-prefix issuer now expects the refusal. The refusal tests fail against the previous library.

## Breaking Changes

A server without Protected Resource Metadata whose authorization server metadata names an issuer other than
its origin is refused instead of used. The TypeScript and Python SDKs already refuse such a server,
and the 2025-03-26 spec places the metadata at the origin.
@koic
koic merged commit 9455c41 into modelcontextprotocol:main Sep 18, 2026
11 checks passed
@koic
koic deleted the validate_the_issuer_of_legacy_authorization_server_metadata branch September 18, 2026 15:11
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.

1 participant