Skip to content

Commit 461546b

Browse files
fix(client): do not put client_id in the token body under client_secret_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. Fixes #3138
1 parent a4f4ccd commit 461546b

2 files changed

Lines changed: 9 additions & 5 deletions

File tree

src/mcp/client/auth/oauth2.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,11 @@ def prepare_token_auth(
262262
credentials = f"{encoded_id}:{encoded_secret}"
263263
encoded_credentials = base64.b64encode(credentials.encode()).decode()
264264
headers["Authorization"] = f"Basic {encoded_credentials}"
265-
# Don't include client_secret in body for basic auth
266-
data = {k: v for k, v in data.items() if k != "client_secret"}
265+
# RFC 6749 section 2.3: with HTTP Basic auth the client credentials
266+
# must not also appear in the body. Some strict token endpoints
267+
# reject a request that carries both the Authorization header and
268+
# client_id in the body as two auth methods at once.
269+
data = {k: v for k, v in data.items() if k not in ("client_secret", "client_id")}
267270
elif auth_method == "client_secret_post" and self.client_info.client_secret:
268271
# Include client_id and client_secret in request body (RFC 6749 §2.3.1)
269272
data["client_id"] = self.client_info.client_id

tests/client/test_auth.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -676,10 +676,10 @@ async def test_basic_auth_token_exchange(self, oauth_provider: OAuthClientProvid
676676
assert unquote(client_id) == client_id_raw
677677
assert unquote(client_secret) == client_secret_raw
678678

679-
# client_secret should NOT be in body for basic auth
679+
# Neither credential belongs in the body for basic auth (RFC 6749 2.3)
680680
content = request.content.decode()
681681
assert "client_secret=" not in content
682-
assert "client_id=test%40client" in content # client_id still in body
682+
assert "client_id=" not in content
683683

684684
@pytest.mark.anyio
685685
async def test_basic_auth_refresh_token(self, oauth_provider: OAuthClientProvider, valid_tokens: OAuthToken):
@@ -712,9 +712,10 @@ async def test_basic_auth_refresh_token(self, oauth_provider: OAuthClientProvide
712712
decoded = base64.b64decode(encoded_creds).decode()
713713
assert decoded == f"{client_id}:{client_secret}"
714714

715-
# client_secret should NOT be in body
715+
# Neither credential belongs in the body for basic auth (RFC 6749 2.3)
716716
content = request.content.decode()
717717
assert "client_secret=" not in content
718+
assert "client_id=" not in content
718719

719720
@pytest.mark.anyio
720721
async def test_none_auth_method(self, oauth_provider: OAuthClientProvider):

0 commit comments

Comments
 (0)