docs · device-flow sign-in
Device-flow sign-in
pakx login uses the RFC 8628 device-authorization grant by default. The CLI never asks for a password, never opens a callback server on localhost, and never sees your GitHub credentials — sign-in happens entirely in your browser against registry.pakx.dev, and the CLI receives a short-lived API token at the end.
What you'll see
Running pakx login on a fresh machine looks like this:
$ pakx login
To sign in, open this URL in your browser:
https://registry.pakx.dev/auth/device?user_code=ABCD-EFGH
Or visit https://registry.pakx.dev/auth/device and enter:
ABCD-EFGH
Waiting for confirmation...
[ok] signed in to https://registry.pakx.dev as alice
→ credentials: /home/alice/.pakx/credentials.json (mode 0600)The CLI attempts to open the verification URL in your default browser. You sign in with GitHub (Auth.js handles the OAuth handshake), land on the confirm page, check the user-code matches what your terminal shows, and click Confirm. The CLI was polling in the background the whole time; it prints the success line as soon as the registry hands it a token.
The token is written to ~/.pakx/credentials.json (mode 0600 on Unix) and never echoed to your terminal. The user-code is the only short string you ever type by hand.
Why this flow
RFC 8628 was designed for "input-constrained devices" — TVs, CLI tools, anything that can't run a browser inline. The two-channel split (CLI shows a code, user pastes it into a real browser on the same or a different device) avoids three classes of problem you'd otherwise hit:
- No localhost callback. A CLI that opens an HTTP listener on a random port to catch an OAuth redirect breaks on locked-down networks, over SSH without port forwarding, inside containers, and behind corporate proxies. Device flow needs only outbound HTTPS to one origin.
- No password prompt in the CLI. You enter your GitHub password into github.com directly, not into a process that could leak it to its terminal, its shell history, or its env vars.
- Tokens never paste through a clipboard. The legacy "copy a token from a webpage, paste it into the CLI" flow puts a long-lived bearer credential into your clipboard for as long as it takes you to paste it. Device flow keeps the token on the wire between the registry and the CLI process — it's never displayed to either screen.
The trade-off is interactivity: device flow assumes you can open a browser and click a button. For CI runners, headless servers, and scripted onboarding, use --token with a token minted at registry.pakx.dev/dashboard/tokens.
CLI usage
pakx login # default — device flow
pakx login --device # explicit; same behaviour
pakx login --token "$PAKX_TOKEN" # opt out — paste a PAT-style token
# All three accept --registry <url> to override the default
# https://registry.pakx.dev base URL.
pakx login --device --registry https://staging.example.com--device and --token are mutually exclusive at the clap layer (conflicts_with = "token") — passing both prints a usage error and exits non-zero. With neither flag set, the CLI falls through to the device flow (default since 0.1.4).
The --token flag also reads from the PAKX_TOKEN environment variable, so CI scripts can set the env var once and run pakx login --token without exposing the value on the command line.
What gets exchanged
The CLI POSTs to /api/v1/auth/device and receives an RFC 8628 §3.2-shaped response. The interesting fields:
device_code— 256 bits of CSPRNG output. Server stores only the sha256 hash; the raw value lives on the wire and in CLI memory for the life of the poll loop, never in the database. You never see it.user_code— 8 characters from a 24-glyph Crockford-base32 alphabet (40 bits). The short code your terminal prints and you paste into the confirm page.verification_uri—https://registry.pakx.dev/auth/device. The page that asks you to paste the user-code.verification_uri_complete— same URL with?user_code=…appended so the form is pre-filled. The CLI tries to open this one in your browser.expires_in—600(10 minutes). The CLI's local timeout is set to match.interval—5(seconds). How often the CLI is allowed to poll.
Full request / response shapes plus the exhaustive error-code list live in the registry HTTP API reference.
Polling
The CLI sends POST /api/v1/auth/device/poll with the device_code every interval seconds (5 by default). Every non-error response is HTTP 200; the CLI branches on the status discriminator in the JSON body:
pending— user hasn't confirmed yet; sleepintervaland poll again.slow_down— RFC 8628 §3.5. The CLI bumps its local interval by at least 5 seconds (SLOW_DOWN_BUMP_SECSfloor) and takes the max of (server-suggested, current + 5). An aggressive server can wind the CLI down further but cannot speed it up past the floor.denied— user clicked Deny. CLI exits 1 with a clear message.expired— the 10-minute window elapsed, or the code was already redeemed. CLI exits 1 and points you atpakx loginagain.success— body carriestoken: pakx_v1_…. CLI verifies it round-trips through/api/v1/whoami, writes it tocredentials.json, prints a success line, and exits 0.
The local timeout uses std::time::Instant (monotonic), not the wall clock. An NTP step or DST jump cannot prematurely expire the loop or stretch it past the registry's own expires_in window.
Security notes
- Rate-limited at three layers. Initiate is capped at 5 burst + 1/min per IP. Poll is capped per-device-code at 1/interval (this is what emits
slow_down) plus a per-IP DoS gate of 60 burst + 1/sec sustained. Confirm is 5 burst + 1/min per IP and 20 burst + 1/min per signed-in user — two orthogonal buckets so a botnet rotating across thousands of residential proxies still hits a hard ceiling bound to the GitHub account they're signed in as. - Generic failure responses. Every non-success outcome on the confirm page redirects to
?status=invalid-coderegardless of whether the code was mistyped, expired, already-approved, already-denied, or never existed. An attacker who guesses a wrong code learns nothing about the keyspace. - Stateless HMAC CSRF on confirm. The confirm form carries a hidden
csrffield. Server-side HMAC binds the token to (current sessionuser_id, 60-second window). A token minted on user A's render is useless to attacker B because the HMAC binds to A's identity and B cannot forge one withoutAUTH_SECRET. - Tokens never reach stdout. Every CLI status / hint line — including the success line — renders on stderr. A caller piping
pakx logininto another command gets a clean empty stdout. Tracing logs that reference the token only print the prefixpakx_v1_********at debug level. - Atomic state transitions. Every device-session state flip (initiate / approve / deny / consume) is a single
UPDATE … WHERE <precondition> RETURNING *statement. Two concurrent polls that both see an approved session can't both mint a token — one wins the consume race, the other gets toldexpired. - All responses are no-store. Every device-flow response carries
Cache-Control: no-store, including thependingones. A confused intermediary cache cannot re-serve a stalesuccessbody to a fresh poll.
Troubleshooting
- "Sign-in window expired" — the 10-minute window elapsed, you missed the confirm click, or you double-confirmed in two browser tabs (first wins, second sees the consumed session as expired). Run
pakx loginagain for a fresh code. - "Polling too fast — backing off to Ns" — the server returned
slow_down. The CLI handles this automatically by bumping the local interval (RFC 8628 §3.5). No user action needed; the next poll uses the new interval. - Browser doesn't open. The CLI's best-effort launcher is hand-rolled per platform —
cmd /C starton Windows,openon macOS,xdg-openon Linux. Headless boxes, containers without xdg-utils, and corp policies that block child processes will all soft-fail this. The verification URL is also printed to your terminal — copy it and open it on any device that can reachregistry.pakx.dev. - "invalid-code" banner. The server intentionally collapses every non-success cause into the same banner. If you're sure the code matches your terminal, the most common explanations are: the code expired (10-minute TTL), you already confirmed it in another tab, or someone else (with the same user-code-prefix typo) clicked Deny on a colliding code. Run
pakx loginagain. - Windows path edge cases. The credentials file lives at
%USERPROFILE%\.pakx\credentials.jsonon Windows. Unix mode bits don't apply; pakx still prints the path on success so you can audit ACLs viaicacls. - CI / scripted runners. Device flow needs an interactive browser. For CI, mint a token in the dashboard and pass it via
PAKX_TOKENorpakx login --token. See CLI reference / pakx login.
Related
pakx login— CLI reference — every flag, exit code, and behaviour.- Device-flow endpoints — HTTP API reference — full request / response shapes for
POST /api/v1/auth/deviceandPOST /api/v1/auth/device/poll. - RFC 8628 — OAuth 2.0 Device Authorization Grant — the spec this flow implements.
- Token dashboard — mint a paste-in token for CI / headless use.