Skip to content

fix(core): preserve OAuth refresh tokens during rotation and retrieval#26924

Merged
cocosheng-g merged 1 commit into
mainfrom
issue-25460
May 13, 2026
Merged

fix(core): preserve OAuth refresh tokens during rotation and retrieval#26924
cocosheng-g merged 1 commit into
mainfrom
issue-25460

Conversation

@cocosheng-g
Copy link
Copy Markdown
Contributor

Summary

This PR addresses two critical defects in OAuth credential handling where valid refresh tokens were being discarded, forcing users into full re-authentication flows instead of silently refreshing.

  1. OAuthCredentialStorage.saveCredentials and MCPOAuthTokenStorage.saveToken now correctly merge the existing refreshToken from storage if the incoming payload (like a rotation response) omits it.
  2. KeychainTokenStorage.getCredentials and getAllCredentials no longer drop expired credentials if they contain a valid refreshToken.

Details

Historically, KeychainTokenStorage implemented a strict policy of returning null for any expired token. This was too blunt, as it hid "refreshable" tokens (those with a refresh_token) from the domain adapters (MCPOAuthProvider and OAuth2AuthProvider) that rely on receiving that expired state to trigger a background refresh.
Additionally, the save methods were blindly overwriting storage with the new token payload, which often lacks a refresh_token during a standard Google OAuth rotation, causing the persistent token to be lost.

This fix delegates the responsibility of merging state to the domain adapters and relaxes the storage layer to return expired but refreshable tokens.

Related Issues

Closes #25460

How to Validate

  1. Check out the branch and build the CLI.
  2. Run npm test -w @google/gemini-cli-core to verify the new reproduction unit tests pass:
    • Issue #25460 Reproduction > OAuthCredentialStorage.saveCredentials > overwrites refresh token when new payload lacks one
    • Issue #25460 Reproduction > KeychainTokenStorage.getCredentials > discards expired credentials even if refresh_token is present
  3. Manually validate a forced rotation:
    • Authenticate normally using gemini auth login.
    • Wait for or artificially force the access token to expire (e.g., by modifying the keychain/file).
    • Execute a command like gemini ask "hello". The CLI should silently refresh the token rather than opening the browser.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

@cocosheng-g cocosheng-g requested a review from a team as a code owner May 12, 2026 16:48
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request improves the reliability of OAuth credential management by ensuring that refresh tokens are not discarded during rotation or retrieval. By allowing the system to retain and recognize expired tokens that still possess a valid refresh token, the application can now perform silent background refreshes, significantly improving the user experience by reducing unnecessary re-authentication prompts.

Highlights

  • OAuth Token Persistence: Updated credential storage logic to merge existing refresh tokens when new payloads omit them, preventing the loss of refresh capabilities during token rotation.
  • Expired Token Handling: Modified the storage layer to return expired credentials if a valid refresh token is present, allowing domain adapters to trigger background refreshes instead of forcing re-authentication.
  • Testing: Added comprehensive unit tests to verify that refresh tokens are preserved during save operations and that expired tokens with refresh capabilities are correctly retrieved.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses issue #25460 by ensuring that OAuth refresh tokens are preserved during credential updates and that expired credentials are not discarded if a refresh token is available. Specifically, it modifies OAuthCredentialStorage and MCPOAuthTokenStorage to merge existing refresh tokens when saving new credentials and updates KeychainTokenStorage to return expired tokens if a refresh token is present. A new reproduction test and updates to existing tests have been added to verify the fix. I have no feedback to provide as no review comments were submitted.

@github-actions
Copy link
Copy Markdown

Size Change: +454 B (0%)

Total Size: 34.1 MB

Filename Size Change
./bundle/chunk-6LL447YA.js 0 B -19.5 kB (removed) 🏆
./bundle/chunk-ERXF46WU.js 0 B -14.8 MB (removed) 🏆
./bundle/chunk-JTTGR7U5.js 0 B -3.8 kB (removed) 🏆
./bundle/chunk-MNVM4433.js 0 B -12.5 kB (removed) 🏆
./bundle/chunk-OPBX7WOS.js 0 B -49.2 kB (removed) 🏆
./bundle/chunk-QQJRITJW.js 0 B -3.43 kB (removed) 🏆
./bundle/chunk-XJ3I7APM.js 0 B -659 kB (removed) 🏆
./bundle/chunk-YYF4PQDB.js 0 B -2.78 MB (removed) 🏆
./bundle/core-UEZFFBOW.js 0 B -49.2 kB (removed) 🏆
./bundle/devtoolsService-OX4VE4PD.js 0 B -28 kB (removed) 🏆
./bundle/gemini-6BUGMPH2.js 0 B -587 kB (removed) 🏆
./bundle/interactiveCli-YCJJEHFF.js 0 B -1.3 MB (removed) 🏆
./bundle/liteRtServerManager-VXWWQ3QD.js 0 B -2.11 kB (removed) 🏆
./bundle/oauth2-provider-HJ6AEQR4.js 0 B -9.16 kB (removed) 🏆
./bundle/chunk-4UCCQUJV.js 2.78 MB +2.78 MB (new file) 🆕
./bundle/chunk-5MKUNTSZ.js 659 kB +659 kB (new file) 🆕
./bundle/chunk-6N5LSTWA.js 12.5 kB +12.5 kB (new file) 🆕
./bundle/chunk-6STCOPTY.js 49.2 kB +49.2 kB (new file) 🆕
./bundle/chunk-CFAJBUZ4.js 14.8 MB +14.8 MB (new file) 🆕
./bundle/chunk-EJLH66V3.js 3.8 kB +3.8 kB (new file) 🆕
./bundle/chunk-EVMFM24W.js 3.43 kB +3.43 kB (new file) 🆕
./bundle/chunk-YP6PA27S.js 19.5 kB +19.5 kB (new file) 🆕
./bundle/core-ZKZOCDFM.js 49.2 kB +49.2 kB (new file) 🆕
./bundle/devtoolsService-JBU4Z35N.js 28 kB +28 kB (new file) 🆕
./bundle/gemini-5DSSMLWH.js 587 kB +587 kB (new file) 🆕
./bundle/interactiveCli-FZ56HZRE.js 1.3 MB +1.3 MB (new file) 🆕
./bundle/liteRtServerManager-DXACX6QF.js 2.11 kB +2.11 kB (new file) 🆕
./bundle/oauth2-provider-OMRF7ZZT.js 9.16 kB +9.16 kB (new file) 🆕
ℹ️ View Unchanged
Filename Size Change
./bundle/bundled/third_party/index.js 8 MB 0 B
./bundle/chunk-34MYV7JD.js 2.45 kB 0 B
./bundle/chunk-5AUYMPVF.js 858 B 0 B
./bundle/chunk-5PS3AYFU.js 1.18 kB 0 B
./bundle/chunk-664ZODQF.js 124 kB 0 B
./bundle/chunk-DAHVX5MI.js 206 kB 0 B
./bundle/chunk-IUUIT4SU.js 56.5 kB 0 B
./bundle/chunk-QTB23IKC.js 1.97 MB 0 B
./bundle/chunk-RJTRUG2J.js 39.8 kB 0 B
./bundle/cleanup-MU63Q7OA.js 0 B -932 B (removed) 🏆
./bundle/devtools-36NN55EP.js 696 kB 0 B
./bundle/dist-T73EYRDX.js 356 B 0 B
./bundle/events-XB7DADIJ.js 418 B 0 B
./bundle/examples/hooks/scripts/on-start.js 188 B 0 B
./bundle/examples/mcp-server/example.js 1.43 kB 0 B
./bundle/gemini.js 5.1 kB 0 B
./bundle/getMachineId-bsd-TXG52NKR.js 1.55 kB 0 B
./bundle/getMachineId-darwin-7OE4DDZ6.js 1.55 kB 0 B
./bundle/getMachineId-linux-SHIFKOOX.js 1.34 kB 0 B
./bundle/getMachineId-unsupported-5U5DOEYY.js 1.06 kB 0 B
./bundle/getMachineId-win-6KLLGOI4.js 1.72 kB 0 B
./bundle/memoryDiscovery-PTF4YLMR.js 980 B 0 B
./bundle/multipart-parser-KPBZEGQU.js 11.7 kB 0 B
./bundle/node_modules/@google/gemini-cli-devtools/dist/client/main.js 222 kB 0 B
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/_client-assets.js 229 kB 0 B
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/index.js 13.4 kB 0 B
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/types.js 132 B 0 B
./bundle/sandbox-macos-permissive-open.sb 890 B 0 B
./bundle/sandbox-macos-permissive-proxied.sb 1.31 kB 0 B
./bundle/sandbox-macos-restrictive-open.sb 3.36 kB 0 B
./bundle/sandbox-macos-restrictive-proxied.sb 3.56 kB 0 B
./bundle/sandbox-macos-strict-open.sb 4.82 kB 0 B
./bundle/sandbox-macos-strict-proxied.sb 5.02 kB 0 B
./bundle/src-QVCVGIUX.js 47 kB 0 B
./bundle/start-343KNR4R.js 0 B -652 B (removed) 🏆
./bundle/tree-sitter-7U6MW5PS.js 274 kB 0 B
./bundle/tree-sitter-bash-34ZGLXVX.js 1.84 MB 0 B
./bundle/cleanup-W5ON43VV.js 932 B +932 B (new file) 🆕
./bundle/start-VTNJ5LEF.js 652 B +652 B (new file) 🆕

compressed-size-action

@gemini-cli gemini-cli Bot added priority/p2 Important but can be addressed in a future release. area/core Issues related to User Interface, OS Support, Core Functionality area/security Issues related to security labels May 12, 2026
@cocosheng-g cocosheng-g added this pull request to the merge queue May 13, 2026
Merged via the queue into main with commit 297d3a3 May 13, 2026
27 checks passed
@cocosheng-g cocosheng-g deleted the issue-25460 branch May 13, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Issues related to User Interface, OS Support, Core Functionality area/security Issues related to security priority/p2 Important but can be addressed in a future release.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OAuth refresh tokens discarded on access_token rotation and on stale-credential delete

2 participants