# How backchannelAuthentication Polls for Tokens in Auth0 CIBA

> Learn how Auth0 SDK's backchannelAuthentication polls for tokens and handles auth_req_id using three methods: automatic polling initiate, and single-shot requests. Discover error handling.

- Repository: [Auth0/auth0-auth-js](https://github.com/auth0/auth0-auth-js)
- Tags: internals
- Published: 2026-02-25

---

**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`](https://github.com/auth0/auth0-auth-js/blob/main/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`.

```typescript
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.

```typescript
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.

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/auth0/auth0-auth-js/blob/main/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.