AbsoluteJS

CIBA — Approve On Your Phone

OpenID Client-Initiated Backchannel Authentication, poll mode. The user isn't at a browser — a call-center agent, an ATM, a kiosk requests auth on their behalf; the user approves on their second device. Banking, healthcare, high-trust enterprise scenarios. Shipped in 0.36.0 as part of the FAPI 2.0 baseline-ready cycle.

#Configure CIBA

You provide the user-resolver (login_hint → user) and the push-notification hook. The package owns the authReq lifecycle (storage, expiry, poll-rate enforcement) and the new endpoints.

TS
// 0.36.0 adds OpenID Client-Initiated Backchannel Authentication
// (CIBA) — the "approve on your phone" flow used by banking, healthcare,
// and call-center scenarios where the user isn't at a browser.
//
// Poll-mode only in this release: the client polls /token until the
// user approves. Ping + push modes follow when a consumer asks.

import { auth, createInMemoryBackchannelAuthStore } from '@absolutejs/auth';

const app = await auth({
  oidc: {
    // ... your usual OIDC config
    backchannelAuthStore: createInMemoryBackchannelAuthStore(),

    // Resolve "who is the user from this login_hint?" The hint can be
    // an email, phone, external id — whatever your customers send.
    resolveBackchannelUser: async ({ loginHint }) => {
      const user = await myDb.findUserByEmail(loginHint);
      return user ? { sub: user.id } : undefined;
    },

    // Out-of-band: push a notification to the user's second device so
    // they can approve. The package never owns this transport — we
    // hand you the context, you wire your FCM / APNs / SMS / email.
    onBackchannelAuthRequest: async (context) => {
      await myPushService.send(context.userSub, {
        title: 'Approve sign-in?',
        bindingMessage: context.bindingMessage
      });
    }
  }
});

#Approval UI

Your approval screen calls approveBackchannelAuth or denyBackchannelAuth. The package handles the rest — the polling client's next call to /oauth2/token receives the token set (or the denial) automatically.

TS
// In your approval UI, the user clicks Approve / Deny — call into
// the package's imperative API to record the decision.

import {
  approveBackchannelAuth,
  denyBackchannelAuth
} from '@absolutejs/auth';

// Approve route handler
app.post('/approve/:authReqId', async ({ params, store }) => {
  await approveBackchannelAuth({
    authReqId: params.authReqId,
    config: oidcConfig
  });
  return { ok: true };
});

// The client's next poll on /oauth2/token receives the token set.

#Wire flow

Three protocol responses the client polls through:

authorization_pendingWhile the user decides.
slow_downIf poll-rate enforcement trips.
token setReturned once the user approves.

Ping + push delivery modes follow when a consumer asks.

BASH
# 1. Client (RP) initiates the flow:
POST /oauth2/bc-authorize
  client_id=acme-rp
  login_hint=alice@example.com
  binding_message="Transfer $500 to Bob?"
  scope=openid

# Returns:
{ "auth_req_id": "urn:...", "expires_in": 300, "interval": 5 }

# 2. Client polls /token with the CIBA grant:
POST /oauth2/token
  client_id=acme-rp
  client_secret=...
  grant_type=urn:openid:params:grant-type:ciba
  auth_req_id=urn:...

# 3. Responses progress:
#    { "error": "authorization_pending" }  — user hasn't decided yet
#    { "error": "slow_down" }               — polling too fast
#    { "access_token": "...", "id_token": "...", ... }  — approved
#    { "error": "access_denied" }           — rejected
#    { "error": "expired_token" }           — TTL elapsed