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.
When to use
Section titled “When to use”- 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.
Requirements
Section titled “Requirements”- Pro+ tier. Free-tier tenants cannot register clients with the CIBA grant.
- Confidential client with
grantTypesincludingurn:openid:params:grant-type:ciba. Public clients are rejected at registration. Agents (Story 23.2) MAY also enable CIBA — the issued token carries anactclaim identifying the agent. scopeMUST includeopenid. Email + profile + custom scopes intersect with the client allowlist at issuance time.
Three-step flow (Node SDK)
Section titled “Three-step flow (Node SDK)”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 });Raw HTTP
Section titled “Raw HTTP”# 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=..."Security model
Section titled “Security model”binding_messageis 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_hint5/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 insidewithTenant. The user must still click an explicit Approve / Deny button. - Single-use
auth_req_id. Replay returnsinvalid_grantand emits a HIGH-severityoauth.ciba.replay_attemptaudit 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.
actclaim is emitted iff the calling client isagent-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.
Pricing
Section titled “Pricing”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.
Audit events
Section titled “Audit events”| Event | Severity | When |
|---|---|---|
oauth.ciba.request_issued | low | After bc-authorize accepts a request. |
oauth.ciba.user_cap_reached | medium | 3-cap reject. |
oauth.ciba.rate_limited | medium | Per-client / per-login_hint cap hit. |
oauth.ciba.unknown_user | medium | login_hint resolution failed. |
oauth.ciba.approved | low | User clicked Approve. |
oauth.ciba.denied | low | User clicked Deny. |
oauth.ciba.expired | low | Reaper swept a pending request. |
oauth.ciba.token_issued | low | Polling client received tokens (also a webhook event). |
oauth.ciba.replay_attempt | high | Polling client replayed a consumed auth_req_id. |
oauth.ciba.scope_denied | medium | Empty intersection at issuance. |
oauth.ciba.notification_delivery_failed | medium | Email / 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.
Compliance notes
Section titled “Compliance notes”- 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: trueand vulnerability records live undersecurity/cra-vulnerability-records/.
Limitations (deferred features)
Section titled “Limitations (deferred features)”- 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— onlylogin_hintis supported in 23.3. Federation use cases will land later.- Signed CIBA request objects (
requestparameter) — rejected withinvalid_requestfor 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.