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
feat: support a two-leg authorization code flow for web-hosted clients
A provider without a callback_handler now stops after the redirect:
Flow#run! saves a pending authorization in storage, keyed by state,
hands the authorization URL to redirect_handler, and returns :redirect,
and MCP::Client::HTTP raises Flow::AuthorizationPendingError instead of
retrying. Flow#finish! completes the authorization in the request that
receives the redirect, which may run in another process.
finish! looks the pending authorization up by state before any request,
validates the RFC 9207 iss against the recorded issuer before consuming
it, reads an error response only after that check, and redeems the code
at the recorded token endpoint with the recorded client registration,
resource, and redirect_uri, without running discovery again.
Copy file name to clipboardExpand all lines: docs/_client/authorization.md
+75-4Lines changed: 75 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -90,12 +90,14 @@ Required keyword arguments to `Provider.new`:
90
90
an explicit value always wins.
91
91
-`redirect_uri`: String. Must use HTTPS or be a loopback URL (`localhost`, `127.0.0.0/8`, `::1`); other values raise `Provider::InsecureRedirectURIError`.
92
92
-`redirect_handler`: Callable invoked with the fully-built authorization `URI`. Typically opens the user's browser.
93
-
-`callback_handler`: Callable that returns `[code, state]` or `[code, state, iss]` after the user is redirected back to `redirect_uri`. Returning the 3-element form
94
-
(with `iss` set to the RFC 9207 `iss` parameter from the redirect, or `nil` when absent) opts into SEP-2468 issuer validation: a present `iss` must match
95
-
the authorization server's issuer, and a missing one is rejected when the server advertises `authorization_response_iss_parameter_supported`.
96
93
97
94
Optional keyword arguments:
98
95
96
+
-`callback_handler`: Callable that returns `[code, state]` or `[code, state, iss]` after the user is redirected back to `redirect_uri`. Returning the 3-element form
97
+
(with `iss` set to the RFC 9207 `iss` parameter from the redirect, or `nil` when absent) opts into SEP-2468 issuer validation: a present `iss` must match
98
+
the authorization server's issuer, and a missing one is rejected when the server advertises `authorization_response_iss_parameter_supported`.
99
+
Omit it when the redirect arrives in a later request, as it does in a web application; see [Two-Leg Authorization for Web Applications](#two-leg-authorization-for-web-applications).
100
+
-`pending_authorization_max_age`: Integer seconds a pending authorization stays redeemable after the redirect when `callback_handler` is omitted. Defaults to 600.
99
101
-`scope`: Space-separated scopes to request when the server's `WWW-Authenticate` does not specify one.
100
102
-`authorization_request_validator`: Callable invoked with an `MCP::Client::OAuth::AuthorizationRequest` before any authorization request is built.
101
103
Returning a falsy value abandons the flow with `Flow::AuthorizationRefusedError`. See [Reviewing the authorization request](#reviewing-the-authorization-request).
@@ -106,7 +108,8 @@ Optional keyword arguments:
106
108
issued it (SEP-2352): when the server's authorization server changes, the SDK discards the stale registration and its tokens and re-registers automatically
107
109
(portable CIMD `client_id`s are kept). Saved `tokens` carry an `"issuer"` member of their own, recording the authorization server that minted them, which is what
108
110
lets a later refresh refuse a server the MCP server has since renamed. Treat both hashes as opaque and persist them as-is; a storage that writes out selected members
109
-
instead drops these bindings with no error.
111
+
instead drops these bindings with no error. Without `callback_handler`, it must also hold pending authorizations; see
112
+
[Two-Leg Authorization for Web Applications](#two-leg-authorization-for-web-applications).
110
113
-`client_id_metadata_document_url`: URL where you publish a Client ID Metadata Document
111
114
(`draft-ietf-oauth-client-id-metadata-document` and the MCP authorization specification).
112
115
When the authorization server advertises `client_id_metadata_document_supported: true`,
`callback_handler` keeps the flow open until the code comes back, so the process that sent the user to the authorization server stays blocked for as long as
182
+
the user takes to sign in and consent. That suits CLI and desktop clients. In a web application the redirect arrives as a separate HTTP request, often served
183
+
by a different process, and relaying the code to a request held open for that long is impractical. Omit `callback_handler` and the flow runs in two legs,
184
+
as the TypeScript SDK's `auth()` and `finishAuth()` and the Rust SDK's `get_authorization_url` and `exchange_code_for_token` do:
185
+
186
+
1. When the transport meets a `401`, or a `403` step-up challenge, the flow runs discovery and registration as usual, saves a pending authorization in `storage`
187
+
keyed by the `state` it generated, hands the authorization URL to `redirect_handler`, and raises `MCP::Client::OAuth::Flow::AuthorizationPendingError`
188
+
instead of retrying. The error's `authorization_url` reader returns the same URL, so the application can send the user there from wherever is convenient.
189
+
2. The request that receives the redirect calls `MCP::Client::OAuth::Flow#finish!` with the redirect's whole query. Requests made afterwards use the stored tokens.
The first leg can also run without a request to the MCP server: `MCP::Client::OAuth::Flow.new(provider: provider).run!(server_url: server_url)` returns `:redirect`,
223
+
and the flow's `authorization_url` reader returns the URL it handed to `redirect_handler`.
224
+
225
+
The storage must also respond to `save_pending_authorization(state, pending)`, `pending_authorization(state)`, and `delete_pending_authorization(state)`;
226
+
`Provider.new` raises `Provider::PendingAuthorizationStorageError` when it does not. A pending authorization is a Hash of JSON-compatible values that includes
227
+
the PKCE verifier, so keep it where you keep credentials, persist it as-is, and share it between the processes that can receive the redirect.
228
+
`InMemoryStorage` implements the methods for a single process.
229
+
230
+
`finish!` redeems the code the way the authorization began, and refuses anything else with `Flow::AuthorizationError`:
231
+
232
+
- The pending authorization is looked up by `state` before any request is made. An unknown, already used, or malformed one is refused,
233
+
and one older than `pending_authorization_max_age` is discarded and refused.
234
+
-`server_url` must name the MCP server the authorization began with.
235
+
- The RFC 9207 `iss` parameter is validated against the recorded issuer before the pending authorization is consumed, so a forged callback carrying a valid `state`
236
+
cannot discard the verifier the legitimate callback needs. Because `finish!` sees the whole query, a missing `iss` is refused whenever the authorization server
- The pending authorization is then consumed, so it is redeemed at most once. An `error` response is raised with its `error` and `error_description`,
239
+
bounded as [token endpoint errors](#token-endpoint-errors) are; it is read only after the `iss` check, since in a mix-up those parameters are the attacker's.
240
+
- The code is redeemed at the recorded token endpoint, with the client registration, `resource`, and `redirect_uri` used when the authorization began,
241
+
without running discovery again (SEP-2352). A registration replaced in the meantime is refused.
242
+
243
+
{: .important }
244
+
> `state` proves that this SDK started the authorization, not which user did. Binding the callback to the user who started it is the application's responsibility:
245
+
> scope `storage` to that user, as in the example, so that a callback delivered to another user's session finds no pending authorization.
246
+
176
247
### Token Endpoint Errors
177
248
178
249
When a token exchange or refresh fails, `MCP::Client::OAuth::Flow::AuthorizationError` includes the HTTP status and
0 commit comments