Repository navigation
fix(auth): ask for re-authorization when the OAuth2 refresh token is revoked - #7731
Closed
inamdarmihir wants to merge 2 commits into
Closed
inamdarmihir wants to merge 2 commits into
inamdarmihir wants to merge 2 commits into
Conversation
…revoked When the token endpoint rejects a refresh with `invalid_grant`, the refresh token is permanently dead, but OAuth2CredentialRefresher swallowed the error and CredentialManager kept returning, and re-saving, the stale access token. Tools then kept sending an expired token and the user was never asked to re-authorize. The refresher now raises CredentialRevokedError for `invalid_grant`, and CredentialManager.get_auth_credential returns None without saving, so the existing request_credential flow asks the user to authorize again. A fresh auth response is still preferred over the stored dead credential. Other failures (network errors, 5xx, other OAuth2 errors) keep the stale token so the 401 recovery in RestApiTool is unaffected. Fixes google#7730
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
Contributor
Author
|
@googlebot I signed it! |
copybara-service Bot
pushed a commit
that referenced
this pull request
Nov 4, 2026
Merge #7731 When the token endpoint rejects a refresh with invalid_grant, OAuth2CredentialRefresher.refresh now raises a CredentialRefresherError subclass instead of returning the stale credential, and CredentialManager.get_auth_credential returns None and removes the dead credential from the credential service, so authenticated tools and toolsets ask the user to authorize again. A client-credentials flow exchanges its configured credential again instead. Other refresh failures still keep the stale token. The credential manager now calls BaseCredentialService.save_credential with exchanged_auth_credential=None to remove a stored credential; the save_credential docstring documents that None means remove, so that load_credential returns None afterwards. If a custom credential service raises on None, the error is logged and the call continues. Fixes #7730 PiperOrigin-RevId: 1013682546
Collaborator
|
Thank you @inamdarmihir for your contribution! 🎉 Your changes have been successfully imported and merged via Copybara in commit 70e1cbd. Closing this PR as the changes are now in the main branch. |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
Problem
When the token endpoint rejects an OAuth2 refresh with
invalid_grant(the refresh token was revoked or expired),OAuth2CredentialRefresher.refreshlogs the error and returns the stale credential, andCredentialManager.get_auth_credentialreturns and re-saves it. Authenticated tools (AuthenticatedFunctionTool, MCP toolset auth) only callrequest_credentialwhenget_auth_credentialreturnsNone, so they keep sending a dead access token and the user is never asked to re-authorize.Solution
OAuth2CredentialRefresher.refreshraises a newCredentialRevokedError(aCredentialRefresherErrorsubclass) when the authlib/requests error isinvalid_grant.CredentialManager.get_auth_credentialcatches it and returnsNonewithout saving, so the existingrequest_credentialflow asks for authorization again. If the dead credential came from the credential store, a fresh auth response from the user is still preferred, exchanged and saved, so a revoked stored credential cannot shadow the new authorization.ToolAuthHandler._refresh_and_storealready wrapsrefresher.refreshinexcept Exception, so theRestApiTool401 recovery is unchanged.Testing Plan
Unit Tests:
New cases:
invalid_grant(bothInvalidGrantErrorandOAuth2Error(error="invalid_grant")) raises in the refresher,server_errorkeeps the stale token, a revoked credential returnsNoneand is not saved, a fresh auth response wins over a revoked stored credential, and_refresh_credentialpropagates the error.credential_manager.pyandoauth2_credential_refresher.pychanges reverted: 4 failed, 73 passed.tests/unittests/auth346 passed,tests/unittests/tools3310 passed.Manual End-to-End (E2E) Tests:
Local HTTP token endpoint, a real
CredentialManagerand authlib session, stored credential expired withaccess_token=STALE:Not run: the full repo suite, and a live Google OAuth flow.
Checklist
Additional context
invalid_grantis detected through the.errorattribute authlib parses from the token response. A provider that signals it in a non-standard body stays on the old keep-stale path.credential_manager.py. The two branches merge cleanly.Authored by Mihir Inamdar.