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:
| Field | Type | Required | Description |
|---|---|---|---|
client_id | string | Yes | OAuth client id (unknown id answers 404) |
scope | string | No | Space-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.
{
"client_id": "1a2b3c4d5e6f7a8b9c0d",
"scope": "read:dataset write:dataset"
}
{
"_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"
}
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:
| Field | Type | Required | Description |
|---|---|---|---|
user_code | string | Yes | The 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).
{
"user_code": "WDJF-7K2P"
}
{
"_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": []
}
}
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:
| Field | Type | Required | Description |
|---|---|---|---|
device_code | string | Yes | The device code from the initial request |
authorize | boolean | No | true 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.
{
"device_code": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
"authorize": true
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/oauth/authorize_device"
}
},
"authorized": true
}
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:
| Field | Type | Required | Description |
|---|---|---|---|
device_code | string | Yes | The device code from the initial request |
Response (pending):
Before the user acts, answers 400 with { error: "authorization_pending" } —
wait interval seconds and retry.
{
"device_code": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
}
{
"error": "authorization_pending"
}
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.
{
"device_code": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
}
{
"_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"
}
- Respect the
interval(5 seconds) between polls. - On
authorization_pending, wait and retry. - Stop polling on
access_denied(user denied) orexpired_token(unknown or expireddevice_code); both answer400.