Skip to content

Fall Back to Legacy 2025-03-26 OAuth Discovery for Servers Without PRM#414

Merged
koic merged 1 commit into
modelcontextprotocol:mainfrom
koic:2025_03_26_backcompat
Jun 17, 2026
Merged

Fall Back to Legacy 2025-03-26 OAuth Discovery for Servers Without PRM#414
koic merged 1 commit into
modelcontextprotocol:mainfrom
koic:2025_03_26_backcompat

Conversation

@koic

@koic koic commented Jun 16, 2026

Copy link
Copy Markdown
Member

Motivation and Context

The MCP 2025-03-26 authorization spec predates Protected Resource Metadata: the MCP server itself acted as the authorization base URL, its RFC 8414 metadata was fetched from the server origin, and the spec's "Fallbacks for Servers without Metadata Discovery" section required clients to use the default endpoints /authorize, /token, and /register relative to the authorization base URL when no metadata was published. Both the TypeScript SDK (discoverOAuthServerInfo falls back to the server base URL on PRM failure; startAuthorization / executeTokenRequest / registerClient default the endpoint paths) and the Python SDK (build_oauth_authorization_server_metadata_discovery_urls(None, ...) plus the same endpoint defaults) keep this as client-side backwards compatibility, and the auth/2025-03-26-oauth-metadata-backcompat and auth/2025-03-26-oauth-endpoint-fallback conformance scenarios exercise it.

The Ruby flow previously raised as soon as PRM discovery failed, so both scenarios were expected conformance failures. This change adds the same fallback:

  • Flow#locate_authorization_server first attempts PRM discovery; on any discovery failure (404s, network errors, malformed documents, matching the TypeScript and Python SDKs' broad fallback) the MCP server's origin becomes the legacy authorization base URL. The Communication Security check (HTTPS or loopback) still applies to that origin, and PRM documents that parse correctly keep their strict shape validation.
  • Flow#authorization_server_metadata fetches RFC 8414 metadata from the base URL. On the legacy path the issuer byte-match is skipped: the 2025-03-26 spec predates that requirement, and a pre-PRM server may host its OAuth endpoints under a path prefix whose issuer legitimately differs from the discovery origin (neither reference SDK validates the issuer on this path). On the modern path the byte-match is unchanged.
  • When even the metadata document is absent, synthetic metadata carrying the legacy spec's default endpoints is used, with PKCE S256 assumed (the legacy spec mandates PKCE; the TypeScript and Python SDKs hardcode S256 on this path too). Endpoint HTTPS checks still apply.
  • run! and refresh! share the new discovery helpers, and both 2025-03-26 scenarios are removed from conformance/expected_failures.yml.

How Has This Been Tested?

New tests in test/mcp/client/oauth/flow_test.rb:

  • the metadata-backcompat shape: no PRM, AS metadata at the server origin with /oauth-prefixed endpoints and a mismatched issuer completes the flow against those endpoints (no issuer error)
  • the endpoint-fallback shape: no metadata at all registers, authorizes, and exchanges the code at /register, /authorize, and /token on the origin, still sending code_challenge (S256) and code_verifier
  • a remote plain-http origin is rejected as the legacy authorization base
  • strict-mode regression: with PRM present, a mismatched issuer still aborts the flow
  • a malformed PRM (top-level JSON array) now selects the legacy path instead of leaking a TypeError, and surfaces a domain error when that path also dead-ends

Two existing tests that asserted PRM failure was terminal were updated to dead-end the legacy path explicitly, preserving their original intent (single flow attempt, domain error type).

Breaking Changes

None for spec-compliant 2025-06-18+ servers, whose PRM-based discovery and issuer validation are unchanged. Servers that previously failed hard during PRM discovery now get one legacy discovery attempt before the flow errors, which can only turn previously failing flows into working ones.

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

Comment thread lib/mcp/client/oauth/flow.rb Outdated
Comment on lines +222 to +224
# and the MCP server's own origin acts
#
# as the authorization base URL, matching the TypeScript and Python SDKs. Any PRM discovery failure

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird comment wrapping

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, It looks like I accidentally introduced an odd line break while working on this. Thanks for catching it. I've fixed it.

## Motivation and Context

The MCP 2025-03-26 authorization spec predates Protected Resource Metadata:
the MCP server itself acted as the authorization base URL, its RFC 8414 metadata was fetched from
the server origin, and the spec's "Fallbacks for Servers without Metadata Discovery" section
required clients to use the default endpoints `/authorize`, `/token`, and `/register` relative to
the authorization base URL when no metadata was published. Both the TypeScript SDK
(`discoverOAuthServerInfo` falls back to the server base URL on PRM failure; `startAuthorization` /
`executeTokenRequest` / `registerClient` default the endpoint paths) and the Python SDK
(`build_oauth_authorization_server_metadata_discovery_urls(None, ...)` plus the same endpoint defaults)
keep this as client-side backwards compatibility, and the `auth/2025-03-26-oauth-metadata-backcompat` and
`auth/2025-03-26-oauth-endpoint-fallback` conformance scenarios exercise it.

The Ruby flow previously raised as soon as PRM discovery failed, so both scenarios were
expected conformance failures. This change adds the same fallback:

- `Flow#locate_authorization_server` first attempts PRM discovery; on any discovery failure
  (404s, network errors, malformed documents, matching the TypeScript and Python SDKs' broad fallback)
  the MCP server's origin becomes the legacy authorization base URL. The Communication Security check
  (HTTPS or loopback) still applies to that origin, and PRM documents that parse correctly keep
  their strict shape validation.
- `Flow#authorization_server_metadata` fetches RFC 8414 metadata from the base URL.
  On the legacy path the `issuer` byte-match is skipped: the 2025-03-26 spec predates that requirement,
  and a pre-PRM server may host its OAuth endpoints under a path prefix whose `issuer` legitimately differs
  from the discovery origin (neither reference SDK validates the issuer on this path). On the modern path
  the byte-match is unchanged.
- When even the metadata document is absent, synthetic metadata carrying the legacy spec's default endpoints
  is used, with PKCE S256 assumed (the legacy spec mandates PKCE; the TypeScript and Python SDKs hardcode S256
  on this path too). Endpoint HTTPS checks still apply.
- `run!` and `refresh!` share the new discovery helpers, and both 2025-03-26 scenarios are removed from
  `conformance/expected_failures.yml`.

## How Has This Been Tested?

New tests in `test/mcp/client/oauth/flow_test.rb`:

- the metadata-backcompat shape: no PRM, AS metadata at the server origin with `/oauth`-prefixed endpoints
  and a mismatched `issuer` completes the flow against those endpoints (no issuer error)
- the endpoint-fallback shape: no metadata at all registers, authorizes, and exchanges the code at `/register`,
  `/authorize`, and `/token` on the origin, still sending `code_challenge` (S256) and `code_verifier`
- a remote plain-http origin is rejected as the legacy authorization base
- strict-mode regression: with PRM present, a mismatched `issuer` still aborts the flow
- a malformed PRM (top-level JSON array) now selects the legacy path instead of leaking a `TypeError`,
  and surfaces a domain error when that path also dead-ends

Two existing tests that asserted PRM failure was terminal were updated to dead-end the legacy path explicitly,
preserving their original intent (single flow attempt, domain error type).

## Breaking Changes

None for spec-compliant 2025-06-18+ servers, whose PRM-based discovery and issuer validation are unchanged.
Servers that previously failed hard during PRM discovery now get one legacy discovery attempt before the flow errors,
which can only turn previously failing flows into working ones.
@koic koic force-pushed the 2025_03_26_backcompat branch from 0779322 to 2c223dd Compare June 16, 2026 08:49
@koic koic merged commit 60f0088 into modelcontextprotocol:main Jun 17, 2026
11 checks passed
@koic koic deleted the 2025_03_26_backcompat branch June 17, 2026 01:49
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.

2 participants