Device Authorization Flow
RFC 8628 device authorization grant for CLI tools and headless devices.
Overview
The device flow allows authentication on devices that lack a browser or have limited input capabilities (CLIs, smart TVs, IoT devices).
sequenceDiagram
participant CLI
participant AuthOS
participant Browser
participant User
CLI->>AuthOS: POST /auth/device/code
AuthOS-->>CLI: {device_code, user_code, verification_uri}
CLI->>User: Display: "Go to sso.example.com/device"
CLI->>User: Display: "Enter code: ABCD-EFGH"
User->>Browser: Navigate to verification_uri
User->>Browser: Enter user_code
Browser->>AuthOS: POST /auth/device/verify
AuthOS-->>Browser: Show login options
User->>Browser: Complete OAuth login
loop Poll every 5 seconds
CLI->>AuthOS: POST /auth/token
AuthOS-->>CLI: "Not authorized" or tokens
end
CLI->>User: Logged in!
Endpoints
| Method | Path | Description |
|---|---|---|
POST |
/auth/device/code |
Request device codes |
POST |
/auth/device/verify |
Verify user code (web) |
POST |
/auth/token |
Exchange device code for token |
POST /auth/device/code
Request device authorization codes for a CLI or headless device.
Synopsis
| Property | Value |
|---|---|
| Authentication | Public |
| Code Lifetime | 15 minutes |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
client_id |
string |
Yes | Service client ID |
org |
string |
Yes | Organization slug |
service |
string |
Yes | Service slug |
Example Request
curl -X POST https://sso.example.com/auth/device/code \
-H "Content-Type: application/json" \
-d '{
"client_id": "service_abc123xyz",
"org": "acme-corp",
"service": "cli-tool"
}'
Response (200 OK)
{
"device_code": "550e8400-e29b-41d4-a716-446655440000",
"user_code": "ABCD-EFGH",
"verification_uri": "https://sso.example.com/device",
"expires_in": 900,
"interval": 5
}
Response Fields
| Field | Type | Description |
|---|---|---|
device_code |
string |
Code for token exchange (keep secret) |
user_code |
string |
Code for user to enter (display to user) |
verification_uri |
string |
URL for user to visit |
expires_in |
integer |
Seconds until codes expire |
interval |
integer |
Minimum seconds between polling |
Errors
| Status | Condition |
|---|---|
400 |
Invalid client_id |
400 |
Service not configured for device flow |
POST /auth/device/verify
Verify user code and return authentication context. Called by web frontend.
Synopsis
| Property | Value |
|---|---|
| Authentication | Public |
| Called By | Web verification page |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
user_code |
string |
Yes | User code from device (e.g., “ABCD-EFGH”) |
Example Request
curl -X POST https://sso.example.com/auth/device/verify \
-H "Content-Type: application/json" \
-d '{
"user_code": "ABCD-EFGH"
}'
Response (200 OK)
{
"org_slug": "acme-corp",
"service_slug": "cli-tool",
"available_providers": ["github", "google", "microsoft"]
}
Errors
| Status | Condition |
|---|---|
400 |
Invalid or expired user code |
400 |
Device already authorized |
POST /auth/token
Exchange device code for access token. Poll this endpoint until authorization completes.
Synopsis
| Property | Value |
|---|---|
| Authentication | Public |
| Poll Interval | 5 seconds minimum |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
grant_type |
string |
Yes | urn:ietf:params:oauth:grant-type:device_code |
client_id |
string |
Yes | Service client ID |
device_code |
string |
Yes | Device code from /auth/device/code |
Example Request
curl -X POST https://sso.example.com/auth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"client_id": "service_abc123xyz",
"device_code": "550e8400-e29b-41d4-a716-446655440000"
}'
Response - Success (200 OK)
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 86400
}
Response - Authorization Pending (401 Unauthorized)
{
"error": "Not authorized"
}
Continue polling when you receive this response.
Response - Expired (400 Bad Request)
{
"error": "Device code expired"
}
Stop polling and start new device flow.
Errors
| Status | Error | Action |
|---|---|---|
401 |
Not authorized |
Continue polling |
400 |
Device code expired |
Start new flow |
400 |
Invalid grant type |
Fix request |
CLI Implementation Example
async function deviceLogin() {
// 1. Request device codes
const codeResponse = await fetch('https://sso.example.com/auth/device/code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: 'service_abc123xyz',
org: 'acme-corp',
service: 'cli-tool'
})
});
const { device_code, user_code, verification_uri, interval } = await codeResponse.json();
// 2. Display instructions to user
console.log(`\nTo login, visit: ${verification_uri}`);
console.log(`And enter code: ${user_code}\n`);
// 3. Poll for authorization
while (true) {
await sleep(interval * 1000);
const tokenResponse = await fetch('https://sso.example.com/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
client_id: 'service_abc123xyz',
device_code
})
});
const data = await tokenResponse.json();
if (data.access_token) {
console.log('Login successful!');
return data.access_token;
}
if (data.error !== 'Not authorized') {
throw new Error(data.error);
}
}
}
Security Considerations
- Device code is secret - Never display to user
- User code is public - Safe to display
- Polling interval - Respect 5-second minimum
- Code expiry - 15-minute window for user to authorize