Device Authorization

The device authorization flow enables CLI tools and desktop applications to authenticate without requiring users to enter credentials directly in the application.

Overview#

This flow is ideal for:

  • Command-line tools
  • Desktop applications
  • Smart TV apps
  • IoT devices
  • Any environment without a browser

Flow Diagram#

┌─────────┐ ┌─────────────┐ │ CLI │ │ Bootspring │ │ App │ │ API │ └────┬────┘ └──────┬──────┘ │ │ │ 1. POST /auth/device/code │ │ ─────────────────────────────────────────────► │ │ │ │ 2. Returns device_code, user_code │ │ ◄───────────────────────────────────────────── │ │ │ │ 3. Display user_code to user │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ User visits bootspring.dev/device │ │ │ │ and enters code: ABCD-1234 │ │ │ └─────────────────────────────────────────┘ │ │ │ │ 4. Poll POST /auth/device/token │ │ ─────────────────────────────────────────────► │ │ │ │ 5. Returns access_token (when authorized) │ │ ◄───────────────────────────────────────────── │ │ │

Step 1: Request Device Code#

Request#

1POST https://api.bootspring.dev/v1/auth/device/code 2Content-Type: application/json 3 4{ 5 "client_id": "bootspring-cli", 6 "scope": "read:projects write:projects read:usage" 7}

Parameters#

ParameterTypeRequiredDescription
client_idstringYesYour application identifier
scopestringNoSpace-separated list of scopes

Response#

1{ 2 "device_code": "dev_abc123def456...", 3 "user_code": "ABCD-1234", 4 "verification_uri": "https://bootspring.dev/device", 5 "verification_uri_complete": "https://bootspring.dev/device?code=ABCD-1234", 6 "expires_in": 900, 7 "interval": 5 8}

Response Fields#

FieldDescription
device_codeCode used to poll for authorization
user_codeCode the user enters on the website
verification_uriURL where user authorizes the device
verification_uri_completeURL with code pre-filled
expires_inSeconds until codes expire (default: 900)
intervalMinimum seconds between poll requests

Step 2: Display to User#

Present the user with clear instructions:

═══════════════════════════════════════════ Bootspring Device Authorization ═══════════════════════════════════════════ To sign in, visit: https://bootspring.dev/device And enter code: ABCD-1234 Or scan this QR code: [QR CODE] Waiting for authorization... ═══════════════════════════════════════════

Best Practices#

  • Display the code prominently
  • Show a QR code for verification_uri_complete when possible
  • Indicate that the app is waiting
  • Show a countdown for expiration

Step 3: Poll for Token#

Poll the token endpoint at the specified interval.

Request#

1POST https://api.bootspring.dev/v1/auth/device/token 2Content-Type: application/json 3 4{ 5 "client_id": "bootspring-cli", 6 "device_code": "dev_abc123def456...", 7 "grant_type": "urn:ietf:params:oauth:grant-type:device_code" 8}

Pending Response#

{ "error": "authorization_pending", "error_description": "The user has not yet authorized this device" }

Success Response#

1{ 2 "access_token": "bsd_live_xyz789...", 3 "token_type": "Bearer", 4 "expires_in": 31536000, 5 "scope": "read:projects write:projects read:usage", 6 "refresh_token": "bsr_abc123..." 7}

Error Responses#

ErrorDescriptionAction
authorization_pendingUser hasn't authorized yetContinue polling
slow_downPolling too fastIncrease interval by 5 seconds
expired_tokenDevice code expiredRestart from step 1
access_deniedUser denied authorizationShow error to user

Implementation Example#

TypeScript#

1// lib/device-auth.ts 2interface DeviceCodeResponse { 3 device_code: string; 4 user_code: string; 5 verification_uri: string; 6 verification_uri_complete: string; 7 expires_in: number; 8 interval: number; 9} 10 11interface TokenResponse { 12 access_token: string; 13 token_type: string; 14 expires_in: number; 15 scope: string; 16 refresh_token?: string; 17} 18 19const API_BASE = 'https://api.bootspring.dev/v1'; 20 21export async function requestDeviceCode( 22 scope: string = 'read:projects write:projects' 23): Promise<DeviceCodeResponse> { 24 const response = await fetch(`${API_BASE}/auth/device/code`, { 25 method: 'POST', 26 headers: { 'Content-Type': 'application/json' }, 27 body: JSON.stringify({ 28 client_id: 'bootspring-cli', 29 scope, 30 }), 31 }); 32 33 if (!response.ok) { 34 throw new Error('Failed to get device code'); 35 } 36 37 return response.json(); 38} 39 40export async function pollForToken( 41 deviceCode: string, 42 interval: number, 43 expiresIn: number 44): Promise<TokenResponse> { 45 const startTime = Date.now(); 46 const expiresAt = startTime + expiresIn * 1000; 47 let pollInterval = interval * 1000; 48 49 while (Date.now() < expiresAt) { 50 await sleep(pollInterval); 51 52 const response = await fetch(`${API_BASE}/auth/device/token`, { 53 method: 'POST', 54 headers: { 'Content-Type': 'application/json' }, 55 body: JSON.stringify({ 56 client_id: 'bootspring-cli', 57 device_code: deviceCode, 58 grant_type: 'urn:ietf:params:oauth:grant-type:device_code', 59 }), 60 }); 61 62 const data = await response.json(); 63 64 if (response.ok) { 65 return data as TokenResponse; 66 } 67 68 if (data.error === 'slow_down') { 69 pollInterval += 5000; 70 } else if (data.error === 'authorization_pending') { 71 // Continue polling 72 } else { 73 throw new Error(data.error_description || data.error); 74 } 75 } 76 77 throw new Error('Device authorization timed out'); 78} 79 80function sleep(ms: number): Promise<void> { 81 return new Promise((resolve) => setTimeout(resolve, ms)); 82} 83 84// Usage 85async function authenticate() { 86 const deviceCode = await requestDeviceCode(); 87 88 console.log(`Visit ${deviceCode.verification_uri}`); 89 console.log(`Enter code: ${deviceCode.user_code}`); 90 91 const token = await pollForToken( 92 deviceCode.device_code, 93 deviceCode.interval, 94 deviceCode.expires_in 95 ); 96 97 console.log('Authenticated!'); 98 return token; 99}

CLI Example#

1// cli/auth.ts 2import ora from 'ora'; 3import open from 'open'; 4import { requestDeviceCode, pollForToken } from './device-auth'; 5 6export async function login() { 7 const spinner = ora('Requesting authorization...').start(); 8 9 try { 10 const deviceCode = await requestDeviceCode(); 11 spinner.stop(); 12 13 console.log('\n╔════════════════════════════════════════╗'); 14 console.log('║ Bootspring Device Authorization ║'); 15 console.log('╠════════════════════════════════════════╣'); 16 console.log('║ ║'); 17 console.log(`║ Visit: ${deviceCode.verification_uri.padEnd(29)}`); 18 console.log('║ ║'); 19 console.log(`║ Code: ${deviceCode.user_code.padEnd(29)}`); 20 console.log('║ ║'); 21 console.log('╚════════════════════════════════════════╝\n'); 22 23 // Open browser automatically 24 await open(deviceCode.verification_uri_complete); 25 26 spinner.start('Waiting for authorization...'); 27 28 const token = await pollForToken( 29 deviceCode.device_code, 30 deviceCode.interval, 31 deviceCode.expires_in 32 ); 33 34 spinner.succeed('Successfully authenticated!'); 35 36 // Store token securely 37 await storeToken(token); 38 39 return token; 40 } catch (error) { 41 spinner.fail(`Authentication failed: ${error.message}`); 42 throw error; 43 } 44}

Token Storage#

Store tokens securely on the device:

macOS (Keychain)#

1import keytar from 'keytar'; 2 3async function storeToken(token: TokenResponse) { 4 await keytar.setPassword('bootspring', 'access_token', token.access_token); 5 if (token.refresh_token) { 6 await keytar.setPassword('bootspring', 'refresh_token', token.refresh_token); 7 } 8} 9 10async function getToken(): Promise<string | null> { 11 return keytar.getPassword('bootspring', 'access_token'); 12}

Linux (Secret Service)#

// Same API as macOS with keytar

Windows (Credential Manager)#

// Same API as macOS with keytar

Token Refresh#

Refresh tokens before they expire:

1POST https://api.bootspring.dev/v1/auth/device/refresh 2Content-Type: application/json 3 4{ 5 "client_id": "bootspring-cli", 6 "refresh_token": "bsr_abc123...", 7 "grant_type": "refresh_token" 8}

Response:

1{ 2 "access_token": "bsd_live_new_token...", 3 "token_type": "Bearer", 4 "expires_in": 31536000, 5 "refresh_token": "bsr_new_refresh..." 6}

Logout#

Revoke the device token:

POST https://api.bootspring.dev/v1/auth/device/revoke Authorization: Bearer bsd_live_xyz789...

Security Considerations#

  1. Never store device_code - It's only used during authorization
  2. Store access_token securely - Use system keychain/credential manager
  3. Implement token refresh - Don't wait for expiration
  4. Handle revocation - Check for 401 responses and re-authenticate