/*
 * Copyright (C) 2019 Authlete, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the
 * License.
 */
package com.authlete.jaxrs;

import java.util.Arrays;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;
import com.authlete.common.dto.BackchannelAuthenticationFailRequest.Reason;
import com.authlete.common.dto.BackchannelAuthenticationIssueResponse;
import com.authlete.common.dto.BackchannelAuthenticationResponse;
import com.authlete.common.types.User;
import com.authlete.common.types.UserIdentificationHintType;
import com.authlete.common.web.BasicCredentials;
import com.authlete.jaxrs.spi.BackchannelAuthenticationRequestHandlerSpi;

/**
 * Handler for backchannel authentication requests to a backchannel authentication
 * endpoint of CIBA (Client Initiated Backchannel Authentication).
 *
 *

 * In an implementation of backchannel authentication endpoint, call {@link #handle(MultivaluedMap, String)
 * handle()} method and use the response as the response from the endpoint to the client
 * application. {@code handle()} method calls Authlete's {@code /api/backchannel/authentication}
 * API, receives a response from the API, and dispatches processing according to the
 * {@code action} parameter in the response.
 *

 *
 * @author Hideki Ikeda
 *
 * @since 2.13
 */
public class BackchannelAuthenticationRequestHandler extends BaseHandler
{
    /**
     * The value for {@code WWW-Authenticate} header on 401 Unauthorized.
     */
    private static final String CHALLENGE = "Basic realm=\"backchannel/authentication\"";

    /**
     * Implementation of {@link BackchannelAuthenticationRequestHandlerSpi} interface.
     */
    private final BackchannelAuthenticationRequestHandlerSpi mSpi;

    /**
     * Constructor with an implementation of {@link AuthleteApi} interface
     * and an implementation of {@link BackchannelAuthenticationRequestHandlerSpi} interface.
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     *
     * @param spi
     *         Implementation of {@link BackchannelAuthenticationRequestHandlerSpi} interface.
     */
    public BackchannelAuthenticationRequestHandler(AuthleteApi api, BackchannelAuthenticationRequestHandlerSpi spi)
    {
        super(api);

        mSpi = spi;
    }

    /**
     * Handle a backchannel authentication request to a backchannel authentication
     * endpoint of CIBA (Client Initiated Backchannel Authentication).
     *
     * @param parameters
     *         Request parameters of a backchannel authentication request.
     *
     * @param authorization
     *         The value of {@code Authorization} header in the backchannel authentication
     *         request. A client application may embed its pair of client ID and
     *         client secret in a backchannel authentication request using Basic
     *         Authentication.
     *
     * @param clientCertificatePath
     *         The path of the client's certificate, each in PEM format. The first
     *         item in the array is the client's certificate itself. May be {@code null}
     *         if the client did not send a certificate or path.
     *
     * @return
     *         A response that should be returned from the endpoint to the
     *         client application.
     *
     * @throws WebApplicationException
     *         An error occurred.
     */
    public Response handle(
            MultivaluedMap<String, String> parameters, String authorization,
            String[] clientCertificatePath) throws WebApplicationException
    {
        // Convert the value of Authorization header (credentials of
        // the client application), if any, into BasicCredentials.
        BasicCredentials credentials = BasicCredentials.parse(authorization);

        // The credentials of the client application extracted from
        // 'Authorization' header. These may be null.
        String clientId     = credentials == null ? null : credentials.getUserId();
        String clientSecret = credentials == null ? null : credentials.getPassword();

        try
        {
            // Process the given parameters.
            return process(parameters, clientId, clientSecret, clientCertificatePath);
        }
        catch (WebApplicationException e)
        {
            throw e;
        }
        catch (Throwable t)
        {
            // Unexpected error.
            throw unexpected("Unexpected error in BackchannelAuthenticationRequest", t);
        }
    }

    private Response process(
            MultivaluedMap<String, String> parameters, String clientId,
            String clientSecret, String[] clientCertificatePath)
    {
        // TODO: Duplicate code.
        String clientCertificate = null;
        if (clientCertificatePath != null && clientCertificatePath.length > 0)
        {
            // The first one is the client's certificate.
            clientCertificate = clientCertificatePath[0];

            // if we have more in the path, pass them along separately without the first one
            if (clientCertificatePath.length > 1)
            {
                clientCertificatePath = Arrays.copyOfRange(
                        clientCertificatePath, 1, clientCertificatePath.length);
            }
        }

        // Call Authlete's /api/backchannel/authentication API.
        BackchannelAuthenticationResponse response =
                getApiCaller().callBackchannelAuthentication(parameters, clientId, clientSecret, clientCertificate, clientCertificatePath);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        BackchannelAuthenticationResponse.Action action = response.getAction();

        // The content of the response to the client application.
        // The format of the content varies depending on the action.
        String content = response.getResponseContent();

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

            case UNAUTHORIZED:
                // 401 Unauthorized
                return ResponseUtil.unauthorized(content, CHALLENGE);

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

            case USER_IDENTIFICATION:
                // Process user identification.
                return handleUserIdentification(response);

            default:
                // This never happens.
                throw getApiCaller().unknownAction("/api/backchannel/authentication", action);
        }
    }

    private Response handleUserIdentification(BackchannelAuthenticationResponse baRes)
    {
        // Identify a user based on the hint contained in the backchannel authentication
        // request.
        User user = identifyUserByHint(baRes);

        // Check the expiration of the login hint token if necessary.
        checkExpirationOfLoginHintToken(baRes);

        // Check the user code contained in the backchannel authentication request
        // if necessary.
        checkUserCode(baRes, user);

        // Check the binding message in the backchannel authentication request
        // if necessary.
        checkBindingMessage(baRes);

        // Issue an 'auth_req_id'.
        BackchannelAuthenticationIssueResponse baiRes =
                getApiCaller().callBackchannelAuthenticationIssue(baRes.getTicket());

        // 'action' in the response denotes the next action which this service
        // implementation should take.
        BackchannelAuthenticationIssueResponse.Action action = baiRes.getAction();

        // The content of the response to the client application.
        // The format of the content varies depending on the action.
        String content = baiRes.getResponseContent();

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

            case OK:
                // Start communicating with the authentication device for end-user
                // authentication and authorization.
                startCommunicationWithAuthenticationDevice(user, baRes, baiRes);

                // 200 OK.
                return ResponseUtil.ok(content);

            default:
                // This never happens.
                throw getApiCaller().unknownAction("/api/backchannel/authentication/issue", action);
        }
    }

    private User identifyUserByHint(BackchannelAuthenticationResponse baRes)
    {
        // Get a user by the hint.
        User user = mSpi.getUserByHint(baRes.getHintType(), baRes.getHint(), baRes.getSub());

        if (user != null)
        {
            String subject = user.getSubject();

            if (subject != null && subject.length() > 0)
            {
                // Found a user.
                return user;
            }
        }

        // Can't identify a user by the hint.
        throw getApiCaller().backchannelAuthenticationFail(baRes.getTicket(), Reason.UNKNOWN_USER_ID);
    }

    private void checkExpirationOfLoginHintToken(BackchannelAuthenticationResponse baRes)
    {
        if (baRes.getHintType() != UserIdentificationHintType.LOGIN_HINT_TOKEN)
        {
            // The hint is not an ID token hint.
            return;
        }

        // The login hint token contained in the backchannel authentication request.
        String loingHintToken = baRes.getHint();

        // Check if the login hint token has expired or not.
        if (mSpi.isLoginHintTokenExpired(loingHintToken) == false)
        {
            // OK. The login hint token is valid.
            return;
        }

        // The login hint token has expired.
        throw getApiCaller().backchannelAuthenticationFail(baRes.getTicket(), Reason.EXPIRED_LOGIN_HINT_TOKEN);
    }

    private void checkUserCode(BackchannelAuthenticationResponse baRes, User user)
    {
        if (mSpi.shouldCheckUserCode(user, baRes) == false)
        {
            // A user code should not be checked.
            return;
        }

        // The user code contained in the backchannel authentication request.
        String userCode = baRes.getUserCode();

        if (mSpi.isValidUserCode(user, userCode))
        {
            // OK. The user code contained is valid.
            return;
        }

        // The user code is invalid.
        throw getApiCaller().backchannelAuthenticationFail(baRes.getTicket(), Reason.INVALID_USER_CODE);
    }

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

        if (bindingMessage == null || bindingMessage.length() == 0)
        {
            // The binding message is not contained in the request.
            return;
        }

        if (mSpi.isValidBindingMessage(bindingMessage))
        {
            // OK. The binding message is valid.
            return;
        }

        // The binding message is invalid.
        throw getApiCaller().backchannelAuthenticationFail(baRes.getTicket(), Reason.INVALID_BINDING_MESSAGE);
    }

    private void startCommunicationWithAuthenticationDevice(User user, BackchannelAuthenticationResponse baRes,
            BackchannelAuthenticationIssueResponse baiRes)
    {
        // Start communicating with an authentication device for end-user authentication
        // and authorization.
        mSpi.startCommunicationWithAuthenticationDevice(user, baRes, baiRes);
    }
}


/*
 * Copyright (C) 2015-2016 Authlete, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the
 * License.
 */
package com.authlete.jaxrs;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;
import com.authlete.common.dto.AuthorizationFailRequest.Reason;
import com.authlete.common.dto.Property;
import com.authlete.jaxrs.spi.AuthorizationDecisionHandlerSpi;

/**
 * Handler for end-user's decision on the authorization request.
 *
 *

 * An authorization endpoint returns an authorization page (HTML) to an end-user,
 * and the end-user will select either "authorize" or "deny" the authorization
 * request. This class handles the decision and calls Authlete's
 * {@code /api/auth/authorization/issue} API or {@code /api/auth/authorization/fail}
 * API accordingly.
 *

 *
 * @author Takahiko Kawasaki
 */
public class AuthorizationDecisionHandler extends BaseHandler
{
    /**
     * Implementation of {@link AuthorizationDecisionHandlerSpi} interface.
     */
    private final AuthorizationDecisionHandlerSpi mSpi;

    /**
     * Constructor with an implementation of {@link AuthleteApi} interface
     * and an implementation of {@link AuthorizationDecisionHandlerSpi}
     * interface.
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     *
     * @param spi
     *         Implementation of {@link AuthorizationDecisionHandlerSpi} interface.
     */
    public AuthorizationDecisionHandler(AuthleteApi api, AuthorizationDecisionHandlerSpi spi)
    {
        super(api);

        mSpi = spi;
    }

    /**
     * Handle an end-user's decision on an authorization request.
     *
     * @param ticket
     *         A ticket that was issued by Authlete's {@code /api/auth/authorization} API.
     *
     * @param claimNames
     *         Names of requested claims. Use the value of the {@code claims}
     *         parameter in a response from Authlete's {@code /api/auth/authorization} API.
     *
     * @param claimLocales
     *         Requested claim locales. Use the value of the {@code claimsLocales}
     *         parameter in a response from Authlete's {@code /api/auth/authorization} API.
     *
     * @return
     *         A response to the client application. Basically, the response
     *         will trigger redirection to the client's redirection endpoint.
     *
     * @throws WebApplicationException
     *         An error occurred.
     */
    public Response handle(String ticket, String[] claimNames, String[] claimLocales) throws WebApplicationException
    {
        try
        {
            // Process the end-user's decision.
            return process(ticket, claimNames, claimLocales);
        }
        catch (WebApplicationException e)
        {
            throw e;
        }
        catch (Throwable t)
        {
            // Unexpected error.
            throw unexpected("Unexpected error in AuthorizationDecisionHandler", t);
        }
    }

    /**
     * Process the end-user's decision.
     */
    private Response process(String ticket, String[] claimNames, String[] claimLocales)
    {
        // If the end-user did not grant authorization to the client application.
        if (mSpi.isClientAuthorized() == false)
        {
            // The end-user denied the authorization request.
            return fail(ticket, Reason.DENIED);
        }

        // The subject (= unique identifier) of the end-user.
        String subject = mSpi.getUserSubject();

        // If the subject of the end-user is not available.
        if (subject == null || subject.length() == 0)
        {
            // The end-user is not authenticated.
            return fail(ticket, Reason.NOT_AUTHENTICATED);
        }

        // The time when the end-user was authenticated.
        long authTime = mSpi.getUserAuthenticatedAt();

        // The ACR (Authentication Context Class Reference) of the
        // end-user authentication.
        String acr = mSpi.getAcr();

        // Collect claim values.
        Map<String, Object> claims = collectClaims(subject, claimNames, claimLocales);

        // Extra properties to associate with an access token and/or
        // an authorization code.
        Property[] properties = mSpi.getProperties();

        // Scopes to associate with an access token and/or an authorization code.
        // If a non-null value is returned from mSpi.getScopes(), the scope set
        // replaces the scopes that have been specified in the original
        // authorization request.
        String[] scopes = mSpi.getScopes();

        // Authorize the authorization request.
        return authorize(ticket, subject, authTime, acr, claims, properties, scopes);
    }

    /**
     * Collect claims of the end-user.
     */
    private Map<String, Object> collectClaims(String subject, String[] claimNames, String[] claimLocales)
    {
        // If no claim is required.
        if (claimNames == null || claimNames.length == 0)
        {
            return null;
        }

        // Drop empty and duplicate entries from claimLocales.
        claimLocales = normalizeClaimLocales(claimLocales);

        // Claim values.
        Map<String, Object> claims = new HashMap<String, Object>();

        // For each requested claim.
        for (String claimName : claimNames)
        {
            // If the claim name is empty.
            if (claimName == null || claimName.length() == 0)
            {
                continue;
            }

            // Split the claim name into the name part and the tag part.
            String[] elements = claimName.split("#", 2);
            String name = elements[0];
            String tag  = (elements.length == 2) ? elements[1] : null;

            // If the name part is empty.
            if (name == null || name.length() == 0)
            {
                continue;
            }

            // Get the claim value of the claim.
            Object value = getClaim(name, tag, claimLocales);

            // If the claim value was not obtained.
            if (value == null)
            {
                continue;
            }

            if (tag == null)
            {
                // Just for an edge case where claimName ends with "#".
                claimName = name;
            }

            // Add the pair of the claim name and the claim value.
            claims.put(claimName, value);
        }

        // If no claim value has been obtained.
        if (claims.size() == 0)
        {
            return null;
        }

        // Obtained claim values.
        return claims;
    }

    private String[] normalizeClaimLocales(String[] claimLocales)
    {
        if (claimLocales == null || claimLocales.length == 0)
        {
            return null;
        }

        // From 5.2. Claims Languages and Scripts in OpenID Connect Core 1.0
        //
        //     However, since BCP47 language tag values are case insensitive,
        //     implementations SHOULD interpret the language tag values
        //     supplied in a case insensitive manner.
        //
        Set<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);

        // Normalized list.
        List<String> list = new ArrayList<String>();

        // Loop to drop empty and duplicate claim locales.
        for (String claimLocale : claimLocales)
        {
            // If the claim locale is empty.
            if (claimLocale == null || claimLocale.length() == 0)
            {
                continue;
            }

            // If the claim locale is a duplicate.
            if (set.contains(claimLocale))
            {
                continue;
            }

            set.add(claimLocale);
            list.add(claimLocale);
        }

        int size = list.size();

        if (size == 0)
        {
            return null;
        }
        else if (size == claimLocales.length)
        {
            // No change.
            return claimLocales;
        }

        // Convert the list to an array.
        String[] array = new String[size];
        list.toArray(array);

        return array;
    }

    private Object getClaim(String name, String tag, String[] claimLocales)
    {
        // If a language tag is explicitly appended.
        if (tag != null && tag.length() != 0)
        {
            // Get the claim value of the claim with the specific language tag.
            return mSpi.getUserClaim(name, tag);
        }

        // If claim locales are not specified by 'claims_locales' request parameter.
        if (claimLocales == null || claimLocales.length == 0)
        {
            // Get the claim value of the claim without any language tag.
            return mSpi.getUserClaim(name, null);
        }

        // For each claim locale. They are ordered by preference.
        for (String claimLocale : claimLocales)
        {
            // Try to get the claim value with the claim locale.
            Object value = mSpi.getUserClaim(name, claimLocale);

            // If the claim value was obtained.
            if (value != null)
            {
                return value;
            }
        }

        // The last resort. Try to get the claim value without any language tag.
        return mSpi.getUserClaim(name, null);
    }

    /**
     * Handle an end-user's decision of granting authorization to the client
     * application. This method calls Authlete's {@code
     * /api/auth/authorization/issue} API.
     *
     *

     * Note about the {@code claims} argument:
     *

     *
     *

     * A response from Authlete's {@code /api/auth/authorization} API contains
     * {@code claims} parameter which is a {@code String} array of claim names
     * such as {@code name}, {@code email} and {@code birthdate}. They are
     * claims requested by the client application. You are expected to collect
     * values of the claims and pass the collected claim key-value pairs to
     * Authlete's {@code /api/auth/authorization/issue} API. The {@code claims}
     * argument of this method is the collected claim key-value pairs.
     *

     *
     *

     * Types of claim values vary depending on claim keys. Types of most
     * standard claims (see "5.1. Standard Claims" in OpenID Connect
     * Core 1.0) are string, but types of {@code email_verified} claim
     * and {@code phone_number_verified} claim are boolean and the type of
     * {@code updated_at} claim is number. In addition, the type of {@code
     * address} claim is JSON object. The detailed format of {@code address}
     * claim is described in 5.1.1. Address Claim. {@link com.authlete.common.dto.Address
     * Address} class in authlete-java-common library can be used to represent a value of
     * {@code address} claim.
     *

     *
     *

     * The following code is an example to prepare {@code claims} argument.
     *

     *
     *

     *

     * Map<String, Object> claims = new HashMap<String, Object>();
     *
     * // Name
     * claims.put("name",        "Takahiko Kawasaki");
     * claims.put("given_name",  "Takahiko");
     * claims.put("family_name", "Kawasaki");
     *
     * // Name with a language tag.
     * // See 5.2. Claims Languages and Scripts for details.
     * claims.put("name#ja",        "\u5DDD\u5D0E \u8CB4\u5F66");  // 川崎 貴彦
     * claims.put("given_name#ja",  "\u8CB4\u5F66");               // 貴彦
     * claims.put("family_name#ja", "\u5DDD\u5D0E");               // 川崎
     *
     * // Postal address.
     * // See 5.1.1. Address Claim for details.
     * Address address = new Address()
     *     .setCountry("JP")
     *     .setRegion("Tokyo")
     *     .setLocality("Itabashi-ku")
     *     .setFormatted("Itabashi-ku, Tokyo, Japan");
     * claims.put("address", address);
     *
     * // Other information.
     * claims.put("gender",    "male");
     * claims.put("birthdate", "1974-05-06");
     * claims.put("zoneinfo",  "Asia/Tokyo");
     * claims.put("locale",    "ja");
     *
     * // FYI: Constant values in {@link com.authlete.common.types.StandardClaims
     * StandardClaims} class can be used as keys.

     *

     *
     * @param ticket
     *         A ticket that was issued by Authlete's {@code /api/auth/authorization} API.
     *
     * @param subject
     *         The subject (= unique identifier) of the end-user.
     *
     * @param authTime
     *         The time when end-user authentication occurred. The number of
     *         seconds since Unix epoch (1970-01-01). This value is used as
     *         the value of {@code auth_time} claim in an ID token that may
     *         be issued. Pass 0 if the time is unknown.
     *
     * @param acr
     *         The authentication context class reference that the end-user
     *         authentication satisfied. This value is used as the value of
     *         {@code acr} claim in an ID token that may be issued. Pass
     *         {@code null} if ACR is unknown.
     *
     * @param claims
     *         Pairs of claim key and claim value. The pairs are embedded
     *         in an ID token that may be issued. Passing {@code null} means
     *         that values of the requested claims are not available.
     *
     * @param properties
     *         Extra properties to associate with an access token and/or
     *         an authorization code.
     *
     * @param scopes
     *         Scopes to associate with an access token and/or an authorization
     *         code. If {@code null} is given, the scopes contained in the
     *         original authorization request are used. Otherwise, including
     *         the case of an empty array, the scopes given to this method
     *         replace the scopes. Note that "openid" scope is
     *         ignored if it is not included in the original authorization
     *         request.
     *
     * @return
     *         A response that should be returned to the client application.
     */
    private Response authorize(
            String ticket, String subject, long authTime, String acr,
            Map<String, Object> claims, Property[] properties, String[] scopes)
    {
        try
        {
            // Generate a redirect response containing an authorization code,
            // an access token and/or an ID token. If the original authorization
            // request had response_type=none, no tokens will be contained in
            // the generated response, though.
            return getApiCaller().authorizationIssue(
                    ticket, subject, authTime, acr, claims, properties, scopes);
        }
        catch (WebApplicationException e)
        {
            return e.getResponse();
        }
    }

    /**
     * Generate an error response to indicate that the authorization
     * request failed. This method calls Authlete's {@code
     * /api/auth/authorization/fail} API and generates a response that
     * triggers redirection.
     *

     *
     * @param ticket
     *         A ticket that was issued by Authlete's {@code /api/auth/authorization} API.
     *
     * @param reason
     *         A reason of the failure of the authorization request.
     *
     * @return
     *         A response that should be returned to the client application.
     */
    private Response fail(String ticket, Reason reason)
    {
        try
        {
            // Generate an error response to indicate that
            // the authorization request failed.
            return getApiCaller().authorizationFail(ticket, reason).getResponse();
        }
        catch (WebApplicationException e)
        {
            return e.getResponse();
        }
    }
}


/*
 * Copyright (C) 2016-2018 Authlete, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the
 * License.
 */
package com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import com.authlete.common.api.AuthleteApi;
import com.authlete.common.dto.IntrospectionResponse;
import com.authlete.common.dto.IntrospectionResponse.Action;

/**
 * Access token validator.
 *
 * @since 1.2
 *
 * @author Takahiko Kawasaki
 */
public class AccessTokenValidator extends BaseHandler
{
    private static final String CHALLENGE_ON_MISSING_ACCESS_TOKEN
        = "Bearer error=\"invalid_token\",error_description=\"An access token is missing.\"";

    /**
     * Constructor with an implementation of {@link AuthleteApi} interface.
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     */
    public AccessTokenValidator(AuthleteApi api)
    {
        super(api);
    }

    /**
     * Validate an access token. This method is an alias of {@link
     * #validate(String, String[], String)
     * validate}(accessToken, null, null).
     *
     *

     * When the given access token is not valid, this method throws a
     * {@link WebApplicationException}. The response contained in the
     * exception complies with the requirements described in RFC 6750 (The OAuth
     * 2.0 Authorization Framework: Bearer Token Usage).
     *

     *
     * @param accessToken
     *         An access token to validate.
     *
     * @return
     *         Information about the access token.
     *
     * @throws WebApplicationException
     *         The access token is invalid. To be concrete, the access
     *         token does not exist or it has expired.
     */
    public AccessTokenInfo validate(String accessToken) throws WebApplicationException
    {
        return validate(accessToken, null, null, null);
    }

    /**
     * Validate an access token. This method is an alias of {@link
     * #validate(String, String[], String)
     * validate}(accessToken, requiredScopes, null).
     *
     *

     * When the given access token is not valid, this method throws a
     * {@link WebApplicationException}. The response contained in the
     * exception complies with the requirements described in RFC 6750 (The OAuth
     * 2.0 Authorization Framework: Bearer Token Usage).
     *

     *
     * @param accessToken
     *         An access token to validate.
     *
     * @param requiredScopes
     *         Scopes that must be associated with the access token.
     *         {@code null} is okay.
     *
     * @return
     *         Information about the access token.
     *
     * @throws WebApplicationException
     *         The access token is invalid. To be concrete, one or more of
     *         the following conditions meet.
     *

     *
The access token does not exist.
     *
The access token has expired.
     *
The access token does not cover the required scopes.
     *

     */
    public AccessTokenInfo validate(String accessToken, String[] requiredScopes) throws WebApplicationException
    {
        return validate(accessToken, requiredScopes, null, null);
    }

    /**
     * Validate an access token.
     *
     *

     * When the given access token is not valid, this method throws a
     * {@link WebApplicationException}. The response contained in the
     * exception complies with the requirements described in RFC 6750 (The OAuth
     * 2.0 Authorization Framework: Bearer Token Usage).
     *

     *
     * @param accessToken
     *         An access token to validate.
     *
     * @param requiredScopes
     *         Scopes that must be associated with the access token.
     *         {@code null} is okay.
     *
     * @param requiredSubject
     *         Subject (= user's unique identifier) that must be associated
     *         with the access token. {@code null} is okay.
     *
     * @param clientCertificate
     *         TLS Certificate of the client presented during a call to
     *         the resource server, used with TLS-bound access tokens.
     *         Can be {@code null} if no certificate is presented.
     *
     * @return
     *         Information about the access token.
     *
     * @throws WebApplicationException
     *         The access token is invalid. To be concrete, one or more of
     *         the following conditions meet.
     *

     *
The access token does not exist.
     *
The access token has expired.
     *
The access token does not cover the required scopes.
     *
The access token is not associated with the required subject.
     *

     */
    public AccessTokenInfo validate(
            String accessToken, String[] requiredScopes, String requiredSubject, String clientCertificate) throws WebApplicationException
    {
        if (accessToken == null)
        {
            // Return "400 Bad Request".
            throw toException(Status.BAD_REQUEST, CHALLENGE_ON_MISSING_ACCESS_TOKEN);
        }

        try
        {
            return process(accessToken, requiredScopes, requiredSubject, clientCertificate);
        }
        catch (WebApplicationException e)
        {
            throw e;
        }
        catch (Throwable t)
        {
            // Unexpected error.
            throw unexpected("Unexpected error in AccessTokenValidator", t);
        }
    }

    private AccessTokenInfo process(
            String accessToken, String[] scopes, String subject, String clientCertificate) throws WebApplicationException
    {
        // Call Authlete's /api/auth/introspection API.
        IntrospectionResponse response = getApiCaller().callIntrospection(accessToken, scopes, subject, clientCertificate);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        Action action = response.getAction();

        // The content of the response to the client application.
        String content = response.getResponseContent();

        // Dispatch according to the action.
        switch (action)
        {
            case INTERNAL_SERVER_ERROR:
                // 500 Internal Server Error
                throw toException(Status.INTERNAL_SERVER_ERROR, content);

            case BAD_REQUEST:
                // 400 Bad Request
                throw toException(Status.BAD_REQUEST, content);

            case UNAUTHORIZED:
                // 401 Unauthorized
                throw toException(Status.UNAUTHORIZED, content);

            case FORBIDDEN:
                // 403 Forbidden
                throw toException(Status.FORBIDDEN, content);

            case OK:
                // Return access token information.
                return new AccessTokenInfo(accessToken, response);

            default:
                // This never happens.
                throw getApiCaller().unknownAction("/api/auth/userinfo", action);
        }
    }

    private WebApplicationException toException(Status status, String challenge)
    {
        Response response = ResponseUtil.bearerError(status, challenge);

        return new WebApplicationException(response);
    }
}


/*
 * Copyright (C) 2016-2018 Authlete, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the
 * License.
 */
package com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import com.authlete.common.api.AuthleteApi;
import com.authlete.common.web.BearerToken;

/**
 * A base class for protected resource endpoints.
 *
 * @since 1.2
 *
 * @author Takahiko Kawasaki
 */
public class BaseResourceEndpoint extends BaseEndpoint
{
    /**
     * Extract an access token from either {@code Authorization} header
     * or request parameters.
     *
     *

     * The first argument expects a value of {@code Authorization} header
     * that complies with RFC 6750 (Bearer Token Usage). If the argument contains an
     * access token, this method returns the access token without checking
     * the second argument.
     *

     *
     *

     * The second argument expects a value of {@code access_token}
     * request parameter. The value of this argument is returned when
     * the first argument does not contain an access token.
     *

     *
     * @param authorization
     *         A value of {@code Authorization} header whose scheme is
     *         Bearer. For example, {@code "Bearer SlAV32hkKG"}.
     *
     * @param accessTokenInRequestParameters
     *         A value of {@code access_token} request parameter.
     *
     * @return
     *         An access token.
     */
    public String extractAccessToken(String authorization, String accessTokenInRequestParameters)
    {
        // Extract an access token from the value of Authorization header.
        String accessToken = BearerToken.parse(authorization);

        // If an access token was not found in Authorization header.
        if (accessToken == null)
        {
            // Use the value given via 'access_token' request parameter.
            accessToken = accessTokenInRequestParameters;
        }

        return accessToken;
    }

    /**
     * Validate an access token. This method is an alias of {@link
     * #validateAccessToken(AuthleteApi, String, String[], String, String)
     * validateAccessToken}(api, accessToken, null, null, null).
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     *
     * @param accessToken
     *         An access token to validate.
     *
     * @return
     *         Information about the access token.
     *
     * @throws WebApplicationException
     *         The access token is invalid. To be concrete, the access
     *         token does not exist or it has expired.
     */
    public AccessTokenInfo validateAccessToken(AuthleteApi api, String accessToken) throws WebApplicationException
    {
        return validateAccessToken(api, accessToken, null, null, null);
    }

    /**
     * Validate an access token. This method is an alias of {@link
     * #validateAccessToken(AuthleteApi, String, String[], String, String)
     * validateAccessToken}(api, accessToken, requiredScopes, null, null).
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     *
     * @param accessToken
     *         An access token to validate.
     *
     * @param requiredScopes
     *         Scopes that must be associated with the access token.
     *         {@code null} is okay.
     *
     * @return
     *         Information about the access token.
     *
     * @throws WebApplicationException
     *         The access token is invalid. To be concrete, one or more of
     *         the following conditions meet.
     *

     *
The access token does not exist.
     *
The access token has expired.
     *
The access token does not cover the required scopes.
     *

     */
    public AccessTokenInfo validateAccessToken(
            AuthleteApi api, String accessToken, String[] requiredScopes) throws WebApplicationException
    {
        return validateAccessToken(api, accessToken, requiredScopes, null, null);
    }

    /**
     * Validate an access token. This method is an alias of {@link
     * #validateAccessToken(AuthleteApi, String, String[], String, String)
     * validateAccessToken}(api, accessToken, requiredScopes, requiredSubject, null).
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     *
     * @param accessToken
     *         An access token to validate.
     *
     * @param requiredScopes
     *         Scopes that must be associated with the access token.
     *         {@code null} is okay.
     *
     * @param requiredSubject
     *         Subject (= user's unique identifier) that must be associated
     *         with the access token. {@code null} is okay.
     *
     * @return
     *         Information about the access token.
     *
     * @throws WebApplicationException
     *         The access token is invalid. To be concrete, one or more of
     *         the following conditions meet.
     *

     *
The access token does not exist.
     *
The access token has expired.
     *
The access token does not cover the required scopes.
     *

     */
    public AccessTokenInfo validateAccessToken(
            AuthleteApi api, String accessToken, String[] requiredScopes, String requiredSubject) throws WebApplicationException
    {
        return validateAccessToken(api, accessToken, requiredScopes, requiredSubject, null);
    }

    /**
     * Validate an access token.
     *
     *

     * This method internally creates a {@link AccessTokenValidator} instance and
     * calls its {@link AccessTokenValidator#validate(String, String[], String, String)
     * validate()} method. Then, this method uses the value returned from the
     * {@code validate()} method as a response from this method.
     *

     *
     *

     * When {@code AccessTokenValidator.validate()} method raises a {@link
     * WebApplicationException}, this method calls {@link #onError(WebApplicationException)
     * onError()} method with the exception. The default implementation of {@code onError()}
     * calls {@code printStackTrace()} of the exception and does nothing else. You
     * can override the method as necessary. After calling {@code onError()} method,
     * this method re-throws the exception. The response contained in the exception
     * complies with the requirements described in RFC 6750 (The OAuth 2.0 Authorization
     * Framework: Bearer Token Usage).
     *

     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     *
     * @param accessToken
     *         An access token to validate.
     *
     * @param requiredScopes
     *         Scopes that must be associated with the access token.
     *         {@code null} is okay.
     *
     * @param requiredSubject
     *         Subject (= user's unique identifier) that must be associated
     *         with the access token. {@code null} is okay.
     *
     * @param clientCertificate
     *         TLS Certificate of the client presented during a call to
     *         the resource server, used with TLS-bound access tokens.
     *         Can be {@code null} if no certificate is presented.
     *
     * @return
     *         Information about the access token.
     *
     * @throws WebApplicationException
     *         The access token is invalid. To be concrete, one or more of
     *         the following conditions meet.
     *

     *
The access token does not exist.
     *
The access token has expired.
     *
The access token does not cover the required scopes.
     *
The access token is not associated with the required subject.
     *

     */
    public AccessTokenInfo validateAccessToken(
            AuthleteApi api, String accessToken, String[] requiredScopes, String requiredSubject, String clientCertificate) throws WebApplicationException
    {
        try
        {
            // Validate the access token and obtain the information about it.
            return new AccessTokenValidator(api)
                    .validate(accessToken, requiredScopes, requiredSubject, clientCertificate);
        }
        catch (WebApplicationException e)
        {
            // The access token is invalid. (Or an network error, or some others.)
            onError(e);

            throw e;
        }
    }
}


/*
 * Copyright (C) 2016 Authlete, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the
 * License.
 */
package com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;
import com.authlete.jaxrs.spi.AuthorizationDecisionHandlerSpi;

/**
 * A base class for authorization decision endpoints.
 *
 * @since 1.2
 *
 * @author Takahiko Kawasaki
 */
public class BaseAuthorizationDecisionEndpoint extends BaseEndpoint
{
    /**
     * Handle an authorization decision request.
     *
     *

     * This method internally creates a {@link AuthorizationDecisionHandler} instance and
     * calls its {@link AuthorizationDecisionHandler#handle(String, String[], String[])}
     * method. Then, this method uses the value returned from the {@code handle()} method
     * as a response from this method.
     *

     *
     *

     * When {@code AuthorizationDecisionHandler.handle()} method raises a {@link
     * WebApplicationException}, this method calls {@link #onError(WebApplicationException)
     * onError()} method with the exception. The default implementation of {@code onError()}
     * calls {@code printStackTrace()} of the exception and does nothing else. You
     * can override the method as necessary. After calling {@code onError()} method,
     * this method calls {@code getResponse()} method of the exception and uses the
     * returned value as a response from this method.
     *

     *
     * @param api
     *         An implementation of {@link AuthleteApi}.
     *
     * @param spi
     *         An implementation of {@link AuthorizationDecisionHandlerSpi}.
     *
     * @param ticket
     *         A ticket that was issued by Authlete's {@code /api/auth/authorization} API.
     *
     * @param claimNames
     *         Names of requested claims. Use the value of the {@code claims}
     *         parameter in a response from Authlete's {@code /api/auth/authorization} API.
     *
     * @param claimLocales
     *         Requested claim locales. Use the value of the {@code claimsLocales}
     *         parameter in a response from Authlete's {@code /api/auth/authorization} API.
     *
     * @return
     *         A response that should be returned to the client application.
     */
    public Response handle(
            AuthleteApi api, AuthorizationDecisionHandlerSpi spi,
            String ticket, String[] claimNames, String[] claimLocales)
    {
        try
        {
            // Create a handler.
            AuthorizationDecisionHandler handler = new AuthorizationDecisionHandler(api, spi);

            // Delegate the task to the handler.
            return handler.handle(ticket, claimNames, claimLocales);
        }
        catch (WebApplicationException e)
        {
            // An error occurred in the handler.
            onError(e);

            // Convert the error to a Response.
            return e.getResponse();
        }
    }
}


/*
 * Copyright (C) 2015-2018 Authlete, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the
 * License.
 */
package com.authlete.jaxrs;

import java.net.URI;
import java.util.List;
import java.util.Map;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import com.authlete.common.api.AuthleteApi;
import com.authlete.common.api.AuthleteApiException;
import com.authlete.common.dto.AuthorizationFailRequest;
import com.authlete.common.dto.AuthorizationFailResponse;
import com.authlete.common.dto.AuthorizationIssueRequest;
import com.authlete.common.dto.AuthorizationIssueResponse;
import com.authlete.common.dto.AuthorizationRequest;
import com.authlete.common.dto.AuthorizationResponse;
import com.authlete.common.dto.BackchannelAuthenticationCompleteRequest;
import com.authlete.common.dto.BackchannelAuthenticationCompleteRequest.Result;
import com.authlete.common.dto.BackchannelAuthenticationCompleteResponse;
import com.authlete.common.dto.BackchannelAuthenticationFailRequest;
import com.authlete.common.dto.BackchannelAuthenticationFailResponse;
import com.authlete.common.dto.BackchannelAuthenticationIssueRequest;
import com.authlete.common.dto.BackchannelAuthenticationIssueResponse;
import com.authlete.common.dto.BackchannelAuthenticationRequest;
import com.authlete.common.dto.BackchannelAuthenticationResponse;
import com.authlete.common.dto.IntrospectionRequest;
import com.authlete.common.dto.IntrospectionResponse;
import com.authlete.common.dto.Property;
import com.authlete.common.dto.RevocationRequest;
import com.authlete.common.dto.RevocationResponse;
import com.authlete.common.dto.StandardIntrospectionRequest;
import com.authlete.common.dto.StandardIntrospectionResponse;
import com.authlete.common.dto.TokenFailRequest;
import com.authlete.common.dto.TokenFailResponse;
import com.authlete.common.dto.TokenIssueRequest;
import com.authlete.common.dto.TokenIssueResponse;
import com.authlete.common.dto.TokenRequest;
import com.authlete.common.dto.TokenResponse;
import com.authlete.common.dto.UserInfoIssueRequest;
import com.authlete.common.dto.UserInfoIssueResponse;
import com.authlete.common.dto.UserInfoRequest;
import com.authlete.common.dto.UserInfoResponse;
import com.authlete.common.web.URLCoder;

/**
 * Utility class to call Authlete APIs.
 *
 * @author Takahiko Kawasaki
 */
class AuthleteApiCaller
{
    /**
     * Implementation of {@link AuthleteApi} interface.
     */
    private final AuthleteApi mApi;

    /**
     * Constructor with an implementation of {@link AuthleteApi} interface.
     */
    AuthleteApiCaller(AuthleteApi api)
    {
        mApi = api;
    }

    /**
     * Create an {@link InternalServerErrorException} instance to indicate
     * that an Authlete API call failed.
     */
    private InternalServerErrorException apiFailure(String path, AuthleteApiException e)
    {
        // Error message.
        String message = String.format("Authlete %s API failed: %s", path, e.getMessage());

        // Response body in the response from the Authlete server.
        if (e.getResponseBody() != null)
        {
            // Append the content of the response body to the error message.
            message = new StringBuilder(message)
                    .append(": ").append(e.getResponseBody()).toString();
        }

        // 500 Internal Server Error
        return internalServerError(message, e);
    }

    /**
     * Create an {@link InternalServerErrorException} instance to indicate
     * that the value of {@code action} parameter contained in a response
     * from an Authlete API is unknown.
     */
    public InternalServerErrorException unknownAction(String path, Enum<?> action)
    {
        // Error message.
        String message = String.format("Authlete %s API returned an unknown action: %s", path, action);

        // 500 Internal Server Error
        return internalServerError(message, null);
    }

    /**
     * Create an {@link InternalServerErrorException} instance having
     * a response body.
     */
    private InternalServerErrorException internalServerError(String message, Throwable cause)
    {
        Response response = ResponseUtil.internalServerError(message, MediaType.TEXT_PLAIN_TYPE);

        if (cause != null)
        {
            return new InternalServerErrorException(message, response, cause);
        }
        else
        {
            return new InternalServerErrorException(message, response);
        }
    }

    /**
     * Call Authlete's {@code /api/auth/authorization} API.
     */
    public AuthorizationResponse callAuthorization(MultivaluedMap<String, String> parameters)
    {
        String params = URLCoder.formUrlEncode(parameters);

        return callAuthorization(params);
    }

    /**
     * Call Authlete's {@code /api/auth/authorization} API.
     */
    private AuthorizationResponse callAuthorization(String parameters)
    {
        if (parameters == null)
        {
            // Authlete returns different error codes for null and an empty string.
            // 'null' is regarded as a caller's error. An empty string is regarded
            // as a client application's error.
            parameters = "";
        }

        // Create a request for Authlete's /api/auth/authorization API.
        AuthorizationRequest request = new AuthorizationRequest()
            .setParameters(parameters);

        try
        {
            // Call Authlete's /api/auth/authorization API.
            return mApi.authorization(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/auth/authorization", e);
        }
    }

    /**
     * Call Authlete's {@code /api/auth/authorization/fail} API.
     */
    private AuthorizationFailResponse callAuthorizationFail(String ticket, AuthorizationFailRequest.Reason reason)
    {
        // Create a request for /api/auth/authorization/fail API.
        AuthorizationFailRequest request = new AuthorizationFailRequest()
            .setTicket(ticket)
            .setReason(reason);

        try
        {
            // Call Authlete's /api/auth/authorization/fail API.
            return mApi.authorizationFail(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/auth/authorization/fail", e);
        }
    }

    /**
     * Create a response that describes the failure. This method
     * calls Authlete's {@code /api/auth/authorization/fail} API.
     */
    private Response createAuthorizationFailResponse(String ticket, AuthorizationFailRequest.Reason reason)
    {
        // Call Authlete's /api/auth/authorization/fail API.
        AuthorizationFailResponse response = callAuthorizationFail(ticket, reason);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        AuthorizationFailResponse.Action action = response.getAction();

        // The content of the response to the client application.
        // The format of the content varies depending on the action.
        String content = response.getResponseContent();

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

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

            case LOCATION:
                // 302 Found
                return ResponseUtil.location(content);

            case FORM:
                // 200 OK
                return ResponseUtil.form(content);

            default:
                // This never happens.
                throw unknownAction("/api/auth/authorization/fail", action);
        }
    }

    /**
     * Create an exception that describes the failure. This method
     * calls Authlete's {@code /api/auth/authorization/fail} API.
     */
    public WebApplicationException authorizationFail(String ticket, AuthorizationFailRequest.Reason reason)
    {
        // Create a response to the client application with the help of
        // Authlete's /api/auth/authorization/fail API.
        Response response = createAuthorizationFailResponse(ticket, reason);

        // Create an exception containing the response.
        return new WebApplicationException(response);
    }

    /**
     * Call Authlete's {@code /api/auth/authorization/issue} API.
     */
    private AuthorizationIssueResponse callAuthorizationIssue(
            String ticket, String subject, long authTime, String acr,
            Map<String, Object> claims, Property[] properties, String[] scopes)
    {
        // Create a request for /api/auth/authorization/issue API.
        AuthorizationIssueRequest request = new AuthorizationIssueRequest()
            .setTicket(ticket)
            .setSubject(subject)
            .setAuthTime(authTime)
            .setAcr(acr)
            .setProperties(properties)
            .setScopes(scopes)
            ;

        if (claims != null && claims.size() != 0)
        {
            request.setClaims(claims);
        }

        try
        {
            // Call Authlete's /api/auth/authorization/issue API.
            return mApi.authorizationIssue(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/auth/authorization/issue", e);
        }
    }

    /**
     * Issue an authorization code, an ID token and/or an access token.
     * This method calls Authlete's {@code /api/auth/authorization/issue} API.
     */
    public Response authorizationIssue(
            String ticket, String subject, long authTime, String acr,
            Map<String, Object> claims, Property[] properties, String[] scopes)
    {
        // Call Authlete's /api/auth/authorization/issue API.
        AuthorizationIssueResponse response =
            callAuthorizationIssue(ticket, subject, authTime, acr, claims, properties, scopes);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        AuthorizationIssueResponse.Action action = response.getAction();

        // The content of the response to the client application.
        // The format of the content varies depending on the action.
        String content = response.getResponseContent();

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

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

            case LOCATION:
                // 302 Found
                return ResponseUtil.location(content);

            case FORM:
                // 200 OK
                return ResponseUtil.form(content);

            default:
                // This never happens.
                throw unknownAction("/api/auth/authorization/issue", action);
        }
    }

    /**
     * Call Authlete's {@code /api/auth/token} API.
     */
    public TokenResponse callToken(
            MultivaluedMap<String, String> parameters, String clientId, String clientSecret,
            Property[] properties, String clientCertificate, String[] clientCertificatePath)
    {
        String params = URLCoder.formUrlEncode(parameters);

        return callToken(params, clientId, clientSecret, properties, clientCertificate, clientCertificatePath);
    }

    /**
     * Call Authlete's {@code /api/auth/token} API.
     */
    private TokenResponse callToken(
            String parameters, String clientId, String clientSecret, Property[] properties, String clientCertificate, String[] clientCertificatePath)
    {
        if (parameters == null)
        {
            // Authlete returns different error codes for null and an empty string.
            // 'null' is regarded as a caller's error. An empty string is regarded
            // as a client application's error.
            parameters = "";
        }

        // Create a request for Authlete's /api/auth/token API.
        TokenRequest request = new TokenRequest()
            .setParameters(parameters)
            .setClientId(clientId)
            .setClientSecret(clientSecret)
            .setProperties(properties)
            .setClientCertificate(clientCertificate)
            .setClientCertificatePath(clientCertificatePath)
            ;

        try
        {
            // Call Authlete's /api/auth/token API.
            return mApi.token(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/auth/token", e);
        }
    }

    /**
     * Call Authlete's {@code /api/auth/token/fail} API.
     */
    private TokenFailResponse callTokenFail(String ticket, TokenFailRequest.Reason reason)
    {
        // Create a request for /api/auth/token/fail API.
        TokenFailRequest request = new TokenFailRequest()
            .setTicket(ticket)
            .setReason(reason);

        try
        {
            // Call Authlete's /api/auth/token/fail API.
            return mApi.tokenFail(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/auth/token/fail", e);
        }
    }

    /**
     * Create a response that describes the failure. This method
     * calls Authlete's {@code /api/auth/token/fail} API.
     */
    private Response createTokenFailResponse(String ticket, TokenFailRequest.Reason reason)
    {
        // Call Authlete's /api/auth/token/fail API.
        TokenFailResponse response = callTokenFail(ticket, reason);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        TokenFailResponse.Action action = response.getAction();

        // The content of the response to the client application.
        String content = response.getResponseContent();

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

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

            default:
                // This never happens.
                throw unknownAction("/api/auth/token/fail", action);
        }
    }

    /**
     * Create an exception that describes the failure. This method
     * calls Authlete's {@code /api/auth/token/fail} API.
     */
    public WebApplicationException tokenFail(String ticket, TokenFailRequest.Reason reason)
    {
        // Create a response to the client application with the help of
        // Authlete's /api/auth/token/fail API.
        Response response = createTokenFailResponse(ticket, reason);

        // Create an exception containing the response.
        return new WebApplicationException(response);
    }

    /**
     * Call Authlete's {@code /api/auth/token/issue} API.
     */
    private TokenIssueResponse callTokenIssue(
            String ticket, String subject, Property[] properties)
    {
        // Create a request for Authlete's /api/auth/token/issue API.
        TokenIssueRequest request = new TokenIssueRequest()
            .setTicket(ticket)
            .setSubject(subject)
            .setProperties(properties)
            ;

        try
        {
            // Call Authlete's /api/auth/token/issue API.
            return mApi.tokenIssue(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/auth/token/issue", e);
        }
    }

    /**
     * Issue an access token and optionally an ID token.
     * This method calls Authlete's {@code /api/auth/token/issue} API.
     */
    public Response tokenIssue(String ticket, String subject, Property[] properties)
    {
        // Call Authlete's /api/auth/token/issue API.
        TokenIssueResponse response = callTokenIssue(ticket, subject, properties);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        TokenIssueResponse.Action action = response.getAction();

        // The content of the response to the client application.
        String content = response.getResponseContent();

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

            case OK:
                // 200 OK
                return ResponseUtil.ok(content);

            default:
                // This never happens.
                throw unknownAction("/api/auth/token/issue", action);
        }
    }

    /**
     * Call Authlete's {@code /api/service/configuration} API.
     */
    public String callServiceConfiguration(boolean pretty)
    {
        try
        {
            // Call Authlete's /api/service/configuration API.
            return mApi.getServiceConfiguration(pretty);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/service/configuration", e);
        }
    }

    /**
     * Get the JWK Set of the service. This method calls Authlete's
     * {@code /api/service/jwks/get} API.
     */
    public Response serviceJwksGet(
            boolean pretty, boolean includePrivateKeys) throws AuthleteApiException
    {
        try
        {
            // Call Authlete's /api/service/jwks/get API.
            String jwks = mApi.getServiceJwks(pretty, includePrivateKeys);

            if (jwks == null || jwks.length() == 0)
            {
                // 204 No Content
                return ResponseUtil.noContent();
            }

            // Response as "application/json;charset=UTF-8" with 200 OK.
            return ResponseUtil.ok(jwks);
        }
        catch (AuthleteApiException e)
        {
            // If the status code is not 302 Found.
            if (e.getStatusCode() != Status.FOUND.getStatusCode())
            {
                // The API call failed.
                throw apiFailure("/api/service/jwks/get", e);
            }

            // The value of 'Location' header.
            String location = getFirst(e.getResponseHeaders(), "Location");

            // 302 Found with Location header.
            return ResponseUtil.location(location);
        }
    }

    private static String getFirst(Map<String, List<String>> map, String key)
    {
        if (map == null)
        {
            return null;
        }

        List<String> list = map.get(key);

        if (list == null || list.size() == 0)
        {
            return null;
        }

        return list.get(0);
    }

    /**
     * Call Authlete's {@code /api/auth/revocation} API.
     */
    public RevocationResponse callRevocation(MultivaluedMap<String, String> parameters, String clientId, String clientSecret)
    {
        String params = URLCoder.formUrlEncode(parameters);

        return callRevocation(params, clientId, clientSecret);
    }

    /**
     * Call Authlete's {@code /api/auth/revocation} API.
     */
    private RevocationResponse callRevocation(String parameters, String clientId, String clientSecret)
    {
        if (parameters == null)
        {
            // Authlete returns different error codes for null and an empty string.
            // 'null' is regarded as a caller's error. An empty string is regarded
            // as a client application's error.
            parameters = "";
        }

        // Create a request for Authlete's /api/auth/revocation API.
        RevocationRequest request = new RevocationRequest()
            .setParameters(parameters)
            .setClientId(clientId)
            .setClientSecret(clientSecret);

        try
        {
            // Call Authlete's /api/auth/revocation API.
            return mApi.revocation(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/auth/revocation", e);
        }
    }

    /**
     * Call Authlete's {@code /api/auth/userinfo} API.
     */
    public UserInfoResponse callUserInfo(String accessToken)
    {
        // Create a request for Authlete's /api/auth/userinfo API.
        UserInfoRequest request = new UserInfoRequest()
            .setToken(accessToken);

        try
        {
            // Call Authlete's /api/auth/userinfo API.
            return mApi.userinfo(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/auth/userinfo", e);
        }
    }

    /**
     * Call Authlete's {@code /api/auth/userinfo/issue} API.
     */
    private UserInfoIssueResponse callUserInfoIssue(String accessToken, Map<String, Object> claims)
    {
        // Create a request for /api/auth/userinfo/issue API.
        UserInfoIssueRequest request = new UserInfoIssueRequest()
            .setToken(accessToken);

        if (claims != null && claims.size() != 0)
        {
            request.setClaims(claims);
        }

        try
        {
            // Call Authlete's /api/auth/userinfo/issue API.
            return mApi.userinfoIssue(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/auth/userinfo/issue", e);
        }
    }

    /**
     * Issue a JSON or a JWT containing user information.
     */
    public Response userInfoIssue(String accessToken, Map<String, Object> claims)
    {
        // Call Authlete's /api/auth/userinfo/issue API.
        UserInfoIssueResponse response = callUserInfoIssue(accessToken, claims);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        UserInfoIssueResponse.Action action = response.getAction();

        // The content of the response to the client application.
        // The format of the content varies depending on the action.
        String content = response.getResponseContent();

        // Dispatch according to the action.
        switch (action)
        {
            case INTERNAL_SERVER_ERROR:
                // 500 Internal Server Error
                return ResponseUtil.bearerError(Status.INTERNAL_SERVER_ERROR, content);

            case BAD_REQUEST:
                // 400 Bad Request
                return ResponseUtil.bearerError(Status.BAD_REQUEST, content);

            case UNAUTHORIZED:
                // 401 Unauthorized
                return ResponseUtil.bearerError(Status.UNAUTHORIZED, content);

            case FORBIDDEN:
                // 403 Forbidden
                return ResponseUtil.bearerError(Status.FORBIDDEN, content);

            case JSON:
                // 200 OK, application/json;charset=UTF-8
                return ResponseUtil.ok(content);

            case JWT:
                // 200 OK, application/jwt
                return ResponseUtil.ok(content, ResponseUtil.MEDIA_TYPE_JWT);

            default:
                // This never happens.
                throw unknownAction("/api/auth/userinfo/issue", action);
        }
    }

    /**
     * Call Authlete's {@code /api/auth/introspection} API.
     */
    public IntrospectionResponse callIntrospection(String accessToken, String[] scopes, String subject, String clientCertificate)
    {
        // Create a request for /api/auth/introspection API.
        IntrospectionRequest request = new IntrospectionRequest()
            .setToken(accessToken)
            .setScopes(scopes)
            .setSubject(subject)
            .setClientCertificate(clientCertificate);

        try
        {
            // Call Authlete's /api/auth/introspection API.
            return mApi.introspection(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/auth/introspection", e);
        }
    }

    /**
     * Call Authlete's {@code /api/auth/introspection/standard} API.
     */
    public StandardIntrospectionResponse callStandardIntrospection(MultivaluedMap<String, String> parameters)
    {
        String params = URLCoder.formUrlEncode(parameters);

        return callStandardIntrospection(params);
    }

    /**
     * Call Authlete's {@code /api/auth/introspection/standard} API.
     */
    private StandardIntrospectionResponse callStandardIntrospection(String parameters)
    {
        if (parameters == null)
        {
            // Authlete returns different error codes for null and an empty string.
            // 'null' is regarded as a caller's error. An empty string is regarded
            // as a client application's error.
            parameters = "";
        }

        // Create a request for Authlete's /api/auth/introspection/standard API.
        StandardIntrospectionRequest request = new StandardIntrospectionRequest()
            .setParameters(parameters);

        try
        {
            // Call Authlete's /api/auth/introspection/standard API.
            return mApi.standardIntrospection(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/auth/introspection/standard", e);
        }
    }

    /**
     * Call Authlete's {@code /api/backchannel/authentication} API.
     */
    public BackchannelAuthenticationResponse callBackchannelAuthentication(
            MultivaluedMap<String, String> parameters, String clientId, String clientSecret,
            String clientCertificate, String[] clientCertificatePath)
    {
        String params = URLCoder.formUrlEncode(parameters);

        return callBackchannelAuthentication(params, clientId, clientSecret, clientCertificate, clientCertificatePath);
    }

    /**
     * Call Authlete's {@code /api/backchannel/authentication} API.
     */
    private BackchannelAuthenticationResponse callBackchannelAuthentication(
            String parameters, String clientId, String clientSecret,
            String clientCertificate, String[] clientCertificatePath)
    {
        if (parameters == null)
        {
            // Authlete returns different error codes for null and an empty string.
            // 'null' is regarded as a caller's error. An empty string is regarded
            // as a client application's error.
            parameters = "";
        }

        // Create a request for Authlete's /api/backchannel/authentication API.
        BackchannelAuthenticationRequest request = new BackchannelAuthenticationRequest()
            .setParameters(parameters)
            .setClientId(clientId)
            .setClientSecret(clientSecret)
            .setClientCertificate(clientCertificate)
            .setClientCertificatePath(clientCertificatePath)
            ;

        try
        {
            // Call Authlete's /api/backchannel/authentication API.
            return mApi.backchannelAuthentication(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/backchannel/authentication", e);
        }
    }

    /**
     * Call Authlete's {@code /api/backchannel/authentication/fail} API.
     */
    private BackchannelAuthenticationFailResponse callBackchannelAuthenticationFail(String ticket, BackchannelAuthenticationFailRequest.Reason reason)
    {
        // Create a request for /api/backchannel/authentication/fail API.
        BackchannelAuthenticationFailRequest request = new BackchannelAuthenticationFailRequest()
            .setTicket(ticket)
            .setReason(reason)
            ;

        try
        {
            // Call Authlete's /api/backchannel/authentication/fail API.
            return mApi.backchannelAuthenticationFail(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/backchannel/authentication/fail", e);
        }
    }

    /**
     * Create a response that describes the failure. This method
     * calls Authlete's {@code /api/backchannel/authentication/fail} API.
     */
    private Response createBackchannelAuthenticationFailResponse(String ticket, BackchannelAuthenticationFailRequest.Reason reason)
    {
        // Call Authlete's /api/backchannel/authentication/fail API.
        BackchannelAuthenticationFailResponse response = callBackchannelAuthenticationFail(ticket, reason);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        BackchannelAuthenticationFailResponse.Action action = response.getAction();

        // The content of the response to the client application.
        // The format of the content varies depending on the action.
        String content = response.getResponseContent();

        // Dispatch 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:
                // This never happens.
                throw unknownAction("/api/backchannel/authentication/fail", action);
        }
    }

    /**
     * Create an exception that describes the failure. This method
     * calls Authlete's {@code /api/backchannel/authentication/fail} API.
     */
    public WebApplicationException backchannelAuthenticationFail(String ticket, BackchannelAuthenticationFailRequest.Reason reason)
    {
        // Create a response to the client application with the help of
        // Authlete's /api/backchannel/authentication/fail API.
        Response response = createBackchannelAuthenticationFailResponse(ticket, reason);

        // Create an exception containing the response.
        return new WebApplicationException(response);
    }

    /**
     * Call Authlete's {@code /api/backchannel/authentication/issue} API.
     */
    public BackchannelAuthenticationIssueResponse callBackchannelAuthenticationIssue(String ticket)
    {
        // Create a request for /api/backchannel/authentication/issue API.
        BackchannelAuthenticationIssueRequest request = new BackchannelAuthenticationIssueRequest()
            .setTicket(ticket)
            ;

        try
        {
            // Call Authlete's /api/backchannel/authentication/issue API.
            return mApi.backchannelAuthenticationIssue(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/backchannel/authentication/issue", e);
        }
    }

    /**
     * Call Authlete's {@code /api/backchannel/authentication/complete} API.
     */
    public BackchannelAuthenticationCompleteResponse callBackchannelAuthenticationComplete(
            String ticket, String subject, Result result, long authTime, String acr,
            Map<String, Object> claims, Property[] properties, String[] scopes,
            String errorDescription, URI errorUri)
    {
        // Create a request for /api/backchannel/authentication/complete API.
        BackchannelAuthenticationCompleteRequest request = new BackchannelAuthenticationCompleteRequest()
            .setTicket(ticket)
            .setSubject(subject)
            .setResult(result)
            .setAuthTime(authTime)
            .setAcr(acr)
            .setProperties(properties)
            .setScopes(scopes)
            .setErrorDescription(errorDescription)
            .setErrorUri(errorUri)
            ;

        if (claims != null && claims.size() != 0)
        {
            request.setClaims(claims);
        }

        try
        {
            // Call Authlete's /api/backchannel/authentication/complete API.
            return mApi.backchannelAuthenticationComplete(request);
        }
        catch (AuthleteApiException e)
        {
            // The API call failed.
            throw apiFailure("/api/backchannel/authentication/complete", e);
        }
    }
}


/*
 * Copyright (C) 2016 Authlete, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the
 * License.
 */
package com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;

/**
 * Handler for requests to an OpenID Provider configuration endpoint.
 *
 *

 * An OpenID Provider that supports OpenID Connect
 * Discovery 1.0 must provide an endpoint that returns its configuration
 * information in a JSON format. Details about the format are described in
 * "3. OpenID Provider Metadata" in OpenID Connect Discovery 1.0.
 *

 *
 *

 * In an implementation of configuration endpoint, call {@link #handle()} method
 * and use the response as the response from the endpoint to the client application.
 *

 *
 *

 * Note that the URI of an OpenID Provider configuration endpoint is defined in
 * "4.1. OpenID Provider Configuration Request" in OpenID Connect Discovery
 * 1.0. In short, the URI must be:
 *

 *
 *

 * Issuer Identifier + {@code /.well-known/openid-configuration}
 *

 *
 *

 * Issuer Identifier is a URL to identify an OpenID Provider. For example,
 * {@code https://example.com}. For details about Issuer Identifier, See {@code issuer}
 * in "3. OpenID Provider Metadata" (OpenID Connect Discovery 1.0) and {@code iss} in
 * "2. ID Token"
 * (OpenID Connect Core 1.0).
 *

 *
 *

 * You can change the Issuer Identifier of your service using the management console
 * (Service Owner Console).
 * Note that the default value of Issuer Identifier is not appropriate for commercial
 * use, so you should change it.
 *

 *
 * @since 1.1
 *
 * @author Takahiko Kawasaki
 */
public class ConfigurationRequestHandler extends BaseHandler
{
    /**
     * Constructor with an implementation of {@link AuthleteApi} interface.
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     */
    public ConfigurationRequestHandler(AuthleteApi api)
    {
        super(api);
    }

    /**
     * Handle a request to an OpenID Provider configuration endpoint. This
     * method is an alias of {@link #handle(boolean) handle}{@code (true)}.
     *
     * @return
     *         A response that should be returned from the endpoint to
     *         the client application.
     *
     * @throws WebApplicationException
     *         An error occurred.
     */
    public Response handle() throws WebApplicationException
    {
        return handle(true);
    }

    /**
     * Handle a request to an OpenID Provider configuration endpoint. This
     * method internally calls Authlete's {@code /api/service/configuration}
     * API.
     *
     * @param pretty
     *         {@code true} to return the output JSON in pretty format.
     *
     * @return
     *         A response that should be returned from the endpoint to
     *         the client application.
     *
     * @throws WebApplicationException
     *         An error occurred.
     */
    public Response handle(boolean pretty) throws WebApplicationException
    {
        try
        {
            // Call Authlete's /api/service/configuration API.
            // The API returns a JSON that complies with
            // OpenID Connect Discovery 1.0.
            String json = getApiCaller().callServiceConfiguration(pretty);

            // Response as "application/json;charset=UTF-8" with 200 OK.
            return ResponseUtil.ok(json);
        }
        catch (WebApplicationException e)
        {
            // The API call raised an exception.
            throw e;
        }
        catch (Throwable t)
        {
            // Unexpected error.
            throw unexpected("Unexpected error in ConfigurationRequestHandler", t);
        }
    }
}


/*
 * Copyright (C) 2019 Authlete, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the
 * License.
 */
package com.authlete.jaxrs;

import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;
import com.authlete.common.dto.BackchannelAuthenticationCompleteResponse;
import com.authlete.common.dto.Property;
import com.authlete.common.dto.BackchannelAuthenticationCompleteRequest.Result;
import com.authlete.jaxrs.spi.BackchannelAuthenticationCompleteRequestHandlerSpi;

/**
 * Handler for the result of end-user authentication and authorization in CIBA
 * (Client Initiated Backchannel Authentication) flow.
 *
 * <p>
 * {@link #handle(String, String[]) handle()} method should be called after the
 * authorization server receives the result of end-user authentication and authorization
 * from the authentication device, or even in the case where the server gave up
 * receiving a response from the authentication device for some reasons. {@code handle()}
 * method calls Authlete's {@code /api/backchannel/authentication/complete} API,
 * receives a response from the API, and dispatches processing according to the
 * {@code action} parameter in the response.
 * </p>
 *
 * @author Hideki Ikeda
 *
 * @since 2.13
 */
public class BackchannelAuthenticationCompleteRequestHandler extends BaseHandler
{
    /**
     * Implementation of {@link BackchannelAuthenticationCompleteRequestHandlerSpi}
     * interface.
     */
    private final BackchannelAuthenticationCompleteRequestHandlerSpi mSpi;

    /**
     * Constructor with an implementation of {@link AuthleteApi} interface
     * and an implementation of {@link BackchannelAuthenticationCompleteRequestHandlerSpi} interface.
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     *
     * @param spi
     *         Implementation of {@link BackchannelAuthenticationCompleteRequestHandlerSpi} interface.
     */
    public BackchannelAuthenticationCompleteRequestHandler(AuthleteApi api, BackchannelAuthenticationCompleteRequestHandlerSpi spi)
    {
        super(api);

        mSpi = spi;
    }

    /**
     * Handle the result of end-user authentication and authorization in CIBA
     * (Client Initiated Backchannel Authentication) flow.
     *
     * @param ticket
     *         A ticket that was issued by Authlete's {@code /api/backchannel/authentication}
     *         API.
     *
     * @param claimNames
     *         Names of requested claims. Use the value of the {@code claims}
     *         parameter in a response from Authlete's {@code /api/backchannel/authentication}
     *         API.
     *
     * @throws WebApplicationException
     *         An error occurred.
     */
    public void handle(String ticket, String[] claimNames) throws WebApplicationException
    {
        try
        {
            // Process the given parameters.
            process(ticket, claimNames);
        }
        catch (WebApplicationException e)
        {
            // Authlete API error or cd communication error.
            throw e;
        }
        catch (Throwable t)
        {
            // Unexpected error.
            throw unexpected("Unexpected error in BackchannelAuthenticationCompleteRequestHandler", t);
        }
    }

    private void process(String ticket, String[] claimNames)
    {
        // Complete the process with the result of end-user authentication and
        // authorization.
        BackchannelAuthenticationCompleteResponse response = complete(ticket, claimNames);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        BackchannelAuthenticationCompleteResponse.Action action = response.getAction();

        // Dispatch according to the action.
        switch (action)
        {
            case SERVER_ERROR:
                // Server error.
                handleServerError(response);
                return;

            case NO_ACTION:
                // No action is required. This happens when the backchannel token
                // delivery mode is "poll".
                return;

            case NOTIFICATION:
                // Send a notification to the client. This happens when the backchannel
                // token delivery mode is "ping" or "push".
                handleNotification(response);
                return;

            default:
                // This never happens.
                throw getApiCaller().unknownAction("/api/backchannel/authentication/complete", action);
        }
    }

    private BackchannelAuthenticationCompleteResponse complete(String ticket, String[] claimNames)
    {
        // Get the authentication result.
        Result result = mSpi.getResult();

        // Get the subject of the user.
        String subject = mSpi.getUserSubject();

        if (result != Result.AUTHORIZED)
        {
            // Get the description of the error.
            String errorDescription = mSpi.getErrorDescription();

            // Get the URI of a document which describes the error in detail.
            URI errorUri = mSpi.getErrorUri();

            // The end-user authorization has not been successfully done.
            // Then, complete the process with failure.
            return fail(ticket, subject, result, errorDescription, errorUri);
        }

        // OK. The end-user has successfully authorized the client.

        // Get the authentication time.
        long authTime = mSpi.getUserAuthenticatedAt();

        // Collect the user's claims.
        Map<String, Object> claims = collectClaims(claimNames);

        // Get the acr value that was actually used.
        String acr = mSpi.getAcr();

        // Scopes to associate with an access token.
        // If a non-null value is returned from mSpi.getScopes(), the scope set
        // replaces the scopes that have been specified in the original
        // backchannel authentication request.
        String[] scopes = mSpi.getScopes();

        // Properties to associate with an access token.
        Property[] properties = mSpi.getProperties();

        // Complete the process with successful authorization.
        return authorize(ticket, subject, authTime, acr, claims, properties, scopes);
    }

    /**
     * Call Authlete's {@code /api/backchannel/authentication/complete} API with an unsuccessful
     * result.
     *
     * @param ticket
     *         A ticket that was issued by Authlete's {@code /api/backchannel/authentication}
     *         API.
     *
     * @param subject
     *         The subject of the authenticated user.
     *
     * @param result
     *         The result of end-user authentication and authorization.
     *
     * @param errorDescription
     *         The description of the error.
     *
     * @param errorUri
     *         The URI of a document which describes the error in detail.
     *
     * @return
     *         A response from Authlete's {@code /api/backchannel/authentication/complete}
     *         API.
     */
    private BackchannelAuthenticationCompleteResponse fail(
            String ticket, String subject, Result result, String errorDescription,
            URI errorUri)
    {
        return callBackchannelAuthenticationComplete(
                ticket, subject, result, 0, null, null, null, null, errorDescription,
                errorUri);
    }

    /**
     * Call Authlete's {@code /api/backchannel/authentication/complete} API with
     * successful authorization.
     *
     * @param ticket
     *         A ticket that was issued by Authlete's {@code /api/backchannel/authentication}
     *         API.
     *
     * @param subject
     *         The subject (= unique identifier) of the end-user.
     *
     * @param authTime
     *         The time when end-user authentication occurred. The number of
     *         seconds since Unix epoch (1970-01-01). This value is used as
     *         the value of {@code auth_time} claim in an ID token that may
     *         be issued. Pass 0 if the time is unknown.
     *
     * @param acr
     *         The authentication context class reference that the end-user
     *         authentication satisfied. This value is used as the value of
     *         {@code acr} claim in an ID token that may be issued. Pass
     *         {@code null} if ACR is unknown.
     *
     * @param claims
     *         Pairs of claim key and claim value. The pairs are embedded
     *         in an ID token that may be issued. Passing {@code null} means
     *         that values of the requested claims are not available.
     *
     * @param properties
     *         Extra properties to associate with an access token.
     *
     * @param scopes
     *         Scopes to associate with an access token. If {@code null} is given,
     *         the scopes contained in the original backchannel authentication
     *         request are used. Otherwise, the scopes given to this method replace
     *         the scopes.
     *
     * @return
     *         A response from Authlete's {@code /api/backchannel/authentication/complete}
     *         API.
     */
    private BackchannelAuthenticationCompleteResponse authorize(
            String ticket, String subject, long authTime, String acr, Map<String, Object> claims,
            Property[] properties, String[] scopes)
    {
        return callBackchannelAuthenticationComplete(
                ticket, subject, Result.AUTHORIZED, authTime, acr, claims, properties,
                scopes, null, null);
    }

    private BackchannelAuthenticationCompleteResponse callBackchannelAuthenticationComplete(
            String ticket, String subject, Result result, long authTime, String acr,
            Map<String, Object> claims, Property[] properties, String[] scopes,
            String errorDescription, URI errorUri)
    {
        return getApiCaller().callBackchannelAuthenticationComplete(
                ticket, subject, result, authTime, acr, claims, properties, scopes,
                errorDescription, errorUri);
    }

    private void handleServerError(BackchannelAuthenticationCompleteResponse response)
    {
        // The result message.
        String message = response.getResultMessage();

        // Create an error response.
        Response errorResponse = ResponseUtil.internalServerError(message, MediaType.TEXT_PLAIN_TYPE);

        // Throw an internal server error exception.
        throw new InternalServerErrorException(errorResponse);
    }

    private Map<String, Object> collectClaims(String[] requestedClaimNames)
    {
        if (requestedClaimNames == null || requestedClaimNames.length == 0)
        {
            // No claim is requested by the client.
            return null;
        }

        // Claim values.
        Map<String, Object> claims = new HashMap<String, Object>();

        for (String name : requestedClaimNames)
        {
            if (name == null || name.length() == 0)
            {
                continue;
            }

            Object value = mSpi.getUserClaim(name);

            if (value == null)
            {
                continue;
            }

            claims.put(name, value);
        }

        if (claims.size() == 0)
        {
            return null;
        }

        return claims;
    }

    private void handleNotification(BackchannelAuthenticationCompleteResponse bacRes)
    {
        // Send the notification to the client.
        mSpi.sendNotification(bacRes);
    }
}