You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
_handle_refresh_response discards existing refresh_token when server omits it #2270
_handle_refresh_response() in src/mcp/client/auth/oauth2.py replaces the stored token object with the parsed refresh response as-is. When the authorization server does not return a new refresh_token in the refresh response, the previously stored refresh_token is lost. After the first successful refresh, can_refresh_token() returns False and all subsequent refreshes fail, forcing full re-authentication.
Per RFC 6749 Section 6, issuing a new refresh token in the refresh response is optional:
"The authorization server MAY issue a new refresh token, in which case the client MUST discard the old refresh token and replace it with the new refresh token."
The implicit corollary is that if the server does not issue a new refresh token, the client must preserve the existing one.
Many OAuth providers omit the refresh_token from refresh responses by default (e.g., Google, Auth0 without rotation enabled, Okta in persistent token mode). This makes the current behavior a practical issue for a wide range of OAuth servers.
Preserve the existing refresh_token when the refresh response omits one:
token_response=OAuthToken.model_validate_json(content)
# Per RFC 6749 Section 6, the server MAY issue a new refresh token.# If the response omits it, preserve the existing one.ifnottoken_response.refresh_tokenandself.context.current_tokensandself.context.current_tokens.refresh_token:
token_response=token_response.model_copy(
update={"refresh_token": self.context.current_tokens.refresh_token}
)
self.context.current_tokens=token_response
...
The refresh path here looks like a straight RFC 6749 §6 mismatch rather than an OAuth-provider quirk.
If _handle_refresh_response() replaces current_tokens with the parsed refresh response as-is, then any provider that omits refresh_token on refresh will silently downgrade the session into a non-refreshable one after the first rotation. That matches the failure mode in the report: first refresh succeeds, can_refresh_token() flips false, later refreshes fail.
The minimal fix surface seems to be local to token replacement:
parse the new token response,
if refresh_token is absent, preserve the previously stored one,
only overwrite it when the server actually returns a new refresh token.
That also lines up with the RFC wording: if a new refresh token is issued, replace the old one; otherwise keep using the existing token. A good regression test would be a provider response that updates access_token/expiry but omits refresh_token, then verifies a second refresh still works without re-auth.
This was fixed on main in #2946: _handle_refresh_response now carries the existing refresh_token forward when the refresh response omits it (the RFC 6749 §6 case you described), and that shipped in v2.0. The 1.x line still has the old wholesale overwrite, but 1.x is only taking critical fixes at this point, so I'm closing this as fixed in v2. Feel free to reopen if you're stuck on 1.x and this is blocking you there.
Activity
fix(oauth): preserve existing refresh_token when refresh response omi…
fix(oauth): preserve existing refresh_token when refresh response omi…
fix(oauth): preserve existing refresh_token when server omits it
fix: preserve existing refresh_token when server omits it in refresh …
fix(auth): preserve existing refresh_token when server omits it
Fix: preserve existing refresh_token when server omits it in refresh …
ekcheungAI commentedon Mar 14, 2026
The refresh path here looks like a straight RFC 6749 §6 mismatch rather than an OAuth-provider quirk.
If
_handle_refresh_response()replacescurrent_tokenswith the parsed refresh response as-is, then any provider that omitsrefresh_tokenon refresh will silently downgrade the session into a non-refreshable one after the first rotation. That matches the failure mode in the report: first refresh succeeds,can_refresh_token()flips false, later refreshes fail.The minimal fix surface seems to be local to token replacement:
refresh_tokenis absent, preserve the previously stored one,That also lines up with the RFC wording: if a new refresh token is issued, replace the old one; otherwise keep using the existing token. A good regression test would be a provider response that updates
access_token/expiry but omitsrefresh_token, then verifies a second refresh still works without re-auth.maxisbey commentedon Aug 14, 2026
This was fixed on
mainin #2946:_handle_refresh_responsenow carries the existingrefresh_tokenforward when the refresh response omits it (the RFC 6749 §6 case you described), and that shipped in v2.0. The 1.x line still has the old wholesale overwrite, but 1.x is only taking critical fixes at this point, so I'm closing this as fixed in v2. Feel free to reopen if you're stuck on 1.x and this is blocking you there.AI Disclaimer