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.

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.