Repository navigation
Conversation
…ential Change AuthFlow to accept a requestRefreshToken preference and return an AuthResult. When the server issues a refresh token the result carries a refreshable Credential whose expiry is anchored to the instant before the flow began; otherwise it carries a plain token. Co-authored-by: Copilot <[email protected]> Copilot-Session: 2dc3f23c-61a6-43a7-85cd-90aa872a7fc6
…t helpers An external git credential helper cannot refresh a short-lived token and would keep serving it after it expires. Teach GitCredentialFlow.Setup to reject any existing credential and return a warning in that case instead of storing one, and extract Updater.RejectExisting so the rejection can be reused. Co-authored-by: Copilot <[email protected]> Copilot-Session: 2dc3f23c-61a6-43a7-85cd-90aa872a7fc6
Request a short-lived token when the user prefers it, store the resulting refreshable credential through LoginRefreshable, and report whether the host issued a short-lived token. The authorization step is indirected so tests can stub it. Co-authored-by: Copilot <[email protected]> Copilot-Session: 2dc3f23c-61a6-43a7-85cd-90aa872a7fc6
Add a --short-lived flag that asks the host for a refreshable credential gh renews automatically. It is a preference, not a guarantee, and is mutually exclusive with --with-token. Co-authored-by: Copilot <[email protected]> Copilot-Session: 2dc3f23c-61a6-43a7-85cd-90aa872a7fc6
Add a --short-lived flag to gh auth refresh and store a refreshable credential when the host issues one. Warn up front when refreshing an existing short-lived token without --short-lived, since that downgrades it to a non-expiring token. Co-authored-by: Copilot <[email protected]> Copilot-Session: 2dc3f23c-61a6-43a7-85cd-90aa872a7fc6
…or git Resolve the token through ActiveTokenWithRefresh so an expired short-lived token is renewed before it is handed to git, and fail with a re-authentication notice when the refresh token is rejected. Under git's authtype capability mark a refreshable token ephemeral so a chained caching helper will not persist it, and warn on older git that cannot express this. Co-authored-by: Copilot <[email protected]> Copilot-Session: 2dc3f23c-61a6-43a7-85cd-90aa872a7fc6
| case result.Refreshable != nil: | ||
| fmt.Fprintf(opts.IO.ErrOut, "%s Received short-lived refreshable token\n", cs.SuccessIcon()) | ||
| case wasRefreshable && !opts.ShortLived: | ||
| fmt.Fprintf(opts.IO.ErrOut, "%s Original token was short-lived; pass --short-lived to preserve it\n", cs.WarningIcon()) |
There was a problem hiding this comment.
I saw this getting printed twice during gh auth refresh:
➜ williammartin-supreme-umbrella git:(babakks/refresh-token-e2e) ./bin/gh auth refresh --insecure-storage
? What account do you want to refresh auth for? github.com
! Original token was short-lived; pass --short-lived to preserve it
! One-time code (536C-8241) copied to clipboard
Press Enter to open https://github.com/login/device in your browser...
✓ Authentication complete.
! Original token was short-lived; pass --short-lived to preserve it
|
|
||
| if !opts.ResetScopes { | ||
| if oldToken := authCfg.ActiveToken(hostname).Token; oldToken != "" { | ||
| if oldScopes, err := shared.GetScopes(plainHTTPClient, hostname, oldToken); err == nil { |
There was a problem hiding this comment.
If the token has expired then what happens here? Are we able to get the scopes? If not, will this just drop the scopes? Maybe we need to use ActiveTokenWithRefresh?
williammartin
left a comment
There was a problem hiding this comment.
in practice most users who let gh manage git already have gh configured as their helper, so this path is uncommon to begin with.
Based on what data?
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Credential replacement can retain stale refresh tokens, and interactive short-lived login can silently bypass the requested OAuth flow.
Review effort: Balanced
Findings: 3
Open (5)
Clear stale refreshable credential when downgrading to plain token · New Force web flow for short-lived interactive login · New Remove refreshable record when storing a permanent token · New Test production OAuth flow and refresh metadata handling · New Warn that rejected credentials require setup before the next Git operation · New
What changed in this PR
Adds user-facing short-lived OAuth credential support across login, refresh, storage, and Git authentication.
Changes:
- Adds
--short-livedto authentication commands. - Stores and refreshes renewable credentials.
- Prevents Git helpers from caching stale short-lived tokens.
| File | Description |
|---|---|
pkg/cmd/auth/shared/login_flow.go |
Handles refreshable login results. |
pkg/cmd/auth/shared/login_flow_test.go |
Tests short-lived login outcomes. |
pkg/cmd/auth/shared/gitcredentials/updater.go |
Extracts credential rejection. |
pkg/cmd/auth/shared/gitcredentials/updater_test.go |
Tests rejection behavior. |
pkg/cmd/auth/shared/git_credential.go |
Protects refreshable credentials from external helpers. |
pkg/cmd/auth/shared/git_credential_test.go |
Tests external-helper policy. |
pkg/cmd/auth/refresh/refresh.go |
Adds short-lived credential refresh support. |
pkg/cmd/auth/refresh/refresh_test.go |
Tests refreshable credential storage. |
pkg/cmd/auth/login/login.go |
Adds the --short-lived login flag. |
pkg/cmd/auth/login/login_test.go |
Tests incompatible login flags. |
pkg/cmd/auth/gitcredential/helper.go |
Refreshes and marks Git credentials ephemeral. |
pkg/cmd/auth/gitcredential/helper_test.go |
Tests ephemeral and expired credentials. |
internal/authflow/refresh.go |
Maps OAuth responses to credentials. |
internal/authflow/flow.go |
Returns refreshable authentication results. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| return err | ||
| } | ||
| } else { | ||
| if _, err := authCfg.Login(hostname, result.Username, result.Token, "", !opts.InsecureStorage); err != nil { |
| if authFlow == nil { | ||
| authFlow = authflow.AuthFlow | ||
| } | ||
| result, err := authFlow(opts.PlainHTTPClient, hostname, opts.IO, "", append(opts.Scopes, additionalScopes...), opts.Interactive, opts.Browser, opts.CopyToClipboard, opts.ShortLived) |
| if refreshableCredential != nil { | ||
| insecureStorageUsed, err = cfg.LoginRefreshable(hostname, username, *refreshableCredential, gitProtocol, opts.SecureStorage) | ||
| } else { | ||
| insecureStorageUsed, err = cfg.Login(hostname, username, authToken, gitProtocol, opts.SecureStorage) |
| ClientSecret: oauthClientSecret, | ||
| CallbackURI: getCallbackURI(oauthHost), | ||
| Scopes: scopes, | ||
| RequestRefreshToken: requestRefreshToken, |
| return fmt.Sprintf( | ||
| "The configured git credential helper for %[1]s cannot refresh short-lived tokens, so git operations will fail once the token expires. Run 'gh auth setup-git --hostname %[1]s' to let gh manage git authentication.", | ||
| hostname, | ||
| ), nil |
williammartin
left a comment
There was a problem hiding this comment.
Options considered for the "refreshable token + non-gh helper" case:
Reject the existing credential and warn, storing nothing, with a hint to run gh auth setup-git --hostname HOST. (Chosen.)
Store the expiring token in the external helper anyway, with a warning that it will expire and the helper cannot refresh it.
Explain the problem, prompt the user, and offer to configure gh as the helper for the host; if yes proceed with that, otherwise reject and warn.
On every token refresh, write the new access token back into the non-gh helper to keep it fresh.
3 seems most reasonable here, and it should be the first thing that is shown. Alternatively, we could let them proceed and inform their credential helper with the warning that it won't handle expiry. Up to them.
williammartin
left a comment
There was a problem hiding this comment.
As I review all this, I get the sense that --expiring is a better flag than --short-lived. The former is a statement of fact about what will happen, and the latter is subjective. In most cases it also seems like expiring is how this feature is referred to https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#expiring-access-tokens


Part of #14449. Based on #14451 (domain and logic).
Description
This is where refreshable tokens become reachable by a user. It adds the
--short-livedopt-in togh auth login, stores the resulting refreshable credential, makesgh auth refreshpreserve or acquire one, and teaches the git credential helper to serve short-lived tokens safely.It lands, bottom to top:
AuthResultfrom the auth flow that optionally carries a refreshable credential.gh auth login --short-livedflag itself.gh auth refreshpreserving an existing refreshable credential or acquiring one.How did you test this change?
This feature is verified end to end as a whole rather than per PR. End-to-end tests should pass.
Key points
git 2.46 and not caching short-lived tokens in git
When short-lived (refreshable) credentials are used for git operations through the gh credential helper (
gh auth git-credential), git 2.46 or newer is recommended. git 2.46 introduced theauthtypecredential capability, which lets a helper mark a credential asephemeral. gh uses that to emit the token asauthtype=Basicwith an ephemeralcredentialfield, so a caching helper (for examplegit-credential-cache) refuses to store it. This prevents a short-lived token from being cached and then reused after it has been refreshed or replaced out of band (for example bygh auth refreshorgh auth login), which would otherwise surface as a stale, rejected token.On older git the capability is unavailable, so gh cannot mark the token non-cacheable. In that case gh still returns the token (as
username/password) and prints a one-line warning on stderr advising the user to upgrade git or avoid a credential caching helper. Non-refreshable tokens are unaffected, so users who do not opt into--short-livedsee no change and have no new requirement.Framing for the changelog and release notes: this is a recommendation tied specifically to the
--short-livedopt-in, not a hard requirement for all gh git operations; the risk only materializes when a credential caching helper is chained in front of gh, which is the user's own git configuration and an uncommon setup; and gh detects the capability from git at runtime, so there is nothing to configure and upgrading git is the only action.Never storing a refreshable token in a non-gh git helper
During
gh auth loginandgh auth refresh, gh may offer to configure git to use the obtained credential. This external-store path (GitCredentialFlow.SetupcallingUpdater.Update, i.e.git credential approve) only runs when all of these hold: the git protocol is https (not ssh); a credential helper other than gh is already configured for the host; the command was run interactively; and the user answered yes to the "Authenticate Git with your GitHub credentials?" prompt.Historically this was safe because the stored token never expired. With refreshable (short-lived) tokens it is no longer safe: a third-party helper has no knowledge of gh's refresh mechanism, so it would persist a soon-stale secret and keep serving it after expiry, and there is no path for gh to re-approve the rotated token into that external helper. Git operations would then fail with a stale, rejected token hours after a login that appeared to succeed.
New behavior: when the issued credential is refreshable and a non-gh helper is configured, gh does not store the token in that helper. It rejects any existing credential for the host (so the problem surfaces immediately rather than silently after expiry) and prints a warning naming the host and the exact remedy,
gh auth setup-git --hostname HOST, which reconfigures git to use gh as the credential helper. Once gh is the helper, the ephemeral/refresh path above handles short-lived tokens correctly. Non-refreshable tokens keep the previous behavior unchanged.Options considered for the "refreshable token + non-gh helper" case:
gh auth setup-git --hostname HOST. (Chosen.)Why option 1: it is the simplest change with the fewest side effects, and it stays consistent with the ephemeral design used elsewhere (we deliberately avoid caching a refreshable token in any store that cannot refresh it). It does not persist a short-lived secret in a store that cannot manage it, so the whole feature has one coherent stance: refreshable tokens live only where gh can rotate them. Rejecting the existing credential makes the incompatibility surface immediately, at login/refresh time, in front of an interactive user who can act on the hint. And the remedy is a single existing command; in practice most users who let gh manage git already have gh configured as their helper, so this path is uncommon to begin with.
Why the others were rejected: option 2 guarantees the exact failure we are trying to prevent (a token that works at first and then silently breaks on expiry, with no mechanism to re-approve the refreshed token). Option 3 is the nicest UX and a reasonable future enhancement, but it is more code and test surface for what is functionally "run
ConfigureOursinline," which is exactly whatgh auth setup-gitalready does. Option 4 is both more complex and arguably sneaky: silently writing to a user's third-party credential helper on every background refresh is surprising behavior, and the refresh happens deep in the HTTP path without git-client access or user consent in scope.Implementation seam: the decision lives in
GitCredentialFlow.Setup(policy), which returns an undecorated warning string; the callers (login_flow.go,refresh.go) own presentation and stdio, prefixing the warning icon and printing to stderr. The reject-only operation is a newUpdater.RejectExistingmethod, factored out ofUpdater.Update.No GHES / feature-detection gate
Refreshable tokens are requested by adding the
offline_accessscope during the OAuth device flow. A server that does not support refreshing simply ignores that scope and returns an ordinary non-expiring token, exactly as before. gh's login path was written to accept whatever kind of token the server returns and to treat "not refreshable" as the normal, non-expiring case, so there is nothing version-specific to detect. Older GHES instances therefore keep working unchanged with no capability probe and no// TODO <cleanup>gate, and none is added.Notes for reviewers
internal/authflow/flow.gogets its publicAuthFlowsignature change here (PR 2 only adjusted the local adapter). Good starting point: theAuthResultchange, then the non-gh-helper policy inauth/shared(the most consequential decision in this PR), then the--short-livedflag, then the git credential helper.Commits (review in order):
fix(authflow): return an AuthResult with an optional refreshable credentialfix(auth/shared): do not store refreshable credentials in external git helpersfix(auth/shared): store refreshable credentials during loginfix(auth login): add --short-lived flagfix(auth refresh): preserve or acquire refreshable credentialsfix(auth gitcredential): refresh tokens and mark them non-cacheable for gitAuthorship and follow-up
Who wrote this:
Who answers review comments: