-
Notifications
You must be signed in to change notification settings - Fork 3.7k
OAuth client: harden SEP-2352/SEP-2350 edge cases; fix conformance comment #2936
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
Merged
+277
−17
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
984ad41
Correct PROTOCOL_VERSION comment for harness per-scenario default
maxisbey 676cc1f
Harden OAuth issuer-binding and step-up scope on edge-case paths
maxisbey a808d14
Only bind issuer when registration targets the discovered AS
maxisbey d4fbec4
Gate DCR issuer stamp on a discovered registration_endpoint
maxisbey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟣 Pre-existing issue in the block this PR touches:
_handle_refresh_responsenow carries an omittedscopeforward per RFC 6749 §6, but an omittedrefresh_token(also allowed by §6 and common with non-rotating ASes) is not — the parsed token hasrefresh_token=Noneand unconditionally overwrites bothcontext.current_tokensand persistent storage, so the next expiry forces a full interactive re-authorization (or fails for headless clients). The fix is the same one-line pattern this PR adds for scope: backfilltoken_response.refresh_tokenfromself.context.current_tokens.refresh_tokenwhen the response omits it.Extended reasoning...
The bug.
_handle_refresh_response(src/mcp/client/auth/oauth2.py:469-495) parses the refresh response into a freshOAuthTokenand then unconditionally doesself.context.current_tokens = token_responsefollowed byawait self.context.storage.set_tokens(token_response).OAuthToken.refresh_tokenisOptionalwith a default ofNone(src/mcp/shared/auth.py), so when the authorization server omitsrefresh_tokenfrom the refresh response, the still-valid stored refresh token is overwritten withNoneboth in memory and in persistent storage. RFC 6749 §6 explicitly allows this server behavior — the AS MAY issue a new refresh token; when it does not, the client is expected to keep using the previously issued one. Non-rotating refresh tokens are common in practice (Google and many enterprise ASes omitrefresh_tokenfrom refresh responses).Code path.
async_auth_flow→can_refresh_token()true →_refresh_token()builds the request →_handle_refresh_responsehandles the 200. The handler validates the JSON, applies the new scope carry-forward this PR adds (lines 480-484), and then replaces the stored token wholesale. Nothing anywhere insrc/mcp/client/authpreserves the priorrefresh_token— the only other uses arecan_refresh_token()(which requires it) and building the refresh request itself. The only carry-forward logic in the module is the scope backfill introduced two lines above the overwrite.Step-by-step proof.
{access_token: A1, refresh_token: R1, expires_in: 3600}; both are persisted.async_auth_flowseescan_refresh_token()is true (R1 is stored) and issues the refresh.200 {"access_token": "A2", "token_type": "Bearer", "expires_in": 3600}— norefresh_token, which §6 permits and non-rotating ASes routinely do.OAuthToken.model_validate_jsonproduces a token withrefresh_token=None. The handler stores it incontext.current_tokensand callsstorage.set_tokens, wiping R1 from memory and from persistent storage.can_refresh_token()(oauth2.py:142-144) returnsFalsebecausecurrent_tokens.refresh_tokenisNone. The client silently falls back to a full interactive 401 re-authorization (redirect + callback) — an unnecessary user interaction for interactive clients, and an outright failure for headless or long-running ones. After a process restart the persisted record has also lost the refresh token, so the degradation survives restarts.Why nothing prevents it. The existing tests always include
refresh_tokenin mocked refresh responses, so the omitted case is uncovered, and there is no other code path that carries the prior value forward.Severity / scope. This is pre-existing — the overwrite line predates this PR and the PR does not change runtime behavior here. It is flagged because the PR edits this exact handler and implements the identical RFC 6749 §6 carry-forward principle for the sibling
scopefield two lines above, so this is the natural place to fix it.Fix. Mirror the scope backfill the PR adds:
placed alongside the new scope carry-forward, before
current_tokensis replaced and the token is persisted. A test analogous totest_handle_refresh_response_carries_prior_scope_when_response_omits_it(asserting the stored token keeps the oldrefresh_token) would cover it.