All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.authlete.jaxrs.AuthorizationDecisionHandler Maven / Gradle / Ivy

/*
 * 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 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 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 claims = new HashMap(); // 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 set = new TreeSet(String.CASE_INSENSITIVE_ORDER); // Normalized list. List list = new ArrayList(); // 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 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(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy