feat(github): add --refresh flag to mint a fresh OAuth token#10317
Conversation
GitHub App user tokens are scoped to the installations and permissions that existed when they were minted. When the app's installation changes afterwards (e.g. the app is installed on a repo after authorizing, or its permissions are expanded), the cached token silently keeps its original access until it expires hours later — and the only workaround was manually deleting ~/.local/state/mise/github-oauth-tokens.toml. `mise token github --oauth --refresh` now forces a fresh mint even when the cached token is still time-valid: the refresh-token grant is tried first (no user interaction), falling back to a new device-code flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 15 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
Comment |
Greptile SummaryAdds
Confidence Score: 4/5Safe to merge with awareness of the existing open thread about cache invalidation when no refresh token is present. The change is small and well-scoped: src/github/oauth.rs — specifically the Important Files Changed
Reviews (2): Last reviewed commit: "chore: render docs/cli and fig spec for ..." | Re-trigger Greptile |
| if let Some(cached) = cache.tokens.get(&cache_key).cloned() { | ||
| match refresh_cached_token(&cache_key, None).await { | ||
| // Passing the cached token as "stale" forces the refresh-token grant | ||
| // even though the token is still time-valid. | ||
| let stale_access_token = req.force_refresh.then_some(cached.access_token.as_str()); | ||
| match refresh_cached_token(&cache_key, stale_access_token).await { | ||
| Ok(Some(token)) => return Ok(token), | ||
| Ok(None) => {} | ||
| Err(err) => { | ||
| debug!("failed to refresh GitHub OAuth token: {err:#}"); | ||
| } | ||
| } | ||
| if cached.expires_at > chrono::Utc::now() { | ||
| if !req.force_refresh && cached.expires_at > chrono::Utc::now() { | ||
| return Ok(cached.access_token); | ||
| } | ||
| } |
There was a problem hiding this comment.
--refresh permanently destroys a valid cached token when no refresh token is available
When force_refresh=true, the current access token is passed as the stale_access_token argument to refresh_cached_token, which sets invalidate_on_none = true. If refresh_token then returns Ok(None) — which happens when cached.refresh_token is None or when refresh_expires_at has passed — the function overwrites the cache entry with expires_at = Utc::now() and returns Ok(None).
Back in token_async, the !req.force_refresh && cached.expires_at > ... guard is skipped (because force_refresh is true), so the still-valid in-memory cached.access_token is never returned. The code falls through to the device-code flow. If the user cancels that prompt (or it times out), token_async returns an error and the previously working token is permanently gone from disk — the user must fully re-authorize.
This matters in practice: a token issued from a GitHub App whose "Expire user authorization tokens" setting is disabled has no refresh token, so --refresh will always hit this path and destroy the valid credential.
| flag --refresh help="[experimental] Mint a fresh OAuth token even if the cached one has not expired, via the refresh-token grant or a new device-code flow" | ||
| flag --unmask help="Show the full unmasked token" | ||
| arg "[HOST]" help="GitHub hostname" required=#false default=github.com |
There was a problem hiding this comment.
The
--refresh help string for the mise github token alias is missing the "Use after changing…" sentence that appears in the mise token github version, giving users of the alias less context about when to use the flag.
| flag --refresh help="[experimental] Mint a fresh OAuth token even if the cached one has not expired, via the refresh-token grant or a new device-code flow" | |
| flag --unmask help="Show the full unmasked token" | |
| arg "[HOST]" help="GitHub hostname" required=#false default=github.com | |
| flag --refresh help="[experimental] Mint a fresh OAuth token even if the cached one has not expired, via the refresh-token grant or a new device-code flow. Use after changing the GitHub App's installations or permissions: cached tokens keep their original access until they expire" | |
| flag --unmask help="Show the full unmasked token" | |
| arg "[HOST]" help="GitHub hostname" required=#false default=github.com |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.6.2 x -- echo |
19.0 ± 0.8 | 17.5 | 24.0 | 1.00 |
mise x -- echo |
19.9 ± 1.4 | 18.3 | 36.1 | 1.05 ± 0.09 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.6.2 env |
18.9 ± 0.9 | 17.3 | 27.7 | 1.00 |
mise env |
19.6 ± 0.8 | 18.1 | 23.6 | 1.04 ± 0.07 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.6.2 hook-env |
19.6 ± 0.8 | 17.9 | 22.9 | 1.00 |
mise hook-env |
20.4 ± 0.8 | 18.6 | 24.1 | 1.04 ± 0.06 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.6.2 ls |
16.0 ± 0.7 | 14.4 | 18.7 | 1.00 |
mise ls |
16.6 ± 0.8 | 14.7 | 20.4 | 1.04 ± 0.07 |
xtasks/test/perf
| Command | mise-2026.6.2 | mise | Variance |
|---|---|---|---|
| install (cached) | 138ms | 138ms | +0% |
| ls (cached) | 61ms | 61ms | +0% |
| bin-paths (cached) | 65ms | 65ms | +0% |
| task-ls (cached) | 129ms | 130ms | +0% |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
GitHub App user tokens are scoped to the installations and permissions that existed when they were minted. If the app's installation changes afterwards — e.g. you authorize via the device flow first and install the app on a repo second, or you expand the app's permissions — the cached token silently keeps its original (lesser) access until it expires hours later. The only workaround was manually deleting
~/.local/state/mise/github-oauth-tokens.toml.This came up in practice while setting up a
github.oauth_client_idapp to push to a repo: authorize → install app → every push still 403s, becausemise token github --oauthkeeps serving the pre-installation token from cache with no way to force a re-mint.Changes
mise token github --oauth --refresh(and the hiddenmise github tokenalias) forces a fresh mint even when the cached token is still time-valid: the refresh-token grant is tried first (no user interaction needed), falling back to a new device-code flow.--refreshrequires--oauth.TokenRequestgains aforce_refreshfield; the existingrefresh_cached_tokenstale-token machinery is reused by passing the cached token as stale.mise.usage.kdland the man page.Tests
force_refresh_mints_new_token_despite_valid_cache: with a time-valid cached token and a mock token endpoint, the non-forced path returns the cached token, the forced path mints and caches the new one (including rotating the refresh token).mise token github --oauth --refresh --rawminted a new token via the refresh grant with no device prompt, and the new token immediately saw an app installation created after the original token was minted.🤖 Generated with Claude Code
Note
Medium Risk
Touches GitHub OAuth token caching and refresh behavior; mistakes could break auth or unexpectedly trigger device flow, but scope is limited to the experimental OAuth CLI path with tests.
Overview
Adds
--refreshtomise token github(and the hiddenmise github tokenalias), gated on--oauth, so users can force a new GitHub App OAuth token without deleting the on-disk cache.When
force_refreshis set onTokenRequest, the OAuth layer skips returning a still-valid cached access token, tries the refresh-token grant by treating the cached token as stale, and only falls back to the unexpired cache or device flow when refresh is not forced. Docs, usage KDL, man page, and Fig completions are updated; a unit test covers forced vs normal refresh against a mock token endpoint.Reviewed by Cursor Bugbot for commit 95b7a86. Bugbot is set up for automated code reviews on this repo. Configure here.