This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is an OAuth 2.0 Device Authorization Flow CLI tool (RFC 8628) written in Go. It authenticates with an AuthGate server and manages tokens for headless/SSH environments.
# Build the binary
make build # Output: bin/device-cli
# Run all tests with coverage
make test
# View test coverage in browser
make coverage
# Run a single test
go test -v -run TestFunctionName ./...
# Run tests for specific file
go test -v ./main_test.go main.go# Run golangci-lint
make lint
# Auto-format code
make fmt
# Check installed tools
make check-tools# Build and run locally
make build
./bin/device-cli -client-id=<id> -server-url=http://localhost:8080
# Clean build artifacts
make clean
# Cross-platform builds
make build_linux_amd64
make build_linux_arm64- main.go: Core application logic — OAuth flow, HTTP client, token management, and CLI entry point
- tui/displayer.go: Output abstraction layer (
Displayerinterface) withPlainDisplayer(non-TTY),ProgramDisplayer(BubbleTea TUI), andNoopDisplayer(tests) - tui/model.go: BubbleTea model for interactive terminal UI
- tui/messages.go: BubbleTea message types for TUI state transitions
- polling_test.go: Tests for device code polling with additive backoff
- main_test.go: Core functionality tests
Configuration uses a three-tier priority system implemented in initConfig():
- Command-line flags (highest priority)
- Environment variables
.envfile / defaults (lowest priority)
The initConfig() function is separated from init() to avoid conflicts with Go's test flag parsing. Tests manually initialize variables in their own init() function.
Token storage is handled by the credstore package from github.com/go-authgate/sdk-go. The -token-store flag controls the backend:
auto(default):SecureStorewraps keyring with file fallback. Warns if keyring is unavailable.keyring: Uses OS keyring viaTokenKeyringStore(macOS Keychain, GNOME Keyring, Windows Credential Manager)file: UsesTokenFileStorewith JSON file at the configured path
Multi-client support: All backends store tokens keyed by client_id. The credstore.Store[credstore.Token] interface provides Load(clientID) and Save(clientID, token) methods.
The main flow is orchestrated by run():
- Token Loading: Try to load existing tokens for current
client_id - Token Validation: Check if access token is expired (compare
ExpiresAtwith current time) - Refresh Flow: If expired, call
refreshAccessToken()with refresh token - Device Flow Fallback: If no tokens or refresh fails, call
performDeviceFlow() - Token Verification: Verify token with server's
/oauth/tokeninfoendpoint - Auto-refresh Demo:
makeAPICallWithAutoRefresh()demonstrates automatic refresh on 401
pollForTokenWithProgress() implements RFC 8628 polling with additive backoff:
- Initial interval from server (default: 5s)
- On
slow_downerror: adds 5s to interval per RFC 8628 §3.5, capped at 60s - Progress reported via
Displayerinterface (TUI or plain text) - Single ticker for polling
- Handles errors:
authorization_pending,slow_down,expired_token,access_denied
retryClient (initialized in initConfig()) wraps a base HTTP client with:
- TLS 1.2+ enforcement
- Connection pooling (MaxIdleConns: 10, IdleConnTimeout: 90s)
- Retry logic via
go-httpretrypackage - Per-operation timeouts (constants at top of main.go):
- Device code request: 10s
- Token exchange: 5s
- Token verification: 10s
- Refresh token: 10s
refreshAccessToken() handles two server modes:
- Rotation mode: Server returns new
refresh_token→ use it - Fixed mode: Server returns empty
refresh_token→ preserve old one
The function checks ErrRefreshTokenExpired for invalid_grant or invalid_token errors, signaling the need for a new device flow.
- Context cancellation: All HTTP operations accept
context.Contextand respect cancellation (Ctrl+C) - OAuth errors: Parsed from JSON error responses (
ErrorResponsestruct) - Validation:
validateTokenResponse()checks access token length (≥10 chars), expiry, and token type - URL validation:
validateServerURL()ensures proper scheme (http/https) and host presence
Tests use a separate init() function that sets defaults without calling initConfig(), avoiding flag parsing conflicts.
Tests use httptest to create mock OAuth servers, returning device codes and tokens.
The CLI automatically warns users when:
- Using HTTP instead of HTTPS (tokens transmitted in plaintext)
- CLIENT_ID is not a valid UUID format
- Keyring backend: OS-level credential encryption (preferred)
- File backend: created with
0600permissions (owner read/write only) - Token files should be added to
.gitignore(already configured) - Default file path:
.authgate-tokens.json
The Makefile supports version injection via git tags:
VERSION=$(git describe --tags --always)
COMMIT=$(git rev-parse --short HEAD)goreleaser uses these to build versioned binaries with naming pattern:
authgate-device-cli-{version}-{os}-{arch}
- Testing: Runs on Ubuntu and macOS with Go 1.25 and 1.26
- Linting: Uses golangci-lint v2.11 with config from
.golangci.yml - Release: goreleaser builds for multiple platforms (see
.goreleaser.yaml)
Requires Go 1.25+. Key dependencies:
golang.org/x/oauth2: OAuth 2.0 client librarygithub.com/appleboy/go-httpretry: HTTP retry logicgithub.com/joho/godotenv: .env file loadinggithub.com/google/uuid: UUID validationgithub.com/go-authgate/sdk-go/credstore: Token storage abstraction (file, keyring, auto)charm.land/bubbletea/v2: Terminal UI frameworkcharm.land/lipgloss/v2: Terminal stylingcharm.land/bubbles/v2: TUI components