Accept additional response_types values from OAuth servers (#1323)
This commit is contained in:
parent
146d7efb0f
commit
1940040ac3
3 changed files with 70 additions and 2 deletions
|
|
@ -77,6 +77,17 @@ class RegistrationHandler:
|
||||||
status_code=400,
|
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_id_issued_at = int(time.time())
|
||||||
client_secret_expires_at = (
|
client_secret_expires_at = (
|
||||||
client_id_issued_at + self.options.client_secret_expiry_seconds
|
client_id_issued_at + self.options.client_secret_expiry_seconds
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,9 @@ class OAuthClientMetadata(BaseModel):
|
||||||
"authorization_code",
|
"authorization_code",
|
||||||
"refresh_token",
|
"refresh_token",
|
||||||
]
|
]
|
||||||
# this implementation only supports code; ie: it does not support implicit grants
|
# The MCP spec requires the "code" response type, but OAuth
|
||||||
response_types: list[Literal["code"]] = ["code"]
|
# servers may also return additional types they support
|
||||||
|
response_types: list[str] = ["code"]
|
||||||
scope: str | None = None
|
scope: str | None = None
|
||||||
|
|
||||||
# these fields are currently unused, but we support & store them for potential
|
# these fields are currently unused, but we support & store them for potential
|
||||||
|
|
|
||||||
|
|
@ -937,6 +937,62 @@ class TestAuthEndpoints:
|
||||||
assert error_data["error"] == "invalid_client_metadata"
|
assert error_data["error"] == "invalid_client_metadata"
|
||||||
assert error_data["error_description"] == "grant_types must be authorization_code and refresh_token"
|
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:
|
class TestAuthorizeEndpointErrors:
|
||||||
"""Test error handling in the OAuth authorization endpoint."""
|
"""Test error handling in the OAuth authorization endpoint."""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue