Keycloak CIBA: Backchannel Authentication for Financial Services
Last updated: March 2026
Traditional OAuth 2.0 flows require the user to interact with a browser redirect. But what about scenarios where authentication is initiated by a third party — a bank teller processing a transaction, a call center agent verifying a customer, or a payment terminal requesting approval? Client Initiated Backchannel Authentication (CIBA) handles exactly this: one party initiates authentication, and the user approves it on a separate device (typically their phone) without any browser redirect.
Keycloak supports CIBA as defined in the OpenID Connect CIBA specification. This guide covers the architecture, Keycloak configuration, and implementation for financial services use cases.
What is CIBA?
CIBA is a decoupled authentication flow with three participants:
-
Consumption Device: The device where the service is consumed (e.g., a bank teller’s terminal, a point-of-sale system, a call center application). This device initiates the authentication request.
-
Authentication Device (AD): The user’s personal device (e.g., smartphone with a banking app) where they approve or deny the authentication request.
-
OpenID Provider (OP): Keycloak, which orchestrates the flow between the consumption device and the authentication device.
The flow differs from standard OAuth in a critical way: the user does not interact with the consumption device at all for authentication. They receive a push notification or similar alert on their personal device and approve the request there.
CIBA Flow Overview
Here is how a typical CIBA transaction works in a financial services context:
-
Initiation: A bank teller enters the customer’s account number into their terminal. The terminal (acting as the Relying Party) sends a backchannel authentication request to Keycloak, identifying the customer by their
login_hint(email, phone number, or username). -
Notification: Keycloak sends a notification to the customer’s registered authentication device. This could be a push notification to a banking app, an SMS, or an email.
-
User Approval: The customer sees the request on their phone: “Bank Teller #42 is requesting access to your account. Approve?” The customer authenticates (e.g., with biometrics or PIN) and approves.
-
Token Delivery: Keycloak issues an access token to the consumption device (the teller’s terminal). The teller can now proceed with the transaction.
The key security property is that the customer authenticates on their own trusted device, not on the teller’s terminal. This prevents credential theft at the point of service.
CIBA Modes
CIBA supports three modes for how the consumption device receives the token after the user approves:
Poll Mode
The consumption device polls Keycloak’s token endpoint at regular intervals until the user approves, denies, or the request expires. This is the simplest mode to implement and the one Keycloak supports most completely.
Ping Mode
Keycloak sends a notification (webhook) to the consumption device’s registered callback URL when the user acts. The device then retrieves the token from the token endpoint. This is more efficient than polling but requires the consumption device to expose an HTTP endpoint.
Push Mode
Keycloak pushes the token directly to the consumption device’s callback URL after user approval. This is the most efficient but requires strong security on the callback endpoint.
Keycloak supports poll and ping modes. Poll mode is the most commonly used in production.
Keycloak Configuration
Step 1: Create a CIBA-Enabled Client
- Navigate to your realm in the Keycloak Admin Console.
- Go to Clients and click Create client.
- Configure:
- Client ID:
banking-teller-app - Client Protocol:
openid-connect
- Client ID:
- On the capability screen:
- Client Authentication: ON (confidential client)
- OIDC CIBA Grant: ON
- In the Advanced Settings:
- CIBA Backchannel Token Delivery Mode:
poll(orping) - If using ping mode, set the Backchannel Client Notification Endpoint to your callback URL.
- CIBA Backchannel Token Delivery Mode:
Step 2: Configure the CIBA Policy
- Go to Authentication > CIBA Policy (in Keycloak 22+, this is under Authentication > Policies > CIBA).
- Configure:
- Backchannel Token Delivery Mode:
poll - Expires In: How long the authentication request is valid (default: 120 seconds).
- Interval: Polling interval in seconds (default: 5).
- Auth Requested User Hint:
login_hint(identifies the user by username, email, or phone).
- Backchannel Token Delivery Mode:
Step 3: Configure the Authentication Channel Provider
The authentication channel provider is responsible for notifying the user on their authentication device. Keycloak includes a test provider for development, but production use requires a custom provider.
For development and testing:
- Go to Authentication > CIBA Policy.
- Set Authentication Channel Provider:
ciba-http-auth-channel(the built-in HTTP-based provider).
The built-in provider sends an HTTP request to a configurable endpoint when authentication is needed. In production, you would implement a custom Authentication Channel SPI that integrates with your push notification service (Firebase Cloud Messaging, APNs, etc.).
Step 4: Configure the CIBA Authentication Flow
- Go to Authentication > Flows.
- Find the CIBA flow (created automatically when CIBA is enabled).
- The default flow includes:
- CIBA – Validate Request: Validates the backchannel authentication request.
- CIBA – User Resolver: Resolves the user from the
login_hint.
You can customize this flow to add additional validation steps, such as verifying that the requesting client is authorized to authenticate the specified user.
CIBA API Walkthrough
Authentication Request
The consumption device sends a backchannel authentication request:
curl -X POST "https://keycloak.example.com/realms/my-realm/protocol/openid-connect/ext/ciba/auth"
-H "Content-Type: application/x-www-form-urlencoded"
-u "banking-teller-app:client-secret"
-d "scope=openid"
-d "[email protected]"
-d "binding_message=Transaction #12345"
Parameters:
login_hint: Identifies the user (email, username, or phone number).binding_message: A human-readable message displayed to the user on their authentication device (e.g., “Transaction #12345 – Transfer $500 to Account XYZ”).scope: Requested scopes.
Response:
{
"auth_req_id": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0...",
"expires_in": 120,
"interval": 5
}
The auth_req_id is used to poll for the result.
Polling for the Token
The consumption device polls the token endpoint:
curl -X POST "https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token"
-H "Content-Type: application/x-www-form-urlencoded"
-u "banking-teller-app:client-secret"
-d "grant_type=urn:openid:params:grant-type:ciba"
-d "auth_req_id=eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0..."
Authorization pending (user has not responded):
{
"error": "authorization_pending",
"error_description": "The authorization request is still pending"
}
Access denied (user denied the request):
{
"error": "access_denied",
"error_description": "The end-user denied the authorization request"
}
Success (user approved):
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 300,
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"scope": "openid",
"id_token": "eyJhbGciOiJSUzI1NiIs..."
}
You can inspect the returned tokens with the JWT Token Analyzer.
Java Client Implementation
Here is a Java client for the consumption device side:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
public class CIBAClient {
private static final String KEYCLOAK_URL = "https://keycloak.example.com";
private static final String REALM = "my-realm";
private static final String CLIENT_ID = "banking-teller-app";
private static final String CLIENT_SECRET = "client-secret";
private final HttpClient httpClient = HttpClient.newHttpClient();
private final ObjectMapper objectMapper = new ObjectMapper();
public String authenticate(String userHint, String bindingMessage) throws Exception {
// Step 1: Send backchannel authentication request
String cibaEndpoint = KEYCLOAK_URL + "/realms/" + REALM
+ "/protocol/openid-connect/ext/ciba/auth";
String body = "scope=openid"
+ "&login_hint=" + userHint
+ "&binding_message=" + bindingMessage;
HttpRequest authRequest = HttpRequest.newBuilder()
.uri(URI.create(cibaEndpoint))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Basic " + encodeCredentials())
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> authResponse = httpClient.send(
authRequest, HttpResponse.BodyHandlers.ofString());
if (authResponse.statusCode() != 200) {
throw new RuntimeException("CIBA auth request failed: " + authResponse.body());
}
JsonNode authData = objectMapper.readTree(authResponse.body());
String authReqId = authData.get("auth_req_id").asText();
int expiresIn = authData.get("expires_in").asInt();
int interval = authData.has("interval") ? authData.get("interval").asInt() : 5;
// Step 2: Poll for the token
String tokenEndpoint = KEYCLOAK_URL + "/realms/" + REALM
+ "/protocol/openid-connect/token";
long deadline = System.currentTimeMillis() + (expiresIn * 1000L);
while (System.currentTimeMillis() < deadline) {
Thread.sleep(interval * 1000L);
String tokenBody = "grant_type=urn:openid:params:grant-type:ciba"
+ "&auth_req_id=" + authReqId;
HttpRequest tokenRequest = HttpRequest.newBuilder()
.uri(URI.create(tokenEndpoint))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Basic " + encodeCredentials())
.POST(HttpRequest.BodyPublishers.ofString(tokenBody))
.build();
HttpResponse<String> tokenResponse = httpClient.send(
tokenRequest, HttpResponse.BodyHandlers.ofString());
if (tokenResponse.statusCode() == 200) {
JsonNode tokenData = objectMapper.readTree(tokenResponse.body());
return tokenData.get("access_token").asText();
}
JsonNode errorData = objectMapper.readTree(tokenResponse.body());
String error = errorData.get("error").asText();
switch (error) {
case "authorization_pending":
continue;
case "slow_down":
interval += 5;
continue;
case "access_denied":
throw new RuntimeException("User denied the authentication request");
case "expired_token":
throw new RuntimeException("Authentication request expired");
default:
throw new RuntimeException("Unexpected error: " + error);
}
}
throw new RuntimeException("Authentication request timed out");
}
private String encodeCredentials() {
return Base64.getEncoder().encodeToString(
(CLIENT_ID + ":" + CLIENT_SECRET).getBytes());
}
}
Usage:
CIBAClient cibaClient = new CIBAClient();
try {
String accessToken = cibaClient.authenticate(
"[email protected]",
"Transaction #12345 - Transfer $500"
);
System.out.println("Authentication successful. Token: " + accessToken);
// Proceed with the transaction using the access token
} catch (Exception e) {
System.err.println("Authentication failed: " + e.getMessage());
}
Custom Authentication Channel SPI
For production use, you need a custom Authentication Channel Provider that notifies users on their device. Here is a skeleton implementation:
import org.keycloak.models.KeycloakSession;
import org.keycloak.services.ciba.CIBAAuthenticationRequest;
public class PushNotificationAuthChannel implements CIBAAuthenticationChannelProvider {
private final KeycloakSession session;
public PushNotificationAuthChannel(KeycloakSession session) {
this.session = session;
}
@Override
public CIBAAuthenticationChannelResponse requestAuthentication(
CIBAAuthenticationRequest request, String infoUsedByAuthenticator) {
String userId = request.getUser().getId();
String bindingMessage = request.getBindingMessage();
String authResultId = request.getAuthResultId();
// Look up the user's registered device token
String deviceToken = getUserDeviceToken(userId);
if (deviceToken == null) {
return CIBAAuthenticationChannelResponse.CANCELLED;
}
// Send push notification via your notification service
boolean sent = sendPushNotification(
deviceToken,
"Authentication Request",
bindingMessage,
authResultId
);
if (sent) {
return CIBAAuthenticationChannelResponse.SUCCEED;
} else {
return CIBAAuthenticationChannelResponse.CANCELLED;
}
}
private String getUserDeviceToken(String userId) {
// Retrieve from user attributes or a device registration service
return session.users()
.getUserById(session.getContext().getRealm(), userId)
.getFirstAttribute("push_device_token");
}
private boolean sendPushNotification(
String deviceToken, String title, String body, String authResultId) {
// Integrate with FCM, APNs, or another push notification service
// The notification payload should include the authResultId
// so the mobile app can call back to Keycloak to approve/deny
return true; // placeholder
}
@Override
public void close() {}
}
The mobile banking app would receive the push notification, display the binding message to the user, and call a Keycloak endpoint to approve or deny the request.
FAPI Compliance
CIBA is a core component of the Financial-Grade API (FAPI) specification. Keycloak supports FAPI-CIBA, which adds security requirements on top of standard CIBA:
- Signed authentication requests: The backchannel auth request must be a signed JWT (request object).
- MTLS or private_key_jwt client authentication: Basic authentication is not sufficient.
- Binding message required: The
binding_messageparameter must be present. - Short token lifetimes: Strict access token lifespan requirements.
To enable FAPI-CIBA in Keycloak:
- Go to Realm Settings > Client Policies.
- Create or assign a client profile with the FAPI-CIBA policy.
For more on securing your Keycloak deployment for financial services, see the security page and the guide to Keycloak auditing best practices.
Use Cases Beyond Banking
While financial services is the primary driver for CIBA adoption, the pattern applies to several other scenarios:
Healthcare
A nurse at a medication dispensing station needs authorization from the prescribing physician. CIBA sends an approval request to the physician’s phone, who reviews the medication details and approves dispensing — all without the physician needing to be physically present at the station.
Call Centers
A call center agent verifies a customer’s identity by initiating a CIBA request. The customer receives a push notification on their registered phone, confirms their identity with biometrics, and the agent proceeds with the support case.
Point of Sale
A high-value transaction at a retail terminal triggers a CIBA request to the customer’s banking app for additional authorization, similar to 3D Secure but using the more flexible CIBA flow.
Administrative Approval
An employee requests access to a sensitive system. CIBA sends an approval request to their manager’s phone. The manager reviews and approves, and the employee’s session is elevated with the appropriate RBAC roles.
Security Considerations
-
Binding message security: Always include a meaningful
binding_messagethat describes the transaction. This helps the user verify they are approving the correct action and prevents approval of fraudulent requests. -
Login hint validation: Restrict which users a client can request authentication for. Use client policies to limit the scope of CIBA requests.
-
Short request expiry: Keep the
expires_invalue short (60-120 seconds for financial transactions) to limit the window for social engineering attacks. -
Rate limiting: Limit the number of concurrent CIBA requests per user to prevent notification fatigue attacks.
-
Audit logging: Log all CIBA requests, approvals, and denials. See audit logs for monitoring capabilities.
Wrapping Up
CIBA is a powerful but specialized authentication flow designed for scenarios where the person authenticating is not the person operating the consumption device. It is standards-based, supported by Keycloak, and essential for FAPI-compliant financial services applications.
The implementation requires a custom Authentication Channel Provider for the notification mechanism, but the OAuth flow itself (authentication request, polling, token delivery) is straightforward. For most deployments, poll mode provides the right balance of simplicity and reliability.
If you are building FAPI-compliant financial services applications and want managed Keycloak hosting with CIBA support, Skycloak offers fully managed Keycloak instances with enterprise SLAs, expert support, and compliance-ready configurations.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.