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

未關閉
#540 5 則留言 1 個 reaction 已指派 0 人 在 GitHub 檢視
主要語言
沒有語言資料
星號
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 還沒有評估資料。

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。