[BUG] MCP OAuth 403 insufficient_scope step-up authorization does not trigger re-authorization flow

Resolved 💬 6 comments Opened Apr 7, 2026 by asoorm Closed Jun 25, 2026

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?

Per the https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#step-up-authorization-flow:

  1. Client parses WWW-Authenticate header from the 403 response
  2. Client determines required scopes from the scope parameter
  3. Client initiates re-authorization with the elevated scope set (e.g., opens browser for a new OAuth flow)
  4. 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

  1. Set up an MCP server with OAuth that enforces different scopes at different levels
  2. Set up a corresponding OAuth authorization server (supports DCR, authorization_code, PKCE)
  3. Configure Claude Code to connect to the MCP server via HTTP transport
  4. Connect via /mcp → Authenticate
  5. Call an unscoped tool → succeeds
  6. 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:

  1. Client authenticates → gets mcp:connect mcp:tools:read mcp:tools:call
  2. list_employees → succeeds (no extra scopes needed)
  3. 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
  4. 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

  1. 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.
  2. 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
  1. 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.

View original on GitHub ↗

This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗