CIBA (decoupled auth)
CIBA is the OpenID Connect spec for flows where the device that initiates the authentication isn't the device that completes it. Canonical use cases: a call-center agent confirming a customer identity by pushing a prompt to the customer's phone; a TPM-less POS terminal asking the user's wallet to authorize a purchase; an IVR step-up that requires the customer's mobile-app fingerprint.
The relying party (RP): 1. POSTs auth_req_id request to the IdP's backchannel_authentication_endpoint with login_hint / login_hint_token / id_token_hint identifying the user, plus scope / acr_values / requested_expiry / binding_message. 2. Receives { auth_req_id, expires_in, interval }. 3. Waits for token delivery via the operator-chosen mode:
- **poll**: RP polls /token with grant_type= urn:openid:params:grant-type:ciba + auth_req_id every interval seconds; gets authorization_pending, slow_down, or the tokens. - **ping**: IdP POSTs { auth_req_id } to the RP's client_notification_endpoint; the RP's handler then calls /token to fetch. - **push**: IdP POSTs { auth_req_id, access_token, id_token, refresh_token, ... } directly. The client_notification_token registered with the IdP authenticates each callback.
This module provides:
b.auth.ciba.client.create({ ... }) .startAuthentication({ loginHint, scope, bindingMessage, ... }) .pollToken({ authReqId }) .receivePingNotification(req) // ping mode handler .receivePushNotification(req) // push mode handler
Composes b.auth.oauth for client_assertion / token-endpoint plumbing (so JWT-bearer client auth, mTLS client auth, and PAR alongside CIBA all share one set of audited credentials).
b.auth.ciba.client.create(opts) #
{
{
issuer: string, // OIDC issuer URL — required
clientId: string, // RP client_id — required
clientAuth: "secret"|"jwt"|"mtls", // token-endpoint auth
clientSecret?: string, // when clientAuth = "secret"
clientAssertionSigner?: fn(payload)→jwt, // when clientAuth = "jwt"
backchannelAuthenticationEndpoint?: string, // optional — discovered when omitted
tokenEndpoint?: string, // optional — discovered
scope?: string|string[],
deliveryMode: "poll"|"ping"|"push",
clientNotificationToken?: string, // fixed token RP mints once + registers with IdP
httpClientOpts?: object,
allowHttp?: boolean,
}
}
Build a CIBA-aware OIDC RP. Operators wire the resulting object's methods onto routes that drive the decoupled-auth flow.
var ciba = b.auth.ciba.client.create({
issuer: "https://idp.example.com",
clientId: "rp-1",
clientAuth: "secret",
clientSecret: process.env.CIBA_CLIENT_SECRET,
scope: ["openid", "profile"],
deliveryMode: "poll",
});
var ticket = await ciba.startAuthentication({
loginHint: "alice@example.com",
bindingMessage: "Authorize wire transfer of $4,200",
acrValues: ["urn:mace:incommon:iap:silver"],
});
// → { authReqId, expiresIn, interval }
var tokens = await ciba.pollToken({ authReqId: ticket.authReqId });
// → { accessToken, idToken, ... } once user approves
b.auth.ciba.client.startAuthentication(opts) #
{
{
loginHint?: string,
loginHintToken?: string,
idTokenHint?: string,
scope?: string|string[],
bindingMessage?: string,
acrValues?: string|string[],
requestedExpiry?: number,
userCode?: string,
}
}
POST to the IdP's backchannel_authentication_endpoint and return a ticket with authReqId + expiresIn + interval. At least one of loginHint / loginHintToken / idTokenHint must be supplied to identify the user.
var ticket = await ciba.startAuthentication({
loginHint: "alice@example.com",
bindingMessage: "Authorize wire transfer of $4,200",
});
// → { authReqId, expiresIn, interval }
b.auth.ciba.client.pollToken(opts) #
{
{ authReqId: string }
}
Poll the IdP's /token endpoint with grant_type=ciba. Returns the tokens once the user approves; throws AuthError with code "auth-ciba/authorization_pending" or "auth-ciba/slow_down" while waiting. Operators wrap with their preferred backoff.
var tokens = await ciba.pollToken({ authReqId: ticket.authReqId });
// → { accessToken, idToken, refreshToken, tokenType, scope, expiresIn, raw }
b.auth.ciba.client.parseNotification(req, opts) #
{
{ body?: object } // pre-parsed body; defaults to req.body
}
Parse + authenticate an IdP-initiated callback to the RP's client_notification_endpoint. Validates the bearer client_notification_token (timing-safe equality) before surfacing the body. Use the returned authReqId to drive the RP-side flow:
- In **ping** mode the body is { auth_req_id }. Call pollToken({ authReqId }) afterwards. - In **push** mode the body carries the full token-response object; no follow-up call needed.
Async because a pushed id_token is verified (signature + iss/aud/exp) via the composed inner OAuth client before it is returned — the verified claims are surfaced as claims. A present-but-invalid id_token throws auth-ciba/id-token-invalid (the notification-token bearer authenticates the caller, never the token itself).
app.post("/ciba/notify", async function (req, res) {
var info = await ciba.parseNotification(req, { body: req.body });
// → { authReqId, accessToken, idToken, claims, ... }
res.statusCode = 204; res.end();
});
Last updated 2026-08-08T16:39:49.652Z by seeder.