A production-ready OAuth 2.0 CLI authentication library for Go.
Building OAuth 2.0 into a CLI tool means solving the same problems every time: detecting whether a browser is available, implementing PKCE correctly, running a local callback server, caching tokens, handling refresh, and gracefully falling back to a headless device flow for SSH/CI environments. Authgate CLI handles all of this so you can focus on your application logic.
This mirrors the authentication strategy used by GitHub CLI, Azure CLI, and Google Cloud SDK — automatically selecting between Authorization Code Flow with PKCE (browser) and Device Authorization Grant (headless/SSH) based on the runtime environment, with no manual configuration required.
- Why This CLI?
- Quick Start
- How It Works
- Interactive Terminal UI
- Configuration
- Authentication Flows
- Token Storage
- Troubleshooting
- Development
Without Authgate CLI, every OAuth-enabled CLI tool must implement the same boilerplate:
| If you implement it yourself | Authgate CLI handles it for you |
|---|---|
| Detect SSH session / headless environment | ✅ Auto-selects PKCE or Device Flow |
Generate PKCE code_verifier + code_challenge (RFC 7636) |
✅ Built-in |
| Spin up a local callback HTTP server | ✅ Built-in, bound to 127.0.0.1 |
Add CSRF state parameter and validate on callback |
✅ Built-in |
| Cache tokens to disk with safe file permissions | ✅ Written as 0600, multi-client keyed by CLIENT_ID |
| Refresh access token silently on expiry | ✅ Built-in, with auto-retry on 401 |
| Fall back to Device Flow when browser fails or times out | ✅ Automatic |
| Handle concurrent writes to the token file | ✅ File-lock with stale-lock timeout |
- Go 1.25+
- A running AuthGate server — get the
CLIENT_IDUUID from its startup logs
cp .env.example .envEdit .env and set at minimum:
SERVER_URL=http://localhost:8080
CLIENT_ID=<uuid-from-server-logs> # Required — all other fields have defaultsgo run .The CLI auto-detects your environment and selects the appropriate flow:
- Local workstation with a browser → opens a browser tab, completes authorization silently
- SSH session / no display / CI → prints a URL and user code for you to authorize from another device
make build
# Binary written to bin/authgate-cli
./bin/authgate-cliAuthgate CLI sits between your terminal and the AuthGate server. It acquires tokens on your behalf and demonstrates how to use them against a protected resource.
flowchart LR
classDef cli fill:#22c55e,color:#fff,stroke:#16a34a
classDef server fill:#6366f1,color:#fff,stroke:#4f46e5
classDef resource fill:#f59e0b,color:#000,stroke:#d97706
subgraph TERMINAL["Your Terminal"]
CLI["Authgate CLI"]:::cli
RESOURCE["Protected<br/>Resource"]:::resource
end
SERVER["AuthGate Server<br/>(OAuth 2.0 AS)"]:::server
CLI <-->|OAuth tokens| SERVER
CLI -->|Bearer token| RESOURCE
The CLI automatically picks the right OAuth flow based on the runtime environment. The two flows use different message exchanges between services:
sequenceDiagram
autonumber
participant U as User
participant CLI as Authgate CLI
participant B as Browser
participant S as AuthGate Server
U->>CLI: launch
CLI->>CLI: generate code_verifier + code_challenge
CLI->>CLI: start callback server on 127.0.0.1:8888
CLI->>B: open /oauth/authorize?code_challenge=...&state=...
B->>S: GET /oauth/authorize
U->>B: login + consent
S-->>B: 302 redirect with code + state
B->>CLI: GET /callback?code=...&state=...
CLI->>CLI: validate state (CSRF)
CLI->>S: POST /oauth/token (code + code_verifier)
S-->>CLI: access_token + refresh_token
CLI->>CLI: cache tokens (0600)
sequenceDiagram
autonumber
participant CLI as Authgate CLI
participant U as User
participant D as User's Other Device
participant S as AuthGate Server
CLI->>S: POST /oauth/device_authorization
S-->>CLI: device_code + user_code + verification_uri + interval
CLI->>U: display verification_uri + user_code
U->>D: visit verification_uri, enter user_code
D->>S: authorize device
loop poll every interval seconds
CLI->>S: POST /oauth/token (device_code)
alt pending
S-->>CLI: 400 authorization_pending
else rate-limited
S-->>CLI: 400 slow_down (back off, RFC 8628)
else authorized
S-->>CLI: access_token + refresh_token
end
end
CLI->>CLI: cache tokens (0600)
The CLI chooses between these flows automatically based on: the --device flag, SSH/headless detection, Linux display availability, and callback port reachability. PKCE falls back to Device Flow on browser failure or 2-minute callback timeout.
On each run the CLI follows this order:
- Load cached tokens — read from
TOKEN_FILEkeyed byCLIENT_ID - Valid access token — use it directly, skip authentication
- Expired access token — attempt a silent refresh with the refresh token
- Expired/missing refresh token — trigger full re-authentication (browser or device flow)
- After any successful auth — verify token at
/oauth/tokeninfo, then demonstrate auto-refresh on401
Authgate CLI features a rich interactive Terminal User Interface (TUI) built with Bubble Tea, providing visual feedback during OAuth authentication flows.
The TUI provides:
- Visual Progress Indicators: Step-by-step progress with animated spinners
- Real-time Timers: Countdown for browser flow, elapsed time for device flow
- Progress Bars: Visual representation of callback timeout
- Polling Status: Live updates showing device flow polling count and intervals
- Backoff Warnings: Clear notifications when server requests slower polling
- Clean Layout: Bordered boxes, color-coded messages, and structured information
╭──────────────────────────────────────────────────╮
│ Authorization Code Flow with PKCE │
╰──────────────────────────────────────────────────╯
● Step 1/3: Opening browser (green, completed)
● Step 2/3: Waiting for callback (purple, current)
○ Step 3/3: Exchanging tokens (gray, pending)
Time remaining: 1:23 / 2:00
████████████░░░░░░░░░░░░░ 48%
⠙ Please complete authorization in your browser
ctrl+c: cancel authentication • ?: toggle help
╭──────────────────────────────────────────────────╮
│ Device Authorization Grant Flow │
╰──────────────────────────────────────────────────╯
● Step 1/2: Requesting device code (green, completed)
● Step 2/2: Waiting for authorization (purple, current)
╔══════════════════════════════════════════════════╗
║ Device Authorization ║
║ ║
║ Please authorize this device: ║
║ ║
║ Visit: https://auth.example.com/device ║
║ ?user_code=ABCD-EFGH ║
║ ║
║ Or go to: https://auth.example.com/device ║
║ And enter: ABCD-EFGH ║
╚══════════════════════════════════════════════════╝
⠙ Waiting for authorization... (poll #8, interval: 5s)
⚠ Server requested slower polling: 5s → 10s
Elapsed: 0:43
ctrl+c: cancel authentication • ?: toggle help
The CLI automatically chooses the appropriate UI mode:
Interactive TUI Mode (default):
- Normal terminal with sufficient size (≥60x20)
- TTY detected
- TERM environment variable set (not "dumb")
Simple Printf Mode (automatic fallback):
- CI environments (GitHub Actions, GitLab CI, CircleCI, etc.)
- Output piped to file or another command
- Terminal too small (< 60 columns or < 20 rows)
TERM=dumbor TERM unset- SSH session without display forwarding
The CLI automatically detects the environment and selects the appropriate UI mode. No configuration or flags are needed - it just works.
Configuration is resolved in priority order: CLI flag → environment variable → default.
| Variable | Default | Description |
|---|---|---|
SERVER_URL |
http://localhost:8080 |
AuthGate server base URL |
CLIENT_ID |
(required) | OAuth client ID (UUID from server logs) |
CLIENT_SECRET |
(empty) | Client secret — omit for public/PKCE clients |
CALLBACK_PORT |
8888 |
Local port for the redirect callback server |
REDIRECT_URI |
(auto-computed) | Override computed redirect URI |
SCOPE |
read write |
Space-separated OAuth scopes |
TOKEN_FILE |
.authgate-tokens.json |
Path to the token cache file |
TOKEN_STORE |
auto |
Storage backend: auto, file, or keyring |
Extra JWT claims are configured via the
--extra-claims/--extra-claims-fileflags (see below). They have no environment-variable equivalent because each claim is its own key=value entry.
| Flag | Env equivalent | Description |
|---|---|---|
--server-url |
SERVER_URL |
AuthGate server URL |
--client-id |
CLIENT_ID |
OAuth client ID |
--client-secret |
CLIENT_SECRET |
Client secret (confidential clients only) |
--redirect-uri |
REDIRECT_URI |
Override computed redirect URI |
--port |
CALLBACK_PORT |
Local callback port |
--scope |
SCOPE |
OAuth scopes |
--token-file |
TOKEN_FILE |
Token cache file path |
--token-store |
TOKEN_STORE |
Storage backend: auto, file, or keyring |
--device |
— | Force Device Code Flow |
--extra-claims |
— | Caller-supplied JWT claim as key=value (repeatable) |
--extra-claims-file |
— | Path to a .env-style file (one key=value per line) |
--version |
— | Print version and exit |
# Auto-detect flow (default)
./bin/authgate-cli
# Force Device Code Flow (useful in scripts or CI)
./bin/authgate-cli --device
# Override server and client
./bin/authgate-cli --server-url https://auth.example.com --client-id <uuid>
# Use a non-default callback port
./bin/authgate-cli --port 9999
# Inspect what the server knows about the stored access token
./bin/authgate-cli token inspect
# Attach caller-supplied JWT claims (sent on every token grant + refresh)
./bin/authgate-cli \
--extra-claims project=acme-prod \
--extra-claims trace_id=req-42 \
--extra-claims count=7
# Or load them from a .env-style file (file values are merged first; flag values override)
./bin/authgate-cli --extra-claims-file ./claims.envFor workflows where one CLI binary needs to attach per-account or per-request context (project code, trace ID, routing hints, …) to the issued JWT, pass them with --extra-claims key=value (repeatable) or load them from a .env-style file via --extra-claims-file.
# claims.env
project=acme-prod
code_partition=ap-northeast-1
trace_id=req-42
count=7 # parses as JSON number
enabled=true # parses as JSON boolean
tags=["a","b"] # parses as JSON arrayValues are inferred as JSON when they parse (numbers, booleans, arrays, objects, quoted strings, null); everything else is treated as a plain string. The CLI sends the merged map as the extra_claims form parameter on every token request — authorization code, device code, and refresh — so the claims survive a refresh without you having to re-invoke the flag (per-process, not persisted to disk).
The server enforces reserved-key rejection (
iss,sub,aud,exp,nbf,iat,jti,scope,client_id, …), size limits, and admin-managed overrides. Caller-supplied claims are appropriate for trace IDs, request context, and routing hints, but must not be trusted by downstream resource servers for authorization decisions without independent verification. See the AuthGate server docs for the full trust model.
Used when a local browser and a free callback port are available. Suitable for developer workstations.
=== AuthGate Hybrid CLI (Browser + Device Code Flow) ===
Client mode : public (PKCE)
Server URL : http://localhost:8080
Client ID : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Auth method : Authorization Code Flow (browser)
Step 1: Opening browser for authorization...
http://localhost:8080/oauth/authorize?...
Browser opened. Please complete authorization in your browser.
Step 2: Waiting for callback on http://localhost:8888/callback ...
Step 3: Exchanging authorization code for tokens...
Security properties:
- PKCE (RFC 7636) — prevents authorization code interception
stateparameter — CSRF protection on the callback- Callback server binds to
127.0.0.1only - 2-minute timeout; falls back to Device Code Flow automatically
Used when no browser is available: SSH sessions without display forwarding, Linux servers, CI environments.
=== AuthGate Hybrid CLI (Browser + Device Code Flow) ===
Auth method : Device Code Flow (SSH session without display forwarding)
Step 1: Requesting device code...
Step 2: Waiting for authorization...
----------------------------------------
Please open this link to authorize:
http://localhost:8080/device?user_code=ABC-12345
Or visit : http://localhost:8080/device
And enter: ABC-12345
----------------------------------------
.....
Authorization successful!
Polling behavior:
- Respects the server-specified polling interval (default 5 s)
- Implements RFC 8628 exponential backoff on
slow_down(up to 60 s)
| Mode | CLIENT_SECRET |
Token exchange |
|---|---|---|
| Public (PKCE) | Not set | Sends code_verifier |
| Confidential | Set | Sends client_secret |
Public/PKCE is the recommended mode for CLI tools.
The CLI supports multiple storage backends, controlled by --token-store / TOKEN_STORE:
| Mode | Description |
|---|---|
auto |
Try OS keyring first; fall back to file if keyring is unavailable (default) |
file |
Always use file-based storage |
keyring |
Always use OS keyring (fails if unavailable) |
When using file-based storage, tokens are saved to TOKEN_FILE (default .authgate-tokens.json) and keyed by CLIENT_ID, so the same file can hold credentials for multiple clients.
{
"tokens": {
"<client-id>": {
"access_token": "...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_at": "2026-01-01T00:00:00Z",
"client_id": "<client-id>",
"flow": "browser"
}
}
}The flow field records whether browser or device was used.
Concurrent write safety: token writes use a .lock file with a 30-second stale-lock timeout, ensuring multiple processes can share the same token file without corruption.
File permissions: written as 0600 (owner read/write only).
The callback server cannot start, so the CLI falls back to Device Code Flow automatically. To use a different port for PKCE:
./bin/authgate-cli --port 9999
# or
CALLBACK_PORT=9999 ./bin/authgate-cliThe authorization URL is always printed to the terminal. Copy and paste it into a browser manually. The CLI will continue waiting for the callback.
If you are in a headless environment (SSH without display forwarding), use --device to skip the browser flow entirely:
./bin/authgate-cli --deviceThe CLIENT_ID must be the UUID shown in the AuthGate server startup logs. It is not a value you create — it is assigned by the server when a client is registered.
# Check your .env
cat .env | grep CLIENT_ID
# Or pass it directly
./bin/authgate-cli --client-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxIf the refresh token has expired, the CLI triggers a full re-authentication. Delete the token cache to start fresh:
rm .authgate-tokens.json
./bin/authgate-cliThe PKCE callback server waits up to 2 minutes for you to complete the browser flow. If it times out, the CLI falls back to Device Code Flow automatically. No action required.
# Install development tools
make install-golangci-lint # lintermake build # Build binary → bin/authgate-cli
make test # Run tests with coverage
make coverage # Open coverage report in browser
make lint # Run golangci-lint
make fmt # Format code
make dev # Hot reload with air
make clean # Remove bin/, release/, coverage.txt
make rebuild # clean + buildmake build_linux_amd64 # Linux x86-64 → release/linux/amd64/authgate-cli
make build_linux_arm64 # Linux ARM64 → release/linux/arm64/authgate-cli