Backchannel Authentication (CIBA)
Client-Initiated Backchannel Authentication (CIBA) (CIBA Core 1.0) is an OpenID Connect extension that lets a client application trigger user authentication on a separate device, without any browser redirect.
In every traditional OpenID Connect flow, the same device that initiates the request also completes the sign-in. CIBA breaks that assumption. A consumption device (for example, a call center terminal or a point-of-sale terminal) starts the request. The user authenticates on an authentication device (for example, their phone). The two devices share no direct connection: ThunderID coordinates between them.
ThunderID supports CIBA in poll mode only. The login_hint_token hint type is defined in the specification but is not supported; use login_hint or id_token_hint instead.
Typical use cases include:
- A call center agent verifying a caller's identity, with the caller approving on their phone.
- A bank teller initiating a payment authorization, with the customer approving on their mobile banking app.
- A point-of-sale terminal requesting payment authentication, with the customer approving on their phone.
How It Works
The flow proceeds in three phases:
Phase 1: Initiation
- The client sends
POST /oauth2/bc-authorizewith a user hint (for example,login_hint), the requested scopes, and an optionalresourcethat binds the request to a single resource server. - ThunderID validates the request and sends a notification to the user's authentication device.
- ThunderID returns an
auth_req_id, a polling interval, and an expiry time.
Phase 2: User Authentication
- The user reviews the request on their authentication device and approves or denies it.
- ThunderID records the decision and marks the
auth_req_idaccordingly.
Phase 3: Token Retrieval
- The client polls
POST /oauth2/tokenwithgrant_type=urn:openid:params:grant-type:cibauntil the user has acted. - ThunderID returns an access token and, when
openidis in the scope, an ID token.
How ThunderID Implements It
| Aspect | Behavior |
|---|---|
| Backchannel authentication endpoint | POST /oauth2/bc-authorize |
| Token endpoint | POST /oauth2/token |
| Grant type | urn:openid:params:grant-type:ciba |
| Client type | Confidential clients only |
| User hint | Exactly one of login_hint or id_token_hint must be provided. The specification also defines login_hint_token, but it is not yet supported; the server returns invalid_request when provided. |
| Poll mode | Only poll mode is supported; push mode is not implemented |
Default expires_in | 120 seconds |
Maximum expires_in | 600 seconds; ThunderID clamps any requested_expiry value above this limit |
| Default polling interval | 5 seconds |
slow_down | Returned when the client polls faster than the current interval; the client must permanently increase the interval by 5 seconds |
| Scope requirement | Must include openid |
| Resource binding (RFC 8707) | An optional resource binds the request to one resource server at initiation. When a permission scope is requested without resource, the configured defaultResourceServer applies; with no default, the request is rejected with invalid_target. The access token's aud is the bound resource server identifier, and its permissions are limited to that server. OIDC-only requests without resource use the application's token.accessToken.defaultAudience, falling back to client_id. A resource supplied when polling must match the binding |
binding_message | Optional; maximum 256 printable characters; ThunderID generates a default message when omitted |
| Token issuance | Access token always issued; ID token issued when openid is in the scope; refresh token issued when refresh_token grant is configured on the client |
| Actor claim | The access token carries an act claim naming the client when that client is an agent, or an application with includeActClaim enabled. See Actor on the Issued Token |
| One-time use | ThunderID atomically marks the auth_req_id as consumed on the first successful token exchange; subsequent polls return invalid_grant |
| Discovery | backchannel_authentication_endpoint is advertised in the OIDC discovery document |
Enable CIBA on a Client
- Console
- Dynamic Client Registration
- Open Applications or Agents in the ThunderID Console and select your client.
- Open the Advanced tab.
- Under Grant Types, select
urn:openid:params:grant-type:ciba. - Choose a confidential Token Endpoint Authentication method. See Client Authentication Methods.
- Save.
POST /oauth2/dcr/register
Content-Type: application/json
{
"client_name": "Call Center App",
"grant_types": ["urn:openid:params:grant-type:ciba"],
"token_endpoint_auth_method": "client_secret_basic",
"scope": "openid profile"
}
Run the Flow
Step 1: Initiate Backchannel Authentication
curl -X POST https://thunderid.example.com/oauth2/bc-authorize \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "scope=openid profile" \
-d "login_hint=user@example.com" \
-d "binding_message=Approve login from call center"
Response:
{
"auth_req_id": "a1b2c3d4-...",
"expires_in": 120,
"interval": 5
}
Step 2: Poll the Token Endpoint
Poll until the user approves, the request expires, or an error is returned.
curl -X POST https://thunderid.example.com/oauth2/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:openid:params:grant-type:ciba" \
-d "auth_req_id=a1b2c3d4-..."
While the user has not yet authenticated, the server returns:
{
"error": "authorization_pending",
"error_description": "The user has not yet completed authentication"
}
Wait for the interval (or a longer interval after a slow_down response), then retry.
When the user authenticates, the server returns:
{
"access_token": "eyJhbGciOiJSUzI1NiI...",
"id_token": "eyJhbGciOiJSUzI1NiI...",
"token_type": "Bearer",
"expires_in": 3600
}
Actor on the Issued Token
The issued access token represents the user who approved the request, so sub holds that user's subject identifier. The token additionally names the client that obtained it in an act (actor) claim, so a resource server sees both the user who approved the request and the client acting for them.
| Client | act claim |
|---|---|
| Agent | Always present. |
Application with includeActClaim enabled | Present. |
Application without includeActClaim | Absent. |
The value of act.sub is the agent or application resource identifier, not the client ID. The claim appears on the access token only, never on the ID token.
{
"sub": "<user-id>",
"act": { "sub": "<agent-or-application-id>" },
"client_id": "<client-id>",
"grant_type": "urn:openid:params:grant-type:ciba"
}
When the client also holds the refresh_token grant, the refresh token records the same actor, and every access token minted from it carries the actor decided at issuance rather than the client's current includeActClaim setting.
Polling Error Reference
| Error | Meaning | Action |
|---|---|---|
authorization_pending | User has not yet acted. | Retry after the current interval. |
slow_down | Client is polling too frequently. | Increase the interval permanently by 5 seconds, then retry. |
expired_token | The auth_req_id has expired. | Stop polling; the request cannot be recovered. |
access_denied | The user denied the request. | Stop polling; restart the flow if appropriate. |
invalid_grant | The auth_req_id has already been consumed. | Stop polling; the flow is complete. |
Related Guides
- Client Authentication Methods, required for confidential clients
- Resource Indicators, bind the backchannel request to a single resource server
- OpenID Connect, add
openidto the scope to receive an ID token - Server Metadata, the
backchannel_authentication_endpointfield in the OIDC discovery document - Claims & Scopes, configure which claims appear in the issued tokens