Como implementar o CIBA com a Authlete

1. Visão geral

Este documento descreve como construir um servidor de autorização que suporte CIBA (Autenticação backchannel iniciada pelo cliente).

1.1. Prefácio

É essencial ter uma visão geral da CIBA com antecedência. Consulte os seguintes documentos, conforme necessário.

Também é necessário entender OAuth 2.0, OpenID Connect e Authlete. Consulte os seguintes documentos, conforme necessário.

1.2. Arquitetura do sistema

Construiremos um servidor de autorização apoiado pela Authlete, como é mostrado no diagrama abaixo.

CIBA system architecture with Authlete

1.3. Implementação da amostra

A implementação da amostra introduzida neste artigo é apenas para apreender como implementar o CIBA usando Authlete. Alguns exemplos não são otimizados intencionalmente para ajudá-lo a entender os fluxos de processamento. Por exemplo, a implementação da amostra neste artigo tem as seguintes restrições:

  • O servidor de autorização na implementação da amostra suporta apenas o fluxo CIBA, não o OAuth 2.0 ou o OpenID Connect.

  • O ponto final de autenticação do backchannel nesta implementação da amostra não suporta parâmetros opcionais, como login_hint_token, id_token_hinte acrs.

  • O ponto final de autenticação do backchannel e o ponto final do token nesta implementação de amostra só suportam client_secret_basic como o método de autenticação do cliente.

Consulte os seguintes códigos-fonte para implementações mais sofisticadas e práticas.

1.4. Nota

2. Implementação

No fluxo CIBA, ponto final de autenticação do backchannel e ponto final token desempenham papéis essenciais. Aqui, explicamos como implementar os pontos finais.

2.1. Fluxo CIBA usando Authlete

2.1.1 Ponto final de autenticação do backchannel

O diagrama abaixo explica o fluxo de processamento em cada ponto final ao implementar o CIBA com authlete. Por favor, note que o diagrama omite manipulações de erros.

Backchannel Authentication Endpoint using Authlete APIs

O ponto final de autenticação do backchannel usa seguintes APIs Authlete.

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

Na situação real, o ponto final também usa /api/backchannel/authentication/fail API para manipulação de erros. Por favor, consulte Identificando um usuário final usando dica para detalhes.

2.1.2. Ponto final do token

O fluxo de processamento do ponto final do token é simples, como é o caso do OAuth 2.0. O servidor de autorização envia uma solicitação de token de um cliente para /api/auth/token API e envia de volta uma resposta de token da Authlete API para o cliente.

Token Endpoint using Authlete API

2.2. Implementando um ponto final de autenticação do backchannel

O ponto final de autenticação do backchannel lida com várias solicitações e respostas. Para implementar o ponto final, você deve entender o básico do ponto final primeiro.

✔︎ Authentication Request

A solicitação enviada ao ponto final de autenticação do backchannel é chamada de “solicitação de autenticação”. A especificação requer que o método HTTP e Content-Type ser POST e application/x-www-form-urlencoded respectivamente.

✔︎ Client Authentication

O ponto final de autenticação do backchannel deve autenticar um cliente. Como é explicado em este artigo, o CIBA permite vários métodos de autenticação do cliente; no entanto, a implementação da amostra aqui suporta apenas client_secret_basic para simplificação. Observe que o método de autenticação do cliente no ponto final de autenticação do backchannel deve ser o mesmo que no ponto final do token.

Usamos o seguinte código como modelo para implementar o ponto final de autenticação do backchannel no servidor de autorização usando 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
    }
}

As seções a seguir adicionarão processos ao modelo e implementarão o ponto final.

2.2.1. Verificando uma solicitação de autenticação

A primeira coisa que o ponto final de autenticação do backchannel deve fazer é verificar a solicitação de autenticação. Os passos serão os seguintes:

  1. Extrair informações relacionadas à autenticação do cliente a partir do authorization parâmetro.
  2. Chamar /backchannel/authentication API com as informações extraídas e parameters parâmetro, e delegar o processo de verificação para Authlete.
  3. Siga o action valor do parâmetro em uma resposta de /backchannel/authentication API.

E os códigos para os processos são os seguintes.

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.");
    }
}

Se a solicitação não tem nenhum problema, o /backchannel/authentication A API envia uma resposta com action=USER_IDENTIFICATION. Neste caso de USER_IDENTIFICATION, o servidor de autorização chamará o handleUserIdentification() método para lidar com o pedido de tiantication. Outros valores no action o parâmetro indica um erro, então o servidor de autorização gera uma resposta de erro e envia para o cliente.

2.2.2. Identificar um usuário final usando uma dica

Quando o /backchannel/authentication A API retorna uma resposta com action=USER_IDENTIFICATION, o servidor de autorização deve identificar o usuário final usando uma dica na resposta. A dica é qualquer um login_hint, login_hint_tokenou id_token_hint. Para simplificação, o servidor de autorização nesta amostra de impementação suporta login_hint somente. O valor pode ser um assunto do usuário final, endereço de e-mail ou número de telefone na amostra a seguir.

Na identificação do usuário final usando com sucesso o login_hint, o servidor de autorização fará os seguintes processos. Caso contrário, o servidor de autorização chama /backchannel/authentication/fail API, gera uma resposta de erro e devolve ao cliente.

Os seguintes códigos incluem processos que levarão em conta a discussão acima.

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. Verificando parâmetros de solicitação

Além da identificação do usuário final, o servidor de autorização deve verificar parâmetros de solicitação no handleUserIdentification() método, como está descrito nas seções a seguir.

2.2.3.1. login_hint_token

Ao usar login_hint_token como dica, o servidor de autorização deve verificar sua expiração; no entanto, neste artigo, não implementaremos a verificação para a simplificação. Por favor, verifique o java-oauth-servidor e authlete-java-jaxrs para detalhes.

2.2.3.2. user_code

2.2.3.2.1. The concept of user_code

Na implementação da amostra acima, o servidor de autorização espera um assunto do usuário final, endereço de e-mail e número de telefone para identificar o usuário final. No entanto, uma terceira pessoa pode ser capaz de adivinhar esses valores.

O user_code é um parâmetro para evitar que terceiros mal-intencionados enviem solicitações de autenticação de fraude. A especificação define o user_code como segue:

user_code OPCIONAL. Um código secreto, como senha ou pino, conhecido apenas pelo usuário, mas verificável pela OP. O código é usado para autorizar o envio de uma solicitação de autenticação ao dispositivo de autenticação do usuário. Este parâmetro só deve estar presente se o parâmetro de registro do cliente backchannel_user_code_parameter indicar suporte para código do usuário.

Os clientes maliciosos não podem adivinhar o user_code e, assim, enviar uma solicitação de autenticação válida. Em outras palavras, um servidor de autorização que verifica user_code pode rejeitar os pedidos maliciosos.

2.2.3.2.2. Verifying a user_code

Vamos verificar a verificação de um user_code.

O servidor Authlete API fará o seguinte processo quando um servidor de autorização chamar o /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();
    }
}

Quando os valores de ambos os backchannelUserCodeParameterSupported metadados do serviço que corresponde ao servidor de autorização e o bcUserCodeRequired metadados do cliente são verdadeiros, Authlete vai verificar se uma solicitação de autenticação contém user_code. A API Authlete retorna uma resposta de erro com action=BAD_REQUEST para o servidor de autorização se o servidor de autorização enviar uma solicitação de autenticação que não contenha user_code. Neste caso, o servidor de autorização faz o processo para o caso de BAD_REQUEST na implementação da amostra acima.

Neste artigo, vamos implementar um handleUserIdentification() método, que lida com o caso para USER_IDENTIFICATION. Durante USER_IDENTIFICATION cenário, temos que implementar o método antecipando um dos casos listados abaixo.

✔º Caso 1 Um user_code foi encontrado no pedido de autenticação. (= No caso do pseudo código acima, as condições de service.isBackchannelUserCodeParameterSupported() && client.isBcUserCodeRequired() e isUserCodeContainedInRequestParameters() foram satisfatificados.)

✔º Caso 2 A API Authlete não verifica o user_code. (= No caso do pseudo código acima, a condição de service.isBackchannelUserCodeParameterSupported() && client.isBcUserCodeRequired() não foi satisfatizado.)

No caso 1, a API Authlete garante que a solicitação de autenticação contém um user_code. Assim, o servidor de autorização deve verificar se o valor de user_code é válido. Podemos pular o cheque para o caso 2 porque uma solicitação de autenticação não tem que conter o user_code Parâmetros.

Aqui está uma implementação amostral de 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

Quando um cliente inicia o fluxo CIBA, um usuário final será solicitado a autorizar em seu dispositivo de autenticação. Ao autorizar, como o usuário final confirma que o cliente que pede autorização através do dispositivo é o único que o usuário final está disposto a autorizar?

O binding_message parâmetro é introduzido para resolver o problema acima. O diagrama abaixo é um caso de uso do binding_message parâmetro.

Use case: "CIBA" pay

  1. Um usuário final pede a uma loja para pagar usando um serviço de pagamento apoiado pela CIBA.

  2. A loja mostra uma mensagem vinculante, sRj89xCg, no terminal de PDV (= aplicativo do cliente).

  3. O terminal de PDV envia uma solicitação de autenticação, incluindo a mensagem vinculante, ao servidor de autorização do serviço de pagamento.

  4. O servidor de autorização começa a interagir com um dispositivo de autenticação do usuário final e envia a mensagem de vinculação para o dispositivo.

  5. O dispositivo de autenticação pede ao usuário final que autorize o cliente, mostrando a mensagem de vinculação de sRj89xCg.

  6. O usuário final confirma o cliente usando a mensagem de vinculação e autoriza a transação.

Verifying the Binding Message

Então, como o servidor de autorização verifica a mensagem de vinculação? Abaixo está uma citação da especificação. A especificação não define os requisitos MUST para a mensagem de vinculação e apenas diz que o binding_message valor DEVE ser relativamente curto e usar um conjunto limitado de caracteres de texto simples.

binding_message OPCIONAL. Um identificador ou mensagem legível humana destinado a ser exibido tanto no dispositivo de consumo quanto no dispositivo de autenticação para interligá-los para a transação por meio de uma sugestão visual para o usuário final. Esta mensagem de bloqueio permite ao usuário final garantir que a ação tomada no dispositivo de autenticação esteja relacionada à solicitação iniciada pelo dispositivo de consumo. O valor DEVE conter algo que permite ao usuário final discernir de forma confiável que a transação está relacionada entre o dispositivo de consumo e o dispositivo de autenticação, como um valor aleatório de entropia razoável (por exemplo, um código de aprovação transacional). Como os vários dispositivos envolvidos podem ter habilidades limitadas de exibição e a mensagem pretende inspeção visual pelo usuário final, o binding_message valor DEVE ser relativamente curto e usar um conjunto limitado de caracteres de texto simples. A invalid_binding_message definida na Seção 13 é utilizada no caso de ser necessário informar ao Cliente que o binding_message fornecido é inaceitável.

Aqui, nós adicionamos um handleUserIdentification() método para verificar o comprimento da mensagem de vinculação.

/**
 * 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 Emitindo um auth_req_id

Depois que o servidor de autorização terminou todas as verificações necessárias, ele emite um auth_req_id Usando /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.");
    }
}

Quando o servidor de autorização recebe uma resposta com action=OK, o servidor inicia um processo de fundo chamando o startCommunicationWithAuthenticationDevice() método, e envia um 200 OK resposta ao cliente. Por favor, note que a resposta ao cliente contém o auth_req_id emitido pela API Authlete.

###2.2.5. Processos de antecedentes

O servidor de autorização inicia o processo de fundo chamando de startCommunicationWithAuthenticationDevice() método antes de enviar o auth_req_id para o cliente. Nesse processo de segundo plano, o servidor de autorização fará as seguintes tarefas.

  1. Comunica-se com um dispositivo de autenticação para perguntar ao usuário final se autoriza a solicitação do cliente.
  2. Chamadas /backchannel/authentication/complete API e diz ao Authlete o resultado da comunicação entre o servidor de autorização e o dispositivo de autenticação.
  3. Segue a ação na resposta do /backchannel/authentication/complete API.

Por favor, note que o conteúdo da resposta do /backchannel/authentication/complete A API varia de acordo com o modo de entrega de tokens backchannel. Para ser concreto, em casos de sucesso, a API retorna action=OK no modo POLL enquanto ele retorna action=NOTIFICATION nos modos PING e PUSH. Quando o action na resposta da API é NOTIFICATION, o servidor de autorização deve enviar uma notificação para o ponto final da notificação do cliente.

Aqui está a implementação amostral dos processos de fundo.

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. Implementando um Ponto final de token

A implementação do ponto final do token usando Authlete é muito simples, como é mostrado em Ponto final do token. O servidor de autorização extrai o conteúdo de uma solicitação de token de um cliente e envia as informações extraídas para /auth/token API de Authlete. Um ponto que deve ser observado é que o método de autenticação do cliente usado no ponto final do token deve ser o mesmo que tem sido usado no ponto final de autenticação do backchannel.

A implementação da amostra neste artigo foca apenas em client_secret_basic, assim, a implementação a seguir implementa apenas o método de autenticação do cliente.

@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. Simulação

Terminamos de implementar o servidor de autorização que suporta o fluxo CIBA. Agora, vamos testá-lo. A seguinte explicação usa java-oauth-servidor como servidor de autorização e Simulador CIBA como dispositivo de autenticação (AD) e dispositivo de consumo (CD).

3.1. Configuração

Configuração Authlete 3.1.1.

3.1.1.1. Service Configuration

Por favor, altere as configurações de um serviço da seguinte forma.

Guia Valor
CIBA Modos de entrega de tokens backchannel suportados PING, POLL, PUSH
CIBA Parâmetro de código do usuário backchannel suportado

3.1.1.2. Client Configuration

Por favor, altere as configurações de um cliente da seguinte forma.

Guia Valor
básica do tipo cliente CONFIDENTIAL
de autorização Método de autenticação do cliente CLIENT_SECRET_BASIC
CIBA Modo de entrega de tokens (Escolha um modo que você deseja testar)
CIBA de endpoint de notificação https://cibasim.authlete.com/api/notification
CIBA Código do usuário exigido

3.1.2. Configuração do simulador CIBA

3.1.2.1. Create a project

Insira quaisquer valores em Namespace e Projeto e empurrar Criar botão em a página superior do CIBA Simulator.

CIBA simulator

Você será redirecionado para a página superior do projeto com a URL de https://cibasim.authlete.com/{namespace}/{project}. Você pode configurar simuladores de AD e CD.

Project top page

Authentication Device Simulator

Para pedir ao usuário final que autorize a solicitação do cliente, java-oauth-servidor com a configuração padrão envia uma solicitação, que inclui um identificador de usuário como é mostrado abaixo, para o simulador /api/authenticate/sync API.

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

A implementação amostral do servidor de autorização, java-oauth-servidor, usa o assunto de um usuário como um identificador. Depois do /api/authenticate/sync A API recebe a solicitação do servidor de autorização, o usuário autoriza ou rejeita a solicitação do cliente no simulador de AD, e o resultado é enviado ao servidor de autorização.

Para iniciar o simulador AD, digite o identificador do usuário final no Identificação de usuário campo e empurrar o Lançar simulador de AD botão. java-oauth-servidor com a configuração padrão usa usuários manequim definidos em Entidade do Usuário.java. Neste artigo, vamos usar subject=1001 como usuário final. Por favor, entre 1001 no Identificação de usuário campo.

Launch AD simulator

Você verá a página assim. Por favor, note que você deve manter esta página aberta durante o teste do fluxo CIBA.

AD simulator

3.1.2.3. Configuração do simulador de CD

Na página superior do projeto, configure o seguinte e pressione Salvar botão para armazenar a mudança.

Chave Valor
URL de ponto final de autenticação BC URL do ponto final de autenticação do backchannel do servidor de autorização
URL do Token Endpoint URL do ponto final do token do servidor de autorização
Modo de entrega de tokens (Escolha um modo de entrega que deseja testar)
ID do cliente Cscient ID do cliente
Autenticação do cliente Básico
Segredo do Cliente Cliente Secreto do cliente

Consumption Device (CD) configuration

E lançar o simulador de CD pressionando o Lançar simulador de CD botão.

Consumption Device (CD) simulator

Configuração de servidor de 3.1.3.java-oauth

Por favor, consulte este artigo para o detalhe do java-oauth-servidor.

Baixe o código-fonte do java-oauth-servidor e configurá-lo em authlete.properties como segue.

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>

E inicie o java-oauth-server usando o seguinte comando:

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

3.2. Testando cada modo de entrega

Por favor, configure o modo de entrega nas seguintes configurações

3.2.1. Testando o modo PUSH

Passo 1. Inicie o simulador de CD e os valores de entrada da seguinte forma. Observe que o valor da dica e do Código do Usuário são definidos em Entidade do Usuário.java neste caso.

Chave Valor
Escopos openid
Valores ACR (qualquer valor)
Dica値 1001
Dica種類 login_hint
Mensagem de vinculação (qualquer valor)
Código do Usuário 675325

CD simulator setup

Passo 2. Envie uma solicitação de autenticação do backchannel pressionando o Enviar botão.

Passo 3. O auth_req_id será enviado ao simulador de CD após o servidor de autorização processar a solicitação.

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

Passo 4. Você verá a página de autorização no simulador de AD.

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

Passo 5. Imprensa Permitir botão e autorizar o cliente.

Passo 6. O servidor de autorização pressionará uma resposta que inclui tokens para o simulador de CD.

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

3.2.2. Testando o modo PING

As etapas 1 a 3 são as mesmas do modo PUSH. O servidor de autorização envia auth_req_id para o simulador de CD depois que ele processou a solicitação de autenticação do backchannel.

Passo 4. O simulador de CD aguardará uma notificatoin do servidor de autorização no ponto final da notificação.

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

Passo 5. Após autorizar o cliente no simulador de AD, o servidor de autorização envia uma notificação para o ponto final de notificação do cliente e recebe os tokens.

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

3.2.3. Testando o modo POLL

As etapas 1 a 3 são as mesmas do modo PUSH. O servidor de autorização envia auth_req_id para o simulador de CD depois que ele processou a solicitação de autenticação do backchannel.

Passo 4. O simulador de CD começa a pesquisar para o ponto final do token. Por favor, note que o ponto final do token retorna authorization_pending erros até que o cliente seja autorizado no simulador de AD.

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

Depois que o cliente é autorizado no simulador de AD, o ponto final do token envia uma resposta que inclui tokens.

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