From fc762dff2636f0cda65be4cbe8fb8760e310492d Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:38:02 +0530 Subject: [PATCH] fix: respect token_endpoint_auth_method=none when a client secret is stored A client that registers with token_endpoint_auth_method="none" is a public client, so ClientAuthenticator sets request_client_secret to None for it. The stored-secret check that follows did not look at the auth method, so a secret left on the client record by an earlier confidential registration made the token request fail with "Client secret is required". RFC 6749 section 2.1 says the authorization server should not make assumptions about the client type. Honouring a leftover secret over the client's own declaration does exactly that, so skip the check when the method is "none". The `and client.client_secret` half of the condition is redundant against the misconfiguration guard above it, but it keeps the type narrowing that the compare_digest call below relies on. Fixes #1842 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01H53v9BN6gzMtKyAGAgdbDb --- src/mcp/server/auth/middleware/client_auth.py | 7 ++- .../mcpserver/auth/test_auth_integration.py | 56 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/auth/middleware/client_auth.py b/src/mcp/server/auth/middleware/client_auth.py index 3d5067d611..b54e57c8a3 100644 --- a/src/mcp/server/auth/middleware/client_auth.py +++ b/src/mcp/server/auth/middleware/client_auth.py @@ -102,8 +102,11 @@ async def authenticate_request(self, request: Request) -> OAuthClientInformation raise AuthenticationError("Client is registered for secret-based authentication but has no stored secret") # If client from the store expects a secret, validate that the request provides - # that secret - if client.client_secret: + # that secret. A client registered for "none" is a public client (RFC 7591 section 2), + # so a secret left on the stored record by an earlier registration is not its + # current credential; demanding it would override the client's own declaration of + # its type, against RFC 6749 section 2.1. + if client.token_endpoint_auth_method != "none" and client.client_secret: if not request_client_secret: raise AuthenticationError("Client secret is required") diff --git a/tests/server/mcpserver/auth/test_auth_integration.py b/tests/server/mcpserver/auth/test_auth_integration.py index e9c1df8465..e10c842e86 100644 --- a/tests/server/mcpserver/auth/test_auth_integration.py +++ b/tests/server/mcpserver/auth/test_auth_integration.py @@ -1382,6 +1382,62 @@ async def test_none_auth_method_public_client( token_response = response.json() assert "access_token" in token_response + @pytest.mark.anyio + async def test_none_auth_method_ignores_stored_client_secret( + self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str] + ): + """Test that 'none' authentication ignores a secret stored against the client. + + A client that declares `token_endpoint_auth_method="none"` is operating as a + public client, so a secret left over from an earlier registration must not be + demanded on the token request. Per RFC 6749 section 2.1 the authorization + server should not override the client's own declaration of its type. + """ + client_metadata = { + "redirect_uris": ["https://client.example.com/callback"], + "client_name": "Public Client With Stored Secret", + "token_endpoint_auth_method": "none", + "grant_types": ["authorization_code", "refresh_token"], + } + + response = await test_client.post("/register", json=client_metadata) + assert response.status_code == 201 + client_info = response.json() + assert client_info["token_endpoint_auth_method"] == "none" + + # Leave a secret on the stored client, as an earlier confidential registration would. + stored_client = await mock_oauth_provider.get_client(client_info["client_id"]) + assert stored_client is not None + mock_oauth_provider.clients[client_info["client_id"]] = stored_client.model_copy( + update={"client_secret": "secret_that_should_be_ignored"} + ) + + auth_code = f"code_{int(time.time())}" + mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode( + code=auth_code, + client_id=client_info["client_id"], + code_challenge=pkce_challenge["code_challenge"], + redirect_uri=AnyUrl("https://client.example.com/callback"), + redirect_uri_provided_explicitly=True, + scopes=["read", "write"], + expires_at=time.time() + 600, + ) + + # Token request without any client secret still succeeds. + response = await test_client.post( + "/token", + data={ + "grant_type": "authorization_code", + "client_id": client_info["client_id"], + "code": auth_code, + "code_verifier": pkce_challenge["code_verifier"], + "redirect_uri": "https://client.example.com/callback", + }, + ) + assert response.status_code == 200 + token_response = response.json() + assert "access_token" in token_response + class TestAuthorizeEndpointErrors: """Test error handling in the OAuth authorization endpoint."""