fix(cli): serialize token refresh across processes#34
Open
appleboy wants to merge 2 commits into
Open
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR prevents concurrent authgate-cli processes from racing during refresh-token rotation by serializing the refresh-and-save critical section using a cross-process advisory lock, and adds coverage to ensure peer-refreshed tokens short-circuit the network call.
Changes:
- Add
lockTokenStore(gofrs/flock-based) to serialize refresh across CLI processes. - Update
refreshAccessTokento accept the full stale token, re-load the store after lock acquisition, and return peer-refreshed tokens without a refresh request. - Add a new test ensuring no network call occurs when a peer already refreshed, and update existing tests/call sites to the new refresh API.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| main_test.go | Updates test config to include TokenFile, adjusts refresh tests to new signature, and adds peer-refresh short-circuit test. |
| main.go | Updates refresh call site to pass the full stored token. |
| auth.go | Implements lock + peer-check logic in refreshAccessToken; updates auto-refresh call site. |
| config.go | Adds TokenFile to AppConfig and wires it through config/store initialization. |
| lock.go | Introduces cross-process advisory lock implementation used by refresh flow. |
| go.mod | Adds github.com/gofrs/flock dependency. |
| go.sum | Adds checksums for the new dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
291eef5 to
b18e909
Compare
- Take a cross-process advisory lock (via gofrs/flock) around the refresh-and-save critical section so concurrent CLI invocations cannot spend the same refresh token twice - Re-read the store under the lock and return a peer process's fresh token when it has already completed a refresh, avoiding an unnecessary network call and a guaranteed invalid_grant on rotation servers - Change refreshAccessToken to accept the full stale Token so peer-refresh detection can compare access and refresh token fields - Expose TokenFile on AppConfig so the lock path is known regardless of the active storage backend - Add a test that pre-populates the store with a peer-refreshed token and asserts refreshAccessToken returns it without hitting the network
b18e909 to
17bddfd
Compare
- Anchor the refresh lock under the user cache dir when the token file path is relative, so keyring/auto runs no longer require a writable working directory and still serialize across directories - Hash the client ID into the lock filename so path separators or traversal segments cannot escape the lock directory or produce an invalid name - Add lock tests covering acquire and release, hostile client IDs, and relative-path placement
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two concurrent
authgate-cliinvocations that both see an expired access token race to call the refresh endpoint with the same refresh token. On servers that rotate refresh tokens (the go-authgate default), the first request consumes the token and the second getsinvalid_grant, forcing the second CLI back into an interactive login. This PR serialises the refresh critical section across processes so only one refresh happens.How it works
github.com/gofrs/flock) wraps theload → refresh → savecritical section. It relies on kernelfcntl/LockFileExand is released automatically if the holder crashes — no stale-lock heuristics required.refreshAccessTokennow takes the full stalecredstore.Token(instead of just the refresh-token string) so peer-refresh detection can compare both the access and refresh token fields.refreshAccessToken, which every refresh path (run,ensureFreshToken,makeAPICallWithAutoRefresh) funnels through. For an explicit absolute token-file path the lock sits next to the file; for relative paths (the default, and keyring/auto backends where the token never touches disk) it is anchored under the user cache dir, so refresh never requires a writable working directory and concurrent runs from different directories still serialise. The client ID is hashed into the lock filename so it can never escape the lock directory.Test plan
go test ./...passes (incl. existingTestRefreshAccessToken_RotationMode)TestRefreshAccessToken_PeerAlreadyRefreshedasserts no network call is made when the store already contains a peer-refreshed tokenlock_test.gocovers acquire/release, hostile client IDs (path traversal), and relative-path placementmake lintis clean (golangci-lint: 0 issues)Notes
*flock.Flockalready satisfiesio.Closer), and the peer-check reuses the sharedtokenUsablepredicate.🤖 Generated with Claude Code