anthropics / anthropics/claude-ai-mcp
Custom connector: backend never calls /oauth/token after a valid auth code — reproduced across manual client_id, CIMD, and DCR on one spec-compliant self-hosted AS
- 主要語言
- 沒有語言資料
- 星號
- 471
- 分支
- 76
- PR 合併指標
- 30 天內沒有已合併 PR
描述
## Summary
A spec-compliant self-hosted OAuth 2.1 AS (same origin as the MCP resource server) completes discovery → authorize → consent and hands claude.ai a valid authorization code (correct `state`, registered client, PKCE S256), but **claude.ai's backend never calls the token endpoint**. No `POST /oauth/token` (or any token request on any path) is ever received. The connector then silently loops back to `POST /mcp` and shows "connection failed".
Reproduced **identically on claude.ai Web (desktop) and the Claude iOS app**, and across **three independent client-provisioning mechanisms** (manually-entered client_id, CIMD, and DCR). Because the token exchange is performed server-side by Anthropic's backend, the same code path fails for all surfaces.
This looks like the same underlying defect as #215 (closed), #506 (open), and #518 (open), but with a broader, single-server reproduction that isolates it to the token-exchange step on Anthropic's side.
## Environment
- **MCP server:** self-hosted, single-operator, Node `node:http` + SQLite, on Fly.io (no WAF, no bot challenge, plain fly-proxy). Streamable HTTP at `POST /mcp`.
- **AS:** embedded in the same process/origin (permitted by the MCP auth spec).
- **Clients tested:** claude.ai Web (Chrome, desktop) and Claude iOS app.
- **Discovery (all HTTP 200):**
- `GET /.well-known/oauth-protected-resource/mcp` → `{"resource":"…/mcp","authorization_servers":[""],"scopes_supported":["read","propose"],"bearer_methods_supported":["header"]}`
- `GET /.well-known/oauth-authorization-server` → issuer, `authorization_endpoint`, `token_endpoint`, `revocation_endpoint`, `registration_endpoint` (when DCR on), `response_types_supported:["code"]`, `grant_types_supported:["authorization_code","refresh_token"]`, `code_challenge_methods_supported:["S256"]`, `token_endpoint_auth_methods_supported:["none"]`, `scopes_supported:["read","propose"]`.
- **401 challenge:** `WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource/mcp", scope="read propose"`
## Exact server-side request trace (one clean attempt; server logs every request path)
```
POST /mcp -> 401 challenge
GET /.well-known/oauth-protected-resource/mcp -> 200
GET /.well-known/oauth-authorization-server -> 200
GET /oauth/authorize (response_type=code, client_id=, redirect_uri=https://claude.ai/api/mcp/auth_callback,
code_challenge=<…>, code_challenge_method=S256, state=, scope="read propose",
resource=/mcp) -> 200 consent page
POST /oauth/authorize (operator approves) -> 302 Location: https://claude.ai/api/mcp/auth_callback?code=&state=
(state echoed byte-for-byte identical)
--- and then NOTHING. No POST /oauth/token. No POST /token. No request on any path. ---
POST /mcp -> 401 (connector loops / restarts)
```
The authorization code `` is single-use, PKCE-bound, and (after we widened it to rule out timeouts) valid for **10 minutes**. It is never redeemed.
## What we ruled out (server side is provably correct)
- **State:** round-trips byte-for-byte (logged in and out).
- **redirect_uri:** exact-match `https://claude.ai/api/mcp/auth_callback`, validated at authorize and bound to the code.
- **PKCE:** S256 advertised and enforced; verified end-to-end by our own test suite (a real MCP TypeScript SDK client completes authorize→consent→token→MCP against this exact code).
- **Transport:** HTTP/2 and HTTP/1.1 behave identically; content types carry `charset=utf-8`; token/error bodies are RFC 6749 JSON; discovery docs carry permissive CORS. (Independently re-probed.)
- **Reachability:** Anthropic's backend demonstrably reaches this host — it performs `POST /mcp` and the discovery GETs server-side seconds earlier. So `/oauth/token` is reachable; it is simply never called.
- **Cold start / timeout:** machine pinned warm, ~0.25s responses; not a factor.
- **Path-derivation (#82/#313):** claude.ai used our *advertised* endpoints exactly; no requests to derived root paths.
## The key finding: it fails across ALL three provisioning mechanisms
| Client provisioning | Discovery | authorize | consent | code delivered | `/oauth/token` called? |
|---|---|---|---|---|---|
| Manual client_id (Advanced settings) | ✅ | ✅ | ✅ | ✅ | **Once, ~20 min late, only replaying already-expired codes** |
| CIMD (`client_id` = `https://claude.ai/oauth/mcp-oauth-client-metadata`) | ✅ | ✅ | ✅ | ✅ | **Never** (7+ consents) |
| DCR (RFC 7591; client registered `oac_…`) | ✅ (+`POST /oauth/register` 201) | ✅ | ✅ | ✅ | **Never** (5+ registrations + consents) |
For a *registered* client (manual or DCR) the backend at least eventually attempted an exchange in one early instance, but only by retrying long-dead codes ~20 minutes after consent. For CIMD it never attempted an exchange at all.
## Two additional findings that may help
1. **claude.ai's own CIMD document is unfetchable server-side.** With CIMD, our AS must fetch `https://claude.ai/oauth/mcp-oauth-client-metadata` to learn the client's `redirect_uris`. That URL returns **HTTP 200 from a residential IP but HTTP 403 from our datacenter (Fly) egress under every `User-Agent` we tried.** So a self-hosted AS on a cloud host cannot resolve claude.ai's CIMD document at all — CIMD registration is effectively unusable for cloud-hosted MCP servers unless that CDN rule is relaxed. (We worked around it with a pinned allowlist, but the exchange still never happened.)
2. **Correlation with the RFC 9207 `iss` authorization-response parameter.** The single instance where the backend attempted a token exchange used redirects **without** `iss`. Every flow after we added `iss` (per RFC 9207) was never exchanged. Removing `iss` again did not, by itself, fix it — but the correlation is worth checking in case the callback rejects an unexpected/`iss`-bearing response instead of ignoring it as RFC 9207 requires.
## Related issues
- #215 (closed) — identical symptom: server issues code, claude.ai never calls `/token`.
- #506 (open) — Entra ID: callback succeeds, code never exchanged.
- #518 (open) — WorkOS AuthKit: never exchanges the code; same server works via ChatGPT/Claude Code/curl.
- #438, #429 (closed) — adjacent "handshake completes, runtime sends nothing" reports.
## What would help
1. Confirmation of where claude.ai's backend performs the code→token exchange, and any server-side error/telemetry it records when it receives a valid authorization-code redirect but does not proceed (an `ofid_…` reference for one of our attempts would let us correlate).
2. Whether a manually-entered client_id is a currently-supported provisioning path for custom connectors, or whether DCR/CIMD is required.
3. Relaxing the CDN 403 on server-side fetches of `https://claude.ai/oauth/mcp-oauth-client-metadata` so cloud-hosted AS implementations can use CIMD.
Happy to provide the live discovery endpoints, a timestamped request trace, or run a targeted repro on request. This is a personal single-operator server; the OAuth surface is password-gated so it cannot be connected without our credentials, but the read-only discovery documents can be shared for inspection.
貢獻指南
這個儲存庫沒有索引到貢獻指南
評估
這個 Issue 還沒有評估資料。