[BUG] MCP OAuth 403 insufficient_scope step-up authorization does not trigger re-authorization flow
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When an MCP server returns HTTP 403 Forbidden with WWW-Authenticate: Bearer error="insufficient_scope", Claude Code does not complete the step-up re-authorization flow. It re-fetches protected resource metadata but never requests a new token from the authorization server with broader scopes.
This prevents MCP servers from implementing per-tool or per-operation scope enforcement as described in the https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#scope-challenge-handling.
The initial 401 flow works correctly - Claude Code reads the scope parameter from the WWW-Authenticate header and requests exactly those scopes during the authorization code flow. The bug is specifically in the 403 step-up path.
Related issues:
- #28258 (closed) — Same bug. @DavidChouinard commented "This will be resolved in the next release" - still broken in v2.1.92
- #4540 (open) — Related: missing scope in DCR/auth when 401 has no scope param (different root cause)
- #39626 (open) — Docs omit step-up re-authorization
What Should Happen?
- Client parses WWW-Authenticate header from the 403 response
- Client determines required scopes from the scope parameter
- Client initiates re-authorization with the elevated scope set (e.g., opens browser for a new OAuth flow)
- Client retries the original request with the new token
The spec states:
Clients SHOULD respond to these errors by requesting a new access token with an increased set of scopes via a step-up authorization flow. Clients acting on behalf of a user SHOULD attempt the step-up authorization flow.
Clients MUST treat the scopes provided in the challenge as authoritative for satisfying the current request.
The scopes_supported field is intended to represent the minimal set of scopes necessary for basic functionality, with additional scopes requested incrementally through the step-up
authorization flow.
Error Messages/Logs
```
# 1st attempt — misleading error (token is valid, issue is insufficient scope):
MCP server "my-server" requires re-authorization (token expired)
# Subsequent attempts — misleading (no upscoping was actually attempted):
Streamable HTTP error: Server returned 403 after trying upscoping
```
Debug proxy output (between Claude Code and MCP server) — initial 401 auth flow (works correctly):
```
# 1. Claude Code sends initialize with no token
POST /mcp → 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp",
scope="mcp:connect mcp:tools:read mcp:tools:call",
resource_metadata="http://localhost:5026/.well-known/oauth-protected-resource/mcp"
# 2. Claude Code discovers protected resource metadata
GET /.well-known/oauth-protected-resource/mcp → 200
Body: { "scopes_supported": ["mcp:connect", "mcp:tools:call", "mcp:tools:read",
"hr:read", "hr:write", "hr:admin"] }
# 3. Claude Code authenticates (DCR + browser auth + code exchange happen at OAuth server)
# 4. Claude Code retries with token — scopes match the 401 challenge exactly
POST /mcp (Authorization: Bearer <token with scope="mcp:connect mcp:tools:read mcp:tools:call">)
Body: { "method": "initialize" }
→ 200 OK ✅ ─
# 5. tools/list succeeds
POST /mcp → 200 OK (tools listed) ✅
# 6. Unscoped tool call succeeds
POST /mcp { "method": "tools/call", "params": { "name": "list_employees" } }
→ 200 OK ✅
```
Debug proxy output — 403 step-up flow (fails):
```
# 7. Scoped tool call
POST /mcp { "method": "tools/call", "params": { "name": "get_employee_salary" } }
→ 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
scope="mcp:connect mcp:tools:read mcp:tools:call hr:read",
resource_metadata="http://localhost:5026/.well-known/oauth-protected-resource/mcp",
error_description="insufficient scopes for tool get_employee_salary"
# 8. Claude Code re-fetches metadata — this is the only action it takes
GET /.well-known/oauth-protected-resource/mcp → 200
# NO FURTHER REQUESTS TO MCP SERVER OR AUTHORIZATION SERVER
# Error shown: "MCP server requires re-authorization (token expired)"
```
Authorization server logs — confirms no step-up attempt:
```
# During initial auth (works correctly):
GET /.well-known/oauth-authorization-server → 200 (AS metadata discovery)
POST /register → 201 (Dynamic Client Registration)
GET /authorize?scope=mcp:connect+mcp:tools:read+mcp:tools:call → 302 (browser auth)
POST /token → 200 (code exchange, token issued)
# After the 403 insufficient_scope:
# ZERO REQUESTS — no /authorize, no /token, no /register
# Claude Code never contacts the authorization server to obtain a broader token
```
Steps to Reproduce
- Set up an MCP server with OAuth that enforces different scopes at different levels
- Set up a corresponding OAuth authorization server (supports DCR, authorization_code, PKCE)
- Configure Claude Code to connect to the MCP server via HTTP transport
- Connect via /mcp → Authenticate
- Call an unscoped tool → succeeds
- Call a scoped tool → fails
Example scenario:
Consider an MCP server exposing HR tools with tiered scope requirements:
| Tool | Description | Required Scopes (in addition to base) |
|------|-------------|---------------------------------------|
| list_employees | List all employees | _(none — base scopes sufficient)_ |
| get_employee_salary | View an employee's salary | hr:read |
| update_employee_salary | Modify an employee's salary | hr:read hr:write |
| approve_bonus | Approve a bonus payment | hr:write finance:approve |
| offboard_employee | Initiate employee offboarding | hr:admin |
The scope hierarchy:
| Level | Scopes | Gates |
|-------|--------|-------|
| Connect | mcp:connect mcp:tools:read mcp:tools:call | Initialize, list tools, call unscoped tools |
| Read sensitive data | + hr:read | Tools accessing confidential data |
| Write sensitive data | + hr:write | Tools modifying confidential data |
| Cross-domain approval | + finance:approve | Tools requiring finance department sign-off |
| Admin actions | + hr:admin | Privileged operations like offboarding |
Expected flow:
- Client authenticates → gets mcp:connect mcp:tools:read mcp:tools:call
- list_employees → succeeds (no extra scopes needed)
- get_employee_salary → server returns 403 with scope="mcp:connect mcp:tools:read mcp:tools:call hr:read" → client re-authorizes → user approves hr:read → retry succeeds
- approve_bonus → server returns 403 with scope="... hr:write finance:approve" → client re-authorizes → user explicitly approves cross-domain scopes → retry succeeds
The user is the security gate at each step. Without working step-up authorization, servers must either grant all scopes upfront (bypassing user consent for sensitive actions) or fail permanently on scoped tools.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
#28258 fix was promised but appears to not have shipped
Claude Code Version
2.1.92
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
- Step-up re-auth never triggers: Claude Code opens the browser for initial 401 auth but does not open it again for 403 step-up. The "upscoping" attempt appears to be limited to re-fetching protected resource metadata — no actual token request is made to the authorization server.
- Misleading error messages:
- "token expired" — the token is valid (not expired), the issue is insufficient_scope
- "Server returned 403 after trying upscoping" — no upscoping was attempted; the authorization server received zero requests after the 403
- Step-up should work at every MCP operation level, not just
tools/call:
The MCP spec does not limit scope challenges to tool invocations. A server may enforce progressively broader scopes across different operation levels, for example:
| Operation | Required Scopes | Purpose |
|-----------|----------------|---------|
| initialize | mcp:connect | Establish session |
| tools/list | + mcp:tools:read | Discover available tools |
| tools/call (any) | + mcp:tools:call | Execute tools |
| tools/call (scoped) | + tool-specific scopes | Access sensitive operations |
Each transition from one level to the next may require a step-up 403 challenge. In our testing, Claude Code could not step up at any of these levels — we had to collapse all base scopes into the initialize requirement to work around the bug.
This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗