Skip to content

Revoke OAuth token on logout - #14362

Closed
sds wants to merge 1 commit into
cli:trunkfrom
sds:sds/revoke-oauth-on-logout
Closed

sds wants to merge 1 commit into
cli:trunkfrom
sds:sds/revoke-oauth-on-logout

Conversation

@sds

@sds sds commented Sep 5, 2026

Copy link
Copy Markdown

Description

Currently, running gh auth logout does not revoke the underlying OAuth token (gho_*). This is dangerous as the token may have been compromised, has a 1-year TTL, and there is a reasonable expectation that logging out revokes the users current session. The currently documented solution of manually revoking the GitHub CLI app is rather manual and not the best experience.

Add support to explicitly revoke the token to close this gap.

This also revokes the old token when the user replaces their token via gh auth login or gh auth refresh.

How did you test this change?

There are tests included in the PR, but here is a step-by-step guide for how you can manually verify.

Note: this is written so you can test each claimed fix individually with the actual CLI. There are some redundant steps but it allows you to test any of these flows in isolation.

Case 1: Verify gh auth logout revokes the old token

Before starting, clear your existing token using the latest gh release: gh auth logout.

  1. Check out the PR locally
  2. Build
    make
  3. Login
    ./bin/gh auth login
  4. Record the current token, log out, confirm the old token is revoked
    old_token=$(./bin/gh auth token)
    ./bin/gh auth logout
    GH_TOKEN=$old_token ./bin/gh api user # Should fail with 401

Case 2: Verify gh auth login revokes the old token if already authenticated

Before starting, clear your existing token using the latest gh release: gh auth logout.

  1. Check out the PR locally
  2. Build
    make
  3. Login
    ./bin/gh auth login
  4. Record the current token, log in again, confirm the old token is revoked, confirm new token is valid
    old_token=$(./bin/gh auth token)
    ./bin/gh auth login
    GH_TOKEN=$old_token ./bin/gh api user # Fails with 401
    ./bin/gh api user # Succeeds with 200

Case 3: Verify gh auth refresh revokes the old token

Before starting, clear your existing token using the latest gh release: gh auth logout.

  1. Check out the PR locally
  2. Build
    make
  3. Login
    ./bin/gh auth login
  4. Record the current token, refresh, confirm the old token is revoked, confirm new token is valid
    old_token=$(./bin/gh auth token)
    ./bin/gh auth refresh
    GH_TOKEN=$old_token ./bin/gh api user # Fails with 401
    ./bin/gh api user # Succeeds with 200

Summary

I ran these tests myself locally, and they returned the expected results. If you run them with the currently gh relelase, any step where you test the old token (e.g. GH_TOKEN=$old_token ./bin/gh api user) will succeed instead of fail.

Key points

I did not consider this a pure security issue given it has existed for so long, but I do see it as a usability issue, and addresses the principle of least surprise.

Notes for reviewers

Fixes #9233
Fixes #13111

Authorship and follow-up

Who wrote this:

  • A human wrote it.
  • An agent wrote it under close human direction.
  • An agent wrote it independently, and no human has guided the implementation beyond the initial prompt.

Who answers review comments:

  • @username will read and reply directly. Name the account.
  • An agent will draft replies and @username will read them before they are posted.
  • Nobody has explicitly committed to replying.

Currently, running `gh auth logout` does not revoke the underlying OAuth
token (`gho_*`). This is dangerous as the token may have been
compromised, has a 1-year TTL, and there is a reasonable expectation that
logging out revokes the users current session. The currently documented
solution of manually revoking the GitHub CLI app is rather manual and
not the best experience.

Add support to explicitly revoke the token to close this gap.

This also revokes the old token when the user replaces their token via
`gh auth login` or `gh auth refresh`.

Fixes #9233
@sds
sds requested a review from a team as a code owner September 5, 2026 00:27
@sds
sds requested review from tidy-dev and a balanced review from Copilot September 5, 2026 00:27
@github-actions github-actions Bot added external pull request originating outside of the CLI core team unmet-requirements needs-triage needs to be reviewed labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Thanks for your pull request! This is a large change (803 lines across 19 files) that doesn't reference a help wanted issue.

Large feature PRs require prior discussion in an issue before implementation — this helps the team assess whether the feature aligns with the project's direction before significant effort is invested.

Please open an issue to discuss this feature first. This PR will be automatically closed in 2 days if requirements are not met.

Full contribution requirements
  1. Include a detailed description of what this PR does
  2. Link to an issue with the help wanted label (use Fixes #123 or Closes #123)

Copilot AI left a comment

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.

🟡 Changes recommended

Logout becomes network-dependent, while replacement flows can leave invalid credentials or lose failed revocations.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds OAuth token revocation and credential cleanup to authentication flows, addressing #9233 and #13111.

Changes:

  • Revokes replaced or logged-out GitHub CLI OAuth tokens.
  • Removes matching keyring and Git credential-helper entries.
  • Prevents sensitive revocation requests from HTTP debug logging.
File summaries
File Description
pkg/cmd/auth/shared/revoke.go Adds shared conditional revocation logic.
pkg/cmd/auth/shared/revoke_test.go Tests revocation eligibility and errors.
pkg/cmd/auth/shared/login_flow.go Revokes replaced tokens during login.
pkg/cmd/auth/shared/login_flow_test.go Tests login revocation.
pkg/cmd/auth/shared/gitcredentials/updater.go Adds username-aware credential rejection.
pkg/cmd/auth/shared/gitcredentials/updater_test.go Tests selective credential removal.
pkg/cmd/auth/refresh/refresh.go Revokes tokens replaced by refresh.
pkg/cmd/auth/refresh/refresh_test.go Tests refresh revocation.
pkg/cmd/auth/logout/logout.go Adds remote and local credential cleanup.
pkg/cmd/auth/logout/logout_test.go Tests logout cleanup behavior.
pkg/cmd/auth/login/login.go Wires revocation into login paths.
pkg/cmd/auth/login/login_test.go Tests non-interactive replacement.
internal/config/config.go Removes stale keyring credentials.
internal/config/auth_config_test.go Tests keyring cleanup.
internal/authflow/revoke.go Implements the revocation API request.
internal/authflow/revoke_test.go Tests revocation responses.
go.mod Promotes HTTP logging dependency.
api/request_test.go Tests suppressed HTTP logging.
api/client.go Adds per-request log suppression.
Review details
  • Files reviewed: 19/19 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +170 to +177
if len(oauthTokens) > 0 && !ghauth.IsEnterprise(hostname) {
httpClient, err := opts.PlainHttpClient()
if err != nil {
return err
}
for _, token := range oauthTokens {
if err := opts.RevokeToken(httpClient, hostname, token); err != nil {
return fmt.Errorf("failed to revoke authentication token: %w", err)
Comment on lines +17 to +18
// Reject removes credentials for a hostname and optional username from the git credential helper.
func (u *Updater) Reject(hostname, username string) error {
Comment on lines +12 to +15
func RevokeOAuthTokenIfChanged(
httpClient *http.Client,
hostname, previousToken, newToken string,
revokeToken func(*http.Client, string, string) error,
return nil
}

return revokeToken(httpClient, hostname, previousToken)
@BagToad BagToad closed this Sep 5, 2026
@sds

sds commented Sep 5, 2026 •

Copy link
Copy Markdown
Author

@BagToad: respectfully, it would be nice to get some kind of feedback.

The two issues I referenced ([1][2]) don't have help wanted, but have existed for quite some time, and even have comments from GitHub staff. The blocker for [1] has been addressed—there is a revocation API now.

I am happy to make changes, but I don't know what the issue is—you haven't given me any clear feedback.

  • Should I break it up into smaller PRs, perhaps one for each flow?
  • Should I address all the Copilot concerns and re-open?
  • Something else?

This isn't some drive-by thoughtless LLM prompt. I went over a few iterations to make sure the flows continued to work as expected.

I hope my prior public contributions to other open source projects in the past make clear I'm legitimately trying to help. This experience of having a PR closed 10 minutes after opening with zero feedback seems to send a very clear message.

@williammartin

Copy link
Copy Markdown
Member

Hi @sds,

I hope my prior public contributions to other open source projects in the past make clear I'm legitimately trying to help.

It's not a matter of your intentions, which I'm sure are great, it's just that there are a lot of people (or LLMs as proxies to people!) who want to help contribute code to gh. and while we'd love to help them all we simply do not have the time.

you haven't given me any clear feedback.

The feedback is that we want people to discuss whether to do something and how to do it in on issues before contributors before time is spent unnecessarily. Ideally people discover this in our contributing guidelines before spending their own time. Based on your response here though, it's probably reasonable to update our bot response to explain a little more in detail why we mark things help wanted and how we expect people to re-engage on issues if their PRs are closed for this reason.

Next week, let's restart the discussion on #9233. In particular I want to make sure we're thinking about:

  • backwards compatibility (whether there are people who use gh auth token and put their token elsewhere)
  • handling error cases (e.g. if people now expect tokens to be revoked, but revocation fails, I believe this PR will leave them stranded because the token has been removed from the keyring)
  • communicating how this relates to oauth tokens that may not be owned by the GitHub CLI OAuth app
  • communicating how this relates to other forms of token that might have been provided with --with-token

It's nice to have your code to refer to as a deeper dive into the solution. I appreciate you highlighting that API; I was aware of the API but had dismissed it based on it relating to app owners revoking a token, without realising that in this case, ownership is determined by having the client id and secret.

@sds

sds commented Sep 5, 2026

Copy link
Copy Markdown
Author

Thanks for the detailed response—I appreciate it.

I understand the intention behind help wanted labels, but this situation seemed like the lack of label was an oversight. Perhaps I misunderstood, but specifically in #13111 (comment) a GitHub staff member opened a pull request (#13450) in an attempt to address the issue. It did not fully address, but it seemed evident to me that there was an acknowledgement of the issue and a desire to address it.

Sounds good to discuss more comprehensively in #9233.

Some quick responses (by no means exhaustive or intended as a final statement—merely additional thoughts) to carry over to that discussion:

  • backwards compatibility (whether there are people who use gh auth token and put their token elsewhere)

Fair point. I don't have data on how the CLI is used by others, I can only speak for how I've seen it used myself and by others at my org, and I've not seen this. That said, I would say principle of least surprise still applies—when I log out, I expect my token to be revoked if the very tool I'm using generated it, regardless of where else I've decided to inject the token.

  • handling error cases (e.g. if people now expect tokens to be revoked, but revocation fails, I believe this PR will leave them stranded because the token has been removed from the keyring)

Yes, this was missed in my own review and I appreciated the automated review pointing it out. It of course should be addressed.

  • communicating how this relates to oauth tokens that may not be owned by the GitHub CLI OAuth app

Relating to the prior point, this case will silently fail. Agree we should at least output some kind of warning that the token was not owned by the GitHub CLI app—this was an oversight. Relevant code

  • communicating how this relates to other forms of token that might have been provided with --with-token

Yes, I realize I left this out in the PR description even though I had intentionally (attempted to) address it. The implementation specifically only does this with OAuth tokens—it does not attempt to revoke other kinds of tokens (relevant code). However, I did not manually verify this particular case.


Thanks again for the response. Hope we can find a path forward.

@sds

sds commented Sep 5, 2026

Copy link
Copy Markdown
Author

One additional point, tangentially related to this PR, but perhaps worthy of its own discussion outside of this repo.

During an earlier implementation, I was using the new revocation API launched in March 2026. However, that API has an interesting behavior where if you provide an OAuth token to revoke, it will revoke ALL tokens associated with the GitHub CLI app (I did not test this with other apps, so perhaps it's only for the GitHub CLI app). Yes, you read that right.

If you have two laptops, you can quickly verify by logging in on each of them separately via gh auth login, verifying via gh auth token that they have different OAuth tokens, and then using the revocation API to revoke one of them:

curl -L \
  -X POST \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  https://api.github.com/credentials/revoke \
  -d "{\"credentials\":[\"$(gh auth token)\"]}"

If you run gh auth status on both laptops, both will say their respective token is no longer valid. The documentation doesn't clearly mention this, so it feels like a bug.

This led me to use the other API for token revocation for the above PR, which is in hindsight the safer approach given the revocation API doesn't check/enforce token ownership before revocation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external pull request originating outside of the CLI core team needs-triage needs to be reviewed unmet-requirements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gh auth logout does not erase and/or revoke OAuth token in MacOS Keychain Invalidate previous OAuth token when a new one is generated with gh auth

4 participants