Skip to main content

Device Authorization Grant

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.

The standard calls this device authorization; this site also calls it "sign in on another device". A grant type is simply the way an app asks for a token; other terms are in the glossary.

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.
16 / 16
  1. Limited Device to Auth Server: POST /device_authorization (client_id, scope)
  2. Auth Server to Limited Device: Returns device_code, user_code, verification_uri
  3. Limited Device to User: Display: "Go to [URL] and enter code: WDJB-MJHT"
  4. User to User's Browser: Visit verification_uri
  5. User's Browser to Auth Server: GET /device
  6. Auth Server to User's Browser: Show code entry form
  7. User to User's Browser: Enter user_code
  8. User's Browser to Auth Server: POST user_code
  9. Auth Server to User's Browser: Show authorization screen
  10. User to User's Browser: Approve authorization
  11. User's Browser to Auth Server: POST approve
  12. Auth Server to User's Browser: Success! Return to device
  13. Limited Device to Auth Server: POST /token (device_code)
  14. Auth Server to Limited Device: authorization_pending (keep polling)
  15. Limited Device to Auth Server: POST /token (device_code)
  16. Auth Server to Limited Device: access_token, refresh_token

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.

The code-entry form carries a CSRF token, so another site cannot post a code on a signed-in user's behalf and walk them onto an attacker's device. A stale or missing token re-shows the form with "Your session expired. Please enter the code again." Build your own entry page only by linking to this one (or to verification_uri_complete), not by posting to it.

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 parameter (for example no client_id)Check request format
invalid_client401Unknown or disabled client_id, or a wrong secret for a confidential clientVerify client registration
unauthorized_client400 at /device_authorization, 403 at /tokenThe client is not allowed the device grantEnable the grant on the client
invalid_scope400A requested scope is not allowed for this clientRequest only allowed scopes
invalid_grant400Invalid device_codeRestart the flow
too_many_requests429More than 30 device authorization requests per minute (20 per minute on the verification page)Back off

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 the protection against brute-force guessing that the device-flow standard calls for.

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:openid:params:grant-type:ciba",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:ietf:params:oauth:grant-type:token-exchange"
]
}

Standards used on this page​

Plain nameStandard
Device authorization (sign in on another device)RFC 8628
OAuth 2.0 (the core sign-in and token protocol)RFC 6749
Discovery documentRFC 8414 and OpenID Connect Discovery

See the standards reference for what each one covers and how LumoAuth uses it.