Device Authorization Grant
The OAuth 2.0 device authorization grant (RFC 8628) is for clients with no browser of their own to redirect through — a CLI (o2idctl login uses this grant), a TV app, or anything else that can show text and make outbound HTTP calls but can't receive a redirect. Instead of a redirect, the client shows the user a short code and a URL; the user opens that URL on any device with a browser, confirms the code, and the client picks up tokens by polling in the background. No redirect_uri or client secret is involved.
Flow Overview
- Client requests a device code — the client posts to
/oauth2/device_authorizationwith itsclient_id. - Client displays the code — O2ID returns a
device_code(kept secret, used only for polling), auser_code(short, shown to the user), and averification_uri/verification_uri_complete. - User verifies — the user opens
verification_uri(orverification_uri_complete, which pre-fills the code) in a browser, logs in if needed, confirms the code matches, and approves or denies. - Client polls — the client posts to
/oauth2/tokenwith thedevice_codeat the giveninterval, gettingauthorization_pendingback until the user acts. - Tokens issued — once approved, the next poll returns an access token and refresh token, exactly like the authorization code grant's token response.
Device Authorization Request
POST /oauth2/device_authorization
Content type: application/x-www-form-urlencoded
| Field | Value |
|---|---|
client_id | The clientId of a registered application |
scope | Optional, space-separated. Defaults to everything the application and the (not-yet-known) approving user's role both allow. |
Success response:
{
"device_code": "…",
"user_code": "WDJB-MJHT",
"verification_uri": "https://your-o2id-instance/t/system/device",
"verification_uri_complete": "https://your-o2id-instance/t/system/device?user_code=WDJB-MJHT",
"expires_in": 600,
"interval": 5
}
expires_in bounds how long the device_code/user_code pair stays
redeemable; interval is the minimum number of seconds the client must
wait between polls.
Verification Page
verification_uri is served as part of the login portal — the same
embedded (or, if configured, externally hosted) pages that serve login and
consent. See Login Portal. If the visitor has no active
session, O2ID redirects into the ordinary login flow first (password, MFA,
...), then returns here. The page shows the code back to the user to
confirm it matches what their client printed — a deliberate anti-phishing
step recommended by RFC 8628 §5.4, shown regardless of whether the
application is trusted.
Endpoint: POST /device (form-encoded), submitted by the verification
page.
| Field | Value |
|---|---|
user_code | The code shown to the user |
action | approve or deny |
Requires an authenticated session (unlike consent's consent_id, a user_code has no separate pending-authorization transaction binding it to an already-known subject).
Token Request
POST /oauth2/token
Content type: application/x-www-form-urlencoded
| Field | Value |
|---|---|
grant_type | urn:ietf:params:oauth:grant-type:device_code |
device_code | The device code from the device authorization response |
client_id | Same clientId used in the device authorization request |
While pending, this returns 400 Bad Request with one of:
error | Meaning |
|---|---|
authorization_pending | Keep polling at the current interval |
slow_down | Increase the polling interval by 5 seconds |
access_denied | The user denied the request — stop polling |
expired_token | The code expired before it was approved — start over |
Success response, once approved, is identical in shape to the authorization code grant's:
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "…",
"scope": "users:read"
}
The device_code is single-use: a poll after a successful exchange (or
after a denial) returns expired_token.
Full cURL Walkthrough
Every endpoint is served under a tenant path prefix (/t/{tenant}/…); this
walkthrough uses the system tenant that o2id serve seeds on first run.
Start O2ID before running these commands:
o2id serve --addr :8080
Create a public application (device flow clients present no client secret):
APP_JSON=$(curl -s -X POST http://localhost:8080/t/system/applications \
-H "Content-Type: application/json" \
-d '{"name":"Demo Device Client","public":true}')
CLIENT_ID=$(printf '%s' "$APP_JSON" | jq -r .clientId)
Request a device authorization:
DEVICE_JSON=$(curl -s -X POST http://localhost:8080/t/system/oauth2/device_authorization \
--data-urlencode "client_id=${CLIENT_ID}")
DEVICE_CODE=$(printf '%s' "$DEVICE_JSON" | jq -r .device_code)
USER_CODE=$(printf '%s' "$DEVICE_JSON" | jq -r .user_code)
echo "Open the verification_uri from \$DEVICE_JSON and enter: $USER_CODE"
In a browser, log in and approve the code on the printed
verification_uri. Then poll for tokens:
curl -s -X POST http://localhost:8080/t/system/oauth2/token \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
--data-urlencode "device_code=${DEVICE_CODE}" \
--data-urlencode "client_id=${CLIENT_ID}"
This returns {"error":"authorization_pending"} until the browser step
above completes, then the access/refresh token pair.