Device Authorization

Implement RFC 8628 device authorization for CLIs, mobile apps, and input-constrained devices

AuthOS release 0.8.2 TypeScript SDK 0.8.0 Latest-only documentation
Updated Jul 15, 2026 Edit this page
On this page

Complete the authentication flow prerequisites before requesting device codes.

Device Flow (CLIs and Mobile Apps)

The device authorization flow (RFC 8628) enables authentication on devices that don’t have a browser or have limited input capabilities, such as CLIs, IoT devices, or smart TVs.

How Device Flow Works

  1. Device requests a user code
  2. Device displays the code and verification URL to the user
  3. User visits the URL on another device (phone/computer)
  4. User enters the code and authenticates
  5. Device polls for the token
  6. Token is granted after user approves

CLI Implementation

import { SsoClient } from '@drmhse/sso-sdk';

const sso = new SsoClient({
  baseURL: 'https://sso.example.com'
});

async function loginCLI() {
  // Step 1: Request device code
  const deviceAuth = await sso.auth.deviceCode.request({
    client_id: 'acme-cli',
    org: 'acme-corp',
    service: 'acme-cli'
  });

  console.log('\n=================================');
  console.log('To authenticate, visit:');
  console.log(deviceAuth.verification_uri);
  console.log('\nAnd enter code:');
  console.log(deviceAuth.user_code);
  console.log('=================================\n');

  // Step 2: Poll for token
  const token = await pollForToken(deviceAuth.device_code, deviceAuth.interval);

  if (token) {
    // Save token to config file
    console.log('Authentication successful!');
    return token.access_token;
  }
}

async function pollForToken(deviceCode: string, interval: number) {
  const maxAttempts = 60; // 5 minutes with 5-second intervals
  let attempts = 0;

  while (attempts < maxAttempts) {
    await sleep(interval * 1000);
    attempts++;

    try {
      const tokenResponse = await this.sso.auth.deviceCode.exchangeToken({
        grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
        device_code: deviceCode,
        client_id: 'acme-cli'
      });

      return tokenResponse;
    } catch (error) {
      if (error instanceof SsoApiError) {
        if (error.errorCode === 'DEVICE_CODE_PENDING') {
          // User hasn't authorized yet, keep polling
          process.stdout.write('.');
          continue;
        } else if (error.errorCode === 'TOO_MANY_REQUESTS') {
          // Increase interval if requested
          interval += 5;
          continue;
        } else if (error.errorCode === 'DEVICE_CODE_EXPIRED') {
          console.error('\nDevice code expired. Please try again.');
          return null;
        } else if (error.isForbidden()) {
          console.error('\nUser denied the request.');
          return null;
        }
      }
      throw error;
    }
  }

  console.error('\nAuthentication timed out.');
  return null;
}

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Web Application: Device Code Verification

Your web application needs a route to handle device code verification (typically at /activate).

// Activation page component
import { useState } from 'react';
import { SsoClient } from '@drmhse/sso-sdk';

const sso = new SsoClient({
  baseURL: process.env.REACT_APP_SSO_URL
});

export function DeviceActivationPage() {
  const [userCode, setUserCode] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');

    try {
      // Verify the user code
      const context = await sso.auth.deviceCode.verify(userCode.trim().toUpperCase());

      // Redirect to OAuth flow with user_code parameter
      const loginUrl = sso.auth.getLoginUrl('github', {
        org: context.org_slug,
        service: context.service_slug,
        user_code: userCode.trim().toUpperCase(), // CRITICAL: pass user_code
        redirect_uri: `${window.location.origin}/device-callback`
      });

      window.location.href = loginUrl;
    } catch (err) {
      if (err instanceof SsoApiError) {
        setError(err.message);
      } else {
        setError('Failed to verify code');
      }
    }
  };

  return (
    <div className="activation-page">
      <h1>Device Activation</h1>
      <p>Enter the code displayed on your device</p>

      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={userCode}
          onChange={(e) => setUserCode(e.target.value)}
          placeholder="XXXX-XXXX"
          maxLength={9}
          style={{ textTransform: 'uppercase' }}
        />
        <button type="submit">Continue</button>
      </form>

      {error && <div className="error">{error}</div>}
    </div>
  );
}

Complete Device Flow: Full Example

// cli.ts - Complete CLI tool with device flow
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
import * as fs from 'fs';
import * as path from 'path';

const CONFIG_PATH = path.join(process.env.HOME!, '.acme-cli', 'config.json');

class AcmeCLI {
  private sso: SsoClient;

  constructor() {
    this.sso = new SsoClient({
      baseURL: 'https://sso.example.com'
    });
  }

  async login() {
    console.log('Starting authentication...\n');

    const deviceAuth = await this.sso.auth.deviceCode.request({
      client_id: 'acme-cli',
      org: 'acme-corp',
      service: 'acme-cli'
    });

    console.log('To authenticate, visit:');
    console.log(`  ${deviceAuth.verification_uri}`);
    console.log('\nAnd enter code:');
    console.log(`  ${deviceAuth.user_code}`);
    console.log('\nWaiting for authentication', { flush: true });

    const token = await this.pollForToken(
      deviceAuth.device_code,
      deviceAuth.interval
    );

    if (token) {
      this.saveToken(token.access_token, token.refresh_token);
      console.log('\nAuthentication successful!');
      return true;
    }

    return false;
  }

  private async pollForToken(deviceCode: string, interval: number) {
    const maxTime = 300; // 5 minutes
    const startTime = Date.now();

    while (Date.now() - startTime < maxTime * 1000) {
      await this.sleep(interval * 1000);

      try {
        const tokenResponse = await this.sso.auth.deviceCode.exchangeToken({
          grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
          device_code: deviceCode,
          client_id: 'acme-cli'
        });

        return tokenResponse;
      } catch (error) {
        if (error instanceof SsoApiError) {
          if (error.errorCode === 'authorization_pending') {
            process.stdout.write('.');
            continue;
          } else if (error.errorCode === 'slow_down') {
            interval += 5;
            continue;
          } else if (error.errorCode === 'expired_token') {
            console.error('\n\nDevice code expired. Please try again.');
            return null;
          } else if (error.errorCode === 'access_denied') {
            console.error('\n\nAuthentication denied.');
            return null;
          }
        }
        throw error;
      }
    }

    console.error('\n\nAuthentication timed out.');
    return null;
  }

  private saveToken(accessToken: string, refreshToken: string) {
    const dir = path.dirname(CONFIG_PATH);
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }

    fs.writeFileSync(
      CONFIG_PATH,
      JSON.stringify({ accessToken, refreshToken }, null, 2)
    );
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// CLI entry point
const cli = new AcmeCLI();
cli.login();