
com.authlete.jaxrs.UserInfoRequestHandler Maven / Gradle / Ivy
Show all versions of authlete-java-jaxrs Show documentation
/*
* Copyright (C) 2016-2025 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.io.Serializable;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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.api.Options;
import com.authlete.common.assurance.VerifiedClaims;
import com.authlete.common.assurance.constraint.VerifiedClaimsConstraint;
import com.authlete.common.assurance.constraint.VerifiedClaimsContainerConstraint;
import com.authlete.common.dto.Pair;
import com.authlete.common.dto.Service;
import com.authlete.common.dto.StringArray;
import com.authlete.common.dto.UserInfoResponse;
import com.authlete.common.dto.UserInfoResponse.Action;
import com.authlete.jaxrs.spi.UserInfoRequestHandlerSpi;
/**
* Handler for userinfo requests to a UserInfo Endpoint defined in OpenID Connect
* Core 1.0.
*
*
* In an implementation of userinfo endpoint, call one of {@code handle()}
* method variants and use the response as the response from the endpoint
* to the client application. {@code handle()} method calls Authlete's
* {@code /api/auth/userinfo} API and {@code /api/auth/userinfo/issue} API.
*
*
* @since 1.2
*
* @author Takahiko Kawasaki
*/
public class UserInfoRequestHandler extends BaseHandler
{
/**
* Parameters passed to the {@link UserInfoRequestHandler#handle(Params)}
* method.
*
* @since 2.27
*/
public static class Params implements Serializable
{
private static final long serialVersionUID = 3L;
private String accessToken;
private String clientCertificate;
private String dpop;
private String htm;
private String htu;
private boolean oldIdaFormatUsed;
private URI targetUri;
private Pair[] headers;
private boolean requestBodyContained;
private boolean dpopNonceRequired;
/**
* Get the access token included in the userinfo request.
*
* @return
* The access token.
*/
public String getAccessToken()
{
return accessToken;
}
/**
* Set the access token included in the userinfo request.
*
* @param accessToken
* The access token.
*
* @return
* {@code this} object.
*/
public Params setAccessToken(String accessToken)
{
this.accessToken = accessToken;
return this;
}
/**
* Get the client certificate included in the userinfo request.
*
* @return
* The client certificate.
*
* @see RFC 8705 : OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
*/
public String getClientCertificate()
{
return clientCertificate;
}
/**
* Set the client certificate included in the userinfo request.
*
* @param clientCertificate
* The client certificate.
*
* @return
* {@code this} object.
*
* @see RFC 8705 : OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
*/
public Params setClientCertificate(String clientCertificate)
{
this.clientCertificate = clientCertificate;
return this;
}
/**
* Get the DPoP proof JWT (the value of the {@code DPoP} HTTP header).
*
*
* See "OAuth 2.0 Demonstration of Proof-of-Possession at the
* Application Layer (DPoP)" for details.
*
*
* @return
* The DPoP proof JWT.
*/
public String getDpop()
{
return dpop;
}
/**
* Set the DPoP proof JWT (the value of the {@code DPoP} HTTP header).
*
*
* See "OAuth 2.0 Demonstration of Proof-of-Possession at the
* Application Layer (DPoP)" for details.
*
*
* @param dpop
* The DPoP proof JWT.
*
* @return
* {@code this} object.
*/
public Params setDpop(String dpop)
{
this.dpop = dpop;
return this;
}
/**
* Get the HTTP method of the userinfo request.
*
* @return
* The HTTP method of the userinfo request.
*/
public String getHtm()
{
return htm;
}
/**
* Set the HTTP method of the userinfo request.
*
*
* The value should be either {@code "GET"} or {@code "POST"} unless
* new specifications allowing other HTTP methods at the userinfo
* endpoint are developed.
*
*
*
* The value passed here will be used to validate the DPoP proof JWT.
* See "OAuth 2.0 Demonstration of Proof-of-Possession at the
* Application Layer (DPoP)" for details.
*
*
* @param htm
* The HTTP method of the userinfo request.
*
* @return
* {@code this} object.
*/
public Params setHtm(String htm)
{
this.htm = htm;
return this;
}
/**
* Get the URL of the userinfo endpoint.
*
* @return
* The URL of the userinfo endpoint.
*/
public String getHtu()
{
return htu;
}
/**
* Set the URL of the userinfo endpoint.
*
*
* If this parameter is omitted, the {@code userInfoEndpoint} property
* of {@link Service} will be used as the default value.
*
*
*
* The value passed here will be used to validate the DPoP proof JWT.
* See "OAuth 2.0 Demonstration of Proof-of-Possession at the
* Application Layer (DPoP)" for details.
*
*
* @param htu
* The URL of the userinfo endpoint.
*
* @return
* {@code this} object.
*/
public Params setHtu(String htu)
{
this.htu = htu;
return this;
}
/**
* Get the flag indicating whether {@link UserInfoRequestHandler}
* uses the old format of {@code "verified_claims"} defined in the
* Implementer's Draft 2 of OpenID Connect for Identity Assurance 1.0
* which was published on May 19, 2020.
*
*
* When this flag is on, {@link UserInfoRequestHandler} calls the
* {@link UserInfoRequestHandlerSpi#getVerifiedClaims(String,
* VerifiedClaimsConstraint) getVerifiedClaims(String, VerifiedClaimsConstraint)}
* method of {@link UserInfoRequestHandlerSpi}. On the other hand,
* if this flag is off, the
* {@link UserInfoRequestHandlerSpi#getVerifiedClaims(String, Object)
* getVerifiedClaims(String, Object)} method is called instead.
* This is a breaking change from
* authlete-java-jaxrs version 2.41. This flag exists to mitigate
* the breaking change.
*
*
*
* The Implementer's Draft 3 of OpenID Connect for Identity Assurance 1.0,
* which was published on September 6, 2021, made many breaking changes.
* In addition, it is certain that further breaking changes will be made
* in the next draft. Considering the instability of the specification,
* it is not a good approach to define Java classes that correspond to
* elements in {@code "verified_claims"}. The
* {@code com.authlete.common.assurance} package in the authlete-java-common
* library was developed based on the approach for the Implementer's
* Draft 2, but it is not useful any more. This is the reason the
* {@link UserInfoRequestHandlerSpi#getVerifiedClaims(String,
* VerifiedClaimsConstraint) getVerifiedClaims(String,
* VerifiedClaimsConstraint)} method (whose second argument is an
* instance of {@link VerifiedClaimsConstraint} which is defined in
* the {@code com.authlete.common.assurance.constraint} package) was
* marked as deprecated.
*
*
* @return
* {@code true} if {@link UserInfoRequestHandler} calls
* {@link UserInfoRequestHandlerSpi#getVerifiedClaims(String,
* VerifiedClaimsConstraint) getVerifiedClaims(String,
* VerifiedClaimsConstraint)} method of
* {@link UserInfoRequestHandlerSpi}. {@code false} if
* {@link UserInfoRequestHandler} calls
* {@link UserInfoRequestHandlerSpi#getVerifiedClaims(String, Object)
* getVerifiedClaims(String, Object)} method instead.
*
* @since 2.42
*
* @see OpenID Connect for Identity Assurance 1.0
*/
public boolean isOldIdaFormatUsed()
{
return oldIdaFormatUsed;
}
/**
* Set the flag indicating whether {@link UserInfoRequestHandler}
* uses the old format of {@code "verified_claims"} defined in the
* Implementer's Draft 2 of OpenID Connect for Identity Assurance 1.0
* which was published on May 19, 2020.
*
*
* When this flag is on, {@link UserInfoRequestHandler} calls the
* {@link UserInfoRequestHandlerSpi#getVerifiedClaims(String,
* VerifiedClaimsConstraint) getVerifiedClaims(String, VerifiedClaimsConstraint)}
* method of {@link UserInfoRequestHandlerSpi}. On the other hand,
* if this flag is off, the
* {@link UserInfoRequestHandlerSpi#getVerifiedClaims(String, Object)
* getVerifiedClaims(String, Object)} method is called instead.
* This is a breaking change from
* authlete-java-jaxrs version 2.41. This flag exists to mitigate
* the breaking change.
*
*
*
* The Implementer's Draft 3 of OpenID Connect for Identity Assurance 1.0,
* which was published on September 6, 2021, made many breaking changes.
* In addition, it is certain that further breaking changes will be made
* in the next draft. Considering the instability of the specification,
* it is not a good approach to define Java classes that correspond to
* elements in {@code "verified_claims"}. The
* {@code com.authlete.common.assurance} package in the authlete-java-common
* library was developed based on the approach for the Implementer's
* Draft 2, but it is not useful any more. This is the reason the
* {@link UserInfoRequestHandlerSpi#getVerifiedClaims(String,
* VerifiedClaimsConstraint) getVerifiedClaims(String,
* VerifiedClaimsConstraint)} method (whose second argument is an
* instance of {@link VerifiedClaimsConstraint} which is defined in
* the {@code com.authlete.common.assurance.constraint} package) was
* marked as deprecated.
*
*
* @param used
* {@code true} to make {@link UserInfoRequestHandler} call
* {@link UserInfoRequestHandlerSpi#getVerifiedClaims(String,
* VerifiedClaimsConstraint)
* getVerifiedClaims(String, VerifiedClaimsConstraint)} method
* of {@link UserInfoRequestHandlerSpi}. {@code false} to make
* {@link UserInfoRequestHandler} call
* {@link UserInfoRequestHandlerSpi#getVerifiedClaims(String, Object)
* getVerifiedClaims(String, Object)} method instead.
*
* @return
* {@code this} object.
*
* @since 2.42
*
* @see OpenID Connect for Identity Assurance 1.0
*/
public Params setOldIdaFormatUsed(boolean used)
{
this.oldIdaFormatUsed = used;
return this;
}
/**
* Get the full URI of the userinfo request, including the query part,
* if any.
*
*
* This parameter is used as the value of the {@code @target-uri} derived
* component for HTTP message signatures (RFC 9421
* HTTP Message Signatures, Section 2.2.2. Target URI). Additionally,
* other derived components such as {@code @authority}, {@code @scheme},
* {@code @path}, {@code @query} and {@code @query-param} are computed
* from this parameter.
*
*
*
* When this parameter is omitted, the value of the {@code htu} parameter
* is used. The {@code htu} parameter represents the URL of the userinfo
* endpoint, which usually serves as the target URI of the userinfo request.
* The only exception is when the access token is specified as a query
* parameter, as defined in RFC 6750
* Section 2.3. However, RFC 6750 states that this method "SHOULD
* NOT be used" unless other methods are not viable.
*
*
*
* If neither this {@code targetUri} parameter nor the {@code htu}
* parameter is specified, the {@code userInfoEndpoint} property of the
* {@link Service} is used as a fallback.
*
*
* @return
* The full URI of the userinfo request.
*
* @since 2.80
*/
public URI getTargetUri()
{
return targetUri;
}
/**
* Set the full URI of the userinfo request, including the query part,
* if any.
*
*
* This parameter is used as the value of the {@code @target-uri} derived
* component for HTTP message signatures (RFC 9421
* HTTP Message Signatures, Section 2.2.2. Target URI). Additionally,
* other derived components such as {@code @authority}, {@code @scheme},
* {@code @path}, {@code @query} and {@code @query-param} are computed
* from this parameter.
*
*
*
* When this parameter is omitted, the value of the {@code htu} parameter
* is used. The {@code htu} parameter represents the URL of the userinfo
* endpoint, which usually serves as the target URI of the userinfo request.
* The only exception is when the access token is specified as a query
* parameter, as defined in RFC 6750
* Section 2.3. However, RFC 6750 states that this method "SHOULD
* NOT be used" unless other methods are not viable.
*
*
*
* If neither this {@code targetUri} parameter nor the {@code htu}
* parameter is specified, the {@code userInfoEndpoint} property of the
* {@link Service} is used as a fallback.
*
*
* @param targetUri
* The full URI of the userinfo request.
*
* @return
* {@code this} object.
*
* @since 2.80
*/
public Params setTargetUri(URI targetUri)
{
this.targetUri = targetUri;
return this;
}
/**
* Get the HTTP headers included in the userinfo request. They are used
* to compute component values, which will be part of the signature base
* for HTTP message signatures.
*
* @return
* HTTP header fields of the userinfo request.
*
* @since 2.80
*/
public Pair[] getHeaders()
{
return headers;
}
/**
* Set the HTTP headers included in the userinfo request. They are used
* to compute component values, which will be part of the signature base
* for HTTP message signatures.
*
* @param headers
* HTTP header fields of the userinfo request.
*
* @return
* {@code this} object.
*
* @since 2.80
*/
public Params setHeaders(Pair[] headers)
{
this.headers = headers;
return this;
}
/**
* Get the flag indicating whether the userinfo request contains a request
* body.
*
*
* When the userinfo request must comply with the HTTP message signing
* requirements defined in the FAPI 2.0 Message Signing specification, the
* {@code "content-digest"} component identifier must be included in the
* signature base of the HTTP message signature (see RFC 9421 HTTP Message
* Signatures) if the userinfo request contains a request body.
*
*
*
* When this {@code requestBodyContained} parameter is true, Authlete
* checks whether {@code "content-digest"} is included in the signature
* base, if the FAPI profile applies to the userinfo request.
*
*
* @return
* {@code true} if the userinfo request contains a request body.
*
* @since 2.80
*/
public boolean isRequestBodyContained()
{
return requestBodyContained;
}
/**
* Set the flag indicating whether the userinfo request contains a request
* body.
*
*
* When the userinfo request must comply with the HTTP message signing
* requirements defined in the FAPI 2.0 Message Signing specification, the
* {@code "content-digest"} component identifier must be included in the
* signature base of the HTTP message signature (see RFC 9421 HTTP Message
* Signatures) if the userinfo request contains a request body.
*
*
*
* When this {@code requestBodyContained} parameter is true, Authlete
* checks whether {@code "content-digest"} is included in the signature
* base, if the FAPI profile applies to the userinfo request.
*
*
* @param contained
* {@code true} to indicate that the userinfo request contains
* a request body.
*
* @return
* {@code this} object.
*
* @since 2.80
*/
public Params setRequestBodyContained(boolean contained)
{
this.requestBodyContained = contained;
return this;
}
/**
* Get the flag indicating whether to check if the DPoP proof JWT
* includes the expected {@code nonce} value.
*
* @return
* {@code true} to have the {@code /auth/userinfo} API check
* whether the DPoP proof JWT includes the expected
* {@code nonce} value, even if the service's
* {@code dpopNonceRequired} property is false.
*
* @since 2.80
*/
public boolean isDpopNonceRequired()
{
return dpopNonceRequired;
}
/**
* Set the flag indicating whether to check if the DPoP proof JWT
* includes the expected {@code nonce} value.
*
* @param dpopNonceRequired
* {@code true} to have the {@code /auth/userinfo} API check
* whether the DPoP proof JWT includes the expected
* {@code nonce} value, even if the service's
* {@code dpopNonceRequired} property is false.
*
* @return
* {@code this} object.
*
* @since 2.80
*/
public Params setDpopNonceRequired(boolean dpopNonceRequired)
{
this.dpopNonceRequired = dpopNonceRequired;
return this;
}
}
private static final String CHALLENGE_ON_MISSING_ACCESS_TOKEN
= "Bearer error=\"invalid_token\",error_description=\""
+ "An access token must be sent as a Bearer Token. "
+ "See OpenID Connect Core 1.0, 5.3.1. UserInfo Request for details.\"";
/**
* Implementation of {@link UserInfoRequestHandlerSpi} interface.
*/
private final UserInfoRequestHandlerSpi mSpi;
/**
* Constructor with an implementation of {@link AuthleteApi} interface
* and an implementation of {@link UserInfoRequestHandlerSpi} interface.
*
* @param api
* Implementation of {@link AuthleteApi} interface.
*
* @param spi
* Implementation of {@link UserInfoRequestHandlerSpi} interface.
*/
public UserInfoRequestHandler(AuthleteApi api, UserInfoRequestHandlerSpi spi)
{
super(api);
mSpi = spi;
}
/**
* Handle a userinfo request to a UserInfo Endpoint defined in OpenID Connect
* Core 1.0. This method is an alias of {@link #handle(String, Options, Options)
* handle}{@code (accessToken, null, null)}.
*
* @param accessToken
* An access token.
*
* @return
* A response that should be returned from the endpoint to the
* client application.
*
* @throws WebApplicationException
* An error occurred.
*/
public Response handle(String accessToken) throws WebApplicationException
{
return handle(accessToken, null, null);
}
/**
* Handle a userinfo request to a UserInfo Endpoint defined in OpenID Connect
* Core 1.0. This method is an alias of the {@link #handle(Params, Options, Options)}
* method.
*
* @param accessToken
* An access token.
*
* @param userInfoOptions
* The request options for the {@code /api/auth/userinfo} API.
*
* @param userInfoIssueOptions
* The request options for the {@code /api/auth/userinfo/issue} API.
*
* @return
* A response that should be returned from the endpoint to the
* client application.
*
* @throws WebApplicationException
* An error occurred.
*
* @since 2.82
*/
public Response handle(
String accessToken, Options userInfoOptions, Options userInfoIssueOptions)
throws WebApplicationException
{
Params params = new Params().setAccessToken(accessToken);
return handle(params, userInfoOptions, userInfoIssueOptions);
}
/**
* Handle a userinfo request to a UserInfo Endpoint defined in OpenID Connect
* Core 1.0. This method is an alias of {@link #handle(Params, Options, Options) handle}{@code
* (params, null, null)}.
*
* @param params
* Parameters needed to handle the userinfo request.
*
* @return
* A response that should be returned from the endpoint to the
* client application.
*
* @throws WebApplicationException
* An error occurred.
*/
public Response handle(Params params) throws WebApplicationException
{
return handle(params, null, null);
}
/**
* Handle a userinfo request to a UserInfo Endpoint defined in OpenID Connect
* Core 1.0.
*
* @param params
* Parameters needed to handle the userinfo request.
*
* @param userInfoOptions
* The request options for the {@code /api/auth/userinfo} API.
*
* @param userInfoIssueOptions
* The request options for the {@code /api/auth/userinfo/issue} API.
*
* @return
* A response that should be returned from the endpoint to the
* client application.
*
* @throws WebApplicationException
* An error occurred.
*
* @since 2.82
*/
public Response handle(
Params params, Options userInfoOptions, Options userInfoIssueOptions)
throws WebApplicationException
{
// If an access token is not available.
if (params == null || params.getAccessToken() == null)
{
// Return "400 Bad Request".
return ResponseUtil.bearerError(
Status.BAD_REQUEST, CHALLENGE_ON_MISSING_ACCESS_TOKEN);
}
try
{
// Process the userinfo request.
return process(params, userInfoOptions, userInfoIssueOptions);
}
catch (WebApplicationException e)
{
throw e;
}
catch (Throwable t)
{
// Unexpected error.
throw unexpected("Unexpected error in UserInfoRequestHandler", t);
}
}
/**
* Process the userinfo request with the access token.
*/
private Response process(
Params params, Options userInfoOptions, Options userInfoIssueOptions)
{
// Call Authlete's /api/auth/userinfo API.
UserInfoResponse response = getApiCaller().callUserInfo(params, userInfoOptions);
// '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();
// Additional HTTP headers.
Map headers = prepareHeaders(response);
// Dispatch according to the action.
switch (action)
{
case INTERNAL_SERVER_ERROR:
// 500 Internal Server Error
return ResponseUtil.bearerError(Status.INTERNAL_SERVER_ERROR, content, headers);
case BAD_REQUEST:
// 400 Bad Request
return ResponseUtil.bearerError(Status.BAD_REQUEST, content, headers);
case UNAUTHORIZED:
// 401 Unauthorized
return ResponseUtil.bearerError(Status.UNAUTHORIZED, content, headers);
case FORBIDDEN:
// 403 Forbidden
return ResponseUtil.bearerError(Status.FORBIDDEN, content, headers);
case OK:
// Return the user information.
return getUserInfo(params, response, headers, userInfoOptions);
default:
// This never happens.
throw getApiCaller().unknownAction("/api/auth/userinfo", action);
}
}
private static Map prepareHeaders(UserInfoResponse response)
{
Map headers = new LinkedHashMap<>();
// DPoP-Nonce
String dpopNonce = response.getDpopNonce();
if (dpopNonce != null)
{
headers.put("DPoP-Nonce", dpopNonce);
}
return headers;
}
/**
* Generate a JSON or a JWT containing user information by calling
* Authlete's {@code /api/auth/userinfo/issue} API.
*/
private Response getUserInfo(
Params params, UserInfoResponse response, Map headers,
Options options)
{
String subject = response.getSubject();
// Collect claim values of the user.
Map claims = collectClaims(subject, response.getClaims());
// Collect claim data that are referenced when Authlete computes
// values of transformed claims.
Map claimsForTx =
collectClaims(subject, response.getRequestedClaimsForTx());
// Values of verified claims that are used to compute values of
// transformed claims under "verified_claims/claims".
List