Skip to content

feat(github): add --refresh flag to mint a fresh OAuth token#10317

Merged
jdx merged 2 commits into
mainfrom
claude/oauth-token-refresh
Jun 11, 2026
Merged

feat(github): add --refresh flag to mint a fresh OAuth token#10317
jdx merged 2 commits into
mainfrom
claude/oauth-token-refresh

Conversation

@jdx

@jdx jdx commented Jun 11, 2026

Copy link
Copy Markdown
Owner

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_id app to push to a repo: authorize → install app → every push still 403s, because mise token github --oauth keeps serving the pre-installation token from cache with no way to force a re-mint.

Changes

  • mise token github --oauth --refresh (and the hidden mise github token alias) 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. --refresh requires --oauth.
  • TokenRequest gains a force_refresh field; the existing refresh_cached_token stale-token machinery is reused by passing the cached token as stale.
  • Regenerated mise.usage.kdl and the man page.

Tests

  • New unit test 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).
  • Verified live: mise token github --oauth --refresh --raw minted 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.
  • All 1131 unit tests and lint pass.

🤖 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 --refresh to mise token github (and the hidden mise github token alias), gated on --oauth, so users can force a new GitHub App OAuth token without deleting the on-disk cache.

When force_refresh is set on TokenRequest, 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.

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>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@jdx, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: be37c3ae-bb21-4994-8be7-a70fcc5ba150

📥 Commits

Reviewing files that changed from the base of the PR and between 4f7e26f and 95b7a86.

📒 Files selected for processing (7)
  • docs/cli/token/github.md
  • man/man1/mise.1
  • mise.usage.kdl
  • src/cli/github/token.rs
  • src/cli/token/github.rs
  • src/github/oauth.rs
  • xtasks/fig/src/mise.ts

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds --refresh to mise token github --oauth (and its hidden mise github token alias) to force a fresh OAuth token even when the cached one is still time-valid — useful when a GitHub App's installations or permissions change after the original token was minted.

  • TokenRequest gains force_refresh: bool; when set, the valid-cache short-circuit in token_async is skipped and the existing refresh_cached_token machinery is reused by passing the current token as stale (refresh-grant first, device flow as fallback).
  • Both CLI entry points wire the flag through with requires = \"oauth\", and a new unit test covers the forced-refresh path against a mock token endpoint.

Confidence Score: 4/5

Safe 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: force_refresh only activates on an explicit opt-in flag, the Default impl sets it to false, and all existing call sites pass force_refresh: false. The previously flagged concern in src/github/oauth.rs — that --refresh can permanently evict a valid cached token when no refresh token exists (classic OAuth tokens) before the device flow is triggered — is a real edge case that remains open.

src/github/oauth.rs — specifically the token_async path when force_refresh = true and cached.refresh_token is None.

Important Files Changed

Filename Overview
src/github/oauth.rs Core OAuth logic: adds force_refresh field to TokenRequest and skips valid-cache short-circuit when set; reuses existing refresh_cached_token by passing current token as stale. The previously flagged token-invalidation edge case is the main concern.
src/cli/token/github.rs Adds --refresh flag with requires = "oauth" constraint and passes force_refresh: self.refresh to TokenRequest. Straightforward and correct.
src/cli/github/token.rs Hidden alias gets --refresh field wired into From<Token> for Github conversion. Correct; the alias's AFTER_LONG_HELP example block is not updated to show --refresh usage, a minor omission consistent with the hidden nature of the alias.
mise.usage.kdl Adds --refresh flag to both mise token github and mise github token KDL entries. The alias entry's help text omits the "Use after changing…" sentence (already flagged in a previous thread).
docs/cli/token/github.md Adds --refresh flag documentation and an example invocation. Accurate and consistent with the implementation.
xtasks/fig/src/mise.ts Adds --refresh completion entry to token github; the hidden github token alias does not appear in this file so no corresponding entry is needed.

Reviews (2): Last reviewed commit: "chore: render docs/cli and fig spec for ..." | Re-trigger Greptile

Comment thread src/github/oauth.rs
Comment on lines 225 to 239
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);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 --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.

Fix in Claude Code

Comment thread mise.usage.kdl
Comment on lines +1010 to 1012
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Suggested change
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!

Fix in Claude Code

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown

Hyperfine Performance

mise x -- echo

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>
@jdx jdx merged commit fe5b7ed into main Jun 11, 2026
35 checks passed
@jdx jdx deleted the claude/oauth-token-refresh branch June 11, 2026 20:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant