Skip to content

Commit e841bba

Browse files
g97iulio1609Copilot
andcommitted
fix(oauth): include client_id in token request body for client_secret_post
Per RFC 6749 §2.3.1, when using client_secret_post authentication, both client_id and client_secret must be included in the request body. Previously, prepare_token_auth() only added client_secret, causing authentication failures with OAuth servers that require client_id in the body for client_secret_post. Fixes #2128 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 62575ed commit e841bba

File tree

2 files changed

+31
-1
lines changed

2 files changed

+31
-1
lines changed

src/mcp/client/auth/oauth2.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,9 @@ def prepare_token_auth(
206206
# Don't include client_secret in body for basic auth
207207
data = {k: v for k, v in data.items() if k != "client_secret"}
208208
elif auth_method == "client_secret_post" and self.client_info.client_secret:
209-
# Include client_secret in request body
209+
# Include client_id and client_secret in request body (RFC 6749 §2.3.1)
210+
if self.client_info.client_id:
211+
data["client_id"] = self.client_info.client_id
210212
data["client_secret"] = self.client_info.client_secret
211213
# For auth_method == "none", don't add any client_secret
212214

tests/client/auth/extensions/test_client_credentials.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,34 @@ async def test_exchange_token_client_credentials(self, mock_storage: MockTokenSt
252252
assert "scope=read write" in content
253253
assert "resource=https://api.example.com/v1/mcp" in content
254254

255+
@pytest.mark.anyio
256+
async def test_exchange_token_client_secret_post_includes_client_id(self, mock_storage: MockTokenStorage):
257+
"""Test that client_secret_post includes both client_id and client_secret in body (RFC 6749 §2.3.1)."""
258+
provider = ClientCredentialsOAuthProvider(
259+
server_url="https://api.example.com/v1/mcp",
260+
storage=mock_storage,
261+
client_id="test-client-id",
262+
client_secret="test-client-secret",
263+
token_endpoint_auth_method="client_secret_post",
264+
scopes="read write",
265+
)
266+
await provider._initialize()
267+
provider.context.oauth_metadata = OAuthMetadata(
268+
issuer=AnyHttpUrl("https://api.example.com"),
269+
authorization_endpoint=AnyHttpUrl("https://api.example.com/authorize"),
270+
token_endpoint=AnyHttpUrl("https://api.example.com/token"),
271+
)
272+
provider.context.protocol_version = "2025-06-18"
273+
274+
request = await provider._perform_authorization()
275+
276+
content = urllib.parse.unquote_plus(request.content.decode())
277+
assert "grant_type=client_credentials" in content
278+
assert "client_id=test-client-id" in content
279+
assert "client_secret=test-client-secret" in content
280+
# Should NOT have Basic auth header
281+
assert "Authorization" not in request.headers
282+
255283
@pytest.mark.anyio
256284
async def test_exchange_token_without_scopes(self, mock_storage: MockTokenStorage):
257285
"""Test token exchange without scopes."""

0 commit comments

Comments
 (0)