Device Authorization Flow
The Problem: Devices Without Keyboards
OAuth 2.0’s most common grant type — Authorization Code with PKCE — assumes the client can open a browser, display a login page, and accept keyboard input from the user. This assumption breaks down for an entire category of devices:
| Device | Why Standard OAuth Fails |
|---|---|
| Smart TVs | No keyboard; on-screen keyboards are painful for typing passwords |
| CLI tools | No browser; terminal-only interface |
| Game consoles | Controller input; entering URLs and credentials is impractical |
| IoT devices | No display at all, or a minimal display (e.g., LED matrix) |
| Digital signage / kiosks | Locked-down environment; no browser navigation allowed |
| Streaming dongles (Chromecast, Fire Stick) | Remote control only; no keyboard |
These devices need to authenticate users, but they cannot host a login form. The user needs to authenticate somewhere else — on their phone or laptop — and the device needs to learn that authentication succeeded.
This is exactly what the OAuth 2.0 Device Authorization Grant (RFC 8628) solves.
How Device Flow Works
The Device Authorization Grant introduces a secondary device pattern: the client device displays a short code, the user enters that code on a device with a browser (their phone or laptop), and the client device polls the authorization server until the user completes authentication.
The Flow Step by Step
Step 1: Client Requests Device and User Codes
The client sends a POST request to the device authorization endpoint with its client_id and the requested scope:
POST /api/oauth/device HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
client_id=tv-app-client-id
&scope=openid profile emailStep 2: Server Returns Device Code, User Code, and Verification URI
The authorization server responds with a JSON object containing everything needed for the flow:
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"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 | Description |
|---|---|
device_code | A long, cryptographically random string the client uses when polling. Never shown to the user. |
user_code | A short, human-readable code the user types into the verification page. |
verification_uri | The URL the user visits to enter the code. Must be short and memorable. |
verification_uri_complete | The full URL with the user code pre-filled (for QR codes). |
expires_in | How long the device and user codes are valid (seconds). Default: 600 (10 minutes). |
interval | Minimum polling interval in seconds. The client must not poll faster than this. |
Step 3: User Authenticates on Their Secondary Device
The client device displays the user_code and verification_uri to the user. Depending on the device capabilities, this might be:
- CLI tool: Prints the URL and code to the terminal
- Smart TV: Shows a large QR code (encoding
verification_uri_complete) alongside the text code - IoT device: Displays the code on an LED screen, with the URL printed on the device housing
The user opens verification_uri on their phone or laptop, enters the user_code, logs in with their credentials (including MFA if required), and approves the device.
Step 4: Client Polls the Token Endpoint
While the user is authenticating, the client device polls the token endpoint at the configured interval:
POST /api/auth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:device_code
&device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
&client_id=tv-app-client-idStep 5: Server Responds Based on User Action
The server responds with one of several outcomes, depending on the current state:
| Response | HTTP Status | Meaning | Client Action |
|---|---|---|---|
authorization_pending | 400 | User has not yet completed authentication | Continue polling at the configured interval |
slow_down | 400 | Client is polling too frequently | Increase polling interval by 5 seconds |
expired_token | 400 | The device_code has expired (user took too long) | Restart the flow from Step 1 |
access_denied | 400 | User explicitly denied the authorization request | Show an error to the user; do not retry |
| Success (access token) | 200 | User approved the request | Store the tokens; authentication is complete |
// Pending response
{
"error": "authorization_pending",
"error_description": "The authorization request is still pending."
}
// Slow down response
{
"error": "slow_down",
"error_description": "Polling too frequently. Increase interval by 5 seconds."
}
// Success response
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"scope": "openid profile email"
}The slow_down response is not just informational — it is a hard requirement. When a client receives slow_down, it must increase its polling interval by at least 5 seconds. Clients that ignore this and continue polling at the original rate may have their device_code revoked.
User Code Design
The user_code is the single most critical UX element in the Device Flow. It is the only thing the user types manually, so its design directly affects success rates.
Requirements
RFC 8628 specifies that user codes must be:
- Short: Easy to type on a phone keyboard (8 characters is the sweet spot)
- Unambiguous: No characters that look alike (avoid
0/O,1/I/l,5/S,2/Z) - Case-insensitive: Users should not need to worry about capitalization
- Groupable: A hyphen or space in the middle helps readability (
WDJB-MJHTvsWDJBMJHT)
Auris User Code Alphabet
Auris generates user codes from a restricted alphabet of 20 characters:
B C D F G H J K M N P Q R T V W X YThis excludes all vowels (preventing accidental generation of offensive words) and all ambiguous characters. With an 8-character code drawn from a 20-character alphabet:
20^8 = 25,600,000,000 possible codes (~25.6 billion)At a rate of 1,000 guesses per second (already well above what rate limiting would allow), brute-forcing a valid code would take approximately 296 days on average. Combined with the 10-minute default expiry, the probability of a successful brute-force attack is negligible.
User codes must be single-use and expire promptly. Auris deletes the DeviceCode record as soon as the user approves or denies the request, or when the expires_in period elapses. Never extend the lifetime of a device code beyond what the UX requires.
Device Code Security
While the user_code is short and human-readable, the device_code is a long, cryptographically random string that serves as the client’s credential during polling. It is never shown to the user.
| Property | User Code | Device Code |
|---|---|---|
| Length | 8 characters | 40+ characters |
| Alphabet | 20 uppercase letters | Full URL-safe base64 |
| Entropy | ~34.6 bits | ~240 bits |
| Shown to user | Yes | No |
| Stored server-side | Plaintext (for user entry matching) | SHA-256 hash (like passwords) |
| Purpose | User identification | Client authentication during polling |
Auris stores device codes as SHA-256 hashes, not plaintext. If the database is compromised, the attacker cannot reconstruct valid device codes from the hashes.
Security Analysis
Phishing Resistance
The Device Flow is inherently vulnerable to a specific phishing attack: an attacker starts a Device Flow on their own device, then tricks a user into entering the attacker’s user code on the legitimate verification page. If the user approves, the attacker’s device receives tokens bound to the user’s account.
Mitigations:
- Clear consent screen: The verification page must clearly state that approving will grant access to a specific device/application. Auris displays the application name, requested scopes, and a warning that the user should only proceed if they initiated the request.
- verification_uri_complete: When the user scans a QR code, the code is pre-filled. The user should verify it matches what their device displays.
- Short lifetime: The 10-minute window limits the attack window.
- Rate limiting: Auris limits device code requests per client to prevent mass-generation attacks.
Comparison with Other Grant Types
| Grant Type | Input Required | User Present | Browser Needed | Best For |
|---|---|---|---|---|
| Authorization Code + PKCE | Browser + keyboard | Yes | Yes (on same device) | Web apps, mobile apps |
| Device Authorization | Display only | Yes (on secondary device) | Yes (on secondary device) | Smart TVs, CLI tools, IoT |
| Client Credentials | None | No | No | Server-to-server (M2M) |
| CIBA | None (push notification) | Yes (on notification device) | No | Call centers, payment approval |
Key differences from CIBA:
- Device Flow requires the user to actively visit a URL and enter a code. The user initiates the secondary-device interaction.
- CIBA pushes a notification to the user. The authorization server initiates the secondary-device interaction.
- Device Flow works for anonymous users — no prior registration for notification delivery is needed.
- CIBA requires the authorization server to know how to reach the user (email, phone, push notification).
Auris Implementation Details
Prisma Model
The Device Flow state is managed via the DeviceCode Prisma model:
model DeviceCode {
id String @id @default(cuid())
tenantId String
applicationId String
deviceCode String @unique // SHA-256 hash
userCode String @unique // plaintext for matching
scope String?
status DeviceCodeStatus @default(PENDING)
expiresAt DateTime
interval Int @default(5)
userId String? // set when user approves
lastPolledAt DateTime?
createdAt DateTime @default(now())
}
enum DeviceCodeStatus {
PENDING
APPROVED
DENIED
EXPIRED
}Configuration
Device Flow is enabled per-application in the Auris Console under Applications > (select application) > Settings:
| Setting | Default | Description |
|---|---|---|
| Enable Device Flow | false | Master toggle |
| User code length | 8 | Number of characters in the user code |
| Code lifetime | 600 | Seconds before the device/user codes expire |
| Polling interval | 5 | Minimum seconds between token endpoint polls |
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/oauth/device | POST | Request device and user codes |
/api/auth/token | POST | Token endpoint (supports device_code grant type) |
/hosted/device | GET | User-facing verification page |
Code Example: CLI Tool with Device Flow
The following example shows a CLI tool implementing the full Device Flow:
async function loginWithDeviceFlow(clientId: string, domain: string) {
// Step 1: Request device codes
const deviceResponse = await fetch(`${domain}/api/oauth/device`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: clientId,
scope: 'openid profile email',
}),
})
const {
device_code,
user_code,
verification_uri,
verification_uri_complete,
interval,
expires_in,
} = await deviceResponse.json()
// Step 2: Display instructions to the user
console.log('\n To sign in, visit:', verification_uri)
console.log(' and enter code: ', user_code)
console.log(`\n Or open: ${verification_uri_complete}`)
console.log(`\n This code expires in ${Math.floor(expires_in / 60)} minutes.\n`)
// Step 3: Poll for completion
let pollInterval = interval * 1000
const deadline = Date.now() + expires_in * 1000
while (Date.now() < deadline) {
await new Promise(resolve => setTimeout(resolve, pollInterval))
const tokenResponse = await fetch(`${domain}/api/auth/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,
client_id: clientId,
}),
})
if (tokenResponse.ok) {
const tokens = await tokenResponse.json()
console.log(' Authenticated successfully!')
return tokens
}
const error = await tokenResponse.json()
if (error.error === 'slow_down') {
pollInterval += 5000 // Increase interval by 5 seconds
continue
}
if (error.error === 'authorization_pending') {
continue // Keep polling
}
// expired_token, access_denied, or other error
throw new Error(`Authentication failed: ${error.error_description}`)
}
throw new Error('Device code expired. Please try again.')
}Related Concepts
- OAuth 2.0 & OIDC — The authorization framework Device Flow extends
- CIBA (Backchannel Auth) — Another grant type for decoupled authentication
- Tokens Explained — JWT structure, access tokens, and refresh tokens
- PKCE Flow — The standard browser-based grant type that Device Flow complements