Skip to content

Add OAuth refresh token support#773

Draft
catkins wants to merge 1 commit intomainfrom
catkins/oauth-refresh-tokens
Draft

Add OAuth refresh token support#773
catkins wants to merge 1 commit intomainfrom
catkins/oauth-refresh-tokens

Conversation

@catkins
Copy link
Copy Markdown

@catkins catkins commented Apr 9, 2026

Description

When the Buildkite API issues short-lived access tokens with refresh tokens during OAuth login, the CLI currently has no way to renew them — every expired token results in a 401 and a manual bk auth login.

This adds transparent refresh token support: expired access tokens are automatically renewed via the OAuth refresh grant without user intervention.

Design

  • Shared TokenSource: A mutable token holder read by AuthTransport on every request (both REST and GraphQL). After a refresh, all subsequent requests immediately use the new token — no stale cached values.
  • Compare-after-lock guard: When concurrent requests hit 401, only the first performs the refresh. Others detect the token already changed and skip. Critical with token rotation where reuse revokes the entire family.
  • Selective error handling: Only terminal OAuth errors (invalid_grant, unauthorized_client) clear the stored refresh token. Transient failures (network, timeouts) preserve it.
  • Body buffering: Request bodies are buffered so POST requests (GraphQL mutations) can be replayed on retry.
  • Debug redaction: Token values are redacted in --debug output for both request bodies and responses.

Testing

  • Tests have run locally (with go test ./...)
  • Code is formatted (with go fmt ./...)
  • Linting passes (golangci-lint run)
  • Unit tests for: compare-after-lock, transient error preservation, body buffering, concurrent coordination, terminal vs transient error classification, TokenSource thread safety, RefreshAccessToken success/error
  • Verified full flow against local dev server (auth code → refresh token → token rotation → reuse rejection)

Disclosures / Credits

This PR was authored with Amp (Claude).

@catkins catkins force-pushed the catkins/oauth-refresh-tokens branch from 2674528 to 83bcd9b Compare April 9, 2026 12:04
When the Buildkite API issues short-lived access tokens alongside
refresh tokens during OAuth login, the CLI now transparently handles
token renewal without user intervention.

Key design decisions:

- TokenSource + AuthTransport: A shared mutable TokenSource is read by
  AuthTransport on every request (both REST and GraphQL), so after a
  refresh all subsequent requests immediately use the new token. This
  avoids the stale-token problem where cached tokens cause repeated
  401s.

- Compare-after-lock guard: When multiple concurrent requests get 401s,
  only the first one performs the refresh. Subsequent goroutines detect
  that the TokenSource already has a newer token and skip the refresh.
  This is critical with token rotation where reuse revokes the family.

- Selective error handling: Only terminal OAuth errors (invalid_grant,
  unauthorized_client) clear the stored refresh token. Transient
  failures (network errors, timeouts) preserve it so the user isn't
  locked out unnecessarily.

- Body buffering: Request bodies are buffered before the first attempt
  so POST requests (including GraphQL mutations) can be safely replayed
  on retry after a token refresh.

- Debug log redaction: Sensitive values (refresh_token, access_token,
  code, code_verifier) are redacted in debug output for both
  form-encoded request bodies and JSON responses.

Changes:
- pkg/oauth: Add RefreshToken/ExpiresIn to TokenResponse, add
  RefreshAccessToken() function
- pkg/keyring: Add refresh token storage (SetRefreshToken,
  GetRefreshToken, DeleteRefreshToken)
- internal/config: Add RefreshToken()/RefreshTokenForOrg() accessors
- internal/http: Add TokenSource, AuthTransport, RefreshTransport
- pkg/cmd/factory: Wire shared transport chain, remove cached token
  from gqlHTTPClient, add body redaction to debug transport
- cmd/auth/login: Persist refresh token, show expiry info
- cmd/auth/logout: Clear refresh tokens on logout

Amp-Thread-ID: https://ampcode.com/threads/T-019d713e-0ddf-736c-a9f3-515d34a7d0e0
Co-authored-by: Amp <amp@ampcode.com>
@catkins catkins force-pushed the catkins/oauth-refresh-tokens branch from 83bcd9b to 08f2c3c Compare April 9, 2026 12:16
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