Device authorization
OAuth-style device flow for CLIs, desktop apps, and AI agents that can't bundle an API key.
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
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /api/auth/device/code | none | Mint a device + user code. Rate-limited 5/min/IP. |
GET | /api/auth/device/lookup?user_code=XXXX-XXXX | session | Dashboard fetches client name + status. |
POST | /api/auth/device/authorize | session | User approves. Mints the API key. Rate-limited 10/min/user. |
POST | /api/auth/device/deny | session | User rejects. |
POST | /api/auth/device/token | none | Client polls. Returns the key once approved. Rate-limited 12/min/code. |
The flow
Client requests a code
bashcurl -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).
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.
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.
Client polls for the key
bashcurl -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
| Status | code | Meaning |
|---|---|---|
| 200 | (none) | Key returned. Store and stop polling. |
| 202 | AUTHORIZATION_PENDING | Keep polling. Respect interval. |
| 403 | ACCESS_DENIED | User pressed "Deny". Stop polling. |
| 410 | EXPIRED_TOKEN | Device code expired or already consumed. Restart the flow. |
| 429 | SLOW_DOWN | Polling 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()(notMath.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).