Skip to content

fix(init): re-auth instead of hard-failing when the stored token is expired#1

Open
piekstra wants to merge 1 commit into
mainfrom
fix/init-expired-token-reauth
Open

fix(init): re-auth instead of hard-failing when the stored token is expired#1
piekstra wants to merge 1 commit into
mainfrom
fix/init-expired-token-reauth

Conversation

@piekstra

Copy link
Copy Markdown
Contributor

Problem

gro init --credentials-file <json> hard-fails when the stored OAuth token is expired or revoked, instead of offering re-auth:

Error: getting profile: Get "https://gmail.googleapis.com/gmail/v1/users/me/profile?alt=json&prettyPrint=false": oauth2: "invalid_grant" "Token has been expired or revoked."
Usage:
  gro init [flags]
...

The only workaround is manually deleting the keyring entry (security delete-generic-password -s google-readonly -a "default/oauth_token") — the exact remediation init exists to provide. Testing-mode OAuth apps expire refresh tokens every ~7 days, so this bites constantly.

Cause

An expired refresh token fails during token refresh inside the oauth2 transport: the token endpoint returns HTTP 400 invalid_grant, surfaced as *oauth2.RetrieveError wrapped in *url.Error. The request never reaches the Gmail API, so there is no googleapi.Error 401 and no "401" substring — isAuthError returned false, and tryExistingToken propagated the error as fatal instead of falling through to the re-auth flow (which already exists and handles googleapi 401s fine).

Fix

  • isAuthError now recognizes *oauth2.RetrieveError with ErrorCode == "invalid_grant" via errors.As (works through the url.Error/fmt.Errorf wrapping), plus the corresponding strings in wrapped/legacy error shapes — those strings only ever come from the OAuth token endpoint, so they're definitive; a bare 401 still requires corroboration, and genuine network failures are unaffected.
  • Under --auth-code-stdin (two-phase install) the re-auth confirmation prompt is now skipped with a notice: there's no TTY and stdin is reserved for the auth code, so the huh prompt could only ever fail with "could not open a new TTY".

Behavior for a valid existing token is unchanged ("Already authenticated as …" short-circuit), as are the stale-scope (#107) paths.

Testing

  • New TestIsAuthError table entries: bare and production-wrapped RetrieveError (invalid_grant → true, other codes → false), no-401 invalid_grant string → true.
  • TestRunWithExpiredRefreshTokenPromptsReauth: regression test — the exact production error shape now routes into confirm → delete → fresh OAuth flow.
  • TestRunWith_AuthCodeStdinExpiredTokenSkipsReauthPrompt: two-phase install with an expired token completes without any interactive prompt.
  • go build ./..., go vet ./..., go test ./..., golangci-lint run all clean.

Note for consumers: google-readonly (gro) and google-readwrite (grw) both get this via the shared initcmd package — each needs a dependency bump after this merges.

https://claude.ai/code/session_018kd6nduhCb2p3RzVvbPnMF

…xpired

An expired/revoked refresh token fails during token refresh inside the
oauth2 transport: the token endpoint returns HTTP 400 invalid_grant,
surfaced as *oauth2.RetrieveError wrapped in *url.Error — the request
never reaches the API, so no googleapi 401 and no "401" substring
exists for isAuthError to match. tryExistingToken then propagated the
error and init aborted:

    Error: getting profile: Get "https://gmail.googleapis.com/...":
    oauth2: "invalid_grant" "Token has been expired or revoked."

forcing users to manually delete the keyring entry before init would
run — the exact remediation init exists to provide. Testing-mode OAuth
apps hit this every 7 days.

isAuthError now recognizes *oauth2.RetrieveError with ErrorCode
invalid_grant (and the corresponding strings in wrapped/legacy shapes,
which only the token endpoint can produce), routing verification
failures into the existing re-auth confirm + fresh OAuth flow.

Also: under --auth-code-stdin the re-auth confirmation prompt is
skipped (with a notice) — the two-phase install has no TTY and stdin
is reserved for the auth code, so the huh prompt could only fail.
@piekstra
piekstra requested a review from piekstra-dev July 24, 2026 18:15

@piekstra-dev piekstra-dev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated PR Review

Reviewed commit: 374e73cfca19
Profile: reviewer - Posting as: piekstra-dev

Summary

Reviewer Findings
go:implementation-tests 0
policies:conventions 0

Reviewer Coverage

Reviewer Status Inspected Skipped Constraints
go:implementation-tests complete_broad initcmd/init.go, initcmd/init_test.go unavailable git commands were unavailable in this sandbox, so the exact diff hunks could not be inspected; review is based on the full head-branch contents of the two assigned files plus the PR intent description.
policies:conventions complete_broad initcmd/init.go, initcmd/init_test.go unavailable Shared CLI standards at open-cli-collective/cli-common/docs and ../.github were not present in the review context, so no comparison against those canonical docs was possible.

0 PR discussion threads considered. 0 summarized; 0 resolved.


Completed in 2m 32s | $1.94 | claude-sonnet-5 | cr 0.10.268
Field Value
Model claude-sonnet-5
Reviewers go:implementation-tests, policies:conventions
Engine claude_cli · claude-sonnet-5
Reviewed by cr · piekstra-dev
Duration 2m 32s wall · 3m 24s compute
Cost $1.94
Tokens 74 in / 13.8k out

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost Duration
orchestrator-selection claude-sonnet-5 6 1.9k 27.9k 31.8k $0.23 23s
go:implementation-tests claude-sonnet-5 32 7.0k 640.7k 87.2k $0.82 1m 50s
policies:conventions claude-sonnet-5 30 4.5k 526.0k 75.0k $0.68 1m 01s
orchestrator-rollup claude-sonnet-5 6 429 41.8k 32.1k $0.21 9s

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.

2 participants