โ All flows
CIBA
Start a login from the back, and let the user approve on their own device
CIBA (Client-Initiated Backchannel Authentication) lets an app ask the login server to authenticate a specific user, with no redirect and no browser. It is part of the OpenID Connect family. It works in three moves:
- The app names the user with a
login_hintand calls the backchannel endpoint. - The login server prompts the user on a device they trust, such as their phone.
- The app gets the tokens by polling, or the server notifies it (poll, ping and push are the three delivery modes). This page shows poll.
See it step by step
๐ง
YouThe person using the app๐ฅ๏ธ
AppFor example a call-centre system๐
Login serverChecks who you are and hands out tokensWho + why
Step 1 of 6
The app asks to sign someone in
It calls the backchannel endpoint (/bc-authorize) with a login_hint naming the user and a binding_message to show them.
When to use this flow
- Call centres and shop counters โ Confirm a customer without them typing anything into your system
- Payment approval โ Ask the account holder to approve a specific action
- Open banking โ Decoupled consent is common in this space
- The user is on the device with the app โ Authorization Code with PKCE is simpler
Good practices
- Show a
binding_messageโ Let the user match the prompt to the real session - Respect
intervalโ Poll no faster than the server says - Stop at
expires_inโ Start a new request instead of polling forever - Authenticate the app strongly โ Use a secret or a signed JWT
Common mistakes
- Guessing the user โ A wrong
login_hintsends a prompt to the wrong person - Logging
auth_req_idโ It identifies a pending login - Skipping
id_tokenvalidation โ Check its signature, issuer and audience - Ignoring
access_deniedโ The user may say no, so plan for it
Code example
CIBA poll mode example
javascript
// CIBA, poll mode
const start = await fetch('https://auth.provider.com/bc-authorize', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: 'Basic ' + btoa(CLIENT_ID + ':' + CLIENT_SECRET),
},
body: new URLSearchParams({
scope: 'openid payments',
login_hint: 'user@example.com',
binding_message: 'W4SJ',
}),
}).then((r) => r.json());
let interval = start.interval ?? 5;
while (true) {
await new Promise((r) => setTimeout(r, interval * 1000));
const res = await fetch('https://auth.provider.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: 'Basic ' + btoa(CLIENT_ID + ':' + CLIENT_SECRET),
},
body: new URLSearchParams({
grant_type: 'urn:openid:params:grant-type:ciba',
auth_req_id: start.auth_req_id,
}),
});
const body = await res.json();
if (body.error === 'authorization_pending') continue;
if (body.error === 'slow_down') { interval += 5; continue; }
if (body.error) throw new Error(body.error); // access_denied or expired_token
return body; // { access_token, id_token, ... }
}