Device Authorization Flow
The device authorization flow (RFC 8628) enables devices that cannot display a URL or receive a redirect — TVs, CLI tools, smart appliances, IoT sensors — to authorize a user without a browser on the device itself.
Overview
- The device requests a code pair from ahdapa.
- The device displays a short user code and the verification URL to the user.
- The user visits the URL on a separate device (phone, laptop) and enters the code to grant access.
- The device polls the token endpoint until the user approves or the code expires.
Step 1 — Device authorization request
POST /device_authorization with the client_id and requested scope.
Client authentication follows the same rules as the token endpoint:
client_secret_basic, client_secret_post, client_secret_jwt,
private_key_jwt, tls_client_auth, self_signed_tls_client_auth, none
(public clients), or kerberos_client_auth (SPNEGO/Negotiate).
curl -s -X POST https://idp.example.com/device_authorization \
-u "CLIENT_ID:CLIENT_SECRET" \
-d "scope=openid profile email offline_access"
Successful response — 200 OK:
{
"device_code": "BASE64URL_DEVICE_CODE",
"user_code": "BCDF-GHJK",
"verification_uri": "https://idp.example.com/device",
"verification_uri_complete": "https://idp.example.com/device?user_code=BCDF-GHJK",
"expires_in": 1800,
"interval": 5
}
| Field | Description |
|---|---|
device_code | Opaque code used to poll the token endpoint. Keep this secret on the device. |
user_code | 8-character code (consonants only, hyphen-separated) displayed to the user. Case-insensitive. |
verification_uri | The URL the user must visit. Always <issuer>/device. |
verification_uri_complete | A pre-filled URL that includes the user code; suitable for QR codes. |
expires_in | Seconds until the device code expires (1800 = 30 minutes). |
interval | Minimum number of seconds to wait between polling attempts (5 seconds). |
Machine clients using kerberos_client_auth
An enrolled machine (e.g. an SSSD host) can authenticate to
/device_authorization using its Kerberos host keytab instead of a client
secret. The machine presents its AP-REQ in the standard HTTP Negotiate header:
# The machine acquires a Kerberos ticket for the IdP's HTTP service principal.
kinit -k -t /etc/krb5.keytab host/node1.example.com@EXAMPLE.COM
curl -s -X POST https://idp.example.com/device_authorization \
--negotiate -u: \
-d "client_id=TEMPLATE_CLIENT_ID" \
-d "scope=openid offline_access"
The client must be registered with:
token_endpoint_auth_method = "kerberos_client_auth"grant_typescontainingurn:ietf:params:oauth:grant-type:device_code- The desired scopes including
offline_accessif a refresh token is wanted
The server verifies the SPNEGO token, matches the machine principal against the
registered kerberos_principal or kerberos_principal_pattern, and issues the
device code pair exactly as for a secret-authenticated client.
Use case: A headless machine (IoT gateway, data-collection appliance) holds a Kerberos keytab and needs to act on behalf of an authorised user. The machine itself has no browser and no client secret. It authenticates the OAuth2 client registration with Kerberos, then prompts a user to visit the verification URL on a phone or laptop to grant the specific user-level authorization.
Polling the token endpoint after the user approves also uses Kerberos:
curl -s -X POST https://idp.example.com/token \
--negotiate -u: \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "client_id=TEMPLATE_CLIENT_ID" \
-d "device_code=BASE64URL_DEVICE_CODE"
See Kerberos client authentication for registration requirements and template-client patterns.
Step 2 — User interaction
Show the user the verification_uri and user_code:
To authorize this device, visit:
https://idp.example.com/device
And enter the code:
BCDF-GHJK
Or scan this QR code: [QR for verification_uri_complete]
This code expires in 30 minutes.
The user visits the URL on a separate authenticated device (or authenticates on arrival if they have no session), enters the code, and approves or denies access. The server records their decision in the database.
Step 3 — Polling the token endpoint
While the user is authorizing, the device polls POST /token at the rate
specified by interval. Authenticate the client the same way as in step 1.
curl -s -X POST https://idp.example.com/token \
-u "CLIENT_ID:CLIENT_SECRET" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "device_code=BASE64URL_DEVICE_CODE"
Poll responses before the user acts:
{"error": "authorization_pending"}
If you poll too quickly:
{"error": "slow_down"}
When slow_down is received, increase the polling interval by at least 5 seconds
for all subsequent attempts.
Step 4 — Successful authorization
Once the user approves, the next poll returns tokens:
{
"access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K0pXVCIsImtpZCI6IkFCQ0QxMjM0In0...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "BASE64URL_ENCRYPTED_REFRESH_TOKEN",
"id_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFCQ0QxMjM0In0...",
"scope": "openid profile email offline_access"
}
The device code is consumed on first successful redemption. Subsequent polls
for the same device_code return invalid_grant.
Step 5 — Expiry and error handling
| Error code | Meaning | What to do |
|---|---|---|
authorization_pending | User has not yet acted. | Keep polling at the specified interval. |
slow_down | Polling too fast. | Increase interval by ≥ 5 s, then retry. |
access_denied | User denied access. | Inform the user; device cannot proceed. |
expired_token | The device code expired (30 minutes). | Restart from step 1 with a new device authorization request. |
invalid_grant | Code already used or not found. | Restart from step 1. |
Using refresh tokens
The device flow issues a refresh token when offline_access is in the granted
scope. Use it to renew the access token without requiring the user to re-authorize
the device. See Refresh Tokens.