5223c4771d
## Summary Three small spec-compliance fixes called out in an audit against the [MCP authorization spec (draft)](https://modelcontextprotocol.io/specification/draft/basic/authorization) and RFC 9728 / RFC 9207. ### 1. Split Protected Resource Metadata by path (RFC 9728 §3.2) > The `resource` value returned MUST be identical to the protected resource's resource identifier value into which the well-known URI path suffix was inserted. Today a single handler serves both \`/.well-known/oauth-protected-resource\` and \`/.well-known/oauth-protected-resource/mcp\` and returns \`resource: <origin>/mcp\` from both. That's wrong for the root form — per RFC 9728 the root URL corresponds to the **origin as resource**, and only the \`/mcp\`-suffixed URL corresponds to \`<origin>/mcp\`. After this PR: | Request | `resource` field | |---|---| | `GET /.well-known/oauth-protected-resource` | `https://<host>` | | `GET /.well-known/oauth-protected-resource/mcp` | `https://<host>/mcp` | Both still return the same `authorization_servers`, `scopes_supported`, and `bearer_methods_supported`. Claude's current flow happens to work because our WWW-Authenticate points at the root form and Claude compares `resource` against what it connected to. Strict clients probing the path-aware URL first were rejecting us. ### 2. Advertise `authorization_response_iss_parameter_supported: true` (RFC 9207) Defense against OAuth mix-up attacks. Required by the [OAuth 2.1 security BCP](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1). Signals that clients receiving an authorization response will find the issuer in the `iss` parameter and can validate it. ### 3. Fix `WWW-Authenticate` challenge: point at path-aware PRM URL, add `scope` param - Was: `Bearer resource_metadata=\"https://<host>/.well-known/oauth-protected-resource\"` - Now: `Bearer resource_metadata=\"https://<host>/.well-known/oauth-protected-resource/mcp\", scope=\"api profile\"` After change (1), only the path-aware URL returns a PRM document whose `resource` matches what the MCP client connected to (\`<host>/mcp\`). Pointing clients at the right URL keeps discovery consistent. The `scope` parameter is a SHOULD in RFC 6750 and lets clients ask for least-privilege scopes on first authorization. ## Not in this PR (queued separately) From the same audit: - **Audit JWT `aud` (audience) validation** — the spec requires the server to reject tokens whose audience doesn't match this resource. Need a read-only code review to confirm; filing as a follow-up. - **Audit PKCE enforcement** — we advertise `code_challenge_methods_supported: [\"S256\"]`; need to confirm the \`/authorize\` flow actually rejects requests missing `code_challenge`. - **403 `insufficient_scope` challenge format** for step-up auth. - **CIMD (Client ID Metadata Documents)** support — newer spec alternative to DCR. ## Test plan - [x] \`yarn jest --testPathPatterns=\"mcp-auth.guard|oauth-discovery.controller\"\` → 4/4 passing - [x] \`tsc --noEmit\` clean on touched files - [ ] After deploy: \`\`\`bash curl -s https://<host>/.well-known/oauth-protected-resource | jq .resource # expect: \"https://<host>\" curl -s https://<host>/.well-known/oauth-protected-resource/mcp | jq .resource # expect: \"https://<host>/mcp\" curl -sI -X POST https://<host>/mcp | grep -i www-authenticate # expect: Bearer resource_metadata=\"…/oauth-protected-resource/mcp\", scope=\"api profile\" \`\`\` ## Related - #19836 — CORS exposes `WWW-Authenticate` + `MCP-Protocol-Version` so browser clients can read them. Pairs with this PR. - #19755 / #19766 / #19824 — the earlier chain that got host-aware discovery and \`TRUST_PROXY\` working. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>