Skip to content

Commit fc762df

Browse files
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H53v9BN6gzMtKyAGAgdbDb
1 parent 9972c21 commit fc762df

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

src/mcp/server/auth/middleware/client_auth.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,11 @@ async def authenticate_request(self, request: Request) -> OAuthClientInformation
102102
raise AuthenticationError("Client is registered for secret-based authentication but has no stored secret")
103103

104104
# If client from the store expects a secret, validate that the request provides
105-
# that secret
106-
if client.client_secret:
105+
# that secret. A client registered for "none" is a public client (RFC 7591 section 2),
106+
# so a secret left on the stored record by an earlier registration is not its
107+
# current credential; demanding it would override the client's own declaration of
108+
# its type, against RFC 6749 section 2.1.
109+
if client.token_endpoint_auth_method != "none" and client.client_secret:
107110
if not request_client_secret:
108111
raise AuthenticationError("Client secret is required")
109112

tests/server/mcpserver/auth/test_auth_integration.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,62 @@ async def test_none_auth_method_public_client(
13821382
token_response = response.json()
13831383
assert "access_token" in token_response
13841384

1385+
@pytest.mark.anyio
1386+
async def test_none_auth_method_ignores_stored_client_secret(
1387+
self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
1388+
):
1389+
"""Test that 'none' authentication ignores a secret stored against the client.
1390+
1391+
A client that declares `token_endpoint_auth_method="none"` is operating as a
1392+
public client, so a secret left over from an earlier registration must not be
1393+
demanded on the token request. Per RFC 6749 section 2.1 the authorization
1394+
server should not override the client's own declaration of its type.
1395+
"""
1396+
client_metadata = {
1397+
"redirect_uris": ["https://client.example.com/callback"],
1398+
"client_name": "Public Client With Stored Secret",
1399+
"token_endpoint_auth_method": "none",
1400+
"grant_types": ["authorization_code", "refresh_token"],
1401+
}
1402+
1403+
response = await test_client.post("/register", json=client_metadata)
1404+
assert response.status_code == 201
1405+
client_info = response.json()
1406+
assert client_info["token_endpoint_auth_method"] == "none"
1407+
1408+
# Leave a secret on the stored client, as an earlier confidential registration would.
1409+
stored_client = await mock_oauth_provider.get_client(client_info["client_id"])
1410+
assert stored_client is not None
1411+
mock_oauth_provider.clients[client_info["client_id"]] = stored_client.model_copy(
1412+
update={"client_secret": "secret_that_should_be_ignored"}
1413+
)
1414+
1415+
auth_code = f"code_{int(time.time())}"
1416+
mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(
1417+
code=auth_code,
1418+
client_id=client_info["client_id"],
1419+
code_challenge=pkce_challenge["code_challenge"],
1420+
redirect_uri=AnyUrl("https://client.example.com/callback"),
1421+
redirect_uri_provided_explicitly=True,
1422+
scopes=["read", "write"],
1423+
expires_at=time.time() + 600,
1424+
)
1425+
1426+
# Token request without any client secret still succeeds.
1427+
response = await test_client.post(
1428+
"/token",
1429+
data={
1430+
"grant_type": "authorization_code",
1431+
"client_id": client_info["client_id"],
1432+
"code": auth_code,
1433+
"code_verifier": pkce_challenge["code_verifier"],
1434+
"redirect_uri": "https://client.example.com/callback",
1435+
},
1436+
)
1437+
assert response.status_code == 200
1438+
token_response = response.json()
1439+
assert "access_token" in token_response
1440+
13851441

13861442
class TestAuthorizeEndpointErrors:
13871443
"""Test error handling in the OAuth authorization endpoint."""

0 commit comments

Comments
 (0)