CIBA (Client Initiated Backchannel Authentication)
The Problem: Authentication Initiated by Someone Else
Traditional OAuth 2.0 flows assume a single user interacts with a single device: the user opens a browser, enters credentials, and receives tokens — all on the same device, in the same session. This model works for web apps and mobile apps, but it falls apart in scenarios where the person requesting authentication is not the same person authenticating.
| Scenario | Why Standard OAuth Fails |
|---|---|
| Call center agent verifying a customer | The agent cannot type the customer’s password |
| Payment terminal at a store | The cashier initiates, the customer approves on their phone |
| Smart speaker purchase | ”Alexa, buy more coffee” — the speaker has no screen for login |
| Healthcare check-in kiosk | The receptionist initiates, the patient approves on their phone |
| Wire transfer authorization | The bank system initiates, the account holder approves remotely |
In all these cases, one party initiates the authentication request and a different party — the actual user — must approve it on a separate device. The authorization server needs to push an authentication request to the user rather than waiting for the user to visit a URL.
What CIBA Is
CIBA (Client Initiated Backchannel Authentication) is an OpenID Connect extension defined in the CIBA Core specification . It allows a client application to initiate an authentication flow for a known user, where the user authenticates on a separate authentication device (typically their phone) via a push notification, SMS, or email.
The key distinction from other OAuth flows:
- Authorization Code + PKCE: User drives the flow from start to finish on the same device.
- Device Flow: Client displays a code, user visits a URL and enters it. User initiates the secondary interaction.
- CIBA: Client initiates, server pushes a notification to the user. The user only reacts.
CIBA decouples the consumption device (where the service runs) from the authentication device (where the user proves their identity).
How CIBA Works: Step by Step
Step 1: Client Sends a Backchannel Authentication Request
The client sends a POST request to the CIBA endpoint with a login_hint identifying the user and a binding_message describing the action:
POST /api/oauth/ciba HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer <client_access_token>
[email protected]
&scope=openid profile
&binding_message=Authorize account verification for agent Sarah (ref: TX-9821)
&requested_expiry=120
&client_notification_token=notification-callback-token-xyz| Parameter | Required | Description |
|---|---|---|
login_hint | Yes | Identifies the user — email, phone number, or user ID |
scope | Yes | Requested OAuth scopes (must include openid) |
binding_message | Recommended | Human-readable description shown to the user on their authentication device |
requested_expiry | Optional | How long the auth request should remain valid (seconds). Default: 300 |
client_notification_token | Conditional | Required for ping and push modes; the token Auris sends back in the callback |
Step 2: Server Returns an Authentication Request ID
If the login_hint resolves to exactly one user and the client is authorized to use CIBA, the server responds:
{
"auth_req_id": "ciba_req_1a2b3c4d5e6f7g8h",
"expires_in": 120,
"interval": 5
}The auth_req_id is the handle the client uses to check on the status of the authentication request.
Step 3: Server Notifies the User
Auris sends a notification to the user’s authentication device through one of the configured channels:
- Push notification: Native mobile push (requires the user to have the authenticator app installed)
- SMS: Text message with a link to the approval page
- Email: Email with a link to the approval page
The notification includes the binding_message so the user knows exactly what they are approving.
Step 4: User Approves or Denies
The user sees the binding message and the requesting application’s name on their authentication device. They can:
- Approve: The authorization server marks the CIBA request as approved and (if required) the user completes MFA
- Deny: The authorization server marks the request as denied; the client receives
access_deniedon the next poll
Step 5: Client Obtains Tokens
How the client receives the tokens depends on the configured notification mode (see next section).
The Three Notification Modes
CIBA defines three ways for the client to obtain tokens after the user approves. Each makes a different trade-off between simplicity, latency, and infrastructure requirements.
Poll Mode
The simplest mode. The client polls the token endpoint at the configured interval, exactly like the Device Flow:
POST /api/auth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:openid:params:grant-type:ciba
&auth_req_id=ciba_req_1a2b3c4d5e6f7g8h
&client_id=call-center-app
&client_secret=client-secretThe server responds with the same error codes as the Device Flow during polling:
| Response | Meaning |
|---|---|
authorization_pending | User has not yet responded |
slow_down | Client is polling too fast; increase interval by 5 seconds |
expired_token | The auth_req_id has expired |
access_denied | User denied the request |
| Success (200) | Tokens returned |
Best for: Simple integrations where 5-second latency is acceptable.
Ping Mode
In ping mode, the authorization server sends an HTTP POST to the client’s registered callback URL when the user responds. The callback body contains only the auth_req_id — the client must then fetch the tokens from the token endpoint.
// Server pings the client's callback
POST /ciba-callback HTTP/1.1
Host: client.example.com
Content-Type: application/json
Authorization: Bearer <client_notification_token>
{
"auth_req_id": "ciba_req_1a2b3c4d5e6f7g8h"
}The client then fetches tokens normally:
POST /api/auth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:openid:params:grant-type:ciba
&auth_req_id=ciba_req_1a2b3c4d5e6f7g8h
&client_id=call-center-app
&client_secret=client-secretBest for: Production systems where the client has a publicly reachable callback URL and needs lower latency than poll mode.
Push Mode
In push mode, the authorization server pushes the actual tokens directly to the client’s callback URL. The client never calls the token endpoint.
// Server pushes tokens to the client's callback
POST /ciba-callback HTTP/1.1
Host: client.example.com
Content-Type: application/json
Authorization: Bearer <client_notification_token>
{
"auth_req_id": "ciba_req_1a2b3c4d5e6f7g8h",
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiJ9..."
}Best for: Lowest latency. However, the tokens travel over the network to the client’s callback, increasing the attack surface.
Mode Comparison
| Dimension | Poll | Ping | Push |
|---|---|---|---|
| Latency | Higher (polling interval) | Low (server-initiated) | Lowest (tokens in callback) |
| Client complexity | Simple (polling loop) | Medium (callback endpoint + token fetch) | Medium (callback endpoint) |
| Client infrastructure | None (outbound only) | Needs public callback URL | Needs public callback URL |
| Token security | Tokens never traverse untrusted networks | Tokens fetched securely by client | Tokens sent to callback (higher exposure) |
| Best for | CLI tools, simple integrations | Production web services | Real-time systems |
Auris supports all three modes. Poll mode is the default for new applications. To use ping or push mode, register a backchannel_client_notification_endpoint in the application settings and include a client_notification_token in each CIBA request.
The Binding Message
The binding_message is arguably the most important security feature of CIBA. It is a short, human-readable string that is displayed to the user on their authentication device, describing exactly what they are approving.
Why It Matters
Without a binding message, CIBA is vulnerable to confused deputy attacks: an attacker initiates a CIBA request for a victim user, and the victim sees a generic “Approve login?” prompt with no context. The victim approves, thinking it is their own login attempt, and the attacker receives tokens.
With a binding message, the victim sees:
“Authorize wire transfer of $5,000 to account ending in 7892 (ref: WT-2024-0918)”
If the victim did not initiate a wire transfer, they know to deny the request.
Best Practices
| Practice | Example |
|---|---|
| Include the action being authorized | ”Authorize payment of EUR 49.99” |
| Include a reference number | ”(ref: TX-9821)“ |
| Include the requesting party | ”Agent Sarah at ACME Bank” |
| Keep it under 200 characters | Mobile push notifications truncate long messages |
| Do not include sensitive data | Do not put full account numbers or SSNs in the message |
The binding_message is displayed on the user’s device, which may be a push notification visible on the lock screen. Never include passwords, full credit card numbers, or other sensitive information in the binding message.
Security Analysis
Authentication Request Lifetime
CIBA requests have a short lifetime (default 300 seconds, configurable via requested_expiry up to a server-imposed maximum). After expiry, the auth_req_id is invalid and the client must start a new flow.
Login Hint Resolution
The login_hint must resolve to exactly one user. If the hint is ambiguous (e.g., a common name matching multiple users), the server rejects the request with unknown_user_id. This prevents attacks where an attacker targets a specific user by providing a vague hint.
Notification Channel Security
The security of CIBA depends on the security of the notification channel:
| Channel | Risk | Mitigation |
|---|---|---|
| Push notification | Compromised phone, notification interception | Require biometric unlock to approve |
| SMS | SIM swapping, SS7 interception | Use push notifications when possible; SMS as fallback only |
| Email account compromise, delivery delay | Use push notifications when possible; email as fallback only |
CIBA + DPoP
CIBA can be combined with DPoP to produce sender-constrained tokens. The client includes a DPoP proof when polling/fetching tokens, and the resulting access token is bound to the client’s key pair. This provides an additional layer of protection if the tokens are intercepted during delivery (especially relevant in push mode).
Comparison: CIBA vs Device Flow
Both CIBA and Device Flow authenticate a user who is not directly interacting with the client device. The fundamental difference is who initiates the secondary-device interaction:
| Dimension | Device Flow | CIBA |
|---|---|---|
| Who initiates | User (visits a URL) | Server (sends notification) |
| User code entry | User types code manually | No code entry; user just approves |
| Prior user registration | Not required | Required (server must know how to reach the user) |
| Anonymous users | Supported | Not supported |
| UX latency | Higher (user must visit URL, type code) | Lower (user taps approve on notification) |
| Notification infrastructure | None | Requires push/SMS/email capability |
| Offline user | Can authenticate later (within code lifetime) | Cannot receive notification if offline |
Auris Implementation Details
Prisma Model
model CibaAuthRequest {
id String @id @default(cuid())
tenantId String
applicationId String
authReqId String @unique
loginHint String
userId String?
scope String?
bindingMessage String?
status CibaAuthStatus @default(PENDING)
notificationMode CibaNotificationMode @default(POLL)
clientNotificationToken String?
expiresAt DateTime
interval Int @default(5)
createdAt DateTime @default(now())
}
enum CibaAuthStatus {
PENDING
APPROVED
DENIED
EXPIRED
}
enum CibaNotificationMode {
POLL
PING
PUSH
}Configuration
CIBA is enabled per-application in the Auris Console under Applications > (select application) > Settings:
| Setting | Default | Description |
|---|---|---|
| Enable CIBA | false | Master toggle |
| Notification mode | POLL | How the client receives tokens (poll, ping, or push) |
| Notification channels | email | Which channels to use for notifying users (push, sms, email) |
| Max request lifetime | 300 | Maximum requested_expiry in seconds |
| Polling interval | 5 | Minimum seconds between token endpoint polls (poll mode only) |
| Callback URL | — | Client’s backchannel notification endpoint (required for ping/push) |
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/oauth/ciba | POST | Initiate a backchannel authentication request |
/api/auth/token | POST | Token endpoint (supports urn:openid:params:grant-type:ciba grant type) |
/hosted/ciba/approve | GET | User-facing approval page (linked from notification) |
Code Example: Call Center Agent Flow
async function verifyCustomerIdentity(
customerEmail: string,
agentName: string,
referenceNumber: string
) {
// Step 1: Initiate CIBA request
const cibaResponse = await fetch('https://auth.example.com/api/oauth/ciba', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${agentAccessToken}`,
},
body: new URLSearchParams({
login_hint: customerEmail,
scope: 'openid profile',
binding_message: `Identity verification by ${agentName} (ref: ${referenceNumber})`,
requested_expiry: '120',
}),
})
const { auth_req_id, interval, expires_in } = await cibaResponse.json()
console.log(`Verification sent to ${customerEmail}. Waiting for approval...`)
// Step 2: Poll for completion (poll mode)
let pollInterval = interval * 1000
const deadline = Date.now() + expires_in * 1000
while (Date.now() < deadline) {
await new Promise(resolve => setTimeout(resolve, pollInterval))
const tokenResponse = await fetch('https://auth.example.com/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:openid:params:grant-type:ciba',
auth_req_id,
client_id: 'call-center-app',
client_secret: 'client-secret',
}),
})
if (tokenResponse.ok) {
const { id_token } = await tokenResponse.json()
console.log('Customer identity verified.')
return id_token // Contains verified user claims
}
const error = await tokenResponse.json()
if (error.error === 'slow_down') {
pollInterval += 5000
continue
}
if (error.error === 'authorization_pending') continue
if (error.error === 'access_denied') {
throw new Error('Customer denied the verification request.')
}
throw new Error(`Verification failed: ${error.error_description}`)
}
throw new Error('Verification request expired. Customer did not respond in time.')
}Related Concepts
- Device Authorization Flow — Another decoupled authentication flow, user-initiated
- OAuth 2.0 & OIDC — The authorization framework CIBA extends
- Tokens Explained — JWT structure, ID tokens, and access tokens
- DPoP (Proof of Possession) — Sender-constrained tokens, combinable with CIBA