Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/mcp/server/auth/middleware/client_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
56 changes: 56 additions & 0 deletions tests/server/mcpserver/auth/test_auth_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
Loading