Device Authorization Flow

The Device Authorization Flow (RFC 8628) enables OAuth 2.0 authorization for devices that either lack a browser or are input-constrained. This flow is particularly useful for smart TVs, gaming consoles, IoT devices, and CLI applications.

Overview

The Device Authorization Flow allows devices to obtain user authorization without requiring the user to enter credentials directly on the device. Instead, the user completes the authorization on a separate device with better input capabilities.

Flow Diagram

Device                    Authorization Server              User's Browser
  |                              |                              |
  |---(A) Device Request-------->|                              |
  |                              |                              |
  |<--(B) Device Response--------|                              |
  |                              |                              |
  |                              |<---(C) User navigates to-----|
  |                              |       verification URI       |
  |                              |                              |
  |                              |<---(D) User authorizes-------|
  |                              |                              |
  |---(E) Token Request--------->|                              |
  |                              |                              |
  |<--(F) Token Response---------|                              |

Step-by-Step Process

Step 1: Device Authorization Request

The device initiates the flow by requesting a device code from the authorization server.

Endpoint:

POST /oauth/device/code

Request:

curl -X POST "https://auth3.upbond.io/oauth/device/code" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "scope": "openid profile email"
  }'

Request Parameters:

Parameter
Type
Required
Description

client_id

string

Yes

The client ID of your application

scope

string

No

Space-separated list of scopes

Step 2: Device Authorization Response

The authorization server responds with a device code and user code.

Response:

{
  "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://auth3.upbond.io/activate",
  "verification_uri_complete": "https://auth3.upbond.io/activate?user_code=WDJB-MJHT",
  "expires_in": 1800,
  "interval": 5
}

Response Parameters:

Parameter
Type
Description

device_code

string

Device verification code (opaque to device)

user_code

string

User verification code (human-readable)

verification_uri

string

URI for user to visit for authorization

verification_uri_complete

string

Complete URI with user code included

expires_in

integer

Lifetime in seconds of device_code and user_code

interval

integer

Minimum interval in seconds between polling requests

Step 3: User Authorization

The device instructs the user to visit the verification URI and enter the user code.

Display to User:

Please visit: https://auth3.upbond.io/activate
Enter code: WDJB-MJHT

Or provide the QR code for the complete verification URI.

Step 4: Device Polls for Authorization

The device polls the token endpoint at regular intervals to check if the user has authorized the request.

Endpoint:

POST /oauth/token

Request:

curl -X POST "https://auth3.upbond.io/oauth/token" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
    "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
    "client_id": "YOUR_CLIENT_ID"
  }'

Request Parameters:

Parameter
Type
Required
Description

grant_type

string

Yes

Must be "urn:ietf:params:oauth:grant-type:device_code"

device_code

string

Yes

Device code from step 2

client_id

string

Yes

The client ID of your application

Step 5: Token Response

Once the user authorizes the request, the server responds with tokens.

Success Response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "GEbRxBN...edjnXbL",
  "scope": "openid profile email",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Error Handling

Polling Errors

While polling, the device may receive the following errors:

Authorization Pending:

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

Slow Down:

{
  "error": "slow_down",
  "error_description": "The device is polling too frequently"
}

Access Denied:

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

Expired Token:

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

Error Codes

Error Code
Description
Action

authorization_pending

User hasn't completed authorization

Continue polling

slow_down

Polling too frequently

Increase polling interval

access_denied

User denied authorization

Stop polling, restart flow

expired_token

Device code expired

Restart flow

invalid_client

Invalid client ID

Check client configuration

invalid_request

Invalid request parameters

Check request format

Implementation Examples

JavaScript/Node.js

const axios = require('axios');

class DeviceAuthFlow {
  constructor(clientId, baseUrl = 'https://auth3.upbond.io') {
    this.clientId = clientId;
    this.baseUrl = baseUrl;
  }

  async initiateFlow(scope = 'openid profile email') {
    try {
      const response = await axios.post(`${this.baseUrl}/oauth/device/code`, {
        client_id: this.clientId,
        scope: scope
      });

      return response.data;
    } catch (error) {
      throw new Error(`Failed to initiate device flow: ${error.response.data.error_description}`);
    }
  }

  async pollForToken(deviceCode, interval = 5) {
    return new Promise((resolve, reject) => {
      const poll = async () => {
        try {
          const response = await axios.post(`${this.baseUrl}/oauth/token`, {
            grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
            device_code: deviceCode,
            client_id: this.clientId
          });

          resolve(response.data);
        } catch (error) {
          const errorCode = error.response?.data?.error;
          
          switch (errorCode) {
            case 'authorization_pending':
              // Continue polling
              setTimeout(poll, interval * 1000);
              break;
            case 'slow_down':
              // Increase interval and continue
              setTimeout(poll, (interval + 5) * 1000);
              break;
            case 'access_denied':
            case 'expired_token':
            case 'invalid_client':
            case 'invalid_request':
              reject(new Error(error.response.data.error_description));
              break;
            default:
              reject(error);
          }
        }
      };

      poll();
    });
  }

  async authenticate(scope = 'openid profile email') {
    try {
      // Step 1: Get device code
      const deviceAuth = await this.initiateFlow(scope);
      
      // Step 2: Display instructions to user
      console.log(`Please visit: ${deviceAuth.verification_uri}`);
      console.log(`Enter code: ${deviceAuth.user_code}`);
      console.log(`Or visit: ${deviceAuth.verification_uri_complete}`);
      
      // Step 3: Poll for tokens
      const tokens = await this.pollForToken(deviceAuth.device_code, deviceAuth.interval);
      
      return tokens;
    } catch (error) {
      throw error;
    }
  }
}

// Usage
const auth = new DeviceAuthFlow('YOUR_CLIENT_ID');
auth.authenticate()
  .then(tokens => {
    console.log('Authentication successful:', tokens);
  })
  .catch(error => {
    console.error('Authentication failed:', error.message);
  });

Python

import requests
import time

class DeviceAuthFlow:
    def __init__(self, client_id, base_url='https://auth3.upbond.io'):
        self.client_id = client_id
        self.base_url = base_url
    
    def initiate_flow(self, scope='openid profile email'):
        try:
            response = requests.post(f'{self.base_url}/oauth/device/code', json={
                'client_id': self.client_id,
                'scope': scope
            })
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            raise Exception(f'Failed to initiate device flow: {e}')
    
    def poll_for_token(self, device_code, interval=5):
        while True:
            try:
                response = requests.post(f'{self.base_url}/oauth/token', json={
                    'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
                    'device_code': device_code,
                    'client_id': self.client_id
                })
                response.raise_for_status()
                return response.json()
            except requests.exceptions.RequestException as e:
                error_data = e.response.json() if e.response else {}
                error_code = error_data.get('error')
                
                if error_code == 'authorization_pending':
                    time.sleep(interval)
                    continue
                elif error_code == 'slow_down':
                    time.sleep(interval + 5)
                    continue
                elif error_code in ['access_denied', 'expired_token', 'invalid_client', 'invalid_request']:
                    raise Exception(error_data.get('error_description', str(e)))
                else:
                    raise e
    
    def authenticate(self, scope='openid profile email'):
        try:
            # Step 1: Get device code
            device_auth = self.initiate_flow(scope)
            
            # Step 2: Display instructions to user
            print(f"Please visit: {device_auth['verification_uri']}")
            print(f"Enter code: {device_auth['user_code']}")
            print(f"Or visit: {device_auth['verification_uri_complete']}")
            
            # Step 3: Poll for tokens
            tokens = self.poll_for_token(device_auth['device_code'], device_auth['interval'])
            
            return tokens
        except Exception as e:
            raise e

# Usage
auth = DeviceAuthFlow('YOUR_CLIENT_ID')
try:
    tokens = auth.authenticate()
    print('Authentication successful:', tokens)
except Exception as e:
    print('Authentication failed:', str(e))

Best Practices

  1. Polling Interval: Respect the interval parameter to avoid rate limiting

  2. Error Handling: Implement proper error handling for all error codes

  3. User Experience: Provide clear instructions and visual feedback

  4. QR Codes: Consider generating QR codes for the verification URI

  5. Timeout Handling: Handle expired tokens gracefully

  6. Security: Validate all tokens before use

Use Cases

  • Smart TVs: Netflix, YouTube, Spotify apps

  • Gaming Consoles: PlayStation, Xbox authentication

  • IoT Devices: Smart home devices, security cameras

  • CLI Applications: Command-line tools requiring user authentication

  • Media Players: Roku, Apple TV, Chromecast apps

Security Considerations

  1. Device Code Security: Device codes should be unpredictable and single-use

  2. User Code Format: User codes should be easy to type and read

  3. Expiration: Device codes should have reasonable expiration times

  4. Rate Limiting: Implement rate limiting on polling endpoints

  5. HTTPS: Always use HTTPS for all communications

Support

For Device Authorization Flow support:

Last updated

Was this helpful?