Skip to main content
Version: Next

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

  1. Client requests a device code — the client posts to /oauth2/device_authorization with its client_id.
  2. Client displays the code — O2ID returns a device_code (kept secret, used only for polling), a user_code (short, shown to the user), and a verification_uri/verification_uri_complete.
  3. User verifies — the user opens verification_uri (or verification_uri_complete, which pre-fills the code) in a browser, logs in if needed, confirms the code matches, and approves or denies.
  4. Client polls — the client posts to /oauth2/token with the device_code at the given interval, getting authorization_pending back until the user acts.
  5. 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

FieldValue
client_idThe clientId of a registered application
scopeOptional, 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.

FieldValue
user_codeThe code shown to the user
actionapprove 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

FieldValue
grant_typeurn:ietf:params:oauth:grant-type:device_code
device_codeThe device code from the device authorization response
client_idSame clientId used in the device authorization request

While pending, this returns 400 Bad Request with one of:

errorMeaning
authorization_pendingKeep polling at the current interval
slow_downIncrease the polling interval by 5 seconds
access_deniedThe user denied the request — stop polling
expired_tokenThe 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.