Skip to main content

Device Authorization Grant (RFC 8628)

The Device Authorization Grant is the OAuth flow for devices that cannot run a browser or have very limited input — smart TVs, game consoles, CLI tools, printers, and similar. The device shows a short user code and a URL; the user types the code on a second device (their phone or laptop) to authorize it. The device polls the token endpoint until the user finishes, then receives tokens.

RFC 8628 — OAuth 2.0 Device Authorization Grant: the flow used by CLIs, smart TVs, and other input-constrained devices. The device shows a short user_code; the user types it on a second device to authorize.

How Device Authorization Works

  1. The device requests authorization and receives a device_code and a user_code.
  2. The device displays the user_code and verification URL to the user.
  3. The user visits the URL and enters the code on their phone or computer.
  4. The user authenticates and approves the authorization request.
  5. The device polls the token endpoint until authorization completes.
  6. On approval, tokens are issued to the device.

Use Cases

Device TypeExampleWhy Device Flow?
Smart TVsNetflix, YouTube, Disney+No keyboard, remote control only
Gaming consolesPlayStation, Xbox, Nintendo SwitchController input is cumbersome
CLI toolsGitHub CLI, AWS CLI, DockerNo browser in terminal environment
IoT devicesPrinters, Smart SpeakersNo screen or minimal display
Kiosk modeDigital signage, point-of-saleLocked-down browser, no URL bar

Device Authorization Endpoint

POST /orgs/{orgId}/api/v1/oauth/device_authorization

The device initiates the flow by requesting a device code and user code.

Request Parameters

ParameterRequiredDescription
client_idYesThe client identifier issued to your application
scopeNoSpace-separated list of requested scopes (e.g., openid profile email)
Public Clients Only

The device flow is intended for public clients that cannot securely store a client secret. No client_secret is required.

Example Request

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/device_authorization \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=my-tv-app" \
-d "scope=openid%20profile"

Success Response

{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "WDJB-MJHT",
"verification_uri": "https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/device",
"verification_uri_complete": "https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/device?user_code=WDJB-MJHT",
"expires_in": 600,
"interval": 5
}
FieldDescription
device_codeHigh-entropy verification code used for polling (keep secret)
user_codeShort, easy-to-type code for the user to enter
verification_uriURL where the user should enter the code
verification_uri_completeURL with user_code embedded (for QR codes)
expires_inSeconds until the codes expire
intervalMinimum seconds between polling requests

User Code Format

User codes use a base-20 character set (BCDFGHJKLMNPQRSTVWXZ) that avoids ambiguous characters and vowels to prevent accidental word formation.

User Verification Page

GET /orgs/{orgId}/api/v1/oauth/device

The page where users enter their code. Display this URL prominently on the device along with the user code.

What Users See

  1. Code entry screen. User enters the 8-character code from their device.
  2. Login (if needed). User signs in if not already authenticated.
  3. Authorization screen. Shows which app is requesting access and which scopes.
  4. Confirmation. Success message telling the user to return to their device.

QR Code Support

Use verification_uri_complete to generate a QR code that users can scan with their phone, skipping manual code entry.

// Generate QR code using the verification_uri_complete
const qr = new QRCode(document.getElementById("qr-container"), {
text: response.verification_uri_complete,
width: 200,
height: 200
});

console.log(`Scan the QR code or visit ${response.verification_uri}`);
console.log(`Enter code: ${response.user_code}`);

Token Endpoint with Device Code Grant

POST /orgs/{orgId}/api/v1/oauth/token

After displaying the code to the user, the device polls this endpoint until the user completes authorization. Wait at least interval seconds between polls.

Request Parameters

ParameterRequiredDescription
grant_typeYesurn:ietf:params:oauth:grant-type:device_code
device_codeYesThe device_code from the device authorization response
client_idYesThe client identifier (for public clients)

Example Polling Request

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code" \
-d "device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS" \
-d "client_id=my-tv-app"

Pending Response (Keep Polling)

{
"error": "authorization_pending",
"error_description": "The authorization request is still pending."
}

Slow Down Response (Increase Interval)

{
"error": "slow_down",
"error_description": "Polling too frequently. Please wait 10 seconds between requests."
}
Respect the Polling Interval

Always wait at least the specified interval seconds between polling requests. Polling too frequently results in slow_down errors.

Access Denied Response

{
"error": "access_denied",
"error_description": "The user denied the authorization request"
}

Expired Token Response

{
"error": "expired_token",
"error_description": "The device code has expired"
}

Success Response (User Approved)

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "8xLOxBtZp8",
"scope": "openid profile"
}

Error Codes Reference

Error CodeHTTP StatusDescriptionAction
authorization_pending400User hasn't completed authorization yetKeep polling
slow_down400Polling too frequentlyIncrease interval by 5 seconds
access_denied400User denied the requestStop polling, show error
expired_token400Device code has expiredStop polling, restart flow
invalid_request400Missing required parameterCheck request format
invalid_client401Unknown client_idVerify client registration
invalid_grant400Invalid device_codeRestart the flow

Complete Implementation Example

import requests
import time

AUTH_SERVER = "https://app.lumoauth.dev/orgs/acme-corp/api/v1"
CLIENT_ID = "my-tv-app"

def device_authorization_flow():
# Step 1: Request device and user codes
resp = requests.post(f"{AUTH_SERVER}/oauth/device_authorization", data={
"client_id": CLIENT_ID,
"scope": "openid profile email"
})
auth_data = resp.json()

# Step 2: Display instructions to user
print(f"\n{'='*50}")
print(f"To sign in, visit: {auth_data['verification_uri']}")
print(f"Enter this code: {auth_data['user_code']}")
print(f"{'='*50}\n")

# Step 3: Poll for authorization
device_code = auth_data['device_code']
interval = auth_data['interval']

while True:
time.sleep(interval)

token_resp = requests.post(f"{AUTH_SERVER}/oauth/token", data={
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": device_code,
"client_id": CLIENT_ID
})

if token_resp.status_code == 200:
tokens = token_resp.json()
print("Authorization successful.")
return tokens

error_data = token_resp.json()
error = error_data.get("error")

if error == "authorization_pending":
continue
elif error == "slow_down":
interval += 5
continue
elif error == "access_denied":
print("User denied the authorization request")
return None
elif error == "expired_token":
print("Device code expired, please restart")
return None
else:
print(f"Error: {error}")
return None

if __name__ == "__main__":
device_authorization_flow()

Security Considerations

User Code Entropy

User codes use a base-20 character set with 8 characters, providing approximately 34 bits of entropy (20^8 ≈ 25 billion possible codes). Combined with rate limiting and short expiration, this provides adequate protection against brute force attacks per RFC 8628 Section 5.2.

Code Expiration

Device codes expire after a short window. Validate the expires_in value and prompt for a new code when the current one expires.

Discovery Metadata

The device authorization endpoint is advertised in the OpenID Connect Discovery document:

{
"device_authorization_endpoint": "https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/device_authorization",
"grant_types_supported": [
"authorization_code",
"refresh_token",
"client_credentials",
"urn:ietf:params:oauth:grant-type:device_code"
]
}