Skip to content

zndx/xai-auth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

xai-auth

QR-based terminal login. Terminal apps can't do browser OAuth well, and the auth often needs to happen on a different device (your phone) than where the app runs (a headless workstation). The classic localhost-callback trick fails there: the phone can't reach localhost:8765 on the workstation.

This library packages two proven patterns for that situation. The core is provider-agnostic (any OAuth 2.0 + PKCE provider); the presets and field-tested gotchas come from X (Twitter) API v2 and the grok CLI.

Pattern A — Authorization code + PKCE + Cloudflare KV relay

For providers that only support redirect-based OAuth. A tiny Cloudflare Worker acts as the OAuth rendezvous:

Terminal (TUI)                          Phone                    Cloudflare
──────────────                          ─────                    ──────────
1. Generate PKCE pair + random state
   persist (state, verifier, TTL) locally
2. Render auth URL as ASCII QR ───────► 3. Scan, log in to provider
                                        4. Provider redirects to
                                           Worker /callback ────► 5. Worker stores
                                                                    oauth-pending/{state}
                                                                    = {code} in KV (TTL 300s)
                                                                    + renders success page
                                                                    (code visible as manual
                                                                     fallback)
6. Terminal polls KV (fast only while ◄──────────────────────────  Workers KV
   auth pending), finds code,
   exchanges code+verifier for tokens,
   DELETES the KV entry
7. on_complete fires ─► dismiss the QR modal

Three security properties, preserved by design:

  1. PKCE keeps the relay low-trust — the code_verifier never leaves the workstation. The Worker/KV only ever see the authorization code, which is useless without the verifier. A compromised relay can't mint tokens.
  2. state is the rendezvous key — random, single-use, TTL'd on both sides (local pending table and KV expirationTtl: 300).
  3. Delete-after-claim — the KV entry is removed immediately after a successful exchange; the 5-minute TTL caps the window if the flow is abandoned.
from xai_auth import KVRelayConfig, KVRelayPoller, PKCEFlow, SqliteStore, qr_ascii, x_provider

provider = x_provider(client_id="...", redirect_uri="https://your-relay.workers.dev/callback")
flow = PKCEFlow(provider, SqliteStore("~/.config/myapp/auth.sqlite3"))

url, state = await flow.begin()
print(qr_ascii(url))
print(url)  # keep the plain URL visible — some users are on the same machine

config = KVRelayConfig.from_env()  # CLOUDFLARE_ACCOUNT_ID / _API_TOKEN / _KV_NAMESPACE_ID
if config:
    tokens = await KVRelayPoller(config, flow).wait_for_completion()
else:
    # Relay unconfigured is a tier, not an error: manual code entry still works
    tokens = await flow.complete_by_state(input("Paste the code shown in the browser: "), state)

For daemon-style hosts, KVRelayPoller.run() is a long-running loop with adaptive cadence: fast polling only while an auth is pending, a slow existence check otherwise (the Cloudflare API is rate-limited and not free at high frequency).

Deploying the relay

cd relay
wrangler kv namespace create AUTH_SESSIONS   # paste the id into wrangler.toml
wrangler deploy

One route, one KV namespace, ~150 lines, no framework. Point your provider app's redirect_uri at https://<worker-host>/callback. ENGINE_CALLBACK_URL (optional) makes the Worker POST {code, state} directly to your engine — a latency optimization only; the engine may be behind NAT/firewall, so KV polling is the reliable path.

Pattern B — Device-code flow + QR (no relay)

If the provider supports RFC 8628 device flow, the provider hosts the rendezvous — skip the Worker entirely:

from xai_auth import DeviceFlowEndpoint, qr_ascii

endpoint = DeviceFlowEndpoint(
    device_authorization_endpoint="https://provider/oauth2/device",
    token_endpoint="https://provider/oauth2/token",
    client_id="...",
)
authorization = await endpoint.authorize()
print(qr_ascii(authorization.qr_url))
token_data = await endpoint.poll_for_token(authorization)

Or wrap a provider CLI that runs its own device flow:

from xai_auth import CLIDeviceLogin, qr_ascii

login = CLIDeviceLogin(["grok", "login", "--device-auth"])
url = await login.start()       # scans stdout AND stderr (grok prints to stderr!)
print(qr_ascii(url))
await login.wait()              # returns once the user approves on their phone

Install

uv add xai-auth                 # core: httpx + qrcode
uv add 'xai-auth[textual]'      # + QRCodeModal Textual frontend
from xai_auth.frontends.textual import QRCodeModal
app.push_screen(QRCodeModal("Sign in to X", url))
# auto-dismiss: in the poller's on_complete, pop the modal if it's the current screen

Dependency / config matrix

Piece Deps Config
QR render (qr_ascii) qrcode
OAuth core (PKCEFlow) httpx provider client_id, redirect_uri, scopes
Relay Worker wrangler KV namespace, optional ENGINE_CALLBACK_URL
KV polling (KVRelayPoller) httpx CF account_id, API token (KV read+delete only — scope it minimally), namespace_id
Device flow httpx / subprocess provider device endpoint or CLI
Textual modal textual (extra)

Token storage defaults to a 2-table sqlite schema (SqliteStore); the Store protocol lets you back it with postgres, keyring, etc.

Tokens are stored unencrypted at rest. SqliteStore is plaintext sqlite — keep the file private (e.g. a chmod 600 path under ~/.config, which is also why *.sqlite3 is git-ignored), or back the Store protocol with a keyring / secret manager on shared hosts.

Pitfalls this library encodes (learned the hard way)

  1. State lookup is by-state, never by-session — the phone completes the flow with zero knowledge of the terminal session; state is the only correlation key.
  2. Worker direct-POST is an optimization, not the mechanism — KV polling is the reliable path; the direct callback just shortens latency when the engine is reachable.
  3. Poll cadence — fast-poll only while a pending auth exists. Unconditional fast polling burns Cloudflare API quota for nothing.
  4. Delete-after-claim — remove the KV entry on successful exchange or replays within the TTL window are possible (PKCE limits the damage, but don't rely on it alone).
  5. QR renderinginvert=True matters on dark terminals; always keep the plain-URL fallback visible.
  6. Provider CLIs are weird — grok prints its verification URL to stderr, and a bare grok failing with ENXIO just means no /dev/tty. CLIDeviceLogin captures both streams.

Development

uv sync
uv run pytest

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors