Skip to content

fix(client): don't send client_id in token body under client_secret_b… - #3175

Draft
manjunathbhaskar wants to merge 2 commits into
modelcontextprotocol:mainfrom
manjunathbhaskar:fix/basic-auth-client-id-body
Draft

fix(client): don't send client_id in token body under client_secret_b…#3175
manjunathbhaskar wants to merge 2 commits into
modelcontextprotocol:mainfrom
manjunathbhaskar:fix/basic-auth-client-id-body

Conversation

@manjunathbhaskar

@manjunathbhaskarmanjunathbhaskar commented Jul 26, 2026

Copy link
Copy Markdown

RFC 6749 section 2.3 requires that with HTTP Basic auth, client credentials must not also appear in the request body. prepare_token_auth stripped client_secret from the body for the client_secret_basic branch but left client_id in, so strict token endpoints (Keycloak, Okta in strict mode, and the RFC 6749 compliance test suite) reject the request as presenting two authentication methods at once.

Fixes#3138

Two existing tests asserted the old behavior explicitly, updated both to assert the corrected one, and added the same check to the refresh token test for symmetry.

Server-side companion fix

The client-only change above breaks this SDK's own reference auth server: ClientAuthenticator.authenticate_request() required client_id in the form body unconditionally, and the three token request models (AuthorizationCodeRequest, RefreshTokenRequest, JwtBearerRequest) had the same assumption baked in as a required field. A client that stops sending client_id in the body got rejected by our own server with invalid_client: Missing client_id — a regression, not a fix, without this half.

Fixed both:

  • authenticate_request() decodes the Basic header up front and falls back to the client_id embedded there when the body omits it (still cross-checks against a body-supplied one if present).
  • client_id is now optional on the three token request models; handle() backfills it from the already-authenticated client before the code/token ownership checks that rely on it.

How Has This Been Tested?

  • tests/interaction/auth/test_authorize_token.py::test_a_client_with_a_secret_authenticates_the_token_request_with_http_basic — the integration test that exercises client and server together, fails without the server-side fix, passes with it.
  • Full suite: uv run pytest — 5580 passed, 0 failed, 10 skipped, 1 xfailed.
  • ruff check, ruff format --check, mypy clean on touched files.

Breaking Changes

None for callers. client_id is optional on the token request models now but always resolved (body or Basic header) before use.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Two other independent attempts at the client-only half (#3159, #3160) hit this same server-side incompatibility in CI and were closed without a fix — this PR includes the missing half.

…et_basic
RFC 6749 section 2.3 says client credentials should not be in the request
body when they are already in the Authorization header. The basic auth
branch stripped client_secret but left client_id, so strict token endpoints
(Keycloak, Okta in strict mode) reject the request as presenting two auth
methods at once. Drop client_id too, and flip the two basic auth tests plus
the refresh test that asserted the old behavior.
Fixesmodelcontextprotocol#3138
@manjunathbhaskar
manjunathbhaskarforce-pushed the fix/basic-auth-client-id-body branch from 6a5cbe6 to 461546bCompareAugust 8, 2026 23:38
authenticate_request() required client_id in the form body unconditionally,
even for client_secret_basic clients that already present it via the
Authorization header. Stripping client_id from the body for Basic auth
(the actual fix here) is correct per RFC 6749 section 2.3, but broke our
own server: it had nothing to look the client up by.
Decode the Basic header up front and fall back to it when the body
doesn't have client_id. The three token request models (authorization_code,
refresh_token, jwt-bearer) had the same body-required assumption baked in;
client_id is now optional there too, backfilled from the already-
authenticated client before the code/token ownership checks that rely on it.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OAuth client sends client_id in token body under client_secret_basic (strict servers reject as multiple auth methods)

1 participant

@manjunathbhaskar