Skip to content

CIBA — Asynchronous User Consent (OIDC CIBA Core 1.0)

The OpenID Connect Client-Initiated Backchannel Authentication flow (OIDC CIBA Core 1.0) lets a back-end agent initiate a user-consent request, deliver a notification to the user’s authentication device (dashboard or email), and poll a token endpoint until the user approves or denies. The agent never sees the user’s credentials; the human reviews the binding_message describing the action and decides on a separate device.

Spec note: the Epic 23 plan referenced “RFC 9126” — that RFC is Pushed Authorization Requests, a different OAuth extension. CIBA is the OpenID Foundation spec linked above. RFC 9126 (PAR) may pair with CIBA in a future story for signed initiation; out of scope here.

  • An AI agent (Claude / GPT / custom MCP server) needs the human’s approval for a sensitive action while the human is not on the same device.
  • A voice / IoT client wants to perform a high-stakes action that benefits from out-of-band human review (transfer, account change, large purchase).
  • A call-center flow where the agent reads the action aloud and the customer approves from their phone.
  • Pro+ tier. Free-tier tenants cannot register clients with the CIBA grant.
  • Confidential client with grantTypes including urn:openid:params:grant-type:ciba. Public clients are rejected at registration. Agents (Story 23.2) MAY also enable CIBA — the issued token carries an act claim identifying the agent.
  • scope MUST include openid. Email + profile + custom scopes intersect with the client allowlist at issuance time.
import { RakomiClient } from '@rakomi/node';
const client = new RakomiClient({
apiKey: process.env.RAKOMI_API_KEY!,
clientId: process.env.AGENT_CLIENT_ID!,
clientSecret: process.env.AGENT_CLIENT_SECRET!,
});
// 1. Initiate. The user receives an email + dashboard notification.
const initiated = await client.ciba.initiate({
scope: ['openid', 'profile'],
loginHint: 'user@example.com',
bindingMessage: 'Approve transfer of €450 to Beneficiary X (ref TX-2026-04-29)',
requestedExpiry: 300,
locale: 'en-US',
});
if (!initiated.ok) throw new Error(initiated.error.code);
// 2. Wait for the user's decision (poll loop with adaptive interval).
const tokens = await client.ciba.awaitDecision({
authReqId: initiated.data.authReqId,
});
// 3. Use the agent token. `tokens.scope` is what was actually granted
// (intersected with the client allowlist + user-approved scope).
console.log({ accessToken: tokens.accessToken, scope: tokens.scope });
Terminal window
# Initiate.
curl -X POST https://api.rakomi.com/oauth/bc-authorize \
-u "$AGENT_CLIENT_ID:$AGENT_CLIENT_SECRET" \
-d "scope=openid profile" \
-d "login_hint=user@example.com" \
-d "binding_message=Approve transfer of EUR 450 to Beneficiary X" \
-d "requested_expiry=300"
# {"auth_req_id":"...","expires_in":300,"interval":5}
# Poll until the user approves.
curl -X POST https://api.rakomi.com/oauth/token \
-u "$AGENT_CLIENT_ID:$AGENT_CLIENT_SECRET" \
-d "grant_type=urn:openid:params:grant-type:ciba" \
-d "auth_req_id=..."
  • binding_message is REQUIRED + verbatim. The user sees exactly what the agent typed. ≤256 chars, plain text only (HTML stripped server-side, NFC normalised). Anti-phishing baseline.
  • Per-user 3-cap. A user has at most 3 pending CIBA requests at any time; the 4th request is rejected with slow_down. Prevents notification spam + MFA-fatigue attacks.
  • Per-(tenant, client) rate-limit 30/min + per-login_hint 5/min.
  • Confidential-only. CIBA Core §6 forbids public clients.
  • Pro+ tier gate at registration time + at runtime.
  • Generic error_descriptions for cross-tenant + unknown-user paths (no enumeration leak); specific descriptions for status-revealing paths (denied, expired, slow_down, authorization_pending).
  • One-time URL token in email (separate from auth_req_id); the dashboard validates the token + tenant + user mapping inside withTenant. The user must still click an explicit Approve / Deny button.
  • Single-use auth_req_id. Replay returns invalid_grant and emits a HIGH-severity oauth.ciba.replay_attempt audit event.
  • Atomic-bundle issuance. Status transition + token mint + audit + outbox webhook are exactly-once.
  • Multi-pod-safe reaper. 60s sweep moves expired pending requests → expired; 6h cleanup hard-deletes terminal rows older than 7 days.
  • act claim is emitted iff the calling client is agent-typed — identifies the agent acting on behalf of the human approver. Non-agent CIBA clients (e.g. enterprise apps wanting OOB consent) get plain user tokens.

CIBA is Pro+ tier only (PRD §23.3). MAU is counted on the human approver (decided_by_user_id) — consistent with device-grant and token-exchange. Token issuance counts as a standard /oauth/token request for rate-limit / billing.

EventSeverityWhen
oauth.ciba.request_issuedlowAfter bc-authorize accepts a request.
oauth.ciba.user_cap_reachedmedium3-cap reject.
oauth.ciba.rate_limitedmediumPer-client / per-login_hint cap hit.
oauth.ciba.unknown_usermediumlogin_hint resolution failed.
oauth.ciba.approvedlowUser clicked Approve.
oauth.ciba.deniedlowUser clicked Deny.
oauth.ciba.expiredlowReaper swept a pending request.
oauth.ciba.token_issuedlowPolling client received tokens (also a webhook event).
oauth.ciba.replay_attempthighPolling client replayed a consumed auth_req_id.
oauth.ciba.scope_deniedmediumEmpty intersection at issuance.
oauth.ciba.notification_delivery_failedmediumEmail / dashboard channel both failed.

The oauth.ciba.token_issued event is the only CIBA event delivered to tenant webhook subscribers; the rest stay audit-only and can be retrieved via the audit-events API.

  • GDPR Art. 22 — CIBA is the human-in-the-loop escape valve for AI agent flows. Every CIBA approval is an explicit, informed, unambiguous Art. 7 consent (the user sees the binding_message, the scopes, the agent name, and an anti-phishing warning).
  • EU AI Act Arts. 12 + 14 — the audit log is automatic record-keeping for high-risk AI deployments; the binding_message + Approve gesture satisfies the human-oversight requirement.
  • EU CRA — SDK code paths are tagged cra_jurisdiction: true and vulnerability records live under security/cra-vulnerability-records/.
  • Push notification channel is a forward-compat hook — flag column exists in the DB; mobile push handler is wired in Epic 22 follow-up.
  • login_hint_token / id_token_hint — only login_hint is supported in 23.3. Federation use cases will land later.
  • Signed CIBA request objects (request parameter) — rejected with invalid_request for now. Pair with RFC 9126 PAR in a future story.
  • acr_values / user_code — accepted but ignored. Step-up auth and number-matching challenge are future hardening.
  • Ping-mode / push-mode — only poll-mode is implemented. Ping callback to the client is a future seed.
  • Multi-party CIBA (manager + user co-approve) — design open for E20 Organizations.