Xeonr Developer Docs
Guides

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 authorizationPOST 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%20profile

Parameters:

ParameterRequiredDescription
client_idYesYour client UUID
client_secretYesYour client secret
scopeYesSpace-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
}
FieldDescription
device_codeFull code used when polling — keep this private
user_code6-character code shown to the user
verification_uriURL the user visits to enter their code
verification_uri_completeURL with the code pre-filled — use this for QR codes
expires_inSeconds until the device code expires (300)
intervalMinimum 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: ABCDEF

Or 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_SECRET

Parameters:

ParameterRequiredDescription
grant_typeYesurn:ietf:params:oauth:grant-type:device_code
device_codeYesThe device_code from step 1
client_idYesYour client UUID
client_secretYesYour client secret

While waiting, the server returns HTTP 400 with one of these errors:

ErrorMeaningAction
authorization_pendingUser hasn't approved yetContinue polling at the same interval
slow_downPolling too fastIncrease interval by 5 seconds
access_deniedUser denied the requestStop polling, inform the user
invalid_grantDevice code expired or invalidStop polling, restart the flow
invalid_clientClient authentication failedCheck 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.
  }
}

On this page