Accept additional response_types values from OAuth servers (#1323)

This commit is contained in:
Jon Shea 2025-09-26 15:29:06 -04:00 • committed by GitHub
commit 1940040ac3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 70 additions and 2 deletions

View file

@ -77,6 +77,17 @@ class RegistrationHandler:
status_code=400,
)
# The MCP spec requires servers to use the authorization `code` flow
# with PKCE
if "code" not in client_metadata.response_types:
return PydanticJSONResponse(
content=RegistrationErrorResponse(
error="invalid_client_metadata",
error_description="response_types must include 'code' for authorization_code grant",
),
status_code=400,
)
client_id_issued_at = int(time.time())
client_secret_expires_at = (
client_id_issued_at + self.options.client_secret_expiry_seconds

View file

@ -51,8 +51,9 @@ class OAuthClientMetadata(BaseModel):
"authorization_code",
"refresh_token",
]
# this implementation only supports code; ie: it does not support implicit grants
response_types: list[Literal["code"]] = ["code"]
# The MCP spec requires the "code" response type, but OAuth
# servers may also return additional types they support
response_types: list[str] = ["code"]
scope: str | None = None
# these fields are currently unused, but we support & store them for potential

View file

@ -937,6 +937,62 @@ class TestAuthEndpoints:
assert error_data["error"] == "invalid_client_metadata"
assert error_data["error_description"] == "grant_types must be authorization_code and refresh_token"
@pytest.mark.anyio
async def test_client_registration_with_additional_response_types(
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider
):
"""Test that registration accepts additional response_types values alongside 'code'."""
client_metadata = {
"redirect_uris": ["https://client.example.com/callback"],
"client_name": "Test Client",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code", "none"], # Keycloak-style response with additional value
}
response = await test_client.post("/register", json=client_metadata)
assert response.status_code == 201
data = response.json()
client = await mock_oauth_provider.get_client(data["client_id"])
assert client is not None
assert "code" in client.response_types
@pytest.mark.anyio
async def test_client_registration_response_types_without_code(self, test_client: httpx.AsyncClient):
"""Test that registration rejects response_types that don't include 'code'."""
client_metadata = {
"redirect_uris": ["https://client.example.com/callback"],
"client_name": "Test Client",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["token", "none", "nonsense-string"],
}
response = await test_client.post("/register", json=client_metadata)
assert response.status_code == 400
error_data = response.json()
assert "error" in error_data
assert error_data["error"] == "invalid_client_metadata"
assert "response_types must include 'code'" in error_data["error_description"]
@pytest.mark.anyio
async def test_client_registration_default_response_types(
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider
):
"""Test that registration uses default response_types of ['code'] when not specified."""
client_metadata = {
"redirect_uris": ["https://client.example.com/callback"],
"client_name": "Test Client",
"grant_types": ["authorization_code", "refresh_token"],
# response_types not specified, should default to ["code"]
}
response = await test_client.post("/register", json=client_metadata)
assert response.status_code == 201
data = response.json()
assert "response_types" in data
assert data["response_types"] == ["code"]
class TestAuthorizeEndpointErrors:
"""Test error handling in the OAuth authorization endpoint."""