Client-Initiated Back-channel Authentication (CIBA) with OpenID Connect

The Client-Initiated Back-channel Authentication (CIBA) flow lets people use their mobile device to authenticate and approve transactions in their everyday life, be it at a PoS, at a counter or in a phone call with a service agent.

In contrast to the common OAuth flows, such as the authorization_code flow, the client requesting the tokens and the application where the user gets authenticated and consents the transaction reside on separate devices in the CIBA flow. The CIBA client thus cannot use a front-channel redirection to the IdP server. Instead, the CIBA client needs to make a back-channel request to the IdP server, which will then invoke the instance of the IdP's user authN / consent application installed on the user's device. In order for the IdP server to identify which mobile app instance needs to be invoked, the client is expected to include a hint, as either login_hint_token, id_token_hint or login_hint in the CIBA request. At a PoS terminal or counter this can be special token generated by the client and passed to the client via NFC. In a call centre scenario this can be the caller ID. Depending on the security context the client may also need to include a user_code, to prevent unsolicited CIBA requests from popping up on a user's device.

CIBA was originally designed as an OpenID Connect extension, but it can also be used in plain OAuth 2.0 scenarios where only authorisation is needed.

Support for CIBA was added in version 8.31 of the SDK.

CIBA client registration

Example registration for a CIBA client where the tokens are going to be delivered via the push mode, with an HTTP POST to the client, after the back-channel authorisation is complete. The client authenticates with the self_signed_tls_client_auth (mTLS) method, which enables issue of client-certificate bound access tokens.

import java.net.*;
import java.util.*;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.auth.*;
import com.nimbusds.oauth2.sdk.ciba.*;
import com.nimbusds.oauth2.sdk.client.*;
import com.nimbusds.oauth2.sdk.http.*;
import com.nimbusds.oauth2.sdk.token.*;
import com.nimbusds.openid.connect.sdk.rp.*;

// The OP / AS client registration endpoint
URI clientRegEndpoint = URI.create("https://demo.c2id.com/clients/");

// The initial registration access token
BearerAccessToken clientRegToken = new BearerAccessToken("...");

// Prepare the client metadata for CIBA with push token delivery,
// the client is going to authenticate with a self-signed certificate (mTLS)
OIDCClientMetadata clientMetadata = new OIDCClientMetadata();
clientMetadata.setGrantTypes(Collections.singleton(GrantType.CIBA));
clientMetadata.setJWKSetURI(URI.create("https://client.example.com/jwks.json"));
clientMetadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH);
clientMetadata.setBackChannelTokenDeliveryMode(BackChannelTokenDeliveryMode.PUSH);
clientMetadata.setBackChannelClientNotificationEndpoint(URI.create("https://client.example.com/ciba"));
clientMetadata.setSupportsBackChannelUserCodeParam(true);
clientMetadata.setScope(new Scope("openid", "email", "profile", "phone"));

// Send the registration request
HTTPResponse httpResponse = new OIDCClientRegistrationRequest(
    clientRegEndpoint, clientMetadata, clientRegToken)
    .toHTTPRequest()
    .send();

ClientRegistrationResponse regResponse = OIDCClientRegistrationResponseParser.parse(httpResponse);

if (! regResponse.indicatesSuccess()) {
    // Registration failed
    System.err.println(regResponse.toErrorResponse().getErrorObject());
    return;
}

// Successful registration
OIDCClientInformation clientInfo = (OIDCClientInformation) regResponse.toSuccessResponse().getClientInformation();
System.out.println("Client ID: " + clientInfo.getID());
System.out.println("Client registration token: " + clientInfo.getRegistrationAccessToken());
System.out.println("Client metadata: " + clientInfo.getOIDCMetadata());

CIBA request

Example CIBA request for an ID token. The identity of the user is hinted by means of a caller ID, which can occur in scenarios where a service agent needs to authenticate the person who is calling in. A user_code to prevent unsolicited CIBA requests it not needed since the agent is typically authenticated and trusted by the client software.

import java.net.*;
import java.util.*;
import javax.net.ssl.*;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.auth.*;
import com.nimbusds.oauth2.sdk.ciba.*;
import com.nimbusds.oauth2.sdk.http.*;
import com.nimbusds.oauth2.sdk.token.*;
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.openid.connect.sdk.rp.*;

// The OP / AS endpoint for back-channel authN and authZ requests
URI cibaEndpoint = URI.create("https://demo.c2id.com/ciba");

// Custom SSL factory for mTLS with the client's certificate
SSLSocketFactory sslSocketFactory = ...;
ClientAuthentication clientAuth = new SelfSignedTLSClientAuthentication(clientInfo.getID(), sslSocketFactory);

// Generate a bearer token to authorise the callback with the token delivery
// at the client notification endpoint
BearerAccessToken clientNotifyToken = new BearerAccessToken();

// Make a CIBA request for an ID token, using a caller ID as login hint
HTTPResponse httpResponse1 = new CIBARequest.Builder(
    clientAuth,
    new Scope(OIDCScopeValue.OPENID))
    .endpointURI(cibaEndpoint)
    .clientNotificationToken(clientNotifyToken)
    .loginHint("+1-541-754-3010")
    .build()
    .toHTTPRequest()
    .send();

CIBAResponse cibaResponse = CIBAResponse.parse(httpResponse);

if (! cibaResponse.indicatesSuccess()) {
    // CIBA request failed
    System.err.println(cibaResponse.toErrorResponse().getErrorObject());
    return;
}

// Get the request acknowledgement
CIBARequestAcknowledgement acknowledgement = cibaResponse.toRequestAcknowledgement();
AuthRequestID cibaRequestID = acknowledgement.getAuthRequestID();
int expiresInSeconds = acknowledgement.getExpiresIn();

// Store the request context (with the client callback token
// and other necessary details), keyed by the auth_req_id and
// set to expire according to the received expires_in value in
// seconds
// ...

Processing callbacks

Example code for processing a callback at the client notification endpoint, assuming the client is registered for the push token delivery method. The IdP is going to POST the issued tokens (or an error message if authentication failed) directly to the client. The client uses the auth_req_id to retrieve the original request context.

import com.nimbusds.jwt.*;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.auth.*;
import com.nimbusds.oauth2.sdk.ciba.*;
import com.nimbusds.oauth2.sdk.http.*;
import com.nimbusds.oauth2.sdk.token.*;
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.openid.connect.sdk.rp.*;

// Get the HTTP request at the client notification endpoint endpoint
HTTPRequest httpRequest = ...;

// Parse the callback
CIBATokenDelivery tokenDelivery = CIBATokenDelivery.parse(httpRequest);

cibaRequestID = acknowledgement.getAuthRequestID();

// Get the request context previously stored by auth_req_id, if expired abort
// ...

// Verify the callback access token with the stored one
if (! clientNotifyToken.equals(tokenDelivery.getAccessToken())) {
    System.err.println("Invalid access token");
    return;
}

if (! tokenDelivery.indicatesSuccess()) {
    // The IdP posted an error
    System.err.println(tokenDelivery.toErrorDelivery().getErrorObject());
    return;
}

// Get the delivered token(s)
CIBATokenDelivery successfulTokenDelivery = tokenDelivery.toTokenDelivery();
JWT idToken = successfulTokenDelivery.getOIDCTokens().getIDToken();

// Verify the ID token
// ...