Skip to content

Device flow

Sign people in on TVs, CLIs and other devices without a usable browser: show a short code, let them approve it on another screen, and poll for tokens (RFC 8628).

The device authorization grant (RFC 8628) is for devices that cannot show a sign-in page, or where typing a password would be painful: a smart TV, a command-line tool, a kiosk. The device asks Authio for a pair of codes, shows the short one to the person, and polls the token endpoint while the person approves the request on their phone or computer.

How it works

  1. The device asks for codes

    It calls the device authorization endpoint and receives a device_code it keeps to itself, a user_code for the person and the address where the code is entered.

  2. It shows the code

    On screen: the address and the code, and often a QR code of the address with the code filled in.

  3. The person approves on another screen

    The realm’s hosted verification page checks the code, signs the person in if needed, and asks them to approve or deny. It wears the branding of the client that asked.

  4. The device polls for tokens

    Meanwhile the device calls the token endpoint every few seconds until it receives tokens, a refusal, or the code expires.

Set up the client

In Clients, enable the device code grant (urn:ietf:params:oauth:grant-type:device_code) on the client the device uses, and allow the scopes it will ask for — openid for an ID token, offline_access for a refresh token. A device that ships to customers cannot keep a secret, so make the client public; a confidential client authenticates both calls as usual.

Request a device code

TerminalBash
curl -X POST https://authio.api.hyperio.tech/realms/acme/protocol/openid-connect/device/auth \
  -d client_id=acme-tv \
  -d 'scope=openid profile offline_access'
FieldWhat it is
device_codeThe device’s secret handle on the request. Never show it.
user_codeEight characters in two groups, from an alphabet without look-alike characters. Case and separators are forgiven when typed.
verification_uriThe hosted verification page, {issuer}/device.
verification_uri_completeThe same page with the code filled in; the address to put in a QR code.
expires_inSeconds until the codes expire: 10 minutes.
intervalThe minimum number of seconds between two polls: 5.

If the client does not have the device code grant, the request is refused before any code is created. Scopes the client does not allow are refused too.

Show the code

Show verification_uri and user_code in large type, and verification_uri_complete as a QR code for phones. The hosted page takes the person through three steps on one screen: enter the code (already filled in from the QR code), sign in, then approve or deny.

  • Signing in on that page is by username and password. An account that needs a second factor cannot finish the sign-in there; people with the Authio app paired can approve the request from the app instead.
  • A code that is unknown, expired, already used or from another realm is refused.
  • Approving opens a session for the client, and the device’s tokens belong to it: ending that session — from the account portal, the console or a sign-out — cuts the device off.

Poll for tokens

Wait interval seconds, then call the token endpoint with the device code. Repeat until the answer is no longer authorization_pending.

TerminalBash
curl -X POST https://authio.api.hyperio.tech/realms/acme/protocol/openid-connect/token \
  -d grant_type=urn:ietf:params:oauth:grant-type:device_code \
  -d device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS \
  -d client_id=acme-tv

Until the person decides, the endpoint answers 400 with an error from RFC 8628. Once they approve, it answers 200 with the same token response as the authorization code flow: an access token, an ID token with openid, and a refresh token with offline_access.

ErrorMeaningWhat the device does
authorization_pendingThe person has not decided yet.Wait interval seconds and poll again.
slow_downThe device polled sooner than interval allows.Add 5 seconds to its interval for every later poll, and poll again.
expired_tokenThe 10 minutes passed before an approval.Stop, and start again with a new device code.
access_deniedThe person denied it, the code was already redeemed, or it belongs to another client.Stop. Offer to start again.

Polling does not extend the code’s life, and a code yields tokens only once. A loop that follows these rules:

device-sign-in.tsTypeScript
const issuer = 'https://authio.api.hyperio.tech/realms/acme'
const clientId = 'acme-cli'

export async function signInOnDevice() {
  const authorization = await fetch(`${issuer}/protocol/openid-connect/device/auth`, {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({ client_id: clientId, scope: 'openid offline_access' }),
  }).then((response) => response.json())

  console.log(`Open ${authorization.verification_uri} and enter ${authorization.user_code}`)

  let interval = authorization.interval * 1000

  while (true) {
    await new Promise((resolve) => setTimeout(resolve, interval))

    const response = await fetch(`${issuer}/protocol/openid-connect/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: authorization.device_code,
        client_id: clientId,
      }),
    })
    const body = await response.json()

    if (response.ok) return body
    if (body.error === 'authorization_pending') continue
    if (body.error === 'slow_down') {
      interval += 5000
      continue
    }

    throw new Error(body.error_description ?? body.error)
  }
}

In .NET, PollDeviceTokenAsync from Authio.Client waits the interval before each poll, adds 5 seconds on slow_down, and throws AuthioProtocolException on access_denied or expired_token. Register the client with AddAuthioClient and ClientAuthentication set to None for a public client.

Keep the device signed in

Store the refresh token on the device and renew the access token with the refresh_token grant, as described in Any OIDC client. Refresh tokens rotate, so keep the new one each time. When a refresh fails with invalid_grant — the session was ended, the refresh token expired — start the device flow again.

Troubleshooting

What you seeWhyWhat to do
unauthorized_client when asking for a codeThe device code grant is not enabled on the client.Enable it in the client’s grant types.
invalid_clientA confidential client sent no secret or the wrong one, or used a method other than the registered one.Send the secret as registered, or make the client public.
The page says the account’s sign-in is not supportedThe account requires a second factor, which the device page does not offer.Approve from the Authio app, or use an account without a required second factor.
access_denied right after approvingThe code was polled by a different client than the one that asked for it.Poll with the same client_id that requested the code.

Next steps