Skip to content

Commit a299324

Browse files
committed
Accept additional response_types values from OAuth servers
OAuth servers may return additional response_types beyond what the client requested (e.g., ["code", "none"] instead of just ["code"]). Per RFC 7591 Section 3.2.1, servers can modify registration metadata and return all registered values including server-provisioned fields. For example, Keycloak returns ["code", "none"] even when just ["code"] was requested. - Changed response_types field from list[Literal["code"]] to list[str] - Added validation in registration handler to ensure "code" is present - Added tests for response_types flexibility This fixes compatibility issues with OAuth servers that return additional response_types while maintaining MCP's requirement for the "code" flow.
1 parent 07ae8c0 commit a299324

File tree

3 files changed

+71
-6
lines changed

3 files changed

+71
-6
lines changed

src/mcp/server/auth/handlers/register.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ async def handle(self, request: Request) -> Response:
7777
status_code=400,
7878
)
7979

80+
# The MCP spec requires servers to use the authorization `code` flow
81+
# with PKCE
82+
if "code" not in client_metadata.response_types:
83+
return PydanticJSONResponse(
84+
content=RegistrationErrorResponse(
85+
error="invalid_client_metadata",
86+
error_description="response_types must include 'code' for authorization_code grant",
87+
),
88+
status_code=400,
89+
)
90+
8091
client_id_issued_at = int(time.time())
8192
client_secret_expires_at = (
8293
client_id_issued_at + self.options.client_secret_expiry_seconds

src/mcp/shared/auth.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,15 @@ class OAuthClientMetadata(BaseModel):
4242
"""
4343

4444
redirect_uris: list[AnyUrl] = Field(..., min_length=1)
45-
# token_endpoint_auth_method: this implementation only supports none &
46-
# client_secret_post;
47-
# ie: we do not support client_secret_basic
48-
token_endpoint_auth_method: Literal["none", "client_secret_post"] = "client_secret_post"
45+
token_endpoint_auth_method: Literal["none", "client_secret_post", "client_secret_basic"] = "client_secret_post"
4946
# grant_types: this implementation only supports authorization_code & refresh_token
5047
grant_types: list[Literal["authorization_code", "refresh_token"]] = [
5148
"authorization_code",
5249
"refresh_token",
5350
]
54-
# this implementation only supports code; ie: it does not support implicit grants
55-
response_types: list[Literal["code"]] = ["code"]
51+
# The MCP spec requires the "code" response type, but OAuth
52+
# servers may also return additional types they support
53+
response_types: list[str] = ["code"]
5654
scope: str | None = None
5755

5856
# these fields are currently unused, but we support & store them for potential

tests/server/fastmcp/auth/test_auth_integration.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,62 @@ async def test_client_registration_invalid_grant_type(self, test_client: httpx.A
942942
assert error_data["error"] == "invalid_client_metadata"
943943
assert error_data["error_description"] == "grant_types must be authorization_code and refresh_token"
944944

945+
@pytest.mark.anyio
946+
async def test_client_registration_with_additional_response_types(
947+
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider
948+
):
949+
"""Test that registration accepts additional response_types values alongside 'code'."""
950+
client_metadata = {
951+
"redirect_uris": ["https://client.example.com/callback"],
952+
"client_name": "Test Client",
953+
"grant_types": ["authorization_code", "refresh_token"],
954+
"response_types": ["code", "none"], # Keycloak-style response with additional value
955+
}
956+
957+
response = await test_client.post("/register", json=client_metadata)
958+
assert response.status_code == 201
959+
data = response.json()
960+
961+
client = await mock_oauth_provider.get_client(data["client_id"])
962+
assert client is not None
963+
assert "code" in client.response_types
964+
965+
@pytest.mark.anyio
966+
async def test_client_registration_response_types_without_code(self, test_client: httpx.AsyncClient):
967+
"""Test that registration rejects response_types that don't include 'code'."""
968+
client_metadata = {
969+
"redirect_uris": ["https://client.example.com/callback"],
970+
"client_name": "Test Client",
971+
"grant_types": ["authorization_code", "refresh_token"],
972+
"response_types": ["token", "none", "nonsense-string"],
973+
}
974+
975+
response = await test_client.post("/register", json=client_metadata)
976+
assert response.status_code == 400
977+
error_data = response.json()
978+
assert "error" in error_data
979+
assert error_data["error"] == "invalid_client_metadata"
980+
assert "response_types must include 'code'" in error_data["error_description"]
981+
982+
@pytest.mark.anyio
983+
async def test_client_registration_default_response_types(
984+
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider
985+
):
986+
"""Test that registration uses default response_types of ['code'] when not specified."""
987+
client_metadata = {
988+
"redirect_uris": ["https://client.example.com/callback"],
989+
"client_name": "Test Client",
990+
"grant_types": ["authorization_code", "refresh_token"],
991+
# response_types not specified, should default to ["code"]
992+
}
993+
994+
response = await test_client.post("/register", json=client_metadata)
995+
assert response.status_code == 201
996+
data = response.json()
997+
998+
assert "response_types" in data
999+
assert data["response_types"] == ["code"]
1000+
9451001

9461002
class TestAuthorizeEndpointErrors:
9471003
"""Test error handling in the OAuth authorization endpoint."""

0 commit comments

Comments
 (0)