Conversation
702e330 to
11c135a
Compare
|
CI note — rebased onto current |
11c135a to
d9fd2a5
Compare
859c895 to
b8f826c
Compare
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. |
STiFLeR7
left a comment
There was a problem hiding this comment.
Reviewed the merge logic in _build_authorization_url. The precedence is correctly safety-conscious: auth_params (flow-generated redirect_uri, state, client_id, code_challenge, etc.) always wins over anything already on the endpoint's query string, since it's applied via .update() after seeding from parse_qsl. That closes off a parameter-pollution risk a naive merge could introduce (e.g. a misbehaving/misconfigured server-advertised authorization_endpoint overriding redirect_uri or state) — confirmed by checking the call site in _perform_authorization_code_grant, where all the security-relevant fields are present in auth_params.
One edge case worth a look: dict(parse_qsl(parsed.query, keep_blank_values=True)) silently collapses duplicate keys in the existing endpoint query string down to the last occurrence. If a server ever advertises something like ?scope=a&scope=b (multi-value query params are rare for OAuth authorization endpoints but not disallowed by the URL spec), only scope=b survives. Given how narrow the practical impact is or OAuth authorization endpoints specifically, this is likely fine to ship as-is, but might be worth either a one-line comment noting the tradeoff, or confirming no provider in the wild actually does this.
Test coverage otherwise looks solid — good catch on asserting url.count("?") == 1 to pin the exact bug (the old f-string producing a double ?).
|
Thanks for the careful review, @STiFLeR7 — the precedence read is exactly right. Good catch on the duplicate-key edge case. Added a regression test Verification on the pushed commit:
|
7219d8c to
496284d
Compare
|
Thanks for the PR, and sorry it sat here without a proper review. We're closing most of the open PR backlog. v2 is out and changed a lot of the SDK, so many older PRs no longer apply as written, and we're a small team that realistically doesn't have the capacity to work through the rest. If this still matters to you on v2, the most useful thing you can do is open an issue (or comment on the existing one) with your use case and a repro. Hearing why it matters to you is what we use to decide what to prioritise. |
|
Thanks for the note, and no worries on the wait. Understood that v2 moved and the backlog is being closed rather than reviewed in place. I won't reopen this PR. The original case (authorization URL built with a second |
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.