You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Server ClientAuthenticator requires client_id in token body, rejecting valid client_secret_basic requests (RFC 6749 §2.3.1) #3545
The server-side ClientAuthenticator reads client_idonly from the token request form body and raises invalid_client: "Missing client_id" when it is absent — even when the client authenticated correctly via HTTP Basic (Authorization: Basic base64(client_id:client_secret)). Per RFC 6749 §2.3.1, a client using client_secret_basic sends its client_id and client_secret in the Authorization header and MAY omit them from the body. Servers advertising client_secret_basic in token_endpoint_auth_methods_supported therefore reject spec-compliant clients (e.g. clients that use Basic and do not duplicate client_id into the body).
form_data=awaitrequest.form()
client_id=form_data.get("client_id")
ifnotclient_id:
raiseAuthenticationError("Missing client_id")
...
ifclient.token_endpoint_auth_method=="client_secret_basic":
...
basic_client_id, request_client_secret=decoded.split(":", 1)
basic_client_id=unquote(basic_client_id)
ifbasic_client_id!=client_id: # only cross-checks; never used as fallbackraiseAuthenticationError("Client ID mismatch in Basic auth")
client_id from the Basic header is only used to cross-check a body-supplied value; it is never used as a fallback source. So a request with credentials solely in the Basic header fails before the client is even looked up.
Steps to reproduce
Register a client with token_endpoint_auth_method=client_secret_basic.
POST /token with grant_type=authorization_code, Authorization: Basic base64(client_id:client_secret), and noclient_id/client_secret in the form body.
Sending the same credentials via client_secret_post (in the body) works.
Expected
When Authorization: Basic is present, the authenticator should derive client_id from the header if it is absent from the body (RFC 6749 §2.3.1), then verify the secret as it does today.
Notes
This is the mirror image of client-side PR #3536 (which stops MCP clients from putting client_id in the body under client_secret_basic). With that client-side change, compliant clients will send client_id only in the Basic header — which this server-side code rejects. The two need to agree.
Confirmed on current main: authenticate_request raises Missing client_id before the Basic header is ever parsed, so a client_secret_basic client that sends credentials only in the Authorization header — which #3536 now makes MCP clients do — is rejected, contrary to RFC 6749 §2.3.1. Proposed fix in src/mcp/server/auth/middleware/client_auth.py: parse the Basic header up front (leniently, so a stray or malformed header still only errors for clients actually registered with client_secret_basic), fall back to the Basic-derived client_id when the form body omits it, keep the existing body-vs-header mismatch check and secret verification unchanged, and keep returning Missing client_id when neither source provides one (preserving the current 401 contract). Tests: add a /token integration test next to test_client_secret_basic_authentication proving header-only credentials succeed; the existing tests already cover the body path and the mismatch failure. Since this is labeled v1+v2, I'd target main per CONTRIBUTING and can also prepare a v1.x backport if you'd like one. If an outside PR is welcome for this, please assign the issue to me and I'll open it immediately. (Disclosure: contributing on behalf of thyn-ai, with AI assistance; I can explain and defend every line of the change.)
Another real-world client hitting this: Vercel Connect (managed MCP connectors). It registers by DCR, picks client_secret_basic from the server's advertised token_endpoint_auth_methods_supported, and, per RFC 6749 §2.3.1, sends the client id only in the Authorization: Basic header. ClientAuthenticator rejects the authorization-code exchange with invalid_client: Missing client_id before it reads the header. Reproduced on mcp 1.28.0 (via FastMCP 3.x) and still present in the client_auth.py of 2.2.0.
We worked around it by making CIMD registration work on our server, so Connect registers as a public client (none + PKCE) and never takes the Basic path. But any DCR client that follows the RFC hits this, while the server advertises client_secret_basic as supported.
Activity
0xamlab commentedon Sep 19, 2026
Confirmed on current
main:authenticate_requestraisesMissing client_idbefore the Basic header is ever parsed, so aclient_secret_basicclient that sends credentials only in theAuthorizationheader — which #3536 now makes MCP clients do — is rejected, contrary to RFC 6749 §2.3.1. Proposed fix insrc/mcp/server/auth/middleware/client_auth.py: parse theBasicheader up front (leniently, so a stray or malformed header still only errors for clients actually registered withclient_secret_basic), fall back to the Basic-derivedclient_idwhen the form body omits it, keep the existing body-vs-header mismatch check and secret verification unchanged, and keep returningMissing client_idwhen neither source provides one (preserving the current 401 contract). Tests: add a/tokenintegration test next totest_client_secret_basic_authenticationproving header-only credentials succeed; the existing tests already cover the body path and the mismatch failure. Since this is labeledv1+v2, I'd targetmainper CONTRIBUTING and can also prepare av1.xbackport if you'd like one. If an outside PR is welcome for this, please assign the issue to me and I'll open it immediately. (Disclosure: contributing on behalf of thyn-ai, with AI assistance; I can explain and defend every line of the change.)AlmogCohen commentedon Sep 23, 2026
Another real-world client hitting this: Vercel Connect (managed MCP connectors). It registers by DCR, picks
client_secret_basicfrom the server's advertisedtoken_endpoint_auth_methods_supported, and, per RFC 6749 §2.3.1, sends the client id only in theAuthorization: Basicheader.ClientAuthenticatorrejects the authorization-code exchange withinvalid_client: Missing client_idbefore it reads the header. Reproduced onmcp1.28.0 (via FastMCP 3.x) and still present in theclient_auth.pyof 2.2.0.We worked around it by making CIMD registration work on our server, so Connect registers as a public client (
none+ PKCE) and never takes the Basic path. But any DCR client that follows the RFC hits this, while the server advertisesclient_secret_basicas supported.