Avnology ID
SDKsTypeScript SDKOAuth 2.1

Device Authorization Flow

Authenticate devices without browsers (CLI tools, IoT, smart TVs, AI agents) using the TypeScript SDK.

Device Authorization Flow

The device authorization flow (RFC 8628) enables authentication on devices with limited input capabilities -- CLI tools, smart TVs, IoT devices, and AI agents. The user authorizes the device by entering a code on a separate device with a browser.

requestDeviceCode()

Request a device code and user code. The user visits the verification URL on a browser and enters the user code.

client.oauth.requestDeviceCode(params: DeviceCodeParams): Promise<DeviceCodeResponse>

Parameters

NameTypeRequiredDescription
scopesstring[]yesOAuth scopes to request

Returns

interface DeviceCodeResponse {
  deviceCode: string;        // Device code for polling (do NOT show to user)
  userCode: string;          // User code to display (e.g., "ABCD-1234")
  verificationUri: string;   // URL for the user to visit
  verificationUriComplete: string; // URL with code pre-filled
  expiresIn: number;         // Seconds until the codes expire (typically 900)
  interval: number;          // Minimum polling interval in seconds (typically 5)
}

Basic usage

import { AvnologyId } from "@avnology/sdk-typescript";

const client = new AvnologyId({
  baseUrl: "https://api.id.avnology.com",
  clientId: "app_cli_tool",
});

// Step 1: Request device code
const device = await client.oauth.requestDeviceCode({
  scopes: ["openid", "profile", "email"],
});

// Step 2: Display instructions to the user
console.log("To sign in, visit:", device.verificationUri);
console.log("Enter code:", device.userCode);
console.log("");
console.log("Or visit this link directly:");
console.log(device.verificationUriComplete);

pollDeviceToken()

Poll the token endpoint until the user authorizes the device or the code expires.

client.oauth.pollDeviceToken(params: PollDeviceTokenParams): Promise<TokenSet>

Parameters

NameTypeRequiredDescription
deviceCodestringyesDevice code from requestDeviceCode()
intervalnumbernoPolling interval in seconds (default: from device code response)
timeoutnumbernoMaximum time to poll in ms (default: from expiresIn)
onPending() => voidnoCallback on each pending poll (for spinner/progress)

Returns

Promise<TokenSet> -- Tokens after successful authorization.

Basic usage

// Step 3: Poll until the user authorizes
const tokens = await client.oauth.pollDeviceToken({
  deviceCode: device.deviceCode,
  interval: device.interval,
});

console.log("Authenticated!");
console.log("Access token:", tokens.accessToken);

Complete CLI flow

import { AvnologyId, DeviceFlowExpiredError } from "@avnology/sdk-typescript";

async function cliLogin() {
  const client = new AvnologyId({
    baseUrl: "https://api.id.avnology.com",
    clientId: "app_cli_tool",
  });

  // Request device code
  const device = await client.oauth.requestDeviceCode({
    scopes: ["openid", "profile", "email", "offline_access"],
  });

  // Show the user what to do
  console.log("\n  Avnology ID Login\n");
  console.log(`  1. Open your browser to: ${device.verificationUri}`);
  console.log(`  2. Enter the code: ${device.userCode}\n`);
  console.log(`  Or visit: ${device.verificationUriComplete}\n`);
  console.log("  Waiting for authorization...\n");

  try {
    // Poll with progress indicator
    let dots = 0;
    const tokens = await client.oauth.pollDeviceToken({
      deviceCode: device.deviceCode,
      interval: device.interval,
      onPending: () => {
        dots = (dots + 1) % 4;
        process.stdout.write(`\r  Waiting${".".repeat(dots)}${" ".repeat(3 - dots)}`);
      },
    });

    console.log("\r  Authenticated!                ");

    // Store tokens locally
    const fs = await import("fs");
    const os = await import("os");
    const path = await import("path");
    const tokenPath = path.join(os.homedir(), ".avnology", "tokens.json");
    fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
    fs.writeFileSync(tokenPath, JSON.stringify(tokens, null, 2));

    console.log(`  Tokens saved to ${tokenPath}`);
    console.log(`  Logged in as: ${client.oauth.decodeIdToken(tokens.idToken).email}`);
  } catch (error) {
    if (error instanceof DeviceFlowExpiredError) {
      console.error("\n  Device code expired. Please try again.");
      process.exit(1);
    }
    throw error;
  }
}

AI agent authentication

import { AvnologyId } from "@avnology/sdk-typescript";

async function authenticateAgent() {
  const client = new AvnologyId({
    baseUrl: "https://api.id.avnology.com",
    clientId: "app_ai_agent",
  });

  const device = await client.oauth.requestDeviceCode({
    scopes: ["openid", "profile", "offline_access"],
  });

  // Send verification URL to user via chat, email, or notification
  sendToUser({
    message: `Please authorize this AI agent by visiting ${device.verificationUriComplete}`,
    userCode: device.userCode,
  });

  // Wait for user authorization
  const tokens = await client.oauth.pollDeviceToken({
    deviceCode: device.deviceCode,
  });

  // Agent is now authenticated and can act on behalf of the user
  client.setTokens(tokens);
  return client;
}

Common errors

Error classHTTP statusWhen
DeviceFlowExpiredError400Device code expired before user authorized
DeviceFlowDeniedError400User explicitly denied authorization
DeviceFlowSlowDownError400Polling too fast (SDK handles this automatically)
RateLimitError429Too many polling requests

See also

On this page