[BUG] v2.1.113 regression: OAuth initiation ignores `resource_metadata` from `WWW-Authenticate` header, breaking RFC 9728 discovery
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?
Since v2.1.113, Claude Code's OAuth initiation path no longer reads the resource_metadata URL from the MCP server's WWW-Authenticate header in the 401 response. This causes the entire RFC 9728 Protected Resource Metadata discovery to fail.
The SDK then falls back to well-known URI probing, which also fails when the MCP server URL contains path components (common with API gateways and multi-tenant platforms), because /.well-known/oauth-protected-resource/* paths are typically not routed to the correct backend.
As a result, when the MCP server (Resource Server) and Authorization Server are separate services, Claude Code cannot discover the correct AS and the OAuth flow fails entirely — both the primary discovery mechanism (header) and the fallback (well-known URI) are broken.
Per MCP Authorization spec (2025-11-25):
MCP clients MUST support both discovery mechanisms and use the resource metadata URL from the parsed WWW-Authenticate headers when present
Additionally, the spec states the two mechanisms are alternatives — servers only need to implement one of them:
MCP servers MUST implement one of the following discovery mechanisms: 1. WWW-Authenticate Header: Include the resource metadata URL in the WWW-Authenticate HTTP header … 2. Well-Known URI: Serve metadata at a well-known URI …
So servers that only implement option 1 (WWW-Authenticate header) are fully spec-compliant, yet Claude Code v2.1.113+ cannot work with them at all.
Root Cause
Transport layer — correct, but never reached in v2.1.113+:
StreamableHTTPClientTransport.send() correctly extracts resource_metadata from the 401 WWW-Authenticate header:
// StreamableHTTPClientTransport.send() — POST 401 handler
if (response.status === 401 && this._authProvider) {
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
this._resourceMetadataUrl = resourceMetadataUrl; // ✅ Extracted from WWW-Authenticate
await auth(this._authProvider, { resourceMetadataUrl, ... }); // ✅ Passed to auth()
}
OAuth initiation path (v2.1.113+) — bypasses transport, reads only from cache:
Claude Code's top-level OAuth handler initiates auth independently. It reads resourceMetadataUrl only from the mcpOAuth cache in ~/.claude.json — it never makes an HTTP request to the MCP server to obtain the 401 response:
// Claude Code's OAuth initiation wrapper
let cached = (await readConfig())?.mcpOAuth?.[cacheKey];
let resourceMetadataUrl = cached?.discoveryState?.resourceMetadataUrl; // ❌ Empty on first connect
await auth(authProvider, {
serverUrl: serverConfig.url,
resourceMetadataUrl: resourceMetadataUrl, // ❌ undefined — 401 header never parsed
...
});
This is the same architectural pattern as #55760 (which was a missing fetchFn in the same initiation path, fixed in v2.1.133).
What Should Happen?
Claude Code should parse the resource_metadata URL from the WWW-Authenticate header in the MCP server's 401 response, then use it to fetch Protected Resource Metadata and discover the correct Authorization Server — exactly as v2.1.112 and earlier versions did, and as the MCP Authorization spec requires.
Error Messages/Logs
MCP server (https://mcp.example.com/v2/server/mcp) returns:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource?resource=https%3A%2F%2Fmcp.example.com%2Fv2%2Fserver%2Fmcp",
scope="files:read"
Claude Code v2.1.113+:
→ Does NOT parse WWW-Authenticate header
→ Reads resourceMetadataUrl from .claude.json cache → undefined (first connect)
→ auth() called with resourceMetadataUrl: undefined
→ SDK falls back to well-known URI probing:
GET https://mcp.example.com/.well-known/oauth-protected-resource/v2/server/mcp → 404
(path-insertion per RFC 9728 — gateway does not route /.well-known/* to backend)
GET https://mcp.example.com/.well-known/oauth-protected-resource → 404
(root fallback — also not served by gateway)
→ PRM discovery fails entirely
→ SDK last resort: treats MCP server origin as AS
→ Sends authorization/token requests to https://mcp.example.com instead of https://auth.example.com
→ OAuth flow fails
Steps to Reproduce
- Set up an MCP server (Streamable HTTP) per the MCP Authorization spec, with RS and AS as separate services, and the RS at a path-based URL:
- RS at
https://mcp.example.com/v2/server/mcpreturns401with:
``http``
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource?resource=https%3A%2F%2Fmcp.example.com%2Fv2%2Fserver%2Fmcp",
scope="files:read"
- PRM at the above
resource_metadataURL returns:
``json``
{
"resource": "https://mcp.example.com/v2/server/mcp",
"authorization_servers": ["https://auth.example.com"]
}
- AS is at
https://auth.example.com(separate service) - Well-known URIs
/.well-known/oauth-protected-resource/v2/server/mcpand/.well-known/oauth-protected-resourceare not served (spec-compliant:WWW-Authenticateheader is the primary mechanism)
- Configure in
~/.claude.json:
``json``
{
"mcpServers": {
"my-server": {
"type": "http",
"url": "https://mcp.example.com/v2/server/mcp"
}
}
}
- Run Claude Code v2.1.112 → Extracts
resource_metadatafrom header → discovers AS → OAuth succeeds ✅ - Run Claude Code v2.1.113+ → Header ignored → well-known fallback 404 → AS discovery fails → OAuth broken ❌
Claude Model
None
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.112
Claude Code Version
2.1.169 (latest as of 2025-06-09)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
Version Bisect
| Version | Runtime | Result |
|---------|---------|--------|
| v2.1.48 | Node.js (bundled JS) | ✅ Works |
| v2.1.112 | Node.js (bundled JS) | ✅ Works |
| v2.1.113 | Bun (native binary) | ❌ Fails |
| v2.1.169 | Bun (native binary) | ❌ Fails (latest) |
v2.1.113 changelog: "Changed the CLI to spawn a native Claude Code binary (via a per-platform optional dependency) instead of bundled JavaScript"
Related Issue
- #55760 — Same architectural pattern: OAuth initiation path was missing
fetchFn, fixed in v2.1.133. This issue is the same gap but forresourceMetadataUrl.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗