Device Authorization Grant (RFC 8628)
The OAuth 2.0 Device Authorization Grant (RFC 8628) lets input-constrained clients — CLIs, AI agents, MCP servers, IoT firmware, smart TVs — authenticate users without embedding a browser, screen-scraping a redirect, or storing user passwords.
How it works
Section titled “How it works”┌──────────┐ 1. POST /oauth/device/code ┌──────────────┐│ Device │ ────────────────────────────► │ Authorization││ (CLI/AI) │ ◄──────── device_code, ────── │ Server ││ │ user_code, │ ││ │ verification_uri │ │└──────────┘ └──────────────┘ │ │ 2. Display user_code + verification_uri to the user. │ (User opens the URL on phone/laptop.) ▼ User signs in → consent screen → Approve. │ │ 3. POST /oauth/token (grant_type=…device_code) ▼┌──────────┐ ┌──────────────┐│ Device │ ────────────────────────────► │ Authorization││ (polls) │ ◄────── access_token, │ Server ││ │ refresh_token, │ ││ │ id_token (if openid) │ │└──────────┘ └──────────────┘Endpoints
Section titled “Endpoints”| Endpoint | Purpose |
|---|---|
POST /oauth/device/code | Issues device_code + human-friendly user_code |
POST /oauth/token (grant_type=urn:ietf:params:oauth:grant-type:device_code) | Polls for the user’s approval |
GET /.well-known/oauth-authorization-server | Advertises the device-authorization endpoint per RFC 8414 |
Both endpoints accept application/x-www-form-urlencoded and follow the
RFC 6749 §5.2 error envelope (error, error_description, error_uri).
Defaults
Section titled “Defaults”| Setting | Value |
|---|---|
expires_in | 1800s (30 minutes) — matches Auth0/Google defaults; longer than the RFC’s 900s suggestion to give phone-bound users typing room |
interval | 5s |
user_code format | 8 chars XXXX-XXXX from a 31-symbol alphabet (no 0/O/1/I/L) |
device_code entropy | 256 bits, base64url-encoded |
| Max active codes per (tenant, client) | 5 — 6th request returns slow_down |
| Max active codes per tenant | 1000 (global ceiling across all clients) |
| Per-IP rate limit | 10 / 15 minutes on /oauth/device/code |
Free for all tiers
Section titled “Free for all tiers”Unlike Auth0 (which gates device flow behind paid tiers), Rakomi includes RFC 8628 in every plan including Free.
Quick starts
Section titled “Quick starts”Node CLI (with @rakomi/node)
Section titled “Node CLI (with @rakomi/node)”import { runDeviceFlow } from '@rakomi/node';
const result = await runDeviceFlow({ clientId: process.env.RAKOMI_CLIENT_ID!, scope: 'openid profile email', onCode: ({ user_code, verification_uri }) => { console.log(`To sign in, open ${verification_uri} and enter: ${user_code}`); },});
if (!result.ok) { console.error('Device flow failed:', result.error.code, result.error.message); process.exit(1);}
const { access_token, refresh_token, id_token } = result.data;console.log('Signed in. Access token expires in', result.data.expires_in, 's');The SDK helper handles slow_down (adds 5s to the interval per RFC 8628 §3.5),
honors AbortSignal for cancellation, and surfaces typed error codes
(device/authorization_pending, device/access_denied, device/expired_token).
Python (with requests)
Section titled “Python (with requests)”import timeimport requests
API = "https://api.rakomi.com"CLIENT_ID = "your_client_id"
# 1. Initiate.issued = requests.post( f"{API}/oauth/device/code", data={"client_id": CLIENT_ID, "scope": "openid profile email"},).json()
print(f"Open {issued['verification_uri']} and enter: {issued['user_code']}")
# 2. Poll until tokens or terminal error.interval = issued["interval"]deadline = time.time() + issued["expires_in"]
while time.time() < deadline: time.sleep(interval) res = requests.post( f"{API}/oauth/token", data={ "grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": issued["device_code"], "client_id": CLIENT_ID, }, ) body = res.json() if res.ok: print("Signed in.", body["access_token"][:20], "…") break err = body.get("error") if err == "authorization_pending": continue if err == "slow_down": interval += 5 # RFC 8628 §3.5 continue print("Failed:", err, body.get("error_description")) breakAI agent (Claude / Cursor / MCP) shell snippet
Section titled “AI agent (Claude / Cursor / MCP) shell snippet”# Initiate.ISSUED=$(curl -sX POST https://api.rakomi.com/oauth/device/code \ -d "client_id=$RAKOMI_CLIENT_ID" \ -d 'scope=openid profile email')
USER_CODE=$(echo "$ISSUED" | jq -r .user_code)VERIFY_URL=$(echo "$ISSUED" | jq -r .verification_uri)DEVICE_CODE=$(echo "$ISSUED" | jq -r .device_code)INTERVAL=$(echo "$ISSUED" | jq -r .interval)
cat <<EOFTo grant this agent access to your Rakomi account, open this URL on any device: $VERIFY_URLAnd enter the code: $USER_CODEEOF
# Poll.while sleep "$INTERVAL"; do RES=$(curl -sX POST https://api.rakomi.com/oauth/token \ -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \ -d "device_code=$DEVICE_CODE" \ -d "client_id=$RAKOMI_CLIENT_ID") ERR=$(echo "$RES" | jq -r '.error // empty') case "$ERR" in authorization_pending) continue ;; slow_down) INTERVAL=$((INTERVAL + 5)); continue ;; "") export ACCESS_TOKEN=$(echo "$RES" | jq -r .access_token); break ;; *) echo "Auth failed: $ERR"; exit 1 ;; esacdoneSecurity model
Section titled “Security model”- Anti-phishing. The consent screen always shows the requesting client’s name and the exact scopes — never auto-approves. RFC 8628 §5.3 mitigation.
- Single-use. Each
device_codeis consumed in the same DB transaction that mints tokens (SELECT FOR UPDATE+DELETE). A second poll after success returnsinvalid_grant. RFC 8628 §3.5. - Constant-time client_secret comparison. Confidential clients have their
secret verified via
crypto.timingSafeEqualagainst an Argon2id-hashed store, with a fallback to the previous-secret-hash within the rotation window. - Audit trail. Every issuance, approval, denial, expiry, and rate-limit
rejection emits a structured
oauth.device.*event with a 12-char SHA-256 prefix of thedevice_codefor forensic correlation. Raw codes are never logged. - Multi-tenant isolation. All device-authorization rows are tenant-scoped
via PostgreSQL RLS (the belt) plus app-level
withTenant(the suspenders). Cross-tenantdevice_codepolls returninvalid_grant.
Client registration
Section titled “Client registration”Enable the device-grant on a new or existing OAuth client:
- Dashboard → OAuth Clients → Add OAuth Client (or edit an existing one).
- Check Device Authorization Grant (RFC 8628) in the Grant types list.
- For CLIs / AI agents / IoT firmware: also set Client type to
Public (no
client_secret). For server-side confidential apps that want device flow: keep Confidential and supplyclient_secretin the body or HTTP Basic header.
Discovery
Section titled “Discovery”The endpoint is advertised in the standard authorization-server metadata:
curl https://api.rakomi.com/.well-known/oauth-authorization-server | jq .device_authorization_endpoint# → "https://api.rakomi.com/oauth/device/code"Spec references
Section titled “Spec references”- RFC 8628 — OAuth 2.0 Device Authorization Grant
- RFC 6749 §5.2 — OAuth error response format
- RFC 8414 — Authorization Server Metadata
- OpenID Connect Core 1.0 §16.2 —
noncereplay defence (Rakomi propagatesnoncefrom/oauth/device/codeinto the issuedid_token)