fix(client): preserve existing query params on OAuth authorization_endpoint#2779
fix(client): preserve existing query params on OAuth authorization_endpoint#2779Bartok9 wants to merge 1 commit into
Conversation
702e330 to
11c135a
Compare
|
CI note — rebased onto current |
11c135a to
d9fd2a5
Compare
859c895 to
b8f826c
Compare
…dpoint Closes modelcontextprotocol#2776 The authorization code grant built the redirect URL with `f"{auth_endpoint}?{urlencode(auth_params)}"`, which produces an invalid URL when the server-advertised authorization_endpoint already carries a query string. For example Salesforce advertises `.../services/oauth2/authorize?prompt=select_account`, yielding `...authorize?prompt=select_account?response_type=code&...` (two `?` separators), so the client navigates to a malformed URL and the server rejects the request. Fix: parse the endpoint, merge its existing query params with the flow-generated auth_params (flow params win on conflict), and re-encode into a single well-formed query string. None-valued params are dropped rather than serialized as the literal "None". Tests: add TestAuthorizationEndpointWithQuery covering the helper (no/with/conflicting existing query) plus an end-to-end _perform_authorization_code_grant assertion that the captured redirect URL preserves the server param and stays well-formed. 101 passed.
b8f826c to
79259a6
Compare
|
Rebased onto latest upstream `main` to resolve the merge conflict (an import collision in `tests/client/test_auth.py` — kept both upstream's `OAuthTokenError` and this PR's `_build_authorization_url` import; `oauth2.py` auto-merged cleanly). The OAuth query-param fix is unchanged. Local verification on the rebased branch:
PR now shows mergeable. Ready for review. |
Summary
authorization_endpointinstead of producing a malformed double-?URL.authorization_endpointcarries query params (e.g. Salesforce's.../authorize?prompt=select_account).Motivation
Closes #2776.
_perform_authorization_code_grantbuilt the redirect URL with:When the server's
authorization_endpointalready has a query string, this yields an invalid URL with two?separators. The reporter's real-world example:https://test.salesforce.com/services/oauth2/authorize?prompt=select_account...authorize?prompt=select_account?response_type=code&...— the second?is invalid, so the existingpromptparam is swallowed into the value and the request is rejected.Root cause
Blind string concatenation of
?+ encoded params, with no handling for a pre-existing query component on the endpoint.Fix
New
_build_authorization_url(auth_endpoint, auth_params)helper:urlparsethe endpoint, merge its existing query (parse_qsl) with the flow-generatedauth_params, thenurlunparseback into a single well-formed query string.response_type,state, PKCE challenge, etc. are authoritative).None-valued params are dropped rather than serialized as the literal string"None"(preserves prior effective behavior; keeps the signature type-correct asMapping[str, str | None]).Diff: 2 files, +103/-3.
Tests
Added
TestAuthorizationEndpointWithQueryintests/client/test_auth.py:test_build_authorization_url_no_existing_query— baseline, single?.test_build_authorization_url_preserves_existing_query— the OAuth handler doesn't support redirect URLs with params #2776 Salesforce case; serverpromptsurvives alongside flow params, exactly one?.test_build_authorization_url_flow_params_win_on_conflict— precedence rule.test_perform_authorization_preserves_endpoint_query— end-to-end through_perform_authorization_code_grant, asserting the captured redirect URL is well-formed.Verification
uv run pytest tests/client/test_auth.py— 100 passed, 1 xfaileduv run pyright src/mcp/client/auth/oauth2.py— 0 errorsuv run ruff format/ruff check— cleanNonehandling preserve the previous effective output for endpoints without a query string.