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
The device asks for codes
It calls the device authorization endpoint and receives a
device_codeit keeps to itself, auser_codefor the person and the address where the code is entered.It shows the code
On screen: the address and the code, and often a QR code of the address with the code filled in.
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.
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
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'| Field | What it is |
|---|---|
device_code | The device’s secret handle on the request. Never show it. |
user_code | Eight characters in two groups, from an alphabet without look-alike characters. Case and separators are forgiven when typed. |
verification_uri | The hosted verification page, {issuer}/device. |
verification_uri_complete | The same page with the code filled in; the address to put in a QR code. |
expires_in | Seconds until the codes expire: 10 minutes. |
interval | The 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.
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-tvUntil 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.
| Error | Meaning | What the device does |
|---|---|---|
authorization_pending | The person has not decided yet. | Wait interval seconds and poll again. |
slow_down | The device polled sooner than interval allows. | Add 5 seconds to its interval for every later poll, and poll again. |
expired_token | The 10 minutes passed before an approval. | Stop, and start again with a new device code. |
access_denied | The 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:
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 see | Why | What to do |
|---|---|---|
unauthorized_client when asking for a code | The device code grant is not enabled on the client. | Enable it in the client’s grant types. |
invalid_client | A 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 supported | The 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 approving | The code was polled by a different client than the one that asked for it. | Poll with the same client_id that requested the code. |