Device Flow
Device Flow is an OAuth 2.0 extension designed for input-constrained devices (smart TVs, IoT devices, CLI tools) that lack a web browser or have limited input capabilities. Users authenticate on a separate device (smartphone/PC) using a simple verification code or QR code.
Overview
- RFC: RFC 8628 - OAuth 2.0 Device Authorization Grant
- Status: Fully Implemented
- Supported Endpoints:
/device_authorization,/device,/token - OIDC Extensions: ID Token issuance supported
Why Use Device Flow?
Key Benefits
-
Input-Constrained Devices
- Perfect for devices without keyboards (Smart TVs, streaming boxes)
- No need for embedded web browser on the device
- Simple 8-character verification code (e.g.,
WDJB-MJHT) - QR code support for instant scanning
-
Secure Authentication
- User authenticates on their trusted device (smartphone/PC)
- OAuth 2.0 security model with PKCE support
- One-time use device codes (prevents replay attacks)
- Automatic code expiration (10 minutes default)
-
Excellent User Experience
- No complex URL typing on TV remotes
- Scan QR code or enter short code (8 characters)
- Authenticate using familiar device (phone/PC)
Use Cases
- Smart TV Apps (Netflix/YouTube-style login)
- IoT Devices (smart home devices, security cameras)
- CLI Tools (GitHub CLI, AWS CLI, developer tools)
- Gaming Consoles (Xbox, PlayStation login flows)
- Kiosk Terminals (public terminals with limited input)
How It Works
Flow Overview
- Device requests authorization: Device sends
POST /device_authorization - Server returns codes: Returns
device_code,user_code,verification_uri - Device displays code: Shows QR code and user code on screen
- User authenticates: User visits URL on phone/PC and enters code
- Device polls for tokens: Device polls
/tokenendpoint - Server returns tokens: After user approval, returns tokens
API Reference
1. Device Authorization Endpoint
POST /device_authorization
Request
POST /device_authorization HTTP/1.1Content-Type: application/x-www-form-urlencoded
client_id=tv_app_123&scope=openid+profile+emailResponse
{ "device_code": "4c9a8e6f-b2d1-4a7c-9e3f-1d2b4a7c9e3f", "user_code": "WDJB-MJHT", "verification_uri": "https://auth.example.com/device", "verification_uri_complete": "https://auth.example.com/device?user_code=WDJB-MJHT", "expires_in": 600, "interval": 5}| Field | Type | Description |
|---|---|---|
device_code | string | UUID for polling (keep secret) |
user_code | string | 8-char code for user entry |
verification_uri | string | URL for manual code entry |
verification_uri_complete | string | URL with code pre-filled |
expires_in | number | Code expiration in seconds |
interval | number | Minimum polling interval in seconds |
2. Token Endpoint (Polling)
POST /token
POST /token HTTP/1.1Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=4c9a8e6f-b2d1-4a7c-9e3f-1d2b4a7c9e3f&client_id=tv_app_123Response States
Pending (keep polling):
{ "error": "authorization_pending", "error_description": "User has not yet authorized the device"}Too Fast (slow down):
{ "error": "slow_down", "error_description": "You are polling too frequently. Please slow down."}Success:
{ "access_token": "eyJhbGc...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "rt_...", "id_token": "eyJhbGc...", "scope": "openid profile email"}Error Codes:
authorization_pending- User hasn’t approved yet (keep polling)slow_down- Polling too fast (increase interval by 5 seconds)access_denied- User denied the requestexpired_token- Device code expired (restart flow)
Usage Examples
Smart TV App Integration
// Step 1: Request device codeconst response = await fetch('https://auth.example.com/device_authorization', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: 'smart_tv_app_123', scope: 'openid profile email' })});
const data = await response.json();// Display: QR Code for verification_uri_complete// Display: "Enter code: WDJB-MJHT"
// Step 2: Poll for authorizationconst pollInterval = data.interval * 1000;let currentInterval = pollInterval;
const pollForAuthorization = async () => { const tokenResponse = await fetch('https://auth.example.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: data.device_code, client_id: 'smart_tv_app_123' }) });
if (tokenResponse.ok) { const tokens = await tokenResponse.json(); console.log('Logged in!', tokens); return tokens; }
const error = await tokenResponse.json();
if (error.error === 'authorization_pending') { setTimeout(pollForAuthorization, currentInterval); } else if (error.error === 'slow_down') { currentInterval += 5000; setTimeout(pollForAuthorization, currentInterval); } else { console.error('Login failed:', error.error_description); }};
setTimeout(pollForAuthorization, currentInterval);CLI Tool Integration
$ awesome-cli login
Device Login
Visit: https://auth.example.com/device
And enter code: WDJB-MJHT
Or scan this QR code:
[QR Code]
Waiting for authorization...Login successful! You're now authenticated.Security Considerations
Device Code Entropy
- UUID v4 provides 122 bits of entropy
- Prevents guessing attacks
Code Expiration
- Default: 600 seconds (10 minutes)
- Automatic cleanup via Durable Object alarms
Rate Limiting
- Minimum interval: 5 seconds (default)
slow_downerror if polling too fast- Max poll count: 120 polls
One-Time Use
- Device codes invalidated after first use
- Replay attack detection
User Code Design
- Charset:
23456789ABCDEFGHJKMNPQRSTUVWXYZ - Excludes confusing characters:
0, O, 1, I, L - Format:
XXXX-XXXX(hyphen for readability)
Troubleshooting
”Invalid or expired verification code”
Cause: Code expired (10 minutes) or already used
Solution: Restart device flow to get a new code
”slow_down” error during polling
Cause: Client polling faster than allowed interval
Solution: Increase polling interval by 5 seconds
UI_BASE_URL not configured
Cause: Environment variable not set
Solution: Set UI_BASE_URL in wrangler.toml or .dev.vars