Device Authorization Flow
The Device Authorization Flow (RFC 8628) enables authentication on input-constrained devices that lack a browser or have limited keyboard input — such as smart TVs, CLI tools, IoT devices, and gaming consoles. Users authorize the device by entering a short code on a separate device like their phone or laptop.
How It Works
The device flow has four steps:
| Step | Description |
|---|---|
| 1. Request authorization | Device sends its client ID to the device endpoint and receives a device code, user code, and verification URL. |
| 2. Display to user | Device displays the user code and verification URL on screen. Optionally shows a QR code. |
| 3. User verification | User opens the verification URL on their phone or computer, enters the code, logs in, and approves. |
| 4. Poll for tokens | Device polls the token endpoint until the user approves, denies, or the code expires. |
Note: The OAuth client must have urn:ietf:params:oauth:grant-type:device_code in its allowed grant types. Configure this in the Admin Panel under OAuth Clients.
Step 1: Request Device Authorization
The device sends a POST request to the device authorization endpoint with its client ID and requested scopes:
POST /api/oauth/device
Content-Type: application/x-www-form-urlencoded
client_id=your-client-id&scope=openid profile emailResponse
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "WDJB-MJHT",
"verification_uri": "https://uniauth.id/device",
"verification_uri_complete": "https://uniauth.id/device?code=WDJB-MJHT",
"expires_in": 600,
"interval": 5
}| Field | Description |
|---|---|
device_code | A long, opaque code the device uses to poll for tokens. Keep this secret. |
user_code | A short, human-readable code in XXXX-XXXX format for the user to enter. |
verification_uri | The URL the user should open to enter the code. |
verification_uri_complete | The URL with the code pre-filled. Useful for QR codes. |
expires_in | Seconds until the device and user codes expire (default: 600 = 10 minutes). |
interval | Minimum seconds the device should wait between polling requests (default: 5). |
Step 2: Display to User
Display the user_code and verification_uri prominently on the device screen. For example:
┌─────────────────────────────────────────────┐
│ │
│ To sign in, open: │
│ https://uniauth.id/device │
│ │
│ And enter code: WDJB-MJHT │
│ │
│ Or scan this QR code: │
│ [QR code for verification_uri_complete] │
│ │
└─────────────────────────────────────────────┘If the device can display QR codes, encode the verification_uri_complete value. This lets users scan and go directly to the verification page with the code pre-filled.
Step 3: User Verification
The user opens https://uniauth.id/device on their phone or computer. They are prompted to:
- Enter the user code (
WDJB-MJHT) - Log in to their UniAuth account if not already authenticated
- Review and approve the requested permissions
If the user navigated via verification_uri_complete, the code is pre-filled and they only need to confirm and approve.
Step 4: Poll for Tokens
While the user is authorizing on their separate device, your application polls the token endpoint at the specified interval:
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:device_code
&device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
&client_id=your-client-idPending Response
While the user has not yet completed authorization:
HTTP/1.1 400 Bad Request
{
"error": "authorization_pending",
"error_description": "The user has not yet completed authorization."
}Keep polling at the specified interval.
Slow Down Response
If you poll too frequently:
HTTP/1.1 400 Bad Request
{
"error": "slow_down",
"error_description": "Polling too frequently. Increase interval by 5 seconds."
}Increase your polling interval by 5 seconds and continue.
Success Response
Once the user approves, you receive the tokens:
HTTP/1.1 200 OK
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJl...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"scope": "openid profile email"
}Error Responses
| Error | Meaning |
|---|---|
authorization_pending | User has not yet completed authorization. Keep polling. |
slow_down | Polling too fast. Increase interval by 5 seconds. |
access_denied | User denied the authorization request. Stop polling. |
expired_token | The device code has expired. Start the flow over from Step 1. |
invalid_grant | The device code is invalid or has already been used. |
Complete Example: CLI Tool
Here is a complete Node.js example implementing the device flow for a command-line tool:
const CLIENT_ID = "your-client-id";
const BASE_URL = "https://uniauth.id";
async function deviceLogin() {
// Step 1: Request device authorization
const deviceRes = await fetch(BASE_URL + "/api/oauth/device", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: CLIENT_ID,
scope: "openid profile email",
}),
});
const device = await deviceRes.json();
// Step 2: Display to user
console.log("\n To sign in, open this URL in your browser:\n");
console.log(" " + device.verification_uri + "\n");
console.log(" And enter code: " + device.user_code + "\n");
console.log(" Waiting for authorization...\n");
// Step 4: Poll for tokens
let interval = device.interval * 1000;
const deadline = Date.now() + device.expires_in * 1000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, interval));
const tokenRes = await fetch(BASE_URL + "/api/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: device.device_code,
client_id: CLIENT_ID,
}),
});
const tokenData = await tokenRes.json();
if (tokenRes.ok) {
console.log(" Authenticated successfully!\n");
return tokenData;
}
if (tokenData.error === "slow_down") {
interval += 5000;
continue;
}
if (tokenData.error === "authorization_pending") {
continue;
}
// access_denied, expired_token, or invalid_grant
throw new Error(tokenData.error_description || tokenData.error);
}
throw new Error("Device code expired. Please try again.");
}
// Usage
deviceLogin()
.then((tokens) => {
console.log("Access token:", tokens.access_token);
console.log("ID token:", tokens.id_token);
})
.catch((err) => {
console.error("Login failed:", err.message);
process.exit(1);
});Grant Type Setup
The device flow grant type must be explicitly enabled for each OAuth client that needs it:
- Navigate to Admin Panel → OAuth Clients → select your client
- Under Grant Types, enable
urn:ietf:params:oauth:grant-type:device_code - Save the client configuration
Confidential clients (those with a client secret) can also include the client_secret parameter in the token request for additional security, though it is not required for the device flow since the device itself may not be able to securely store a secret.
Error Handling
Your device application should handle these scenarios gracefully:
- Code expired — The device code expires after
expires_inseconds (default 10 minutes). Display a message and offer to restart the flow. - User denied — The user clicked "Deny" on the consent screen. Display a message indicating the login was cancelled.
- Network errors — If a polling request fails due to a network error, retry after the polling interval. Do not treat transient failures as permanent.
- Invalid client — Ensure the client ID is correct and the device flow grant type is enabled for the client.
Note: For more details on the OAuth2 token endpoint and available scopes, see the OAuth2 / OIDC documentation.