[BUG] v2.1.113 regression: OAuth initiation ignores `resource_metadata` from `WWW-Authenticate` header, breaking RFC 9728 discovery

Open 💬 1 comment Opened Jun 9, 2026 by nannan00

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

  1. 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/mcp returns 401 with:

``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_metadata URL 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/mcp and /.well-known/oauth-protected-resource are not served (spec-compliant: WWW-Authenticate header is the primary mechanism)
  1. Configure in ~/.claude.json:

``json
{
"mcpServers": {
"my-server": {
"type": "http",
"url": "https://mcp.example.com/v2/server/mcp"
}
}
}
``

  1. Run Claude Code v2.1.112 → Extracts resource_metadata from header → discovers AS → OAuth succeeds ✅
  2. 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 for resourceMetadataUrl.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗