You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Derive the expected issuer before fetching authorization server metadata
The discovery step now knows which issuer the authorization server
metadata must carry before fetching it: the PRM-advertised server, or on
the 2025-03-26 no-PRM fallback the resource server's origin, which is
what that well-known URL is built from (RFC 8414 section 3.3). The
metadata issuer check runs on both paths instead of only when PRM was
found (on the no-PRM path a root issuer rendered with its trailing slash
still names the origin, and the origin is written the way metadata
issuers are, so host case or an explicit default port in `server_url`
do not matter). The SEP-2352 stored-credential binding is evaluated once
against that same value before metadata discovery, so it also applies
when no metadata is served and the default endpoints are used, and the
special case that re-evaluated it against the served issuer goes away;
newly registered clients are bound to it when metadata for it was found.
A 403 insufficient_scope step-up takes the same path as a 401. Until now
it re-authorized with whatever metadata was in memory, and with none
after a restart, in which case it used the 2025-03-26 default endpoints
on the resource server's origin regardless of what the server advertises
and never consulted the binding. It now discovers first when no metadata
is held (extract_resource_metadata_from_www_auth also reads the
`resource_metadata` hint from a 403 challenge), then re-authorizes with
the SEP-2350 scope union, keeping the granted scope in the union even if
discovery dropped the old token; metadata already discovered in this
process is reused as before. A 403 that is not a scope challenge is
handed back to the caller instead of being retried unchanged, as
IdentityAssertionOAuthProvider already does.
credentials_match_issuer treats a root issuer recorded with and without
its trailing slash as the same server, so records stamped in either form
keep matching on both paths.
This brings the no-PRM path in line with the TypeScript client, which
applies the section 3.3 check on every discovery path (that client also
offers an opt-out; this one does not, as on the PRM path since 2.0).
Copy file name to clipboardExpand all lines: docs/client/oauth-clients.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -76,7 +76,7 @@ Look at `main()`. The provider goes on the **httpx2 client**, the httpx2 client
76
76
77
77
The first time `Client` sends a request, the server answers `401`. The provider takes over:
78
78
79
-
1.**Discovery.** It reads the `WWW-Authenticate` header, fetches the server's Protected Resource Metadata from `/.well-known/oauth-protected-resource`, learns which authorization server protects this resource, and fetches *that* server's metadata.
79
+
1.**Discovery.** It reads the `WWW-Authenticate` header, fetches the server's Protected Resource Metadata from `/.well-known/oauth-protected-resource`, learns which authorization server protects this resource, and fetches *that* server's metadata. (An older server that publishes no resource metadata is asked for authorization server metadata at its own origin instead.) Either way the metadata must name, as its `issuer`, the server it was fetched for; anything else is refused.
80
80
2.**Registration.** Nothing in storage? It registers you dynamically with your `OAuthClientMetadata` and stores the result.
81
81
3.**Authorization.** It generates the PKCE pair and a `state`, builds the authorization URL, awaits your `redirect_handler`, then awaits your `callback_handler` for the code.
82
82
4.**Exchange.** It trades the code for an `OAuthToken`, stores it, and replays your original request with `Authorization: Bearer ...`.
There is no client-side override. Fix the deployment instead: make the authorization server's
2519
2521
`issuer` string-equal the URL in the protected resource metadata's `authorization_servers`
2520
-
list. See [OAuth metadata URLs no longer gain a trailing slash](#oauth-metadata-urls-no-longer-gain-a-trailing-slash)
2522
+
list (or the MCP server's origin, without protected resource metadata). See [OAuth metadata URLs no longer gain a trailing slash](#oauth-metadata-urls-no-longer-gain-a-trailing-slash)
2521
2523
for how v2 preserves the exact string form of these URLs.
2522
2524
2523
2525
### OAuth client requests `offline_access` and adds `prompt=consent` when the authorization server supports it ([SEP-2207](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2207))
0 commit comments