Skip to main content

Device Authorization Flow

The OAuth 2.0 Device Authorization Grant (RFC 8628) for input-constrained devices such as CLI tools, smart displays, and IoT devices.

The device requests a code pair, the user validates and authorizes the code in a browser, and the device polls for an access token. The device-side endpoints (device_token, device_access_token) use the tenant auth strategy: the device sends only an x-tenant header, no user credentials. The browser-side endpoints (device_user_code, authorize_device) require a signed-in user.

POST /api/oauth/device_token

Start the flow. The device requests a device_code and user_code.

Authentication: x-tenant header (tenant auth strategy) + token API feature

Request Body:

FieldTypeRequiredDescription
client_idstringYesOAuth client id (unknown id answers 404)
scopestringNoSpace-delimited requested scopes

Example Request:

{ "client_id": "your-client-id", "scope": "read:dataset write:dataset" }

Response:

Answers 200. The device_code is 16 random bytes (32 hex chars); the user_code is 8 uppercase consonants formatted XXXX-XXXX. interval is always 5 and expires_in always 1800. The verification_uri_complete appends ?user_code=<code>. Codes expire from Redis after 180 seconds.

Request device and user codesPOST /api/oauth/device_token
POST /api/oauth/device_token starts the flow. Uses the `tenant` auth strategy — a device sends only an `x-tenant` header, no user credentials. Payload is { client_id (required), scope? }. Requires the token/api license feature and a known client_id (unknown -> 404). Returns 200 with device_code (16 random bytes, 32 hex chars), user_code (8 uppercase consonants as XXXX-XXXX), interval (always 5), expires_in (always 1800), and verification_uri[_complete]. The complete URL appends `?user_code=<code>`. Codes expire from redis after 180s.
Request body
{
"client_id": "1a2b3c4d5e6f7a8b9c0d",
"scope": "read:dataset write:dataset"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/oauth/device_token"
}
},
"device_code": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
"user_code": "WDJF-7K2P",
"interval": 5,
"expires_in": 1800,
"verification_uri": "https://informer.example.com/oauth/device",
"verification_uri_complete": "https://informer.example.com/oauth/device?user_code=WDJF-7K2P"
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/oauth/device_user_code

The browser step: the signed-in user submits the user_code shown on the device, and the server returns the client details to confirm.

Authentication: Signed-in user

Request Body:

FieldTypeRequiredDescription
user_codestringYesThe user code (case- and hyphen-insensitive)

Response:

Returns the merged device-token and user-code data plus the full OAuth client under app (the model's HAL output, including _links).

Validate a user codePOST /api/oauth/device_user_code
POST /api/oauth/device_user_code is the browser step: the signed-in user submits the user_code shown on the device. Returns the merged redis device-token + user-code data plus the full OAuth client under `app` (the model toHal output, including _links — not a trimmed sub-object). The user_code is case- and hyphen-insensitive and is DELETED after this call (one-time use); re-submitting it 404s. An unknown code also 404s. The device_code itself survives for the authorize/poll steps.
Request body
{
"user_code": "WDJF-7K2P"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/oauth/device_user_code"
}
},
"client_id": "1a2b3c4d5e6f7a8b9c0d",
"scope": "read:dataset write:dataset",
"device_code": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
"app": {
"isInformerCli": false,
"isInformerGo": false,
"permissions": {},
"id": "00000000-0000-4000-8000-000000000001",
"name": "Informer CLI",
"description": "Command-line interface for Informer",
"url": null,
"client_id": "1a2b3c4d5e6f7a8b9c0d",
"redirect_uri": [
"https://informer.example.com/callback"
],
"pkce": true,
"enableRefreshTokens": true,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"icon": null,
"secrets": []
}
}
Captured from the API examples test suite; ids and timestamps are normalized.
One-time use

The user_code is deleted after this call; re-submitting it (or an unknown code) answers 404. The device_code itself survives for the authorize and poll steps.


POST /api/oauth/authorize_device

The user approve/deny step, after reviewing the client details.

Authentication: Signed-in user

Request Body:

FieldTypeRequiredDescription
device_codestringYesThe device code from the initial request
authorizebooleanNotrue to approve, false to deny

Response:

Stamps authorized and the caller's username onto the device-code record and returns { authorized: <authorize> }. An unknown or expired device_code answers 404.

Authorize the devicePOST /api/oauth/authorize_device
POST /api/oauth/authorize_device is the user approve/deny step (requires a signed-in user). Payload is { device_code (required), authorize? (boolean) }. It stamps `authorized` and the caller username onto the redis device-code hash and returns { authorized: <authorize> }. An unknown or expired device_code 404s. Pass authorize:false to deny.
Request body
{
"device_code": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
"authorize": true
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/oauth/authorize_device"
}
},
"authorized": true
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/oauth/device_access_token

The device poll step. The device calls this at the interval from the initial response until the user acts.

Authentication: x-tenant header (tenant auth strategy)

Request Body:

FieldTypeRequiredDescription
device_codestringYesThe device code from the initial request

Response (pending):

Before the user acts, answers 400 with { error: "authorization_pending" } — wait interval seconds and retry.

Poll for an access token (pending)POST /api/oauth/device_access_token
POST /api/oauth/device_access_token is the device poll step (tenant auth strategy, x-tenant header). Before the user acts, it answers HTTP 400 with { error: "authorization_pending" } — the device should wait `interval` seconds and retry. The other 400 errors are access_denied (user denied) and expired_token (unknown/expired device_code).
Request body
{
"device_code": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
}
Response · 400
{
"error": "authorization_pending"
}
Captured from the API examples test suite; ids and timestamps are normalized.

Response (granted):

Once authorized, issues a token (in a transaction) and answers 200 with access_token, token_type, and scope. refresh_token and expires_in (900 seconds) are included when the client has enableRefreshTokens. The device_code is consumed once a token is issued.

Poll for an access token (granted)POST /api/oauth/device_access_token
Once the user has authorized the device_code, POST /api/oauth/device_access_token issues a Token row (in a DB transaction) and returns 200 with { access_token, token_type: "bearer", scope }. When the OAuth client has enableRefreshTokens, the response also includes refresh_token and expires_in (900s). The device_code is consumed once a token is issued. access_token/refresh_token are JWTs (re-signed every run, so the fixture shows stable placeholders).
Request body
{
"device_code": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/oauth/device_access_token"
}
},
"access_token": "eyJhbGciOiJIUzI1NiJ9.example-token-1.signature",
"token_type": "bearer",
"scope": "read:dataset write:dataset"
}
Captured from the API examples test suite; ids and timestamps are normalized.
Polling best practices
  • Respect the interval (5 seconds) between polls.
  • On authorization_pending, wait and retry.
  • Stop polling on access_denied (user denied) or expired_token (unknown or expired device_code); both answer 400.