Device Authorization Grant
Overview
The Device Authorization Grant (RFC 8628) enables authentication on devices with limited input capabilities — smart TVs, CLI tools, IoT devices, and gaming consoles. The user authenticates on a separate device (e.g., a smartphone) by entering a short code.
sequenceDiagram
participant D as Device (TV/CLI)
participant SDK as SDK
participant AS as Auth Server
participant B as User's Browser
D->>SDK: Start flow
SDK->>AS: Device authz request
AS->>SDK: user_code + verification_uri
SDK->>D: Show code
B->>AS: Visit URL + enter code
B->>AS: Authenticate
SDK->>AS: Poll token
AS->>SDK: Tokens
SDK->>D: Authenticated
Starting the Device Flow
Use client.deviceFlow.start() to initiate the flow:
const state = await client.deviceFlow.start({ scope: 'openid profile',});
// Display to the userconsole.log(`Go to: ${state.verificationUri}`);console.log(`Enter code: ${state.userCode}`);DeviceFlowState
| Property | Type | Description |
|---|---|---|
deviceCode | string | Device verification code (used internally for polling) |
userCode | string | Short code displayed to the user |
verificationUri | string | URL where the user enters the code |
verificationUriComplete | string | undefined | URL with the code pre-filled (for QR codes) |
expiresAt | number | When the device code expires (epoch seconds) |
interval | number | Minimum polling interval in seconds |
Start Options
| Parameter | Type | Default | Description |
|---|---|---|---|
scope | string | 'openid' | Requested scopes |
extraParams | Record<string, string> | — | Additional parameters |
Polling for Completion
Automatic Polling
Use pollUntilComplete() to automatically poll until the user completes authentication:
const state = await client.deviceFlow.start({ scope: 'openid profile' });
// Display code to user...
// Poll until complete (handles slow_down and retries)const tokens = await client.deviceFlow.pollUntilComplete(state);console.log('Authenticated:', tokens.accessToken);Cancellation with AbortSignal
const controller = new AbortController();
// Cancel after 5 minutessetTimeout(() => controller.abort(), 5 * 60 * 1000);
try { const tokens = await client.deviceFlow.pollUntilComplete(state, { signal: controller.signal, });} catch (error) { if (error.code === 'device_authorization_expired') { console.log('Device code expired'); }}Manual Polling
For custom polling logic, use pollOnce():
const state = await client.deviceFlow.start();
// Custom polling looplet result;do { result = await client.deviceFlow.pollOnce(state);
switch (result.status) { case 'pending': // User hasn't completed yet await sleep(result.retryAfter * 1000); break; case 'slow_down': // Server requests slower polling await sleep(result.retryAfter * 1000); break; case 'completed': console.log('Tokens:', result.tokens); break; case 'expired': console.error('Device code expired'); break; case 'access_denied': console.error('User denied the request'); break; }} while (result.status === 'pending' || result.status === 'slow_down');DeviceFlowPollResult
| Status | Properties | Description |
|---|---|---|
'pending' | retryAfter: number | User hasn’t authorized yet |
'slow_down' | retryAfter: number | Poll interval increased by server |
'completed' | tokens: TokenSet | Authentication successful |
'expired' | — | Device code has expired |
'access_denied' | — | User denied authorization |
Checking Availability
Before using the device flow, verify that the authorization server supports it:
const isAvailable = await client.deviceFlow.isAvailable();
if (isAvailable) { const state = await client.deviceFlow.start(); // ...} else { console.log('Device flow not supported by this server');}Complete Example: CLI Application
import { createAuthrimClient } from '@authrim/core';
async function cliLogin() { const client = await createAuthrimClient({ issuer: 'https://auth.example.com', clientId: 'cli-app', crypto: nodeCryptoProvider, storage: fileStorageProvider, http: nodeHttpClient, });
// Check availability if (!(await client.deviceFlow.isAvailable())) { console.error('Device flow not supported'); process.exit(1); }
// Start device flow const state = await client.deviceFlow.start({ scope: 'openid profile email', });
// Display instructions console.log(''); console.log(' To sign in, open this URL in your browser:'); console.log(` ${state.verificationUri}`); console.log(''); console.log(` Enter code: ${state.userCode}`); console.log('');
// If available, show QR code if (state.verificationUriComplete) { console.log(` Or scan: ${state.verificationUriComplete}`); }
console.log(' Waiting for authentication...');
// Poll until complete try { const tokens = await client.deviceFlow.pollUntilComplete(state); console.log(' Login successful!');
const user = await client.getUser(); console.log(` Welcome, ${user.name}!`); } catch (error) { if (error.code === 'device_authorization_expired') { console.error(' Code expired. Please try again.'); } else if (error.code === 'device_access_denied') { console.error(' Authorization denied.'); } else { console.error(' Login failed:', error.message); } }}Error Handling
| Error Code | Description | Recovery |
|---|---|---|
device_authorization_error | Failed to start device flow | Retry or check configuration |
device_authorization_expired | Device code expired before user authenticated | Restart the flow |
device_access_denied | User denied the authorization request | Inform user, allow retry |
Next Steps
- Token Management — Manage the tokens after authentication
- Error Handling — Comprehensive error recovery strategies
- Events — Monitor authentication progress with events