Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
Validate the iss authorization-response parameter (RFC 9207 / SEP-2468)#2921
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
9373eab7eeee6c109c39115f11a5File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -62,6 +62,35 @@ async with http_client: | ||
| v1's internal client set `follow_redirects=True`; set it explicitly when supplying your own `httpx.AsyncClient` to preserve that behavior. | ||
| ### OAuth `callback_handler` returns `AuthorizationCodeResult` | ||
| The `callback_handler` passed to `OAuthClientProvider` now returns an `AuthorizationCodeResult` instead of a `tuple[str, str | None]` of `(code, state)`. The new object adds an `iss` field so the client can validate the RFC 9207 authorization-response issuer (SEP-2468): when the redirect carries an `iss` query parameter it must match the authorization server's issuer, and a missing `iss` is rejected when the server advertised `authorization_response_iss_parameter_supported`. | ||
| **Before (v1):** | ||
| ```python | ||
| async def callback_handler() -> tuple[str, str | None]: | ||
| params = parse_qs(urlparse(await wait_for_redirect()).query) | ||
| return params["code"][0], params.get("state", [None])[0] | ||
| ``` | ||
| **After (v2):** | ||
| ```python | ||
| from mcp.client.auth import AuthorizationCodeResult | ||
| async def callback_handler() -> AuthorizationCodeResult: | ||
| params = parse_qs(urlparse(await wait_for_redirect()).query) | ||
| return AuthorizationCodeResult( | ||
| code=params["code"][0], | ||
| state=params.get("state", [None])[0], | ||
| iss=params.get("iss", [None])[0], | ||
| ) | ||
| ``` | ||
| Forward the `iss` query parameter from the redirect so the validation can run: omitting it makes the flow fail with `OAuthFlowError` against servers that advertise `authorization_response_iss_parameter_supported`, and silently skips the check for servers that send `iss` without advertising it. | ||
claude[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| ### `get_session_id` callback removed from `streamable_http_client` | ||
| The `get_session_id` callback (third element of the returned tuple) has been removed from `streamable_http_client`. The function now returns a 2-tuple `(read_stream, write_stream)` instead of a 3-tuple. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -35,9 +35,12 @@ | ||
| handle_token_response_scopes, | ||
| is_valid_client_metadata_url, | ||
| should_use_client_metadata_url, | ||
| validate_authorization_response_iss, | ||
| validate_metadata_issuer, | ||
| ) | ||
| from mcp.client.streamable_http import MCP_PROTOCOL_VERSION | ||
| from mcp.shared.auth import ( | ||
| AuthorizationCodeResult, | ||
| OAuthClientInformationFull, | ||
| OAuthClientMetadata, | ||
| OAuthMetadata, | ||
| @@ -97,7 +100,7 @@ class OAuthContext: | ||
| client_metadata: OAuthClientMetadata | ||
Kludex marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| storage: TokenStorage | ||
| redirect_handler: Callable[[str], Awaitable[None]] | None | ||
| callback_handler: Callable[[], Awaitable[tuple[str, str | None]]] | None | ||
| callback_handler: Callable[[], Awaitable[AuthorizationCodeResult]] | None | ||
| timeout: float = 300.0 | ||
| client_metadata_url: str | None = None | ||
| @@ -227,7 +230,7 @@ def __init__( | ||
| client_metadata: OAuthClientMetadata, | ||
| storage: TokenStorage, | ||
| redirect_handler: Callable[[str], Awaitable[None]] | None = None, | ||
| callback_handler: Callable[[], Awaitable[tuple[str, str | None]]] | None = None, | ||
| callback_handler: Callable[[], Awaitable[AuthorizationCodeResult]] | None = None, | ||
| timeout: float = 300.0, | ||
| client_metadata_url: str | None = None, | ||
| validate_resource_url: Callable[[str, str | None], Awaitable[None]] | None = None, | ||
| @@ -356,16 +359,19 @@ async def _perform_authorization_code_grant(self) -> tuple[str, str]: | ||
| await self.context.redirect_handler(authorization_url) | ||
| # Wait for callback | ||
| auth_code, returned_state = await self.context.callback_handler() | ||
| result = await self.context.callback_handler() | ||
| if returned_state is None or not secrets.compare_digest(returned_state, state): | ||
| raise OAuthFlowError(f"State parameter mismatch: {returned_state} != {state}") | ||
| if result.state is None or not secrets.compare_digest(result.state, state): | ||
| raise OAuthFlowError(f"State parameter mismatch: {result.state} != {state}") | ||
| if not auth_code: | ||
| # RFC 9207: validate the authorization-response issuer | ||
| validate_authorization_response_iss(result.iss, self.context.oauth_metadata) | ||
Kludex marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| if not result.code: | ||
| raise OAuthFlowError("No authorization code received") | ||
| # Return auth code and code verifier for token exchange | ||
| return auth_code, pkce_params.code_verifier | ||
| return result.code, pkce_params.code_verifier | ||
| def _get_token_endpoint(self) -> str: | ||
| if self.context.oauth_metadata and self.context.oauth_metadata.token_endpoint: | ||
| @@ -570,6 +576,9 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx. | ||
| if not ok: | ||
| break | ||
| if ok and asm: | ||
| # SEP-2468: metadata issuer must match the discovery issuer | ||
| if self.context.auth_server_url is not None: | ||
| validate_metadata_issuer(asm, self.context.auth_server_url) | ||
Kludex marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| self.context.oauth_metadata = asm | ||
| break | ||
| else: | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.