Skip to Content
ConceptsDevice Authorization Flow

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:

DeviceWhy Standard OAuth Fails
Smart TVsNo keyboard; on-screen keyboards are painful for typing passwords
CLI toolsNo browser; terminal-only interface
Game consolesController input; entering URLs and credentials is impractical
IoT devicesNo display at all, or a minimal display (e.g., LED matrix)
Digital signage / kiosksLocked-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 email

Step 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 }
FieldDescription
device_codeA long, cryptographically random string the client uses when polling. Never shown to the user.
user_codeA short, human-readable code the user types into the verification page.
verification_uriThe URL the user visits to enter the code. Must be short and memorable.
verification_uri_completeThe full URL with the user code pre-filled (for QR codes).
expires_inHow long the device and user codes are valid (seconds). Default: 600 (10 minutes).
intervalMinimum 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-id

Step 5: Server Responds Based on User Action

The server responds with one of several outcomes, depending on the current state:

ResponseHTTP StatusMeaningClient Action
authorization_pending400User has not yet completed authenticationContinue polling at the configured interval
slow_down400Client is polling too frequentlyIncrease polling interval by 5 seconds
expired_token400The device_code has expired (user took too long)Restart the flow from Step 1
access_denied400User explicitly denied the authorization requestShow an error to the user; do not retry
Success (access token)200User approved the requestStore 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:

  1. Short: Easy to type on a phone keyboard (8 characters is the sweet spot)
  2. Unambiguous: No characters that look alike (avoid 0/O, 1/I/l, 5/S, 2/Z)
  3. Case-insensitive: Users should not need to worry about capitalization
  4. Groupable: A hyphen or space in the middle helps readability (WDJB-MJHT vs WDJBMJHT)

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 Y

This 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.

PropertyUser CodeDevice Code
Length8 characters40+ characters
Alphabet20 uppercase lettersFull URL-safe base64
Entropy~34.6 bits~240 bits
Shown to userYesNo
Stored server-sidePlaintext (for user entry matching)SHA-256 hash (like passwords)
PurposeUser identificationClient 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:

  1. 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.
  2. 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.
  3. Short lifetime: The 10-minute window limits the attack window.
  4. Rate limiting: Auris limits device code requests per client to prevent mass-generation attacks.

Comparison with Other Grant Types

Grant TypeInput RequiredUser PresentBrowser NeededBest For
Authorization Code + PKCEBrowser + keyboardYesYes (on same device)Web apps, mobile apps
Device AuthorizationDisplay onlyYes (on secondary device)Yes (on secondary device)Smart TVs, CLI tools, IoT
Client CredentialsNoneNoNoServer-to-server (M2M)
CIBANone (push notification)Yes (on notification device)NoCall 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:

SettingDefaultDescription
Enable Device FlowfalseMaster toggle
User code length8Number of characters in the user code
Code lifetime600Seconds before the device/user codes expire
Polling interval5Minimum seconds between token endpoint polls

API Endpoints

EndpointMethodDescription
/api/oauth/devicePOSTRequest device and user codes
/api/auth/tokenPOSTToken endpoint (supports device_code grant type)
/hosted/deviceGETUser-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.') }