Skip to content

OAuth2 Device Authorization Grant — Browserless and IoT Authorization Flow

DodaTech Updated 2026-06-28 5 min read

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

  1. What is the device flow designed for?
  2. How does the device code polling work?
  3. What is a user code and how should it be formatted?
  4. What error does the server return while waiting for user approval?
  5. Why include verification_uri_complete?

Answers:

  1. Devices without browser capability (smart TVs, CLI tools, IoT devices).
  2. The client polls the token endpoint with the device_code every interval seconds until the user approves or the code expires.
  3. A short, human-readable code (e.g., ABCD-1234) the user enters on a separate device.
  4. authorization_pending — indicates the user hasn't approved yet but the code is still valid.
  5. 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

Is the device flow secure?

: Yes, when implemented correctly. The user authorizes on their own device, and the device code is a one-time, short-lived credential.

Can the device flow be used for SPAs?

: No. SPAs should use Authorization Code with PKCE. Device flow is for devices that can't redirect.

What happens if a user enters the wrong code?

: They see an error and must request a new code. Old device codes remain valid until expiry.

How long should the polling interval be?

: 5 seconds is typical. The server can request slow_down if needed.

Can I use refresh tokens with device flow?

: Yes. The token response includes a refresh_token for long-lived access after the initial device authorization.

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