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
11 changes: 11 additions & 0 deletions src/mcp/server/auth/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ async def handle(self, request: Request) -> Response:
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
Expand Down
5 changes: 3 additions & 2 deletions src/mcp/shared/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions tests/server/fastmcp/auth/test_auth_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,62 @@ async def test_client_registration_invalid_grant_type(self, test_client: httpx.A
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."""
Expand Down
Loading