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:

StepDescription
1. Request authorizationDevice sends its client ID to the device endpoint and receives a device code, user code, and verification URL.
2. Display to userDevice displays the user code and verification URL on screen. Optionally shows a QR code.
3. User verificationUser opens the verification URL on their phone or computer, enters the code, logs in, and approves.
4. Poll for tokensDevice 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 email

Response

{
  "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
}
FieldDescription
device_codeA long, opaque code the device uses to poll for tokens. Keep this secret.
user_codeA short, human-readable code in XXXX-XXXX format for the user to enter.
verification_uriThe URL the user should open to enter the code.
verification_uri_completeThe URL with the code pre-filled. Useful for QR codes.
expires_inSeconds until the device and user codes expire (default: 600 = 10 minutes).
intervalMinimum 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-id

Pending 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

ErrorMeaning
authorization_pendingUser has not yet completed authorization. Keep polling.
slow_downPolling too fast. Increase interval by 5 seconds.
access_deniedUser denied the authorization request. Stop polling.
expired_tokenThe device code has expired. Start the flow over from Step 1.
invalid_grantThe 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_in seconds (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.