Device authorization flow

The device flow lets an interactive client (CLI, desktop app, IDE extension) obtain an API key without ever handling the user's password or asking them to copy-paste a key.

This is what powers rogeriq auth login. Use it when shipping any client that runs on a user's machine.

Endpoints

MethodPathAuthPurpose
POST/api/auth/device/codenoneMint a device + user code. Rate-limited 5/min/IP.
GET/api/auth/device/lookup?user_code=XXXX-XXXXsessionDashboard fetches client name + status.
POST/api/auth/device/authorizesessionUser approves. Mints the API key. Rate-limited 10/min/user.
POST/api/auth/device/denysessionUser rejects.
POST/api/auth/device/tokennoneClient polls. Returns the key once approved. Rate-limited 12/min/code.

The flow

1

Client requests a code

bash
curl -X POST https://api.rogeriq.com/api/auth/device/code \ -H "Content-Type: application/json" \ -d '{"client_name": "rogeriq-cli (sean@laptop)"}'

Response:

json
{ "device_code": "abc...48chars...", "user_code": "ABCD-1234", "verification_uri": "https://rogeriq.com/device", "verification_uri_complete": "https://rogeriq.com/device?code=ABCD-1234", "expires_in": 900, "interval": 5}

The user_code uses an unambiguous alphabet (no O/0/I/1/L).

2

Client displays the code + opens browser

Print the code to the terminal and try to open verification_uri_complete in the user's default browser. Always print the URL too — SSH / headless boxes can't auto-open.

3

User approves in the browser

On rogeriq.com/device the user picks an org (auto-selected if they belong to one) and clicks Authorize. The dashboard calls POST /api/auth/device/authorize.

4

Client polls for the key

bash
curl -X POST https://api.rogeriq.com/api/auth/device/token \ -H "Content-Type: application/json" \ -d '{"device_code": "abc..."}'

Until the user approves, returns 202 with code: AUTHORIZATION_PENDING.

On approval, returns the API key once:

json
{ "access_token": "riq_a1b2c3d4...", "token_type": "bearer", "api_key": "riq_a1b2c3d4...", "org_id": "org_xxxxx", "scopes": ["read", "write"]}

The device record is deleted on first successful read — a second poll returns 410 EXPIRED_TOKEN. Store the key immediately.

Status codes from /device/token

StatuscodeMeaning
200(none)Key returned. Store and stop polling.
202AUTHORIZATION_PENDINGKeep polling. Respect interval.
403ACCESS_DENIEDUser pressed "Deny". Stop polling.
410EXPIRED_TOKENDevice code expired or already consumed. Restart the flow.
429SLOW_DOWNPolling too fast. Double your interval.

Polling interval

Start at the server-suggested interval (5s). If you receive 429 SLOW_DOWN, double it. Respect any interval value in the 429 body.

Security model

  • Device codes carry 240 bits of entropy (48 chars × log₂(36)).
  • User codes are 8 chars from a 31-char alphabet ≈ 40 bits — short enough to type, long enough to defeat brute force given the 5/min/IP limit at code generation and 12/min/code at polling.
  • Single-use enforcement: the reverse lookup (user_code → device_code) is deleted before the API key is minted, preventing concurrent authorize calls from racing. The device record is deleted on first successful token poll.
  • Codes expire after 15 minutes regardless of activity.
  • All generation uses crypto.getRandomValues() (not Math.random()).

Reference implementation

The rogeriq auth login command is the reference implementation. The polling loop, browser-open fallback, and SLOW_DOWN backoff handling are in cli/src/lib/device-auth.ts (shipped in the @rogeriq/cli npm tarball).

Ask a question... ⌘I