anthropics / anthropics/claude-code

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

Ouverte
#91,199 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub
area:auth area:mcp bug has repro platform:macos
Langage dominant
Python
Étoiles
145k
Forks
23.1k
Métriques de merge des PR
Métriques de PR en attente

Description

## 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.

Guide de contribution

Aucun guide de contribution indexé pour ce dépôt

Piste de recherche

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.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
python
Domaine
api, authentication, cli
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
Active
Clarté
Plutôt claire
Accessibilité débutants
45/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.