View as Markdown

CIBA flow (decoupled authorization)

Backchannel flow — POST /v1/oauth2/bc-authorize (form) returns auth_req_id, the user approves on their device, and you poll POST /v1/oauth2/token with the CIBA grant URN.

How-to

CIBA flow (decoupled authorization)

CIBA (Client-Initiated Backchannel Authentication) lets your application request a user’s authorization when the user is not in your application’s browser — for example from a call center, a point of sale, or a purely backend process. Instead of redirecting a browser, your backend starts a backchannel request; Truora Pass sends an approval request to the user’s own device, and your backend polls the token endpoint until the user approves.

Prerequisites

  • Your application must be registered with the CIBA grant type, urn:openid:params:grant-type:ciba. See Registering your application.
  • The user must already have a Truora Pass account whose email matches your login_hint, with a verified phone number — the approval request is delivered to the user over WhatsApp.

Flow at a glance

  1. Your backend calls POST /v1/oauth2/bc-authorize and receives an auth_req_id.
  2. Truora Pass sends the user a WhatsApp message with an approval link.
  3. The user opens the link and approves (or denies) the request in Truora Pass on their device.
  4. Your backend polls POST /v1/oauth2/token with the CIBA grant until it receives tokens.

1. Start the backchannel request

Send a form-encoded request to the backchannel authorization endpoint:

curl -X POST https://api.pass.truora.com/v1/oauth2/bc-authorize \
  --data-urlencode "client_id=WLT_APP_your_client_id" \
  --data-urlencode "client_secret=your_client_secret" \
  --data-urlencode "scope=openid identity documents" \
  --data-urlencode "login_hint=user@example.com" \
  --data-urlencode "binding_message=Share your documents with CapiBank" \
  --data-urlencode "requested_expiry=600"
Parameter Required Description
client_id Yes Your application’s client ID.
client_secret Yes Your application’s client secret.
scope Yes Space-delimited scopes to request. See the Scopes reference.
login_hint Yes The user’s email address (it must contain @). This is how Truora Pass resolves which user to ask.
binding_message Conditional A short human-readable message shown to the user on the approval screen. Required when the request includes a sensitive scope such as documents or background. Longer messages are truncated to 200 characters.
requested_expiry No How long the request stays approvable, in seconds. Default 300; values above 900 are capped at 900.
acr_values No Advanced: requested authentication context (step-up).

Note: binding_message is displayed to the user exactly as you send it. Use it to tell the user what they are approving and for whom — a missing binding_message on a sensitive-scope request is rejected with invalid_request.

2. Read the response

On success the endpoint returns:

{
  "auth_req_id": "<opaque request identifier>",
  "expires_in": 600,
  "interval": 5
}
  • auth_req_id identifies this authorization request in your token polling.
  • expires_in is how many seconds the request remains approvable.
  • interval is the minimum number of seconds to wait between polls (5).

Errors at this step use the standard OAuth 2.0 envelope {"error": "...", "error_description": "..."}:

Error Meaning
invalid_request A required parameter is missing or invalid — including a missing binding_message on a sensitive-scope request, or a resolved user with no verified phone for delivery.
unknown_user_id No Truora Pass user could be resolved from the login_hint.
invalid_client Wrong client_id / client_secret (HTTP 401).
notifier_unavailable The WhatsApp approval message could not be delivered (HTTP 503). Retry later.

3. The user approves on their device

Truora Pass delivers a WhatsApp message to the user’s verified phone with a link to the approval screen. There the user sees your application, the requested scopes, and your binding_message, and approves or denies the request. Nothing happens in your application’s UI during this step — your backend just keeps polling.

The Truora Pass approval screen on mobile, showing the requesting application, the requested scopes, and the Approve and Deny buttons

For the WhatsApp side of this flow in depth — message contents, delivery modes, a worked chatbot example — see the CIBA over WhatsApp implementation guide.

4. Poll the token endpoint

Poll POST /v1/oauth2/token with a form-encoded body (unlike the authorization code exchange, which is JSON), waiting at least interval seconds (5) between attempts:

curl -X POST https://api.pass.truora.com/v1/oauth2/token \
  --data-urlencode "grant_type=urn:openid:params:grant-type:ciba" \
  --data-urlencode "auth_req_id=<auth_req_id from step 2>" \
  --data-urlencode "client_id=WLT_APP_your_client_id" \
  --data-urlencode "client_secret=your_client_secret"

5. Handle the polling responses

While the request is pending or after it terminates without approval, the endpoint answers with the OAuth 2.0 error envelope:

Error What to do
authorization_pending The user has not decided yet. Keep polling, respecting interval.
access_denied The user declined. The request is consumed — stop polling; start a new request only with the user’s renewed intent.
expired_token The request expired before approval, or was already redeemed. Start a new backchannel request.
invalid_grant The auth_req_id was issued to a different client.
invalid_client Wrong client_id / client_secret (HTTP 401).
unauthorized_client Your application is not allowed to use the CIBA grant.
unsupported_grant_type The grant_type value is wrong.

6. Receive the tokens

Once the user approves, the poll returns a token response:

{
  "access_token": "<access token>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid identity documents",
  "id_token": "<id token, only when the openid scope was granted>"
}

Note: the CIBA flow does not issue a refresh_token. When the access token expires (60 minutes by default), start a new backchannel request if you still need access.

Treat the tokens as opaque and read the user’s identity with the access token at GET /v1/oauth2/userinfo — see UserInfo and claims.

Alternative to polling: push delivery

Instead of polling the token endpoint, your backend can receive the result as a webhook: Truora Pass POSTs the outcome to an endpoint you register, the moment the user decides.

To use it:

  1. On your application in the dashboard, register a Backchannel Client Notification Endpoint (an HTTPS URL you control) and set the Token Delivery Mode to push — see Registering your application.
  2. Include a client_notification_token in every bc-authorize call — a bearer credential you generate per request (up to 1024 characters, RFC 6750 token68 format). It is required once a callback endpoint is registered:
curl -X POST https://api.pass.truora.com/v1/oauth2/bc-authorize \
  --data-urlencode "client_id=WLT_APP_your_client_id" \
  --data-urlencode "client_secret=your_client_secret" \
  --data-urlencode "scope=openid identity documents" \
  --data-urlencode "login_hint=user@example.com" \
  --data-urlencode "binding_message=Share your documents with CapiBank" \
  --data-urlencode "client_notification_token=a-secret-you-generate"

At decision time Truora Pass sends a single POST to your endpoint with Authorization: Bearer <your client_notification_token> — verify it matches the token you sent for that request before trusting the body. On approval the body is the token response plus the auth_req_id it fulfills:

{
  "auth_req_id": "<the request's auth_req_id>",
  "access_token": "<access token>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid identity documents",
  "id_token": "<id token, only when the openid scope was granted>"
}

On denial:

{ "auth_req_id": "<the request's auth_req_id>", "error": "access_denied" }

Respond with a 2xx quickly — delivery is a single best-effort POST (no retries), and once the result is pushed the request is consumed, so it can no longer be redeemed at the token endpoint. If your endpoint might miss deliveries, stay on poll mode instead.

See also

  • CIBA over WhatsApp — implementation guide — a worked, end-to-end walkthrough of this flow for the common case: a WhatsApp chatbot or contact-center agent authenticating a user mid-conversation.
  • Approval requests (Consent Gate) — replace binding_message with a rich, multi-block prompt (text, images, files), collect structured answers, or deliver the approval link yourself instead of over WhatsApp.