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#
| Parameter | Type | Required | Description |
|---|---|---|---|
client_id | string | Yes | Your application identifier |
scope | string | No | Space-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#
| Field | Description |
|---|---|
device_code | Code used to poll for authorization |
user_code | Code the user enters on the website |
verification_uri | URL where user authorizes the device |
verification_uri_complete | URL with code pre-filled |
expires_in | Seconds until codes expire (default: 900) |
interval | Minimum 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_completewhen 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#
| Error | Description | Action |
|---|---|---|
authorization_pending | User hasn't authorized yet | Continue polling |
slow_down | Polling too fast | Increase interval by 5 seconds |
expired_token | Device code expired | Restart from step 1 |
access_denied | User denied authorization | Show 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 keytarWindows (Credential Manager)#
// Same API as macOS with keytarToken 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#
- Never store device_code - It's only used during authorization
- Store access_token securely - Use system keychain/credential manager
- Implement token refresh - Don't wait for expiration
- Handle revocation - Check for 401 responses and re-authenticate