Skip to content

go-authgate/cli

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

89 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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/code
    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. Resolve endpoints — fetch OIDC Discovery (/.well-known/openid-configuration); fall back to hardcoded paths if Discovery fails
  2. Load cached tokens — read from the configured store (keyring or file) keyed by CLIENT_ID
  3. Valid access token — use it directly, skip authentication
  4. Expired access token — attempt a silent refresh with the refresh token
  5. Expired/missing refresh token — trigger full re-authentication (browser or device flow)
  6. After selecting a token to use (cached, refreshed, or newly issued) — verify the token at /oauth/tokeninfo and fetch UserInfo from the resolved UserInfo endpoint (default: /oauth/userinfo) in parallel, then demonstrate auto-refresh on 401

The process responds to SIGINT / SIGTERM and cancels the in-flight flow cleanly.


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 email profile Space-separated OAuth scopes
TOKEN_FILE .authgate-tokens.json Path to the token cache file
TOKEN_STORE auto Storage backend: auto, file, or keyring

Timeouts and limits

Each OAuth flow step listed below has its own configurable timeout. Timeout values (the *_TIMEOUT variables) are parsed with time.ParseDuration (e.g. 10s, 2m, 1m30s); invalid or non-positive values fall back to the default (with a warning), and any value above 10 minutes is capped. MAX_RESPONSE_BODY_SIZE is parsed as an integer byte count (not a duration) and is capped separately at 100 MiB. The post-auth auto-refresh demo (makeAPICallWithAutoRefresh) reuses the parent context and is bounded by SIGINT / SIGTERM rather than a dedicated timeout.

Variable Default Description
TOKEN_EXCHANGE_TIMEOUT 10s Authorization-code → token exchange
TOKEN_VERIFICATION_TIMEOUT 10s /oauth/tokeninfo request
REFRESH_TOKEN_TIMEOUT 10s Refresh token grant
DEVICE_CODE_REQUEST_TIMEOUT 10s Device authorization request
CALLBACK_TIMEOUT 2m How long the local callback server waits for the browser before falling back
USERINFO_TIMEOUT 10s /oauth/userinfo request
DISCOVERY_TIMEOUT 10s OIDC Discovery fetch
REVOCATION_TIMEOUT 10s RFC 7009 revocation request issued by token delete
MAX_RESPONSE_BODY_SIZE 1048576 (1 MiB) Bytes read from OAuth responses the CLI parses or prints (token, tokeninfo, userinfo, device-flow); revocation drains a fixed 4 KiB and is unaffected. Capped at 100 MiB

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 (also available as version subcommand)

Each timeout / size environment variable also has a matching flag with the same name in lower-kebab-case (e.g. --callback-timeout, --max-response-body-size).

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

# Print the stored access token (raw, or as JSON with --json)
./bin/authgate-cli token get

# Inspect what the server knows about the stored access token
./bin/authgate-cli token inspect

# Decode the stored JWT locally without contacting the server
./bin/authgate-cli token decode --field sub

# Revoke and remove the stored token
./bin/authgate-cli token delete

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


Token Subcommands

authgate-cli token exposes four utility commands for managing the credentials cached by the main flow. They share the global flags (--client-id, --token-file, --token-store, …); commands that need network access additionally honour --server-url and the timeout flags.

token get — print the stored access token

./bin/authgate-cli token get             # prints the access token (raw)
./bin/authgate-cli token get --json      # prints access_token, token_type, expires_at, expired, client_id

The refresh token is never printed (it is a long-lived secret). Exits non-zero with a hint if no token is stored.

token inspect — server-side introspection

./bin/authgate-cli token inspect

Sends an HTTP GET request to /oauth/tokeninfo with the stored access token in the Authorization: Bearer ... header and pretty-prints the JSON response. Useful when the local cache disagrees with the server.

token decode — local JWT decode

./bin/authgate-cli token decode                 # full claims map
./bin/authgate-cli token decode --field aud     # single claim (string values printed raw, others as JSON)
./bin/authgate-cli token decode -f sub

Base64-decodes the JWT payload locally without verifying the signature. Fails with a clear error if the token is opaque (not a JWT).

token delete — revoke and remove

./bin/authgate-cli token delete                 # revokes on the server (RFC 7009), then deletes locally
./bin/authgate-cli token delete --local-only    # skip server revocation

By default the access token, and the refresh token if present, are revoked concurrently against the resolved revocation endpoint (default: /oauth/revoke). If revocation fails (network down, server unreachable) the local token is still deleted — graceful degradation. Use --local-only to skip the network round-trip entirely.


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

About

A CLI example that combines Authorization Code Flow (browser-based) and Device Code Flow into a single binary. The flow is selected automatically based on the environment — no configuration required.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors