How to implement CIBA with Authlete

1. Overview

This document describes how to build an authorization server that supports CIBA (Client initiated Backchannel Authentication).

1.1. Preface

It is essential to have an overview of CIBA in advance. Please refer to the following documents as necessary.

It is also required to understand OAuth 2.0, OpenID Connect and Authlete. Please refer to the following documents as necessary.

1.2. System Architecture

We will build an authorization server backed by Authlete as is shown in the diagram below.

CIBA system architecture with Authlete

1.3. Sample Implementation

The sample implementation introduced in this article is only for grasping how to implement CIBA using Authlete. Some examples are not optimized intentionally to help you understand the processing flows. For example, the sample implementation in this article has the following restrictions:

  • The authorization server in the sample implementation only supports CIBA flow, not OAuth 2.0 or OpenID Connect.

  • The backchannel authentication endpoint in this sample implementation does not support optional parameters, such as login_hint_token, id_token_hint, and acrs.

  • The backchannel authentication endpoint and token endpoint in this sample implementation only support client_secret_basic as the client authentication method.

Please refer to the following source codes for more sophisticated and practical implementations.

1.4. Note

2. Implementation

In the CIBA flow, backchannel authentication endpoint and token endpoint play essential roles. Here, we explain how to implement the endpoints.

2.1. CIBA flow using Authlete

2.1.1 Backchannel Authentication Endpoint

The diagram below explains the processing flow in each endpoint when implementing CIBA with Authlete. Please note that the diagram omits error handlings.

Backchannel Authentication Endpoint using Authlete APIs

The backchannel authentication endpoint uses following Authlete APIs.

  • /api/backchannel/authentication API
  • /api/backchannel/authentication/issue API
  • /api/backchannel/authentication/complete API

In the real situation, the endpoint also uses /api/backchannel/authentication/fail API for error handling. Please refer to Identifying an end-user using hint for details.

2.1.2. Token Endpoint

The processing flow of the token endpoint is simple, as is the case with OAuth 2.0. The authorization server sends a token request from a client to /api/auth/token API and sends back a token response from Authlete API to the client.

Token Endpoint using Authlete API

2.2. Implementing a Backchannel Authentication Endpoint

The backchannel authentication endpoint handles several requests and responses. To implement the endpoint, you must understand the basics of the endpoint first.

✔︎ Authentication Request

The request that is sent to the backchannel authentication endpoint is called an “authentication request.” The spec requires that the HTTP method and Content-Type be POST and application/x-www-form-urlencoded respectively.

✔︎ Client Authentication

The backchannel authentication endpoint must authenticate a client. As is explained in this article, CIBA allows several client authentication methods; however, the sample implementation here supports only client_secret_basic for simplification. Please note that the client authentication method at the backchannel authentication endpoint must be the same as that at the token endpoint.

We use the following code as a template to implement the backchannel authentication endpoint in the authorization server using Java (JAX-RS).

@Path("/api/backchannel/authentication")
public class BackchannelAuthenticationEndpoint
{
    /**
     * Backchanel Authentication Endpoint
     * The endpoint accepts a request with POST and application/x-www-form-urlencoded.
     * Also, in this artichle, we explain the case of the client_secret_basic only.
     *
     * @param authorization
     *         The value of Authorization header in the backchannel authentication request.
     *
     * @param parameters
     *         Request parameters of a backchannel authentication request.
     */
    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response post(@HeaderParam(HttpHeaders.AUTHORIZATION) String authorization, MultivaluedMap<String, String> parameters)
    {
        try
        {
            // main process
            return doProcess(authorization, parameters);
        }
        catch (WebApplicationException e)
        {
            // known errors
            return e.getResponse();
        }
        catch (Throwable t)
        {
            // unknown errors
            return ResponseUtil.internalServerError("Unknown error occurred.");
        }
    }

    private Response doProcess(String authorization, MultivaluedMap<String, String> parameters)
    {
        // main process implementation
    }
}

The following sections will add processes into the template and implement the endpoint.

2.2.1. Verifying an authentication request

The first thing that the backchannel authentication endpoint must do is to verify the authentication request. The steps will be as follows:

  1. Extract information related to client authentication from the authorization parameter.
  2. Call /backchannel/authentication API with the extracted information and parameters parameter, and delegate the verification process to Authlete.
  3. Follow the action parameter’s value in a response from /backchannel/authentication API.

And the codes for the processes are as follows.

private Response doProcess(String authorization, MultivaluedMap<String, String> parameters)
{
    // Parse the Authorization header and extract client credentials.
    BasicCredentials credentials = BasicCredentials.parse(authorization);

    // Extract the client ID and the client secret from the client credentials.
    String clientId     = credentials == null ? null : credentials.getUserId();
    String clientSecret = credentials == null ? null : credentials.getPassword();

    // Call Authlete's /api/backchannel/authentication API to delegate the
    // verification process of the request to Authlete.
    BackchannelAuthenticationResponse response =
        callBackchannelAuthenticationApi(parameters, clientId, clientSecret);

    // The 'action' parameter in the response denotes the next action that
    // this authorization server should take.
    BackchannelAuthenticationResponse.Action action = response.getAction();

    // The content of the response that should be returned to the client.
    // The content varies depending on the 'action'.
    String content = response.getResponseContent();

    // Process according to the 'action'.
    switch (action)
    {
        case INTERNAL_SERVER_ERROR:
            // The API call was wrong or an error occurred on Authlete side.
            // 500 Internal Server Error
            return ResponseUtil.internalServerError(content);

        case UNAUTHORIZED:
            // The client authentication failed. For example, the client ID was wrong.
            // 401 Unauthorized
            return ResponseUtil.unauthorized(content, "Basic realm=\"backchannel/authentication\"");

        case BAD_REQUEST:
            // The request was wrong. For example, mandatory request parameters are not included.
            // 400 Bad Request
            return ResponseUtil.badRequest(content);

        case USER_IDENTIFICATION:
            // The request is valid. In this case, handleUserIdentification() handles
            // the remaining steps.
            return handleUserIdentification(response);

        default:
            // Other cases. This should not happen.
            // 500 Internal Server Error
            return ResponseUtil.internalServerError("Unknown action returned from /api/backchannel/authentication API.");
    }
}

If the request has no problem, the /backchannel/authentication API sends a response with action=USER_IDENTIFICATION. In this case of USER_IDENTIFICATION, the authorization server will call the handleUserIdentification() method to handle the aunthentication request. Other values in the action parameter indicate an error, so the authorization server generates an error response and sends it to the client.

2.2.2. Identifying an end-user using a hint

When the /backchannel/authentication API returns a response with action=USER_IDENTIFICATION, the authorization server must identify the end-user using a hint in the response. The hint is either login_hint, login_hint_token, or id_token_hint. For simplification, the authorization server in this sample impementation supports login_hint only. The value can be either an end-user subject, email address or telephone number in the following sample.

In identifying the end-user successfully using the login_hint, the authorization server will do the following processes. Otherwise, the authorization server calls /backchannel/authentication/fail API, generates an error response, and returns it to the client.

The following codes include processes taking the discussion above into account.

private Response handleUserIdentification(BackchannelAuthenticationResponse baRes)
{
    // Identify the end-user using the hint included in the request.
    User user = identifyUserByHint(baRes);
}

private User identifyUserByHint(BackchannelAuthenticationResponse baRes)
{
    // The type of the hint included in the request. The value is
    // either LOGIN_HINT, LOGIN_HINT_TOKEN, or ID_TOKEN_HINT.
    UserIdentificationHintType hintType = baRes.getHintType();

    // The hint included in the request.
    String hint = baRes.getHint();

    // Find the end-user using the hint.
    User user = getUserByHint(hintType, hint);

    if (user != null)
    {
        // The end-user was found.
        return user;
    }

    // No end-user was found with the hint. In this case, call Authlete's
    // /api/backchannel/authentication/fail API in order to generate an
    // appropriate response and throw it as an exception. Note that the
    // exception will be caught by the post() method.
    throw backchannelAuthenticationFail(baRes.getTicket(), Reason.UNKNOWN_USER_ID);
}

private User getUserByHint(UserIdentificationHintType hintType, String hint)
{
    if (hintType != UserIdentificationHintType.LOGIN_HINT)
    {
        // This implementation supports login_hint only, so other hint
        // types are ignored.
        return null;
    }

    // Find the end-user using the login_hint. The implementation below
    // assumes that either an end-user's subject, email address or phone
    // number is used as the value of the login_hint parameter.

    // First, look up the end-user assuming that the login_hint is an
    // end-user's subject.
    User user = UserDao.getBySubject(hint);

    if (user != null)
    {
        // The end-user was found.
        return user;
    }

    // Next, assume the login_hint is an end-user's email address.
    user = UserDao.getByEmail(hint);

    if (user != null)
    {
        // The end-user was found.
        return user;
    }

    // Finally, assume the login_hint is an end-user's phone number.
    return UserDao.getByPhoneNumber(hint);
}

private WebApplicationException backchannelAuthenticationFail(String ticket, BackchannelAuthenticationFailRequest.Reason reason)
{
    // Call Authlete's /api/backchannel/authentication API to generate an
    // appropriate response.
    Response response = createBackchannelAuthenticationFailResponse(ticket, reason);

    // Generate an exception that respresents the response.
    return new WebApplicationException(response);
}

private Response createBackchannelAuthenticationFailResponse(String ticket, BackchannelAuthenticationFailRequest.Reason reason)
{
    // Call Authlete's /api/backchannel/authentication/fail API.
    BackchannelAuthenticationFailResponse response = callBackchannelAuthenticationFail(ticket, reason);

    // The 'action' parameter in the response denotes the next action that
    // this authorization server should take.
    BackchannelAuthenticationFailResponse.Action action = response.getAction();

    // The content of the response that should be returned to the client.
    // The content varies depending on the 'action'.
    String content = response.getResponseContent();

    // Process according to the 'action'.
    switch (action)
    {
        case INTERNAL_SERVER_ERROR:
            // 500 Internal Server Error
            return ResponseUtil.internalServerError(content);

        case FORBIDDEN:
            // 403 Forbidden
            return ResponseUtil.forbidden(content);

        case BAD_REQUEST:
            // 400 Bad Request
            return ResponseUtil.badRequest(content);

        default:
            // Other cases. This should not happen.
            // 500 Internal Server Error
            return ResponseUtil.internalServerError("Unknown action returned from /api/backchannel/authentication/fail.");
    }
}

2.2.3. Verifying Request Parameters

On top of the end-user identification, the authorization server must verify request parameters in the handleUserIdentification() method, as is described in the following sections.

2.2.3.1. login_hint_token

When using login_hint_token as a hint, the authorization server must verify its expiration; however, in this article, we will not implement the verification for simplification. Please check the java-oauth-server and authlete-java-jaxrs for details.

2.2.3.2. user_code

2.2.3.2.1. The concept of user_code

In the sample implementation above, the authorization server expects an end-user subject, email address, and telephone number to identify the end-user. However, a third person may be able to guess these values.

The user_code is a parameter to avoid malicious third parties from sending fraud authentication requests. The spec defines the user_code as follows:

user_code OPTIONAL. A secret code, such as password or pin, known only to the user but verifiable by the OP. The code is used to authorize sending an authentication request to user’s authentication device. This parameter should only be present if client registration parameter backchannel_user_code_parameter indicates support for user code.

The malicious clients cannot guess the user_code and thus send a valid authentication request. In other words, an authorization server that verifies user_code can reject the malicious requests.

2.2.3.2.2. Verifying a user_code

Let’s check the verification of a user_code.

Authlete API server will do the following process when an authorization server calls the /backchannel/authentication API.

if (service.isBackchannelUserCodeParameterSupported() && client.isBcUserCodeRequired())
{
    if (isUserCodeContainedInRequestParameters() == false)
    {
        // returns an error response with action=BAD_REQUEST to the requesting authorization server
        throw missingUserCodeError();
    }
}

When the values of both the backchannelUserCodeParameterSupported metadata of the service that corresponds to the authorization server and the bcUserCodeRequired metadata of the client are true, Authlete will check whether an authentication request contains user_code. Authlete API returns an error response with action=BAD_REQUEST to the authorization server if the authorization server sends an authentication request that does not contain user_code. In this case, the authorization server does the process for the case of BAD_REQUEST in the sample implementation above.

In this article, we will implement a handleUserIdentification() method, which handles the case for USER_IDENTIFICATION. For USER_IDENTIFICATION scenario, we have to implement the method anticipating one of the cases listed below.

✔︎ Case 1 A user_code was found in the authentication request. (= In the case of the pseudo code above, the conditions of service.isBackchannelUserCodeParameterSupported() && client.isBcUserCodeRequired() and isUserCodeContainedInRequestParameters() were satisified.)

✔︎ Case 2 The Authlete API does not check the user_code. (= In the case of the pseudo code above, the condition of service.isBackchannelUserCodeParameterSupported() && client.isBcUserCodeRequired() was not satisified.)

In the case 1, Authlete API guarantees that the authentication request contains a user_code. Thus, the authorization server must check whether the value of user_code is valid. We can skip the check for the case 2 because an authentication request does not have to contain the user_code parameters.

Here is a sample implementation of handleUserIdentification().

private Response handleUserIdentification(BackchannelAuthenticationResponse baRes)
{
    ...

    // Check the user code.
    checkUserCode(baRes, user);
}

private void checkUserCode(BackchannelAuthenticationResponse baRes, User user)
{
    // If a user code is not mandatory (= not Case 1)
    //
    // Note that isUserCodeRequired() method returns true when both the
    // backchannelUserCodeParameterSupported metadata of the service (the
    // authorization server) and the bcUserCodeRequired metadata of the
    // client are true. In other cases, the method returns false.
    if (baRes.isUserCodeRequired() == false)
    {
        // Nothing is checked here.
        return;
    }

    // The value of the user_code in the request.
    String userCodeInRequest = baRes.getUserCode();

    // The valid user code.
    String userCode = user.getCode();

    // If they match.
    if (userCodeInRequest.equals(userCode))
    {
        // The request includes the valid user code.
        return;
    }

    // The user code included in the request is wrong. In this case,
    // call Authlete's /api/backchannel/authentication/fail API in
    // order to generate an appropriate response and then throw it
    // as an exception. Note that the exception will be caught by
    // the post() method later.
    throw backchannelAuthenticationFail(baRes.getTicket(), Reason.INVALID_USER_CODE);
}

2.2.3.3. Binding Message

2.2.3.3.1. The concept of Binding Message

When a client starts the CIBA flow, an end-user will be asked to authorize on their authentication device. When authorizing, how does the end-user confirm that the client asking authorization via the device is the one the end-user is willing to authorize?

The binding_message parameter is introduced to solve the issue above. The diagram below is a use case of using the binding_message parameter.

Use case: "CIBA" pay

  1. An end-user asks a store to pay using a CIBA-backed payment service.

  2. The store shows a binding message, sRj89xCg, in the POS terminal (= client app).

  3. The POS terminal sends an authentication request, including the binding message, to the authorization server of the payment service.

  4. The authorization server starts to interact with an authentication device of the end-user and sends the binding message to the device.

  5. The authentication device asks the end-user to authorize the client, showing the binding message of sRj89xCg.

  6. The end-user confirms the client using the binding message and authorize the transaction.

Verifying the Binding Message

Then, how does the authorization server verify the binding message? Below is a quote from the spec. The spec does not define MUST requirements for the binding message and just tells that the binding_message value SHOULD be relatively short and use a limited set of plain text characters.

binding_message OPTIONAL. A human readable identifier or message intended to be displayed on both the consumption device and the authentication device to interlock them together for the transaction by way of a visual cue for the end-user. This interlocking message enables the end-user to ensure that the action taken on the authentication device is related to the request initiated by the consumption device. The value SHOULD contain something that enables the end-user to reliably discern that the transaction is related across the consumption device and the authentication device, such as a random value of reasonable entropy (e.g. a transactional approval code). Because the various devices involved may have limited display abilities and the message is intending for visual inspection by the end-user, the binding_message value SHOULD be relatively short and use a limited set of plain text characters. The invalid_binding_message defined in Section 13 is used in the case that it is necessary to inform the Client that the provided binding_message is unacceptable.

Here, we add a handleUserIdentification() method to check the length of the binding message.

/**
 * The maximum number of characters in a binding message.
 */
private static String MAX_BINDING_MESSAGE_LENGTH = 100;

...

private Response handleUserIdentification(BackchannelAuthenticationResponse baRes)
{
    ...

    // Check the binding message.
    checkBindingMessage(baRes);
}

private void checkBindingMessage(BackchannelAuthenticationResponse baRes)
{
    // The binding message included in the request.
    String bindingMessage = baRes.getBindingMessage();

    // If no binding message is available.
    if (bindingMessage == null || bindingMessage.length() == 0)
    {
        // Nothing is checked here.
        return;
    }

    // If the length of the binding message exceeeds the upper limit.
    if (bindingMessage.length() > MAX_BINDING_MESSAGE_LENGTH)
    {
        // The request includes an invalid binding message (whose length
        // exceeds the upper limit). In this case, call Authlete's
        // /api/backchannel/authentication/fail API in order to generate
        // an appropriate response and throw it as an exception. Note
        // that the exception will be caught later by the post() method.
        throw backchannelAuthenticationFail(baRes.getTicket(), Reason.INVALID_BINDING_MESSAGE);
    }
}

###2.2.4 Issuing an auth_req_id

After the authorization server finished all necessary verifications, it issues an auth_req_id using /backchannel/authentication/issue API.

private Response handleUserIdentification(BackchannelAuthenticationResponse baRes)
{
    ...

    // Issue an auth_req_id.
    return issueAuthReqId(baRes);
}

private Response issueAuthReqId(BackchannelAuthenticationResponse baRes)
{
    // Call Authlete's /api/backchannel/authentication/issue API.
    // The API issues an 'auth_req_id'.
    BackchannelAuthenticationIssueResponse baiRes = callBackchannelAuthenticationIssue(baRes.getTicket());

    // The 'action' parameter in the response denotes the next action
    // that this authorization server should take.
    BackchannelAuthenticationIssueResponse.Action action = baiRes.getAction();

    // The content of the response that should be returned to the client.
    // The content varies depending on the 'action'.
    String content = baiRes.getResponseContent();

    // Process according to the 'action'.
    switch (action)
    {
        case INTERNAL_SERVER_ERROR:
            // The API call was wrong or an error occurred on Authlete side.
            // 500 Internal Server Error
            return ResponseUtil.internalServerError(content);

        case INVALID_TICKET:
            // The ticket included in the API call was invalid.
            // 500 Internal Server Error
            return ResponseUtil.internalServerError(content);

        case OK:
            // Start a background process.
            startCommunicationWithAuthenticationDevice(user, baRes);

            // 200 OK with an 'auth_req_id'.
            return ResponseUtil.ok(content);

        default:
            // Other cases. This should not happen.
            // 500 Internal Server Error
            return ResponseUtil.internalServerError("Unknown action returned from /api/backchannel/authentication/issue API.");
    }
}

When the authorization server receives a response with action=OK, the server starts a background process by calling the startCommunicationWithAuthenticationDevice() method, and sends a 200 OK response to the client. Please note that the response to the client contains the auth_req_id issued by the Authlete API.

###2.2.5. Background Processes

The authorization server starts the background process by calling the startCommunicationWithAuthenticationDevice() method before sending the auth_req_id to the client. In this background process, the authorization server will do the following tasks.

  1. Communicates with an authentication device to ask the end-user whether to authorize the request from the client.
  2. Calls /backchannel/authentication/complete API and tells Authlete the result of the communication between the authorization server and the authentication device.
  3. Follows the action in the response from the /backchannel/authentication/complete API.

Please note that the content of the response from the /backchannel/authentication/complete API varies based on the backchannel token delivery mode. To be concrete, in successful cases, the API returns action=OK in the POLL mode while it returns action=NOTIFICATION in the PING and PUSH modes. When the action in the response from the API is NOTIFICATION, the authorization server must send a notification to the notification endpoint of the client.

Here is the sample implementation of the background processes.

private void startCommunicationWithAuthenticationDevice(User user, BackchannelAuthenticationResponse info)
{
    // The ticket which is necessary to call Authlete's
    // /api/backchannel/authentication/complete API after the communication
    // with the authentication device is done.
    final String ticket = info.getTicket();

    // The name of the client.
    final String clientName = info.getClientName();

    // The scopes requested by the client.
    final Scope[] scopes = info.getScopes();

    // The claims requested by the client.
    final String[] claimNames = info.getClaimNames();

    // The binding message which will be displayed on the authentication device.
    final String bindingMessage = info.getBindingMessage();

    // Start a background process.
    Executors.newSingleThreadExecutor().execute(new Runnable() {
        try
        {
            // The main part of the background process.
            doInBackground(ticket, user, clientName, scopes, claimNames, bindingMessage);
        }
        catch (WebApplicationException e)
        {
            // Log the error.
            Logger.log(e);
        }
        catch (Throwable t)
        {
            // Log the error.
            Logger.log(t);
        }
    });
}

private void doInBackground(String ticket, User user, String clientName, Scope[] scopes, String[] claimNames, String bindingMessage)
{
    // A response from the authentication device.
    MyAuthenticationDeviceResponse response;

    try
    {
        // Communicate with the authentication device.
        response = communicateWithMyAuthenticationDevice(user.getSubject(), buildMessage());
    }
    catch (Throwable t)
    {
        // An error occurred during communication with the authentication device.
        // In this case, call Authlete's /api/backchannel/authentication/complete
        // API with result=TRANSACTION_FAILED.
        completeWithTransactionFailed(ticket, user);
        return;
    }

    // The decision made by the end-user on the authentication device
    // (= whether the end-user has authorized the request from the client).
    MyAuthenticationDeviceResult result = response.getResult();

    if (result == null)
    {
        // The result is empty. This should not happen. In this case,
        // call Authlete's /api/backchannel/authentication/complete API
        // with result=TRANSACTION_FAILED.
        completeWithTransactionFailed(ticket, user);
        return;
    }

    switch (result)
    {
        case allow:
            // When the end-user has authorized the request. Call the
            // Authlete API with result=AUTHORIZED.
            completeWithAuthorized(ticket, user, claimNames, new Date());
            return;

        case deny:
            // When the end-user has rejected the request. Call the
            // Authlete's API with result=ACCESS_DENIED.
            completeWithAccessDenied(ticket, user);
            return;

        case timeout:
            // When the communication with the authentication device
            // timed out. Call the Authlete's API with
            // result=TRANSACTION_FAILED.
            completeWithTransactionFailed(ticket, user);
            return;

        default:
            // Unknown result. This should not happen. Call the Authlete's
            // API with result=TRANSACTION_FAILED.
            completeWithTransactionFailed(ticket, user);
            return;
    }
}

/**
 * Call Authlete's /api/backchannel/authentication/complete API
 * with result=AUTHORIZED.
 */
private void completeWithAuthorized(String ticket, User user, String[] claimNames, Date authTime)
{
    complete(ticket, user, Result.AUTHORIZED, claimNames, authTime);
}

/**
 * Call Authlete's /api/backchannel/authentication/complete API
 * with result=ACCESS_DENIED.
 */
private void completeWithAccessDenied(String ticket, User user)
{
    complete(ticket, user, Result.ACCESS_DENIED, null, null);
}

/**
 * Call Authlete's /api/backchannel/authentication/complete API
 * with result=TRANSACTION_FAILED.
 */
private void completeWithTransactionFailed(String ticket, User user)
{
    complete(ticket, user, Result.TRANSACTION_FAILED, null, null);
}

/**
 * Call Authlete's /api/backchannel/authentication/complete API to
 * convey the result of the communication with the authentication
 * device to Authlete.
 */
private void complete(String ticket, User user, Result result, String[] claimNames, Date authTime)
{
    // The subject of the end-user.
    String subject = user.getUserSubject();

    // The time at which the end-user was authenticated.
    // This is used only in the case of result=AUTHORIZED.
    long userAuthenticatedAt = (result == Result.AUTHORIZED) ? authTime.getTime() / 1000L : 0;

    // Claims of the end-user. This is used only in the case
    // of result=AUTHORIZED.
    Map<String, Object> claims = (result == Result.AUTHORIZED) ? collectClaims(user, claimNames) : null;

    // Call Authlete's /api/backchannel/authentication/complete API
    BackchannelAuthenticationCompleteResponse response =
        callBackchannelAuthenticationComplete(ticket, subject, result, userAuthenticatedAt, claims);

    // The 'action' in the response denotes the next action that
    // this authorization server should take.
    BackchannelAuthenticationCompleteResponse.Action action = response.getAction();

    // Process according to the 'action'.
    switch (action)
    {
        case SERVER_ERROR:
            // The API call was wrong or an error occurred on Authlete side.
            //
            // Throw an exception.
            throw new WebApplicationException( ResponseUtil.internalServerError(content) );

        case NO_ACTION:
            // Nothing to do. This happens when the backchannel token delivery
            // mode of the client is the POLL mode.
            return;

        case NOTIFICATION:
            // A notification must be sent to the client's notification endpoint.
            // This happens when the backchannel token delivery mode of the
            // client is either the PING mode or the PUSH mode.
            //
            // Notify the client.
            handleNotification(response);
            return;

        default:
            // Other cases. This should not happen.
            //
            // Throw an exception.
            throw new WebApplicationException(
                ResponseUtil.internalServerError("Unknown action returned from /api/backchannel/authentication/complete API.")
            );
    }
}

/**
 * Send a notification to the client's notification endpoint.
 */
private void handleNotification(BackchannelAuthenticationCompleteResponse info)
{
    // The URL of the client's notification endpoint.
    URI clientNotificationEndpointUri = info.getClientNotificationEndpoint();

    // The notification token which is used as a Bearer token.
    String notificationToken = info.getClientNotificationToken();

    // The content of the notification sent to the client in JSON format.
    String notificationContent = info.getResponseContent();

    // A response from the client's notification endpoint.
    Response response;

    try
    {
        // Send the notification to the client's notification endpoint.
        response = WEB_CLIENT.target(clientNotificationEndpointUri).request()
            .header(HttpHeaders.AUTHORIZATION, "Bearer " + notificationToken)
            .post(Entity.json(notificationContent));
    }
    catch (Throwable t)
    {
        // Failed to send the notification.
        throw new WebApplicationException(
            ResponseUtil.internalServerError("Failed to send the notification to the client", t)
        );
    }

    // The HTTP status code of the response from the client's notification endpoint.
    Status status = Status.fromStatusCode(response.getStatusInfo().getStatusCode());

    // Process according to the HTTP status code.
    //
    // Note that the specification does not describe how the response from
    // the client notification endpoint should be handled in the case where
    // the authorization server sends an error notification in the PUSH mode.
    // Therefore, this implementation handles the responses in the same way as
    // in other cases even when an error notification is sent in the PUSH mode.

    // When the status code is either '200 OK' or '204 No Content'.
    if (status == Status.OK || status == Status.NO_CONTENT)
    {
        // Based on the specification excerpted as below, regard that
        // the request has been processed successfully.
        //
        //   CIBA Core spec, 10.2. Ping Callback and 10.3. Push Callback
        //     For valid requests, the Client Notification Endpoint SHOULD
        //     respond with an HTTP 204 No Content.  The OP SHOULD also accept
        //     HTTP 200 OK and any body in the response SHOULD be ignored.
        //
        return;
    }

    // When the status code is '3xx'.
    if (status.getFamily() == Status.Family.REDIRECTION)
    {
        // Based on the specification excerpted as below, ignore this case.
        //
        //   CIBA Core spec, 10.2. Ping Callback, 10.3. Push Callback
        //     The Client MUST NOT return an HTTP 3xx code.  The OP MUST
        //     NOT follow redirects.
        //
        return;
    }
}

2.3. Implementing a Token Endpoint

The implementation of the token endpoint using Authlete is very simple, as is shown in Token Endpoint. The authorization server extracts the content of a token request from a client and sends the extracted information to /auth/token API of Authlete. A point which should be noted is that the client authentication method used at the token endpoint must be the same as has been used at the backchannel authentication endpoint.

The sample implementation in this article focuses on only client_secret_basic, so the following implementation implements the client authentication method only.

@Path("/api/token")
public class TokenEndpoint
{
   ...

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response post(
            @HeaderParam(HttpHeaders.AUTHORIZATION) String authorization,
            MultivaluedMap<String, String> parameters)
    {
        try
        {
            // The main process.
            return doProcess(authorization, parameters);
        }
        catch (WebApplicationException e)
        {
            // Known error
            return e.getResponse();
        }
        catch (Throwable t)
        {
            // Unknown error
            return ResponseUtil.internalServerError("Unknown error occurred.");
        }
    }

    private Response doProcess(String authorization, MultivaluedMap<String, String> parameters)
    {
        // Extract client credentials from the Authorization header.
        BasicCredentials credentials = BasicCredentials.parse(authorization);

        // Extract the client ID and the client secret from the client credentials.
        String clientId     = credentials == null ? null : credentials.getUserId();
        String clientSecret = credentials == null ? null : credentials.getPassword();

        // Call Authlete's /api/auth/token API.
        TokenResponse response = callToken(parameters, clientId, clientSecret);

        // The 'action' parameter in the response denotes the next action that
        // this authorization server should take.
        Action action = response.getAction();

        // The content of the response that should be returned to the client.
        // The content varies depending on the 'action'.
        String content = response.getResponseContent();

        // Process according to the 'action'.
        switch (action)
        {
            case INVALID_CLIENT:
                // Client authentication failed.
                // 401 Unauthorized
                return ResponseUtil.unauthorized(content, "Basic realm=\"token\"");

            case INTERNAL_SERVER_ERROR:
                // The API call was wrong or an error occurred on Authlete side.
                // 500 Internal Server Error
                return ResponseUtil.internalServerError(content);

            case BAD_REQUEST:
                // The token request was invalid.
                // 400 Bad Request
                return ResponseUtil.badRequest(content);

            case OK:
                // The token request was valid.
                // 200 OK
                return ResponseUtil.ok(content);

            case PASSWORD:
                // In the case of "Resource Owner Password Credentials" flow
                // which is defined in OAuth 2.0 (RFC 6749). This does not
                // happen in CIBA flows.
                // 400 Bad Request
                return ResponseUtil.badRequest("PASSWORD action returned from /api/auth/token API but this authorization server does not allow Resource Owner Password Credentials flow.");

            default:
                // Other cases. This should not happen.
                // 500 Internal Server Error
                return ResponseUtil.internalServerError("Unknown action returned from /api/auth/token API.");
        }
    }

    ...
}

3. Simulation

We have finished implementing the authorization server that supports the CIBA flow. Now, let’s test it. The following explanation uses java-oauth-server as an authorization server and CIBA Simulator as an authentication device (AD) and consumption device (CD).

3.1. Configuration

3.1.1. Authlete Configuration

3.1.1.1. Service Configuration

Please change the settings of a service as follows.

Tab Key Value
CIBA Supported Backchannel Token Delivery Modes PING, POLL, PUSH
CIBA Backchannel User Code Parameter Supported Supported

3.1.1.2. Client Configuration

Please change the settings of a client as follows.

Tab Key Value
Basic Client Type CONFIDENTIAL
Authorization Client Authentication Method CLIENT_SECRET_BASIC
CIBA Token Delivery Mode (Choose a mode that you want to test)
CIBA Notification Endpoint https://cibasim.authlete.com/api/notification
CIBA User Code Required Required

3.1.2. CIBA Simulator Configuration

3.1.2.1. Create a project

Input any values in Namespace and Project and push Create button in the top page of CIBA Simulator.

CIBA simulator

You will be redirected to the project top page with the URL of https://cibasim.authlete.com/{namespace}/{project}. You can configure both AD and CD simulators.

Project top page

Authentication Device Simulator

To ask the end-user to authorize the request from the client, java-oauth-server with the default configuration sends a request, which includes a user identifier as is shown below, to the simulator’s /api/authenticate/sync API.

{
  ...
  "user" : "{the end-user identifier}",
  ...
}

The sample implementation of the authorization server, java-oauth-server, uses a user’s subject as an identifier. After the /api/authenticate/sync API receives the request from the authorization server, the user authorizes or rejects the request from the client in the AD simulator, and the result is sent to the authorization server.

To launch the AD simulator, enter the end-user identifier in the User ID field and push the Launch AD simulator button. java-oauth-server with the default configuration uses dummy users defined in UserEntity.java. In this article, let’s use subject=1001 as an end-user. Please enter 1001 in the User ID field.

Launch AD simulator

You will see the page like this. Please note that you must keep this page open while testing the CIBA flow.

AD simulator

3.1.2.3. CD Simulator Configuration

In the top page of the project, please configure as follows and press Save button to store the change.

Key Value
BC Authentication Endpoint URL URL of the backchannel authentication endpoint of the authorization server
Token Endpoint URL URL of the token endpoint of the authorization server
Token Delivery Mode (Choose a delivery mode you want to test)
Client ID Client ID of the client
Client Authentication Basic
Client Secret Client Secret of the client

Consumption Device (CD) configuration

And Launch the CD simulator by pressing the Launch CD simulator button.

Consumption Device (CD) simulator

3.1.3. java-oauth-server Configuration

Please refer to this article for the detail of the java-oauth-server.

Download the source code of the java-oauth-server and configure it at authlete.properties as follows.

base_url = <base URL of Authlete API (e.g. https://api.authlete.com)>
service.api_key = <API Key of the service>
service.api_secret = <API Secret of the service>

And launch the java-oauth-server using the following command:

mvn jetty:run -Dauthlete.ad.workspace=<CIBA simulator's namespace>/<CIBA simulator's project>

3.2. Testing each delivery mode

Please configure the delivery mode in the following settings

  • Token delivery mode in the developer console
  • Token Delivery Mode in the CD simulator

3.2.1. Testing the PUSH mode

Step 1. Launch the CD simulator and input values as follows. Please note that the value of hint and User Code are defined in UserEntity.java in this case.

Key Value
Scopes openid
ACR Values (any value)
Hint の値 1001
Hint の種類 login_hint
Binding Message (any value)
User Code 675325

CD simulator setup

Step 2. Send a backchannel authentication request by pressing the Send button.

Step 3. The auth_req_id will be sent to the CD simulater after the authorization server processes the request.

スクリーンショット 2019-03-04 19.22.45のコピー2.png

Step 4. You will see the authorization page in the AD simulator.

スクリーンショット 2019-03-04 19.32.31.png

Step 5. Press Allow button and authorize the client.

Step 6. The authorization server will push a response that includes tokens to the CD simulator.

スクリーンショット 2019-03-04 19.23.14.png

3.2.2. Testing the PING mode

Steps 1 to 3 are the same as those in the PUSH mode. The authorization server sends auth_req_id to the CD simulator after it processed the backchannel authentication request.

Step 4. The CD simulator will wait for a notificatoin from the authorization server at the notification endpoint.

スクリーンショット 2019-03-05 15.00.43.png

Step 5. After authorizing the client in the AD simulator, the authorization server sends a notification to the client notification endpoint and receives the tokens.

スクリーンショット 2019-03-05 15.01.03.png

3.2.3. Testing the POLL mode

Steps 1 to 3 are the same as those in the PUSH mode. The authorization server sends auth_req_id to the CD simulator after it processed the backchannel authentication request.

Step 4. The CD simulator starts polling to the token endpoint. Please note that the token endpoint returns authorization_pending errors until the client is authorized in the AD simulator.

スクリーンショット 2019-03-05 15.19.59.png

After the client is authorized in the AD simulator, the token endpoint sends a response that includes tokens.

スクリーンショット 2019-03-05 15.21.12.png