Device Authorization Grant
The Device Authorization Grant (RFC 8628) lets a device that can’t easily show a login page obtain tokens for a user. The device displays a short code; the user opens a URL on their phone or laptop, signs in, and approves the request. Typical clients are command-line tools, smart TVs, and IoT devices.
This is the same flow used by gh auth login and az login --use-device-code.
How it works
Section titled “How it works”- The device calls
POST /oauth2/device_authorizationand receives adevice_code(kept secret) and auser_code(shown to the user). - The device tells the user to open
verification_uriand enter theuser_code, or to openverification_uri_complete, which has the code pre-filled (useful as a QR code). - The user signs in to the account UI (including MFA, if enabled), reviews the requested scopes, and clicks Allow or Deny.
- Meanwhile, the device polls
POST /oauth2/tokenwith thedevice_codeeveryintervalseconds. - Once the user approves, the next poll returns an access token, ID token, and refresh token.
Register a client
Section titled “Register a client”The client must have urn:ietf:params:oauth:grant-type:device_code in its grant_types. CLIs and devices can’t keep a secret, so they are usually registered as public clients. Add refresh_token so the device can stay signed in.
curl -X POST https://auth.example.com/admin/api/clients \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_id": "my-cli", "client_name": "My CLI", "redirect_uris": ["http://localhost/unused"], "grant_types": ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"], "response_types": ["code"], "scopes": "openid profile email offline_access", "client_type": "public", "token_endpoint_auth_method": "none" }'The client_name is shown to the user on the approval screen, so use a name they will recognize.
Step 1: Request a device code
Section titled “Step 1: Request a device code”curl -X POST https://auth.example.com/oauth2/device_authorization \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "client_id=my-cli" \ -d "scope=openid profile email offline_access"| Parameter | Required | Description |
|---|---|---|
client_id |
Unless using HTTP Basic | The registered client ID; public clients always send it |
client_secret |
For confidential form authentication | Send with client_id, or use HTTP Basic instead |
scope |
No | Space-separated scopes. Must be allowed for the client. Defaults to the client’s configured scopes |
Confidential clients can authenticate with HTTP Basic without repeating client_id in the body:
curl -X POST https://auth.example.com/oauth2/device_authorization \ -u "DEVICE_CLIENT_ID:CLIENT_SECRET" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "scope=openid profile"Form-encode each Basic credential before Base64-encoding when it contains reserved characters; see credential encoding. Alternatively, send client_id and client_secret only as form fields. Missing or incorrect confidential credentials return invalid_client without creating a device code. Failed HTTP Basic attempts return HTTP 401 with WWW-Authenticate: Basic; form failures return HTTP 400. Inactive clients and disallowed grants/scopes remain rejected. Public devices continue to send only client_id and should not embed a shared secret.
Response:
{ "device_code": "e07330ab7c24141bc8801c5d64bef27f4fe0b8da", "user_code": "KTTM-RJMS", "verification_uri": "https://auth.example.com/account/device", "verification_uri_complete": "https://auth.example.com/account/device/KTTM-RJMS", "expires_in": 600, "interval": 5}| Field | Description |
|---|---|
device_code |
Secret code the device uses to poll the token endpoint. Do not show it to the user |
user_code |
8 consonants shown as XXXX-XXXX. Case, spaces, and hyphens are ignored when the user types it |
verification_uri |
Account UI page where the user enters the code |
verification_uri_complete |
Same page with the code pre-filled |
expires_in |
Seconds until the codes expire (device_code_expiration, default 10 minutes) |
interval |
Minimum seconds between polls (device_code_polling_interval, default 5) |
Errors:
| Error | Cause |
|---|---|
invalid_request |
Missing identification without Basic, incomplete/duplicate credentials, URI credentials or conflicting authentication methods |
invalid_client |
Unknown/inactive client or missing/wrong credentials; Basic failures include a 401 challenge |
unauthorized_client |
The client doesn’t have the device code grant type |
invalid_scope |
A requested scope isn’t allowed for the client |
Step 2: Show the code to the user
Section titled “Step 2: Show the code to the user”Display both the URL and the code, for example:
To sign in, open https://auth.example.com/account/device and enter: KTTM-RJMSIf the device has a browser available (a CLI on a desktop), open verification_uri_complete for the user so they only need to confirm the code. If it has a screen but no keyboard (a TV), render verification_uri_complete as a QR code.
Step 3: The user approves
Section titled “Step 3: The user approves”The user opens the URL, signs in if they don’t already have an SSO session, and sees an Authorize Device screen. It shows the client name, the user code, and the requested scopes in plain language. The user should check that the code matches the one on their device before clicking Allow.
Step 4: Poll for tokens
Section titled “Step 4: Poll for tokens”curl -X POST https://auth.example.com/oauth2/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \ -d "device_code=e07330ab7c24141bc8801c5d64bef27f4fe0b8da" \ -d "client_id=my-cli"Until the user acts, the token endpoint returns 400 with one of these errors:
| Error | What the device should do |
|---|---|
authorization_pending |
The user hasn’t acted yet. Wait interval seconds and poll again |
slow_down |
Polling too fast. Add 5 seconds to the interval and keep polling |
access_denied |
The user clicked Deny. Stop polling |
expired_token |
The codes expired. Start over from step 1 |
invalid_grant |
Unknown or already used device_code, or it was issued to a different client. Start over |
Once approved, the next poll returns a normal token response:
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 900, "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "scope": "openid profile email offline_access"}A device_code can only be exchanged once. Concurrent polls have at most one successful exchange. Consumption and token/session storage commit together; if storage fails, the approved code remains available for a later poll. Continue to observe the polling interval after an error.
Example: CLI login script
Section titled “Example: CLI login script”A complete login for a shell CLI using curl and jq:
#!/usr/bin/env bashISSUER=https://auth.example.comCLIENT=my-cli
resp=$(curl -s -X POST $ISSUER/oauth2/device_authorization \ -d "client_id=$CLIENT&scope=openid profile email offline_access")device_code=$(jq -r .device_code <<<"$resp")interval=$(jq -r .interval <<<"$resp")
echo "Open $(jq -r .verification_uri <<<"$resp") and enter code: $(jq -r .user_code <<<"$resp")"echo "Or open: $(jq -r .verification_uri_complete <<<"$resp")"
while sleep "$interval"; do tok=$(curl -s -X POST $ISSUER/oauth2/token \ -d "grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=$device_code&client_id=$CLIENT") case $(jq -r '.error // empty' <<<"$tok") in "") echo "$tok" > ~/.my-cli-token.json; echo "Logged in"; break ;; authorization_pending) ;; slow_down) interval=$((interval + 5)) ;; *) echo "Login failed: $tok"; exit 1 ;; esacdoneOn later runs, send the stored access token as Authorization: Bearer .... When it expires, use the refresh token grant. Refresh tokens are single-use, so save the new refresh token from every response.
Configuration
Section titled “Configuration”| Setting | Default | Description |
|---|---|---|
device_code_expiration |
10m |
How long a device code and user code stay valid |
device_code_polling_interval |
5 |
Minimum seconds between token polls, returned as interval |
Both are runtime settings.
Discovery
Section titled “Discovery”The endpoint is published in the discovery document as device_authorization_endpoint, and urn:ietf:params:oauth:grant-type:device_code is listed in grant_types_supported. Client libraries that support RFC 8628 can configure themselves from discovery.
Approving or denying a device request is a single decision. If requests race, one succeeds and the other returns HTTP 409 (already_used). Token polling follows the successful decision.