Device Code
Implement user login for CLIs and headless devices using the device code flow.
The device code flow lets a device with no browser (a CLI, smart TV, or headless app) authenticate a user. The device displays a short code and a URL; the user visits the URL on another device and approves the request while the device polls for a token.
Requires supports_device_code_grant to be enabled on your client.
Endpoints
| Device authorization | POST https://auth.xeonr.io/api/v1/oauth/code |
| Token (polling) | POST https://auth.xeonr.io/api/v1/oauth/token |
Step 1 — Request a device code
POST /api/v1/oauth/code HTTP/1.1
Host: auth.xeonr.io
Content-Type: application/x-www-form-urlencoded
client_id=550e8400-e29b-41d4-a716-446655440000
&client_secret=YOUR_CLIENT_SECRET
&scope=openid%20profileParameters:
| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your client UUID |
client_secret | Yes | Your client secret |
scope | Yes | Space-separated scopes |
Response:
{
"device_code": "ABCDEF-550e8400-e29b-41d4-a716-446655440000",
"user_code": "ABCDEF",
"verification_uri": "https://auth.xeonr.io/sc",
"verification_uri_complete": "https://auth.xeonr.io/auth/device?code=ABCDEF",
"expires_in": 300,
"interval": 5
}| Field | Description |
|---|---|
device_code | Full code used when polling — keep this private |
user_code | 6-character code shown to the user |
verification_uri | URL the user visits to enter their code |
verification_uri_complete | URL with the code pre-filled — use this for QR codes |
expires_in | Seconds until the device code expires (300) |
interval | Minimum seconds between poll attempts (5) |
Step 2 — Show the user code
Display user_code and verification_uri to the user. For example:
Open https://auth.xeonr.io/sc and enter the code: ABCDEFOr generate a QR code from verification_uri_complete so the user can scan it directly.
Step 3 — Poll for the token
Start polling POST /api/v1/oauth/token at the interval returned in step 1. Do not poll faster than every 5 seconds.
POST /api/v1/oauth/token HTTP/1.1
Host: auth.xeonr.io
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code
&device_code=ABCDEF-550e8400-e29b-41d4-a716-446655440000
&client_id=550e8400-e29b-41d4-a716-446655440000
&client_secret=YOUR_CLIENT_SECRETParameters:
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | urn:ietf:params:oauth:grant-type:device_code |
device_code | Yes | The device_code from step 1 |
client_id | Yes | Your client UUID |
client_secret | Yes | Your client secret |
While waiting, the server returns HTTP 400 with one of these errors:
| Error | Meaning | Action |
|---|---|---|
authorization_pending | User hasn't approved yet | Continue polling at the same interval |
slow_down | Polling too fast | Increase interval by 5 seconds |
access_denied | User denied the request | Stop polling, inform the user |
invalid_grant | Device code expired or invalid | Stop polling, restart the flow |
invalid_client | Client authentication failed | Check client_id and client_secret |
On success (HTTP 200):
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile"
}Example polling loop
const POLL_INTERVAL_MS = deviceCodeResponse.interval * 1000;
let interval = POLL_INTERVAL_MS;
while (true) {
await sleep(interval);
const res = await fetch('https://auth.xeonr.io/api/v1/oauth/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: deviceCodeResponse.device_code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
const data = await res.json();
if (res.ok) {
// Success — store data.access_token
break;
}
if (data.error === 'slow_down') {
interval += 5000;
} else if (data.error !== 'authorization_pending') {
throw new Error(data.error); // access_denied, invalid_grant, etc.
}
}