anthropics / anthropics/claude-code

[BUG] MCP OAuth: --client-secret and oauth.clientSecret are silently discarded, making confidential-client MCP servers impossible to authenticate

Đang mở
#91,199 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
area:auth area:mcp bug has repro platform:macos
Ngôn ngữ chính
Python
Star
145k
Fork
23.1k
Chỉ số merge pull request
Chỉ số pull request đang chờ

Mô tả

## Summary

`claude mcp add --client-secret` is accepted by the CLI but the secret is **persisted nowhere**, and a hand-written `oauth.clientSecret` in `~/.claude.json` is ignored. As a result an MCP server whose authorization server requires a **confidential client** can never complete the OAuth token exchange — `/mcp` shows the authorization server's error (`client_secret is missing.`) indefinitely.

Reproduced against `https://chatmcp.googleapis.com/mcp/v1` (Google Chat MCP), whose authorization server is `accounts.google.com`. Google advertises `token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"]` and **not** `none`, so a PKCE-only public-client exchange is rejected — a client secret is mandatory for Desktop and Web OAuth client types.

## Environment

- Claude Code **2.1.252**
- macOS (Darwin 25.6.0), Apple Silicon
- Transport: `http`
- Credential store: macOS Keychain, generic password, service `Claude Code-credentials`

## Steps to reproduce

The bug reproduces with a throwaway server name and fake credentials — no real secret needed:

```bash
export MCP_CLIENT_SECRET='probe-secret-value-12345'
claude mcp add --transport http \
--client-id 'probe-client-id' \
--client-secret \
zz-probe https://example.invalid/mcp -s user
```

Then inspect both persistence locations:

```bash
# 1. the config
python3 -c "import json;print(json.load(open('$HOME/.claude.json'))['mcpServers']['zz-probe'])"

# 2. the keychain
security find-generic-password -s "Claude Code-credentials" -w \
| python3 -c "import sys,json;print(json.load(sys.stdin)['mcpOAuth'])"
```

### Expected

The client secret is stored somewhere retrievable, and included in the token request as `client_secret_post` / `client_secret_basic`.

### Actual

```json
{"type": "http", "url": "https://example.invalid/mcp", "oauth": {"clientId": "probe-client-id"}}
```

- `~/.claude.json` → `oauth` contains **`clientId` only**. No `clientSecret`.
- Keychain → **no entry at all** for the server.

The secret is silently dropped. `--client-secret` produces no warning and no error.

> Tested via the `MCP_CLIENT_SECRET` environment variable. I did not test the interactive prompt path, which may behave differently.

## The config workaround is also ignored

Writing the keys directly into `~/.claude.json` does not work either:

```json
"google-chat": {
"type": "http",
"url": "https://chatmcp.googleapis.com/mcp/v1",
"oauth": {
"clientId": ".apps.googleusercontent.com",
"clientSecret": "<35-char secret>",
"authorizationServer": ["https://accounts.google.com/", "https://accounts.google.com"]
}
}
```

The effective state lives in the keychain under `mcpOAuth`, and it is re-derived from the config with the secret dropped. Observed directly: I removed the cached entry, and a running session recreated it within seconds carrying `clientId` (picked up from the config) and **still no `clientSecret`**:

```json
"google-chat|": {
"serverName": "google-chat",
"serverUrl": "https://chatmcp.googleapis.com/mcp/v1",
"accessToken": "",
"clientId": ".apps.googleusercontent.com",
"discoveryState": {
"authorizationServerUrl": "https://accounts.google.com/",
"resourceMetadataUrl": "https://chatmcp.googleapis.com/.well-known/oauth-protected-resource/mcp",
"oauthMetadataFound": true
}
}
```

The keychain schema clearly supports the field — an entry for another server created via **dynamic client registration** does contain `clientSecret`:

```
keys present: ['accessToken', 'clientId', 'clientSecret', 'discoveryState', 'redirectUri', 'serverName', 'serverUrl']
```

So the storage exists; only the path from user-supplied configuration into it is missing. Servers that support DCR work; servers that require a pre-registered confidential client cannot be configured.

## Proof that the credential itself is valid

To rule out a bad secret, I probed Google's token endpoint directly with a deliberately invalid authorization code. Only the client-authentication outcome matters:

| client auth sent | Google's response |
|---|---|
| `client_id` only | `400 invalid_request` — **`client_secret is missing.`** ← identical to the `/mcp` error |
| `client_id` + the configured secret | `400 invalid_grant` — `Malformed auth code.` (client auth **accepted**) |
| `client_id` + a wrong secret | `401 invalid_client` — `The provided client secret is invalid.` |

Row 2 confirms the secret is correct and would be accepted. Row 1 is what Claude Code actually sends.

## Secondary observations

1. **Stale `resourceMetadataUrl`.** The cached `discoveryState` records `.../.well-known/oauth-protected-resource/mcp`, while the server's live metadata is served at `.../.well-known/oauth-protected-resource/mcp/v1` (note the `/v1`). The cached value does not appear to be refreshed.
2. **`authorization_servers` trailing slash.** The protected-resource metadata advertises `["https://accounts.google.com/"]` with a trailing slash, while Google's own `issuer` is `https://accounts.google.com` without one. If the config's `authorizationServer` is matched by exact string anywhere, that mismatch is a trap; I supplied both forms to be safe.
3. **Long-running sessions overwrite the credential store.** With several `claude` processes running, edits to `~/.claude.json` or to the keychain are reverted from a live session's in-memory state within seconds. This makes any manual repair unreliable unless every session is closed first, and it may explain reports of MCP auth settings "resetting on their own".

## Impact

Any MCP server whose authorization server mandates client authentication — which includes anything fronted by Google OAuth using a Desktop or Web client type — cannot be authenticated at all. There is no supported configuration and no working workaround on this version. The CLI advertising `--client-secret` while discarding the value makes this hard to diagnose: the user sees only the authorization server's `client_secret is missing.` and reasonably concludes their own credential is wrong.

## Suggested fix

Persist `--client-secret` / `oauth.clientSecret` into the same keychain `mcpOAuth` entry that DCR already populates, and include it in the token request using the method the authorization server advertises in `token_endpoint_auth_methods_supported`. Failing that, at minimum warn when `--client-secret` is supplied but not retained, rather than dropping it silently.

Hướng dẫn đóng góp

Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này

Hướng nghiên cứu

Start by tracing `claude mcp add --client-secret` and `oauth.clientSecret` through configuration persistence, the `mcpOAuth` keychain entry, and the OAuth token exchange. Verify that the secret survives into stored credentials and is sent using an advertised client-authentication method, while preserving existing dynamic client registration behavior.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python
Lĩnh vực
api, authentication, cli
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Sôi nổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
45/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.