How backchannelAuthentication Polls for Tokens in Auth0 CIBA

The Auth0 SDK implements Client-Initiated Backchannel Authentication (CIBA) polling through three coordinated methods: backchannelAuthentication for automatic polling loops, initiateBackchannelAuthentication to retrieve the auth_req_id with expiry metadata, and backchannelAuthenticationGrant for single-shot token requests, handling errors like authorization_pending and slow_down per the CIBA specification.

The auth0-auth-js repository provides a complete implementation of the Client-Initiated Backchannel Authentication (CIBA) protocol, enabling decoupled authentication flows where the authorization server communicates directly with the user's authentication device. Understanding how backchannelAuthentication handles polling for tokens and manages the auth_req_id is essential for implementing secure, spec-compliant login flows that respect server-side rate limits and expiry windows.

The Three-Method CIBA Architecture

The SDK exposes CIBA functionality through three distinct methods in packages/auth0-auth-js/src/auth-client.ts, each serving a specific purpose in the authentication lifecycle.

Automatic Full-Flow with backchannelAuthentication

The backchannelAuthentication method (lines 456–492) orchestrates the complete CIBA flow, hiding the complexity of polling behind a single promise. It begins by discovering server metadata via this.#discover(), then constructs a request payload including binding_message, login_hint, and optional RFC 8628 parameters like requested_expiry or authorization_details.

async backchannelAuthentication(options) {
  const { configuration, serverMetadata } = await this.#discover();
  
  const params = new URLSearchParams({
    scope: DEFAULT_SCOPES,
    client_id: this.#options.clientId,
    binding_message: options.bindingMessage,
    login_hint: JSON.stringify({
      format: 'iss_sub',
      iss: serverMetadata.issuer,
      sub: options.loginHint.sub,
    }),
  });

  try {
    const backchannelAuthenticationResponse = await client.initiateBackchannelAuthentication(configuration, params);
    const tokenEndpointResponse = await client.pollBackchannelAuthenticationGrant(
      configuration,
      backchannelAuthenticationResponse
    );
    return TokenResponse.fromTokenEndpointResponse(tokenEndpointResponse);
  } catch (e) {
    throw new BackchannelAuthenticationError(e as OAuth2Error);
  }
}

This method chains two internal operations: first calling /bc-authorize to obtain the auth_req_id, then invoking pollBackchannelAuthenticationGrant to repeatedly query the token endpoint until the user completes authentication.

Manual Control with initiateBackchannelAuthentication

For applications requiring custom UI updates or specific timeout handling, initiateBackchannelAuthentication (lines 500–545) sends the initial POST to /bc-authorize and returns the auth_req_id, expires_in, and recommended polling interval without entering a polling loop.

const { authReqId, expiresIn, interval } = await authClient.initiateBackchannelAuthentication({
  bindingMessage: 'Approve login',
  loginHint: { sub: 'user-123' },
});

This separation allows developers to store the auth_req_id in application state, display progress indicators to users, or implement custom retry strategies while respecting the server's recommended polling frequency.

Single-Shot Token Requests with backchannelAuthenticationGrant

The backchannelAuthenticationGrant method (lines 560–575) performs individual token endpoint requests using grant_type=urn:openid:params:grant-type:ciba and the provided auth_req_id. Unlike the automatic polling method, this performs exactly one HTTP request and returns a TokenResponse or throws a BackchannelAuthenticationError containing the specific OAuth2 error code.

const tokenResponse = await authClient.backchannelAuthenticationGrant({ authReqId });

When used in a manual polling implementation, this method requires the caller to handle authorization_pending responses and implement appropriate delays between attempts.

How the Polling Mechanism Works

The internal polling implementation follows the algorithm defined in the CIBA specification, managing timing and error states through a deterministic loop.

Polling Interval and Retry Logic

The poller reads the interval from the initial response (defaulting to 5 seconds if omitted) and POSTs to /oauth/token with the auth_req_id and CIBA grant type. The implementation handles three specific error conditions:

  • authorization_pending – The authentication request is valid but the user has not yet completed the action. The SDK waits for the specified interval before retrying.
  • slow_down – The server requests reduced polling frequency. The SDK increases the interval by 5 seconds per the specification.
  • expired_token – The auth_req_id has exceeded its expires_in lifetime. The SDK aborts the flow and throws a BackchannelAuthenticationError.

The polling loop continues until either a successful token set is returned (containing access_token and id_token) or the auth_req_id expires, at which point the backchannelAuthentication promise resolves or rejects accordingly.

Implementation Examples

One-Call Automatic Flow

For most use cases, the automatic polling method provides the simplest integration:

import { AuthClient } from '@auth0/auth0-auth-js';

const authClient = new AuthClient({
  domain: 'YOUR_DOMAIN',
  clientId: 'YOUR_CLIENT_ID',
});

async function loginWithCIBA() {
  try {
    const tokenSet = await authClient.backchannelAuthentication({
      bindingMessage: 'Approve login',
      loginHint: { sub: 'user-123' },
      requestedExpiry: 300,
    });
    console.log('Authenticated! Tokens:', tokenSet);
  } catch (e) {
    console.error('CIBA failed:', e);
  }
}

This approach delegates all polling logic, error handling, and timeout management to the SDK according to the CIBA specification.

Manual Polling with Custom Error Handling

For applications requiring UI updates during the authentication process:

import { AuthClient, BackchannelAuthenticationError } from '@auth0/auth0-auth-js';

const client = new AuthClient({ domain: 'YOUR_DOMAIN', clientId: 'YOUR_CLIENT_ID' });

async function loginWithCustomPolling() {
  const { authReqId, interval } = await client.initiateBackchannelAuthentication({
    bindingMessage: 'Approve login on your device',
    loginHint: { sub: 'user-123' },
  });

  displayAuthReqId(authReqId);

  while (true) {
    try {
      const tokenResponse = await client.backchannelAuthenticationGrant({ authReqId });
      console.log('Success:', tokenResponse);
      break;
    } catch (err) {
      if (err instanceof BackchannelAuthenticationError) {
        switch (err.error) {
          case 'authorization_pending':
            await new Promise(r => setTimeout(r, interval * 1000));
            continue;
          case 'slow_down':
            await new Promise(r => setTimeout(r, (interval + 5) * 1000));
            continue;
          case 'expired_token':
            throw new Error('CIBA request timed out');
        }
      }
      throw err;
    }
  }
}

This pattern enables real-time progress indicators, custom backoff strategies, or integration with external state management systems while maintaining compliance with the CIBA protocol.

Summary

  • The backchannelAuthentication method in packages/auth0-auth-js/src/auth-client.ts (lines 456–492) provides automatic polling until tokens are received or the auth_req_id expires.
  • Use initiateBackchannelAuthentication (lines 500–545) to obtain the auth_req_id, expires_in, and interval for custom polling implementations.
  • The backchannelAuthenticationGrant method (lines 560–575) performs single token requests using the CIBA grant type urn:openid:params:grant-type:ciba.
  • The SDK handles standard CIBA errors including authorization_pending, slow_down, and expired_token according to the specification, with automatic interval management and timeout handling.

Frequently Asked Questions

What is the difference between backchannelAuthentication and initiateBackchannelAuthentication?

backchannelAuthentication is a high-level method that orchestrates the entire CIBA flow including automatic polling, while initiateBackchannelAuthentication only performs the initial request to /bc-authorize and returns the auth_req_id metadata. Use the former for simple implementations and the latter when you need custom control over the polling logic or UI state management.

How does the SDK handle the auth_req_id expiry?

The SDK tracks the expires_in value returned with the initial auth_req_id response. In the automatic polling flow, the poller stops and throws an error if the token endpoint returns expired_token or if the expiry time is reached. When using manual polling, the auth_req_id remains valid until the server returns an expired_token error or the expires_in duration elapses.

What CIBA-specific errors does the polling loop handle?

The implementation handles three primary CIBA error responses from the token endpoint: authorization_pending (user has not yet authenticated), slow_down (reduce polling frequency), and expired_token (the auth_req_id is no longer valid). These are encapsulated in BackchannelAuthenticationError objects containing the specific OAuth2 error code from the server response.

Can I implement custom polling logic instead of using the automatic method?

Yes. Call initiateBackchannelAuthentication to obtain the auth_req_id and recommended interval, then use backchannelAuthenticationGrant in your own loop to poll the token endpoint. This allows custom UI updates, specific timeout handling, or integration with application state managers, while the SDK still validates responses and handles the urn:openid:params:grant-type:ciba grant type correlation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →