This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A CLI tool that implements the OAuth 2.0 Authorization Code Flow with PKCE for authenticating with an AuthGate server. Opens the browser for user authorization, receives the callback via a local HTTP server, exchanges the code for tokens, and manages token lifecycle (storage, refresh, reuse).
Supports both public clients (PKCE-only) and confidential clients (PKCE + client secret).
Build the binary:
make build # Builds to bin/oauth-cli
go run . # Run directly without buildingTesting:
make test # Run all tests with coverage
make coverage # View coverage in browser
go test ./... -v # Run tests verbose
go test -run TestSpecificFunction # Run a single testLinting and formatting:
make lint # Run golangci-lint
make fmt # Format code with golangci-lintDevelopment workflow:
make dev # Hot reload with air (installs air if needed)Other commands:
make clean # Remove build artifacts and coverage files
make rebuild # Clean then build
make help # Show all available make targetsAll code is in the main package at the repository root. No subdirectories or internal packages.
Key files:
main.go- Entry point, config, token lifecycle, OAuth flow orchestrationcallback.go- Local HTTP server for OAuth callback handlingpkce.go- PKCE code verifier/challenge generation (RFC 7636)filelock.go- File locking for concurrent token file accessbrowser.go- Cross-platform browser opening
- Initialization (
initConfig): Parse flags, load.env, validate config, create HTTP retry client with TLS 1.2+ - Token Check: Try to load existing tokens from disk
- Valid token → use immediately
- Expired token → attempt refresh
- No token or refresh fails → start Authorization Code Flow
- Authorization Code Flow (
performAuthCodeFlow):- Generate PKCE verifier/challenge + state (CSRF protection)
- Open browser to
/oauth/authorizewith all params - Start local callback server on port 8888 (default)
- Wait for OAuth callback with authorization code
- Exchange code for tokens (PKCE verifier + client secret if confidential)
- Save tokens to file with atomic write
- Token Exchange in Callback: The token exchange happens inside the HTTP callback handler so the browser tab shows the true outcome (success/failure) rather than a premature success page
- Token Storage: Multi-client JSON file with file locking for concurrent safety
Context Propagation: All HTTP requests and long-running operations accept context.Context. The main function uses signal.NotifyContext to handle SIGINT/SIGTERM gracefully.
PKCE Always Enabled: Even confidential clients use PKCE (defense in depth). Both code_verifier and client_secret are sent during token exchange for confidential clients.
Token Refresh: The refreshAccessToken function handles refresh token rotation (preserves old refresh token if server doesn't return a new one).
Callback Server Lifecycle:
- Starts before opening browser
- Validates state parameter (CSRF protection)
- Holds HTTP response open during token exchange (browser sees true outcome)
- Uses
sync.Onceto ensure exchange happens exactly once even if browser retries - Shuts down after first callback or context cancellation
File Locking: Uses a separate .lock file to coordinate concurrent access to the token file. Implements stale lock detection (removes locks older than 30 seconds).
HTTP Client: Uses github.com/appleboy/go-httpretry for automatic retries with exponential backoff. TLS 1.2+ enforced. Warns when using HTTP (not HTTPS) for development.
Flag > Environment Variable > Default
All config is initialized once via initConfig() which sets global variables. The configInitialized flag prevents double initialization.
JSON file with per-client-id tokens:
{
"tokens": {
"client-id-uuid-here": {
"access_token": "...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_at": "2026-02-19T12:00:00Z",
"client_id": "..."
}
}
}File written with 0600 permissions via atomic rename (write to .tmp then rename).
- OAuth errors (e.g.,
access_denied,invalid_grant) are parsed from JSON responses ErrRefreshTokenExpiredsignals when a refresh token is invalid → triggers full re-auth- Context cancellation checked after all blocking operations (graceful shutdown)
- Exit codes: 0 (success), 1 (error), 130 (interrupted via SIGINT)
- PKCE (RFC 7636) always enabled — code verifier never leaves the client
- State parameter validated on every callback (CSRF protection)
- TLS 1.2+ enforced for all HTTPS connections
- Token file written with 0600 permissions
- Warns when SERVER_URL uses plain HTTP
- Client ID validated as UUID format (warning only)
Tests use table-driven tests with subtests (t.Run). Mock HTTP servers created with httptest.NewServer for testing OAuth endpoints.
Key test files:
main_test.go- Token storage, refresh, config validationcallback_test.go- Callback server behavior, state validation, concurrent requestspkce_test.go- PKCE generation, verifier/challenge encodingfilelock_test.go- Concurrent access, stale lock detection
github.com/joho/godotenv- Load.envfilesgithub.com/google/uuid- UUID validationgithub.com/appleboy/go-httpretry- HTTP client with retry and backoff
Changing callback port: Update both -port flag and Redirect URI in AuthGate Admin (must match).
Adding new OAuth endpoints: Follow the existing pattern in main.go — create request with context timeout, use retryClient.DoWithContext, check for OAuth error responses in JSON.
Token storage changes: Modify TokenStorage struct and update loadTokens/saveTokens. The atomic write pattern (temp file + rename) should be preserved.
Browser opening: Platform-specific logic is in browser.go. Uses xdg-open (Linux), open (macOS), rundll32 (Windows).