fix(oauth): honor token_endpoint_auth_method: "none" for DCR public clients (PKCE-only)#909
Open
ding-modding wants to merge 1 commit into
Open
Conversation
…ients
RFC 7591 §2 specifies that clients registering via Dynamic Client
Registration with token_endpoint_auth_method "none" are public clients
and MUST NOT receive a client_secret. PKCE alone authenticates the
authorization code exchange.
Before this fix, registerClient unconditionally generated a client_secret
and stored its hash in oauth_clients.client_secret_hash. The MCP SDK's
clientAuth middleware (server/auth/middleware/clientAuth.js) inspects
the stored client and, when client.client_secret is truthy, requires
client_secret_post auth at /token. PKCE-only clients like Claude Code
and Cursor (both register with token_endpoint_auth_method: "none") sent
PKCE code_verifier with no secret, hit "Client secret is required", and
the SDK's client-side OAuth flow then surfaced "Existing OAuth client
information is required when exchanging an authorization code".
Fix: skip client_secret generation when token_endpoint_auth_method ===
"none", and omit client_secret from the DCR response in that case. The
oauth_clients.client_secret_hash column was already nullable (schema
unchanged). Confidential clients (default, or any non-"none" auth
method) continue to receive a secret exactly as before.
Tests: two new cases in client registration —
- DCR with "none" issues no client_secret; stored secret is falsy
- DCR without explicit auth method defaults to confidential (secret
issued and stored)
Repro of the original bug:
1. Start: gbrain serve --http --enable-dcr --public-url https://...
2. From Claude Code: claude mcp add --transport http gbrain <url>
3. Trigger auth → browser flow completes → token exchange fails with
"Existing OAuth client information is required"
After fix, the same flow lands on a healthy token + green Connected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
registerClient(DCR) unconditionally generates aclient_secreteven when the client requeststoken_endpoint_auth_method: "none". The MCP SDK'sclientAuthmiddleware then requires a secret on every/tokenexchange becauseclient.client_secretis truthy from the stored row — which rejects every valid PKCE-only public-client flow (Claude Code, Cursor, MCP Inspector, …).This PR makes
registerClienthonor"none"per RFC 7591 §2: no secret generated, NULL stored, no secret in the DCR response.Repro
From a public-client MCP host (Claude Code shown — same behavior on Cursor):
Before this PR: the
/tokenexchange fails. The MCP SDK then surfaces:After this PR: the same flow lands on a green ✓ Connected.
Root cause
src/core/oauth-provider.ts:172-194always emits a freshclient_secret:getClientreadsclient_secret_hashback asclient.client_secret. The MCP SDK's middleware (@modelcontextprotocol/sdk/server/auth/middleware/clientAuth.js:19) then gates:Public clients (per RFC 7591) cannot safely store a secret, so they send
client_id+code_verifieronly — and get rejected.Fix
Gate secret generation on
token_endpoint_auth_method:And omit
client_secretfrom the DCR response for public clients. Theoauth_clients.client_secret_hashcolumn was already nullable — no schema migration needed. Confidential-client behavior is unchanged.Tests
Two new cases in
test/oauth.test.tsunderclient registration:DCR with token_endpoint_auth_method "none" issues no client_secret— response has no secret, stored row'sclient_secretis falsyDCR without token_endpoint_auth_method defaults to confidential client— regression guard so the default path still mints + stores a secretSpec references
token_endpoint_auth_methodvalues including"none"Affected clients
Any MCP host that uses DCR with PKCE-only auth — i.e., any public OAuth 2.1 client. Confirmed in the wild for Claude Code and Cursor; the same break-mode applies to MCP Inspector and the reference clients in
@modelcontextprotocol/sdk.Need help on this PR? Tag
@codesmithwith what you need.