Skip to content

Latest commit

 

History

History
532 lines (381 loc) · 20.4 KB

File metadata and controls

532 lines (381 loc) · 20.4 KB

Authgate CLI

Lint and Testing Trivy Security Scan

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.


Table of Contents


Why This CLI?

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

Quick Start

Prerequisites

  • Go 1.25+
  • A running AuthGate server — get the CLIENT_ID UUID from its startup logs

1. Configure

cp .env.example .env

Edit .env and set at minimum:

SERVER_URL=http://localhost:8080
CLIENT_ID=<uuid-from-server-logs>   # Required — all other fields have defaults

2. Run

go 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

3. Build a binary

make build
# Binary written to bin/authgate-cli
./bin/authgate-cli

How It Works

System architecture

Authgate 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
Loading

Flow selection

The CLI automatically picks the right OAuth flow based on the runtime environment. The two flows use different message exchanges between services:

Authorization Code Flow with PKCE

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)
Loading

Device Authorization Grant

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)
Loading

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.

Token lifecycle

On each run the CLI follows this order:

  1. Load cached tokens — read from TOKEN_FILE keyed by CLIENT_ID
  2. Valid access token — use it directly, skip authentication
  3. Expired access token — attempt a silent refresh with the refresh token
  4. Expired/missing refresh token — trigger full re-authentication (browser or device flow)
  5. After any successful auth — verify token at /oauth/tokeninfo, then demonstrate auto-refresh on 401

Interactive Terminal UI

Authgate CLI features a rich interactive Terminal User Interface (TUI) built with Bubble Tea, providing visual feedback during OAuth authentication flows.

Features

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

Browser Flow (Authorization Code + PKCE)

╭──────────────────────────────────────────────────╮
│   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 Flow (Device Authorization Grant)

╭──────────────────────────────────────────────────╮
│     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

UI Mode Selection

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=dumb or TERM unset
  • SSH session without display forwarding

Note on UI Selection

The CLI automatically detects the environment and selects the appropriate UI mode. No configuration or flags are needed - it just works.


Configuration

Configuration is resolved in priority order: CLI flag → environment variable → default.

Environment variables

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-file flags (see below). They have no environment-variable equivalent because each claim is its own key=value entry.

CLI flags

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

Usage examples

# 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.env

Caller-supplied extra JWT claims

For 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 array

Values 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.


Authentication Flows

Authorization Code Flow with PKCE (browser)

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
  • state parameter — CSRF protection on the callback
  • Callback server binds to 127.0.0.1 only
  • 2-minute timeout; falls back to Device Code Flow automatically

Device Authorization Grant (headless/SSH)

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)

Public vs. confidential clients

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.


Token Storage

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).


Troubleshooting

Port 8888 is already in use

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-cli

Browser does not open automatically

The 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 --device

"CLIENT_ID is required" error

The 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-xxxxxxxxxxxx

Token refresh fails / kept asking to re-authenticate

If 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-cli

Authorization timeout after 2 minutes

The 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.


Development

Prerequisites

# Install development tools
make install-golangci-lint   # linter

Common commands

make 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 + build

Cross-platform builds

make build_linux_amd64    # Linux x86-64 → release/linux/amd64/authgate-cli
make build_linux_arm64    # Linux ARM64  → release/linux/arm64/authgate-cli