OAuth2 Device Authorization Grant — Browserless and IoT Authorization Flow
In this tutorial, you will learn about OAuth2 Device Authorization Grant. We cover key concepts, practical examples, and best practices to help you master this topic.
The OAuth2 Device Authorization Grant (RFC 8628), also called the device flow, enables devices with limited input capabilities (smart TVs, CLI tools, IoT devices) to obtain access tokens by directing the user to authorize on a separate device with a browser.
What You'll Learn
- Device code flow overview and use cases
- Implementing device authorization endpoints
- Polling for token approval
- User experience for device authorization
- Security considerations for the device flow
Why It Matters
The device flow solves authentication for the growing number of IoT and limited-input devices. Smart TVs, game consoles, and CLI tools cannot display a browser or accept complex input. DodaTech's security scanner CLI uses device flow for user authentication without requiring browser automation.
Real-World Use
A command-line threat scanner needs user authentication but cannot open a browser. The device flow shows a short code and URL. The user visits the URL on their phone, enters the code, and approves access. The CLI polls and receives the token automatically.
sequenceDiagram
participant Device as CLI Scanner
participant Auth as Authorization Server
participant User as User (Phone)
Device->>Auth: POST /device_authorization
Auth-->>Device: device_code, user_code, verification_uri
Device->>User: Display "Visit https://dodatech.com/activate\nEnter code: ABCD-1234"
User->>Auth: Visit URL, enter code, approve
Device->>Auth: POST /token (device_code)
Note over Device,Auth: Poll every 5 seconds
Auth-->>Device: authorization_pending
Device->>Auth: POST /token (device_code)
Auth-->>Device: authorization_pending
User->>Auth: Approve consent
Device->>Auth: POST /token (device_code)
Auth-->>Device: access_token, refresh_token
Code Examples
Example 1: Device Authorization Endpoint
from flask import Flask, request, jsonify
import uuid
import secrets
app = Flask(__name__)
@app.route('/device_authorization', methods=['POST'])
def device_authorization():
"""Start device authorization flow."""
client_id = request.form.get('client_id')
scope = request.form.get('scope', '').split()
client = authenticate_device_client(request)
if not client:
return jsonify({'error': 'invalid_client'}), 401
device_code = str(uuid.uuid4())
user_code = generate_user_code()
verification_uri = 'https://dodatech.com/activate'
expires_in = 600 # 10 minutes
# Store device authorization pending approval
store_device_auth(device_code, {
'client_id': client_id,
'scopes': scope,
'status': 'pending',
'expires_at': datetime.now(timezone.utc) + timedelta(seconds=expires_in),
'interval': 5
})
return jsonify({
'device_code': device_code,
'user_code': user_code,
'verification_uri': verification_uri,
'verification_uri_complete': f'{verification_uri}?code={user_code}',
'expires_in': expires_in,
'interval': 5
})
def generate_user_code():
"""Generate a short, readable user code."""
import random
import string
chars = string.ascii_uppercase + string.digits
# Format: ABCD-1234
part1 = ''.join(random.choices(chars, k=4))
part2 = ''.join(random.choices(chars, k=4))
return f'{part1}-{part2}'
# Usage
# Client POST to /device_authorization
# Response: {"device_code": "...", "user_code": "XYZ9-K7RM", ...}
Example 2: Polling Token Endpoint
@app.route('/token', methods=['POST'])
def device_token():
"""Token endpoint that handles device code polling."""
grant_type = request.form.get('grant_type')
if grant_type != 'urn:ietf:params:oauth:grant-type:device_code':
return handle_other_grants()
device_code = request.form.get('device_code')
client_id = request.form.get('client_id')
auth = get_device_auth(device_code)
if not auth:
return jsonify({'error': 'invalid_grant'}), 400
if auth['expires_at'] < datetime.now(timezone.utc):
delete_device_auth(device_code)
return jsonify({'error': 'expired_token'}), 400
if auth['status'] == 'pending':
# Client should poll again after `interval` seconds
return jsonify({'error': 'authorization_pending'}), 400
if auth['status'] == 'approved':
# Generate tokens
access_token = create_access_token(
auth['user_id'], auth['scopes']
)
refresh_token = create_refresh_token(auth['user_id'])
delete_device_auth(device_code)
return jsonify({
'access_token': access_token,
'token_type': 'Bearer',
'expires_in': 900,
'refresh_token': refresh_token,
'scope': ' '.join(auth['scopes'])
})
return jsonify({'error': 'access_denied'}), 400
Example 3: Device Client Implementation
import requests
import time
class DeviceFlowClient:
def __init__(self, client_id, auth_server_url):
self.client_id = client_id
self.auth_url = auth_server_url
self.device_code = None
self.interval = 5
def start_authorization(self, scope):
"""Start device authorization and return user code."""
response = requests.post(
f'{self.auth_url}/device_authorization',
data={'client_id': self.client_id, 'scope': scope}
)
data = response.json()
self.device_code = data['device_code']
self.interval = data.get('interval', 5)
print(f"Visit: {data['verification_uri']}")
print(f"Enter code: {data['user_code']}")
return data['user_code']
def poll_for_token(self):
"""Poll token endpoint until user approves or request expires."""
while True:
response = requests.post(
f'{self.auth_url}/token',
data={
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
'device_code': self.device_code,
'client_id': self.client_id
}
)
data = response.json()
if 'access_token' in data:
print("Authorization approved!")
return data
if data.get('error') == 'authorization_pending':
print("Waiting for user approval...")
time.sleep(self.interval)
elif data.get('error') == 'expired_token':
raise Exception("Authorization timed out")
elif data.get('error') == 'access_denied':
raise Exception("User denied authorization")
else:
raise Exception(f"Unexpected error: {data.get('error')}")
# Usage
client = DeviceFlowClient('my-cli-tool', 'https://auth.dodatech.com')
client.start_authorization('read:threats write:remediation')
tokens = client.poll_for_token()
print(f"Access: {tokens['access_token'][:50]}...")
Common Mistakes
1. Polling Too Aggressively
Respect the interval value from the server. Polling faster wastes bandwidth and may trigger Rate Limiting.
2. Short Expiry for Device Codes
Device codes should expire in 5-10 minutes. Users need time to switch devices and enter the code.
3. Poor User Code Format
Use short, readable codes with a separator (e.g., ABCD-1234). Avoid ambiguous characters (O/0, I/1).
4. Not Providing a QR Code
Mobile users benefit from scanning a QR code instead of typing the URL. Include verification_uri_complete.
5. Ignoring Slow Polling Responses
The server may return slow_down error. Double the polling interval when received.
Practice Questions
- What is the device flow designed for?
- How does the device code polling work?
- What is a user code and how should it be formatted?
- What error does the server return while waiting for user approval?
- Why include
verification_uri_complete?
Answers:
- Devices without browser capability (smart TVs, CLI tools, IoT devices).
- The client polls the token endpoint with the device_code every
intervalseconds until the user approves or the code expires. - A short, human-readable code (e.g., ABCD-1234) the user enters on a separate device.
authorization_pending— indicates the user hasn't approved yet but the code is still valid.- It combines verification_uri and user_code into a single URL that can be encoded as a QR code for mobile scanning.
Challenge: Build a CLI tool that authenticates via device flow. Display the URL and code, poll for approval, and output the access token. Add QR code display for mobile users.
FAQ
What's Next
Compare the device flow with {{< ilink "OAuth" "Authorization Code Grant" }} for browser-based clients, then explore {{< ilink "OAuth" "OAuth2 Token Exchange" }} for advanced token scenarios.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro