Commit 51cd78a
committed
Re-run OAuth flow on 403 insufficient_scope (step-up)
## Motivation and Context
The MCP authorization specification (2025-11-25 scope-selection-strategy)
and RFC 6750 Section 3.1 describe OAuth 2.0 step-up authentication:
the client already holds a valid access token, but the server returns a 403
with `WWW-Authenticate: Bearer error="insufficient_scope", scope="..."`
to demand a higher-privileged scope for a specific operation.
The client is expected to run a fresh authorization request with
the escalated scope and retry.
Until now the Ruby SDK only ran the OAuth flow on 401 responses.
A 403 surfaced unchanged as a `RequestHandlerError` and the operation
that needed the escalated scope failed permanently, even with a valid token
in storage. This lined up with the `auth/scope-step-up` conformance
scenario being listed in `conformance/expected_failures.yml`.
The change adds:
- `MCP::Client::HTTP#send_request` recognises a 403 carrying
an `insufficient_scope` Bearer challenge as a step-up signal,
re-runs a full authorization request with the scope from the challenge,
and retries the original request. Retries are capped at
one per `send_request` call so a server that repeatedly demands more
scope cannot loop the transport.
- `run_step_up_flow!` deliberately bypasses the refresh-token path that
`run_oauth_flow!` uses on 401. Refreshing would re-issue the same
scope set the existing token already has and the server already
rejected. Only a fresh `authorization_code` grant with the escalated
scope can succeed.
- A plain 403 with no `insufficient_scope` challenge continues to
surface as `RequestHandlerError` with `error_type: :forbidden`.
- `conformance/client.rb` invokes a tool for the `auth/scope-step-up`
scenario; the scenario's authorization middleware only requires
the escalated scope on `tools/call`, so the previous `tools/list`-only
client could never trigger the second authorization.
- `auth/scope-step-up` is removed from `conformance/expected_failures.yml`.
## How Has This Been Tested?
New `HTTPOAuthTest` cases cover: the 403 insufficient_scope path
re-authorizes with the escalated scope from the challenge and retries
the original request with the new bearer token; a plain 403 without
the `insufficient_scope` challenge is surfaced as
`error_type: :forbidden` with no OAuth side effects; repeated
`insufficient_scope` responses retry at most once and then surface the 403;
a provider with a saved `refresh_token` still goes through
the full authorization request rather than exchanging the refresh token,
because the existing scope set is precisely what was rejected.
Conformance: `auth/scope-step-up` now passes 22/22 with no warnings.
`bundle exec rake test`, `bundle exec rake rubocop`, and
`bundle exec rake conformance` are all green.
## Breaking Changes
None. Behaviour on 401 is unchanged. A 403 without the `insufficient_scope`
Bearer challenge still raises the same `RequestHandlerError` as before.
The new retry only fires when both the WWW-Authenticate header signals step-up
and the transport was constructed with an OAuth provider.1 parent cf44475 commit 51cd78a
5 files changed
Lines changed: 493 additions & 18 deletions
File tree
- conformance
- lib/mcp/client
- test/mcp/client/oauth
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1913 | 1913 | | |
1914 | 1914 | | |
1915 | 1915 | | |
| 1916 | + | |
| 1917 | + | |
| 1918 | + | |
1916 | 1919 | | |
1917 | 1920 | | |
1918 | 1921 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
116 | 116 | | |
117 | 117 | | |
118 | 118 | | |
119 | | - | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
120 | 129 | | |
121 | 130 | | |
122 | 131 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
9 | 8 | | |
10 | 9 | | |
11 | 10 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
185 | 185 | | |
186 | 186 | | |
187 | 187 | | |
| 188 | + | |
188 | 189 | | |
189 | 190 | | |
190 | 191 | | |
| |||
217 | 218 | | |
218 | 219 | | |
219 | 220 | | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
220 | 234 | | |
221 | 235 | | |
222 | 236 | | |
| |||
337 | 351 | | |
338 | 352 | | |
339 | 353 | | |
340 | | - | |
341 | | - | |
342 | | - | |
343 | | - | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
344 | 357 | | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
345 | 375 | | |
346 | | - | |
347 | | - | |
348 | | - | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
349 | 409 | | |
| 410 | + | |
350 | 411 | | |
351 | 412 | | |
352 | 413 | | |
| |||
0 commit comments