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.24+
- 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/cli
./bin/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.
┌─────────────────────────────────────────────────────────┐
│ Your Terminal │
│ │
│ ┌────────────┐ OAuth tokens ┌──────────────────┐ │
│ │ Authgate │ ◄──────────────► │ AuthGate Server │ │
│ │ CLI │ │ (OAuth 2.0 AS) │ │
│ └─────┬──────┘ └──────────────────┘ │
│ │ │
│ │ Bearer token │
│ ▼ │
│ ┌─────────────┐ │
│ │ Protected │ │
│ │ Resource │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
The CLI automatically picks the right OAuth flow based on the runtime environment:
flowchart TD
classDef start fill:#6366f1,color:#fff,stroke:#4f46e5
classDef pkce fill:#22c55e,color:#fff,stroke:#16a34a
classDef device fill:#f59e0b,color:#000,stroke:#d97706
classDef done fill:#3b82f6,color:#fff,stroke:#2563eb
classDef check fill:#f8fafc,color:#1e293b,stroke:#94a3b8
START([CLI Launch]):::start --> FORCE_FLAG{--device / --no-browser?}:::check
FORCE_FLAG -->|Yes| DEVICE([Device Code Flow]):::device
FORCE_FLAG -->|No| SSH{SSH without display?}:::check
SSH -->|Yes| DEVICE
SSH -->|No| LINUX{Linux, no display?}:::check
LINUX -->|Yes| DEVICE
LINUX -->|No| PORT{Callback port free?}:::check
PORT -->|In use| DEVICE
PORT -->|Free| PKCE([Auth Code + PKCE]):::pkce
PKCE --> BROWSER{Browser opens?}:::check
BROWSER -->|Failed| DEVICE
BROWSER -->|Success| CALLBACK{Callback received?}:::check
CALLBACK -->|"Timeout (2 min)"| DEVICE
CALLBACK -->|Received| DONE([Tokens Saved]):::done
DEVICE --> DONE
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 ✓ │
│ ● Step 2/3: Waiting for callback ◐ │
│ ○ Step 3/3: Exchanging tokens │
│ │
│ Time remaining: 1:23 / 2:00 │
│ ▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░ 48% │
│ │
│ ◐ Please complete authorization in your browser │
│ │
│ Press Ctrl+C to cancel │
╰─────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────╮
│ Device Authorization Grant Flow │
├─────────────────────────────────────────────────┤
│ │
│ ╔═══════════════════════════════════════════╗ │
│ ║ Device Authorization ║ │
│ ║ ║ │
│ ║ Visit: https://auth.example.com/device ║ │
│ ║ ?user_code=ABCD-EFGH ║ │
│ ║ ║ │
│ ║ Or go to: https://auth.example.com ║ │
│ ║ And enter: ABCD-EFGH ║ │
│ ╚═══════════════════════════════════════════╝ │
│ │
│ ◐ Waiting for authorization... (poll #8, 5s) │
│ │
│ Elapsed: 0:43 │
│ │
│ Press Ctrl+C to cancel │
╰─────────────────────────────────────────────────╯
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 |
SCOPE |
read write |
Space-separated OAuth scopes |
TOKEN_FILE |
.authgate-tokens.json |
Path to the token cache file |
| 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 |
— | Override computed redirect URI |
--port |
CALLBACK_PORT |
Local callback port |
--scope |
SCOPE |
OAuth scopes |
--token-file |
TOKEN_FILE |
Token cache file path |
--device |
— | Force Device Code Flow |
--no-browser |
— | Alias for --device |
# Auto-detect flow (default)
./bin/cli
# Force Device Code Flow (useful in scripts or CI)
./bin/cli --device
./bin/cli --no-browser
# Override server and client
./bin/cli --server-url https://auth.example.com --client-id <uuid>
# Use a non-default callback port
./bin/cli --port 9999Used when a local browser and a free callback port are available. Suitable for developer workstations.
=== AuthGate CLI ===
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 ...
Authorization code received!
Step 3: Exchanging authorization code for tokens...
Tokens saved to .authgate-tokens.json
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 CLI ===
Auth method : Device Code Flow (SSH session without display forwarding)
Step 1: Requesting device code...
----------------------------------------
Please open this link to authorize:
http://localhost:8080/device?user_code=ABC-12345
Or visit : http://localhost:8080/device
And enter: ABC-12345
----------------------------------------
Step 2: Waiting for authorization...............
Authorization successful!
Tokens saved to .authgate-tokens.json
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.
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/cli --port 9999
# or
CALLBACK_PORT=9999 ./bin/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/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/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/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 # linter
make install-templ # template code generator (if used)make build # Build binary → bin/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/cli
make build_linux_arm64 # Linux ARM64 → release/linux/arm64/cli