Skip to content

Commit 9e6feff

Browse files
shigechikaclaude
andauthored
feat(oauth): select token_endpoint_auth_method from AS metadata (RFC 8414) (#62)
* feat(oauth): select token_endpoint_auth_method from AS metadata (RFC 8414) Read `token_endpoint_auth_methods_supported` from RFC 8414 Authorization Server Metadata and pick the best supported auth method for the token endpoint (preference: none → client_secret_post → client_secret_basic). Apply the selected method consistently across DCR, code exchange, token refresh, and Device Authorization Grant polling. RFC 6749 §2.3.1 `client_secret_basic` sends credentials as `Authorization: Basic base64(percent_encode(id):percent_encode(secret))` instead of in the request body. The selected method is stored in `TokenData.token_endpoint_auth_method` so refresh reuses it without re-reading the discovery document. Fixes compatibility with Microsoft Entra ID v2 and other enterprise OIDC providers that publish only `client_secret_basic` in `token_endpoint_auth_methods_supported` and reject `none`/body-credential requests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(oauth): add device authorization Step 1 Basic auth test Verify that client_secret_basic sends credentials in the Authorization header (not the request body) for the device authorization request itself (Step 1, RFC 8628 §3.1), not just the token polling loop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1bb9ee5 commit 9e6feff

6 files changed

Lines changed: 507 additions & 33 deletions

File tree

README.ja.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ Bearer token、カスタムヘッダー、OAuth 2.1 認証情報をリモート
4040
- §3.4–3.5 `authorization_pending` / `slow_down`(interval +=5 s)/ `expired_token` / `access_denied` ハンドリング
4141
- DCR の `grant_types``urn:ietf:params:oauth:grant-type:device_code` を登録(RFC 7591 §2)
4242
- [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591) Dynamic Client Registration
43-
- §3 クライアント登録リクエスト(公開クライアント、`token_endpoint_auth_method: none`
43+
- §3 クライアント登録リクエスト。AS メタデータの `token_endpoint_auth_methods_supported` から最適な認証方式を選択(`none``client_secret_post``client_secret_basic` の優先順
4444
- §3.2.1 `client_secret_expires_at` に対応、期限切れ時に自動再登録
45+
- [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749) OAuth 2.0
46+
- §2.3.1 `client_secret_basic`:percent-encode した認証情報を `Authorization: Basic` ヘッダーで送信(コード交換・トークンリフレッシュ・Device Authorization Grant ポーリングに適用)
4547
- [RFC 6750](https://www.rfc-editor.org/rfc/rfc6750) Bearer Token の利用
4648
- §2.1 `Authorization: Bearer <token>` リクエストヘッダー
4749
- **バックオフ付きリトライ** — 接続エラー時に最大3回リトライ

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ Bearer tokens, custom headers, and OAuth 2.1 credentials are forwarded to the re
4242
- §3.4–3.5 token polling with `authorization_pending` / `slow_down` (interval +=5 s) / `expired_token` / `access_denied` handling
4343
- DCR registers `urn:ietf:params:oauth:grant-type:device_code` in `grant_types` (RFC 7591 §2)
4444
- [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591) Dynamic Client Registration
45-
- §3 client registration request (public client with `token_endpoint_auth_method: none`)
45+
- §3 client registration request; `token_endpoint_auth_method` chosen from `token_endpoint_auth_methods_supported` in AS metadata (prefers `none``client_secret_post``client_secret_basic`)
4646
- §3.2.1 `client_secret_expires_at` handling — auto re-register on expiry
47+
- [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749) OAuth 2.0
48+
- §2.3.1 `client_secret_basic`: `Authorization: Basic` header with percent-encoded credentials (applied to code exchange, token refresh, and Device Authorization Grant polling)
4749
- [RFC 6750](https://www.rfc-editor.org/rfc/rfc6750) Bearer Token usage
4850
- §2.1 `Authorization: Bearer <token>` request header
4951
- **Retry with backoff** — retries up to 3 times on connection errors

WORKAROUNDS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ Works around known issues in Claude Code's HTTP transport:
3535
- **Cannot connect to servers that only support static bearer tokens** — mcp-remote always initiates an OAuth discovery handshake before any tool call; servers that authenticate via static bearer tokens (e.g. [Zabbix MCP Server](https://github.com/initMAX/zabbix-mcp-server)) respond with 404 on `/.well-known/oauth-authorization-server` and the proxy gives up before the bearer-auth path is ever reached ([zabbix-mcp-server#36](https://github.com/initMAX/zabbix-mcp-server/issues/36)); mcp-stdio connects directly with `--bearer-token YOUR_TOKEN http://your-server:8080/mcp`, skipping OAuth entirely.
3636
- **OAuth token exchange fails with URL-encoded responses** — TypeScript SDK's token exchange assumes the response is always JSON; servers that return `application/x-www-form-urlencoded` (e.g. GitHub OAuth) cause a JSON parse error, blocking authentication ([typescript-sdk#759](https://github.com/modelcontextprotocol/typescript-sdk/issues/759)); mcp-stdio's `_parse_token_response()` checks the `Content-Type` header and parses `application/x-www-form-urlencoded` via `urllib.parse.parse_qs`, so GitHub MCP and similar servers work without extra configuration.
3737
- **No proactive token refresh window** — mcp-remote (and `adaptOAuthProvider` in TypeScript SDK) only refreshes the access token after a 401 has already fired, with no early-refresh leeway. ASes that issue refresh tokens whose lifetime is barely longer than the access token's leave no margin for clock skew, so a refresh attempt can race the token expiry and fail ([mcp-remote#252](https://github.com/geelen/mcp-remote/issues/252), [typescript-sdk#1954](https://github.com/modelcontextprotocol/typescript-sdk/issues/1954)). mcp-stdio's `ensure_token()` performs proactive refresh: a cached access token is treated as expired when its expiry is within `--oauth-refresh-leeway` seconds (default 60, configurable via flag or `MCP_OAUTH_REFRESH_LEEWAY`), so the refresh hits well before the AS revokes the token.
38+
- **Session affinity cookies not forwarded across requests** — mcp-remote creates a new HTTP client per request, so load-balancer session cookies (e.g. `AWSALB`, `AWSALBCORS`) returned in a `Set-Cookie` response header are discarded; subsequent requests land on a different backend node, breaking server-side session state ([mcp-remote#168](https://github.com/geelen/mcp-remote/issues/168)). mcp-stdio reuses a single `httpx.Client` instance across all requests within a session; httpx automatically stores `Set-Cookie` values and re-sends them on subsequent requests to the same origin, so ALB and similar sticky-session cookies are forwarded transparently without any extra configuration.
3839
- **No OAuth support in headless/SSH environments** — mcp-remote's OAuth flow requires opening a browser window, making it unusable in SSH sessions, CI/CD pipelines, or other browserless environments; there is no Device Authorization Grant support ([mcp-remote#228](https://github.com/geelen/mcp-remote/issues/228)). mcp-stdio supports RFC 8628 Device Authorization Grant via `--oauth-device`: it displays a short user code and verification URI on stderr so the user can authenticate from any browser, while the device polls the token endpoint in the background.
3940
- **`resource` indicator gets a trailing slash appended** — TypeScript SDK normalises the resource URL via `new URL(...).href`, converting `https://api.example.com` to `https://api.example.com/`; Atlassian authv2 and similar servers reject this with `InvalidTargetError: Incorrect resource parameters` ([typescript-sdk#1968](https://github.com/modelcontextprotocol/typescript-sdk/issues/1968), [mcp-remote#261](https://github.com/geelen/mcp-remote/issues/261)); mcp-stdio passes `resource=server_url` verbatim in both code exchange and refresh requests — Python's URL handling does not add trailing slashes.
41+
- **DCR hardcodes `token_endpoint_auth_method: none`, breaking confidential-client servers** — mcp-remote always registers with `token_endpoint_auth_method: none` and sends `client_id`/`client_secret` in the POST body; authorization servers that publish only `client_secret_basic` in `token_endpoint_auth_methods_supported` (e.g. Microsoft Entra ID v2, some enterprise OIDC providers) reject the resulting token request ([mcp-remote#184](https://github.com/geelen/mcp-remote/issues/184), [mcp-remote#217](https://github.com/geelen/mcp-remote/issues/217)); mcp-stdio reads `token_endpoint_auth_methods_supported` from RFC 8414 AS metadata, picks the best supported method (`none` → `client_secret_post` → `client_secret_basic`), registers with that method via DCR, and applies it consistently across code exchange, token refresh, and Device Authorization Grant polling — `client_secret_basic` sends credentials as `Authorization: Basic base64(percent_encode(client_id):percent_encode(client_secret))` per RFC 6749 §2.3.1.
4042

4143
## Windows
4244

0 commit comments

Comments
 (0)