Skip to content

Commit 7bff13c

Browse files
committed
fix: validate PRM resource match and handle DCR failure with pre-registered credentials
Two conformance auth scenario fixes: 1. Resource mismatch validation (RFC 8707): After discovering Protected Resource Metadata, validate that the resource field matches the server URL before proceeding with authorization. If the PRM returns a resource from a different origin, raise OAuthFlowError. 2. Pre-registration fallback: When Dynamic Client Registration fails (e.g. server returns 404), fall back to pre-registered client credentials from storage instead of crashing. The conformance client now pre-loads client credentials from MCP_CONFORMANCE_CONTEXT when available.
1 parent d3133ae commit 7bff13c

File tree

2 files changed

+46
-5
lines changed

2 files changed

+46
-5
lines changed

.github/actions/conformance/client.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,27 @@ async def run_client_credentials_basic(server_url: str) -> None:
275275
async def run_auth_code_client(server_url: str) -> None:
276276
"""Authorization code flow (default for auth/* scenarios)."""
277277
callback_handler = ConformanceOAuthCallbackHandler()
278+
storage = InMemoryTokenStorage()
279+
280+
# Check for pre-registered client credentials from context
281+
context_json = os.environ.get("MCP_CONFORMANCE_CONTEXT")
282+
if context_json:
283+
try:
284+
context = json.loads(context_json)
285+
client_id = context.get("client_id")
286+
client_secret = context.get("client_secret")
287+
if client_id:
288+
await storage.set_client_info(
289+
OAuthClientInformationFull(
290+
client_id=client_id,
291+
client_secret=client_secret,
292+
redirect_uris=[AnyUrl("http://localhost:3000/callback")],
293+
token_endpoint_auth_method="client_secret_basic" if client_secret else "none",
294+
)
295+
)
296+
logger.debug(f"Pre-loaded client credentials: client_id={client_id}")
297+
except json.JSONDecodeError:
298+
pass
278299

279300
oauth_auth = OAuthClientProvider(
280301
server_url=server_url,
@@ -284,7 +305,7 @@ async def run_auth_code_client(server_url: str) -> None:
284305
grant_types=["authorization_code", "refresh_token"],
285306
response_types=["code"],
286307
),
287-
storage=InMemoryTokenStorage(),
308+
storage=storage,
288309
redirect_handler=callback_handler.handle_redirect,
289310
callback_handler=callback_handler.handle_callback,
290311
client_metadata_url="https://conformance-test.local/client-metadata.json",

src/mcp/client/auth/oauth2.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import httpx
1919
from pydantic import BaseModel, Field, ValidationError
2020

21-
from mcp.client.auth.exceptions import OAuthFlowError, OAuthTokenError
21+
from mcp.client.auth.exceptions import OAuthFlowError, OAuthRegistrationError, OAuthTokenError
2222
from mcp.client.auth.utils import (
2323
build_oauth_authorization_server_metadata_discovery_urls,
2424
build_protected_resource_metadata_discovery_urls,
@@ -476,6 +476,15 @@ async def _handle_oauth_metadata_response(self, response: httpx.Response) -> Non
476476
metadata = OAuthMetadata.model_validate_json(content)
477477
self.context.oauth_metadata = metadata
478478

479+
def _validate_resource_match(self, prm: ProtectedResourceMetadata) -> None:
480+
"""Validate that PRM resource matches the server URL per RFC 8707."""
481+
if not prm.resource:
482+
return
483+
default_resource = resource_url_from_server_url(self.context.server_url)
484+
prm_resource = str(prm.resource)
485+
if not check_resource_allowed(requested_resource=default_resource, configured_resource=prm_resource):
486+
raise OAuthFlowError(f"Protected resource {prm_resource} does not match expected {default_resource}")
487+
479488
async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]:
480489
"""HTTPX auth flow integration."""
481490
async with self.context.lock:
@@ -517,6 +526,8 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
517526

518527
prm = await handle_protected_resource_response(discovery_response)
519528
if prm:
529+
# Validate PRM resource matches server URL (RFC 8707)
530+
self._validate_resource_match(prm)
520531
self.context.protected_resource_metadata = prm
521532

522533
# todo: try all authorization_servers to find the OASM
@@ -575,9 +586,18 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
575586
self.context.get_authorization_base_url(self.context.server_url),
576587
)
577588
registration_response = yield registration_request
578-
client_information = await handle_registration_response(registration_response)
579-
self.context.client_info = client_information
580-
await self.context.storage.set_client_info(client_information)
589+
try:
590+
client_information = await handle_registration_response(registration_response)
591+
self.context.client_info = client_information
592+
await self.context.storage.set_client_info(client_information)
593+
except OAuthRegistrationError:
594+
# DCR failed — check for pre-registered client credentials
595+
stored_client_info = await self.context.storage.get_client_info()
596+
if stored_client_info:
597+
logger.debug("DCR failed, using pre-registered client credentials")
598+
self.context.client_info = stored_client_info
599+
else:
600+
raise
581601

582602
# Step 5: Perform authorization and complete token exchange
583603
token_response = yield await self._perform_authorization()

0 commit comments

Comments
 (0)