Skip to content

Server ClientAuthenticator requires client_id in token body, rejecting valid client_secret_basic requests (RFC 6749 §2.3.1) #3545

Description

@andrewmaturo

Summary

The server-side ClientAuthenticator reads client_id only 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).

Location

src/mcp/server/auth/middleware/client_auth.py, authenticate_request:

form_data = await request.form()
client_id = form_data.get("client_id")
if not client_id:
    raise AuthenticationError("Missing client_id")
...
if client.token_endpoint_auth_method == "client_secret_basic":
    ...
    basic_client_id, request_client_secret = decoded.split(":", 1)
    basic_client_id = unquote(basic_client_id)
    if basic_client_id != client_id:   # only cross-checks; never used as fallback
        raise AuthenticationError("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

  1. Register a client with token_endpoint_auth_method=client_secret_basic.
  2. POST /token with grant_type=authorization_code, Authorization: Basic base64(client_id:client_secret), and no client_id/client_secret in the form body.
  3. Response: 401 {"error":"invalid_client","error_description":"Missing client_id"}.

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.

Observed on mcp 1.27.0.

Activity

  1. added
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Sep 18, 2026
  2. 0xamlab commented on Sep 19, 2026

    @0xamlab

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

  3. AlmogCohen commented on Sep 23, 2026

    @AlmogCohen

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions