# 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](/truora-pass/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:

```bash
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](/truora-pass/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:

```json
{
  "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.

{{<img width="40%" src="/images/truora-pass/ciba-approval-screen-mobile.png" alt="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](/truora-pass/ciba_whatsapp_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:

```bash
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:

```json
{
  "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](/truora-pass/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 `POST`s 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](/truora-pass/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:

```bash
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:

```json
{
  "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:

```json
{ "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](/truora-pass/ciba_whatsapp_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)](/truora-pass/approval_requests/) — 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.
