โ† All flows

CIBA

Start a login from the back, and let the user approve on their own device

Who's involved
Backend, auth server, user's device
Best for
Approval on a separate device
Difficulty
Advanced

1Overview

How it works

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:

  1. The app names the user with a login_hint and calls the backchannel endpoint.
  2. The login server prompts the user on a device they trust, such as their phone.
  3. 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.

2Use cases

When would you use this?

Call centres

An agent asks the caller to approve the call on their own banking app, rather than reading out a PIN.

Payment approval

A shop till starts a payment and you confirm it on your phone.

Open banking

A finance app asks your bank to confirm a payment you requested.

Not the right fit? User is on the same device? Use Authorization Code with PKCE. It is simpler, because a redirect works.

3Interactive

Try it step by step

See it step by step

๐Ÿง‘
You
๐Ÿ–ฅ๏ธ
App
๐Ÿ”
Login server
Who + 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.

4Practical advice

Do this, avoid that

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_hint sends a prompt to the wrong person
  • Logging auth_req_id โ€” It identifies a pending login
  • Skipping id_token validation โ€” Check its signature, issuer and audience
  • Ignoring access_denied โ€” The user may say no, so plan for it

5Code

Code example

Show the code
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, ... }
}