diff --git a/pom.xml b/pom.xml index 0b2acbe..0836e96 100644 --- a/pom.xml +++ b/pom.xml @@ -12,8 +12,8 @@ UTF-8 - 3.81 - 2.65 + 3.83 + 2.66 3.0.1 2.30.1 9.4.27.v20200227 diff --git a/src/main/java/com/authlete/jaxrs/server/api/vci/AbstractCredentialEndpoint.java b/src/main/java/com/authlete/jaxrs/server/api/vci/AbstractCredentialEndpoint.java index 43c2acb..aca6f9c 100644 --- a/src/main/java/com/authlete/jaxrs/server/api/vci/AbstractCredentialEndpoint.java +++ b/src/main/java/com/authlete/jaxrs/server/api/vci/AbstractCredentialEndpoint.java @@ -17,72 +17,299 @@ package com.authlete.jaxrs.server.api.vci; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.HttpHeaders; import com.authlete.common.api.AuthleteApi; +import com.authlete.common.dto.CredentialIssuanceOrder; +import com.authlete.common.dto.CredentialIssuerMetadataRequest; +import com.authlete.common.dto.CredentialIssuerMetadataResponse; +import com.authlete.common.dto.CredentialRequestInfo; import com.authlete.common.dto.IntrospectionRequest; import com.authlete.common.dto.IntrospectionResponse; +import com.authlete.common.types.ErrorCode; import com.authlete.jaxrs.BaseResourceEndpoint; import com.authlete.jaxrs.server.util.ExceptionUtil; +import com.authlete.jaxrs.server.vc.InvalidCredentialRequestException; +import com.authlete.jaxrs.server.vc.OrderContext; +import com.authlete.jaxrs.server.vc.OrderFormat; +import com.authlete.jaxrs.server.vc.UnsupportedCredentialFormatException; +import com.authlete.jaxrs.server.vc.UnsupportedCredentialTypeException; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; public abstract class AbstractCredentialEndpoint extends BaseResourceEndpoint { - protected String checkContentExtractToken(final HttpServletRequest request, - final String requestContent) + /** + * Get the configured value of the endpoint of the credential issuer. + * The value is used as the expected value of the {@code htu} claim + * in the DPoP proof JWT. + * + *

+ * When {@code dpop} is null, this method returns null. Otherwise, this + * method calls the {@code /vci/metadata} API to get the metadata of the + * credential issuer, and extracts the value of the specified endpoint + * from the metadata. + *

+ * + * @param api + * An instance of the {@link AuthleteApi} instance. + * + * @param dpop + * A DPoP proof JWT, specified by the {@code DPoP} HTTP header. + * + * @param endpointName + * The name of an endpoint, such as "{@code credential_endpoint}". + * + * @return + * The configured value of the endpoint. If {@code dpop} is null, + * this method returns null. + */ + protected String computeHtu(AuthleteApi api, String dpop, String endpointName) { - if (requestContent == null) + if (dpop == null) { - throw ExceptionUtil.badRequestException("Missing request content."); + // When a DPoP proof JWT is not available, computing the value + // of "htu" is meaningless. We skip the computation to avoid + // making a call to the /vci/metadata API. + return null; } - final String accessToken = processAccessToken(request); - if (accessToken == null) + // Get the credential issuer metadata and extract the value of the + // endpoint from the metadata. + return (String)getCredentialIssuerMetadata(api).get(endpointName); + } + + + /** + * Get the credential issuer metadata by calling the {@code /vci/metadata} API. + * + * @param api + * An instance of the {@link AuthleteApi} instance. + * + * @return + * The credential issuer metadata. + */ + @SuppressWarnings("unchecked") + private Map getCredentialIssuerMetadata(AuthleteApi api) + { + // Call the /vci/metadata API to get the metadata of the credential issuer. + CredentialIssuerMetadataResponse response = + api.credentialIssuerMetadata(new CredentialIssuerMetadataRequest()); + + // The response content. + String content = response.getResponseContent(); + + // If something wrong was reported by the /vci/metadata API. + if (response.getAction() != CredentialIssuerMetadataResponse.Action.OK) { - throw ExceptionUtil.badRequestException("Missing access token."); + // 500 Internal Server Error + application/json + throw ExceptionUtil.internalServerErrorExceptionJson(content); } - return accessToken; + // Convert the credential issuer metadata into a Map instance. + return new Gson().fromJson(content, Map.class); } - private String processAccessToken(final HttpServletRequest request) + /** + * Validate the access token and get the information about it. + * + * @param req + * The HTTP request that this endpoint has received. + * + * @param api + * An instance of the {@link AuthleteApi} interface. + * + * @param at + * The access token. + * + * @param dpop + * A DPoP proof JWT, specified by the {@code DPoP} HTTP header. + * + * @param htu + * The URL of this endpoint, the expected value of the {@code htu} + * claim in the DPoP proof JWT. + * + * @return + * The response from the {@code /auth/introspection} API. + */ + protected IntrospectionResponse introspect( + HttpServletRequest req, AuthleteApi api, + String at, String dpop, String htu) { - // The value of the "Authorization" header. - final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + // The client certificate. This is needed for certificate-bound + // access tokens. See RFC 8705 for details. + String certificate = extractClientCertificate(req); - return super.extractAccessToken(authorization, null); + // The request to the /auth/introspection API. + IntrospectionRequest request = + new IntrospectionRequest() + .setToken(at) + .setClientCertificate(certificate) + .setDpop(dpop) + .setHtm("POST") + .setHtu(htu) + ; + + // Validate the access token. + return validateAccessToken(api, request); } - protected IntrospectionResponse introspect(final AuthleteApi api, - final String accessToken) - throws WebApplicationException + /** + * Prepare additional HTTP headers that the response from this endpoint + * should include. + * + * @param introspection + * The response from the {@code /auth/introspection} API. + * + * @return + * A map including pairs of a header name and a header value. + */ + protected Map prepareHeaders(IntrospectionResponse introspection) { - final IntrospectionRequest introspectionRequest = new IntrospectionRequest() - .setToken(accessToken); + Map headers = new LinkedHashMap<>(); - final IntrospectionResponse response = api.introspection(introspectionRequest); - final String content = response.getResponseContent(); + // The expected nonce value for DPoP proof JWT. + String dpopNonce = introspection.getDpopNonce(); + if (dpopNonce != null) + { + headers.put("DPoP-Nonce", dpopNonce); + } + + return headers; + } - switch (response.getAction()) + + /** + * Prepare a credential issuance order. + * + * @param context + * The context in which this method is called. + * + * @param introspection + * The response from the {@code /auth/introspection} API. + * + * @param info + * The information about the credential request. + * + * @param headers + * The additional headers that should be included in the response + * from this endpoint. + * + * @return + * A credential issuance order. + */ + protected CredentialIssuanceOrder prepareOrder( + OrderContext context, + IntrospectionResponse introspection, CredentialRequestInfo info, + Map headers) + { + try { - case BAD_REQUEST: - throw ExceptionUtil.badRequestException(content); + // Get an OrderFormat instance corresponding to the credential format. + OrderFormat format = getOrderFormat(info); + + // Let the processor for the format create a credential issuance + // order based on the credential request. + return format.getProcessor().toOrder(context, introspection, info); + } + catch (UnsupportedCredentialFormatException cause) + { + // 400 Bad Request + "error":"unsupported_credential_format" + throw ExceptionUtil.badRequestExceptionJson( + errorJson(ErrorCode.unsupported_credential_format, cause), headers); + } + catch (UnsupportedCredentialTypeException cause) + { + // 400 Bad Request + "error":"unsupported_credential_type" + throw ExceptionUtil.badRequestExceptionJson( + errorJson(ErrorCode.unsupported_credential_type, cause), headers); + } + catch (InvalidCredentialRequestException cause) + { + // 400 Bad Request + "error":"invalid_credential_request" + throw ExceptionUtil.badRequestExceptionJson( + errorJson(ErrorCode.invalid_credential_request, cause), headers); + } + catch (WebApplicationException cause) + { + throw cause; + } + catch (Exception cause) + { + // 500 Internal Server Error + "error":"server_error" + throw ExceptionUtil.internalServerErrorExceptionJson( + errorJson(ErrorCode.server_error, cause), headers); + } + } + + + /** + * Prepare credential issuance orders. The method is supposed to be called + * from the implementation of the batch credential endpoint. + * + * @param introspection + * The response from the {@code /auth/introspection} API. + * + * @param infos + * The list of credential requests. + * + * @param headers + * The additional headers that should be included in the response + * from this endpoint. + * + * @return + * The list of credential issuance orders. + */ + protected CredentialIssuanceOrder[] prepareOrders( + IntrospectionResponse introspection, CredentialRequestInfo[] infos, + Map headers) + { + // Convert the array of CredentialRequestInfo instances + // into an array of CredentialIssuanceOrder instances. + return Arrays.stream(infos) + .map(info -> prepareOrder(OrderContext.BATCH, introspection, info, headers)) + .collect(Collectors.toList()) + .toArray(new CredentialIssuanceOrder[infos.length]); + } + + + private OrderFormat getOrderFormat(CredentialRequestInfo info) throws UnsupportedCredentialFormatException + { + // Get an OrderFormat instance that corresponds to the credential format. + OrderFormat format = OrderFormat.byId(info.getFormat()); + + // If the format is not supported. + if (format == null) + { + throw new UnsupportedCredentialFormatException(String.format( + "The credential format '%s' is not supported.", info.getFormat())); + } + + return format; + } - case UNAUTHORIZED: - throw ExceptionUtil.unauthorizedException(accessToken, content); - case FORBIDDEN: - throw ExceptionUtil.forbiddenException(content); + protected String errorJson(ErrorCode errorCode, Throwable cause) + { + Map map = new LinkedHashMap<>(); - case OK: - return response; + // "error" + map.put("error", errorCode.name()); - case INTERNAL_SERVER_ERROR: - default: - throw ExceptionUtil.internalServerErrorException(content); + if (cause != null) + { + // "error_description" + map.put("error_description", cause.getMessage()); } + + // The content of the error response. + return new GsonBuilder().setPrettyPrinting().create().toJson(map); } } diff --git a/src/main/java/com/authlete/jaxrs/server/api/vci/BatchCredentialEndpoint.java b/src/main/java/com/authlete/jaxrs/server/api/vci/BatchCredentialEndpoint.java index 7535391..b7e0b4e 100644 --- a/src/main/java/com/authlete/jaxrs/server/api/vci/BatchCredentialEndpoint.java +++ b/src/main/java/com/authlete/jaxrs/server/api/vci/BatchCredentialEndpoint.java @@ -17,12 +17,15 @@ package com.authlete.jaxrs.server.api.vci; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; +import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.authlete.common.api.AuthleteApi; @@ -34,7 +37,6 @@ import com.authlete.common.dto.CredentialIssuanceOrder; import com.authlete.common.dto.CredentialRequestInfo; import com.authlete.common.dto.IntrospectionResponse; -import com.authlete.jaxrs.server.util.CredentialUtil; import com.authlete.jaxrs.server.util.ExceptionUtil; import com.authlete.jaxrs.server.util.ResponseUtil; @@ -44,96 +46,109 @@ public class BatchCredentialEndpoint extends AbstractCredentialEndpoint { @POST @Consumes(MediaType.APPLICATION_JSON) - public Response post(@Context HttpServletRequest request, - final String requestContent) + public Response post( + @Context HttpServletRequest request, + @HeaderParam(HttpHeaders.AUTHORIZATION) String authorization, + @HeaderParam("DPoP") String dpop, + String requestContent) { final AuthleteApi api = AuthleteApiFactory.getDefaultApi(); - // Check request content - final String accessToken = super.checkContentExtractToken(request, requestContent); + // Extract the access token from the request. + String accessToken = extractAccessToken(authorization, null); - // Validate access token - final IntrospectionResponse introspection = introspect(api, accessToken); + // The expected value of the 'htu' claim in the DPoP proof JWT. + String htu = computeHtu(api, dpop, "batch_credential_endpoint"); - // Parse credential and make it an order - final CredentialRequestInfo[] credential = - credentialBatchParse(api, requestContent, accessToken); + // Validate the access token. + IntrospectionResponse introspection = + introspect(request, api, accessToken, dpop, htu); - final CredentialIssuanceOrder[] orders; - try { - orders = CredentialUtil.toOrder(introspection, credential); - } catch (final CredentialUtil.UnknownCredentialFormatException e) { - return ResponseUtil.badRequestJson(e.getJsonError()); - } + // The headers that the response from this endpoint should include. + Map headers = prepareHeaders(introspection); + + // Parse the batch credential request. + CredentialRequestInfo[] infos = parseRequest( + api, requestContent, accessToken, headers); + + // Prepare credential issuance orders. + CredentialIssuanceOrder[] orders = prepareOrders(introspection, infos, headers); - // Issue - return credentialIssue(api, orders, accessToken); + // Issue credentials and return a batch credential response. + return issue(api, orders, accessToken, headers); } - private CredentialRequestInfo[] credentialBatchParse(final AuthleteApi api, - final String requestContent, - final String accessToken) - throws WebApplicationException + private CredentialRequestInfo[] parseRequest( + AuthleteApi api, String requestContent, String accessToken, + Map headers) throws WebApplicationException { - final CredentialBatchParseRequest parseRequest = + // Prepare a request to the /vci/batch/parse API. + CredentialBatchParseRequest request = new CredentialBatchParseRequest() .setRequestContent(requestContent) .setAccessToken(accessToken); - final CredentialBatchParseResponse response = - api.credentialBatchParse(parseRequest); - final String content = response.getResponseContent(); + // Call the /vci/batch/parse API and get the response. + CredentialBatchParseResponse response = api.credentialBatchParse(request); + + // The response content. + String content = response.getResponseContent(); switch (response.getAction()) { case BAD_REQUEST: - throw ExceptionUtil.badRequestExceptionJson(content); + throw ExceptionUtil.badRequestExceptionJson(content, headers); case UNAUTHORIZED: - throw ExceptionUtil.unauthorizedException(accessToken, content); + throw ExceptionUtil.unauthorizedException(accessToken, content, headers); case FORBIDDEN: - throw ExceptionUtil.forbiddenExceptionJson(content); + throw ExceptionUtil.forbiddenExceptionJson(content, headers); case OK: return response.getInfo(); case INTERNAL_SERVER_ERROR: default: - throw ExceptionUtil.internalServerErrorException(content); + throw ExceptionUtil.internalServerErrorExceptionJson(content, headers); } } - private Response credentialIssue(final AuthleteApi api, - final CredentialIssuanceOrder[] orders, - final String accessToken) + private Response issue( + AuthleteApi api, CredentialIssuanceOrder[] orders, String accessToken, + Map headers) throws WebApplicationException { - final CredentialBatchIssueRequest credentialBatchIssueRequest = new CredentialBatchIssueRequest() - .setAccessToken(accessToken) - .setOrders(orders); + // Prepare a request to the /vci/batch/issue API. + CredentialBatchIssueRequest request = + new CredentialBatchIssueRequest() + .setAccessToken(accessToken) + .setOrders(orders); + + // Call the /vci/batch/issue API and get the response. + CredentialBatchIssueResponse response = api.credentialBatchIssue(request); - final CredentialBatchIssueResponse response = api.credentialBatchIssue(credentialBatchIssueRequest); - final String content = response.getResponseContent(); + // The response content. + String content = response.getResponseContent(); switch (response.getAction()) { case CALLER_ERROR: - return ResponseUtil.badRequest(content); + return ResponseUtil.internalServerErrorJson(content, headers); case UNAUTHORIZED: - return ResponseUtil.unauthorized(accessToken, content); + return ResponseUtil.unauthorized(accessToken, content, headers); case FORBIDDEN: - return ResponseUtil.forbiddenJson(content); + return ResponseUtil.forbiddenJson(content, headers); case OK: - return ResponseUtil.okJson(content); + return ResponseUtil.okJson(content, headers); case INTERNAL_SERVER_ERROR: default: - throw ExceptionUtil.internalServerErrorExceptionJson(content); + return ResponseUtil.internalServerErrorJson(content, headers); } } } diff --git a/src/main/java/com/authlete/jaxrs/server/api/vci/CredentialEndpoint.java b/src/main/java/com/authlete/jaxrs/server/api/vci/CredentialEndpoint.java index cf7d2db..de4ef47 100644 --- a/src/main/java/com/authlete/jaxrs/server/api/vci/CredentialEndpoint.java +++ b/src/main/java/com/authlete/jaxrs/server/api/vci/CredentialEndpoint.java @@ -17,12 +17,15 @@ package com.authlete.jaxrs.server.api.vci; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; +import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.authlete.common.api.AuthleteApi; @@ -34,9 +37,9 @@ import com.authlete.common.dto.CredentialSingleParseRequest; import com.authlete.common.dto.CredentialSingleParseResponse; import com.authlete.common.dto.IntrospectionResponse; -import com.authlete.jaxrs.server.util.CredentialUtil; import com.authlete.jaxrs.server.util.ExceptionUtil; import com.authlete.jaxrs.server.util.ResponseUtil; +import com.authlete.jaxrs.server.vc.OrderContext; @Path("/api/credential") @@ -44,99 +47,113 @@ public class CredentialEndpoint extends AbstractCredentialEndpoint { @POST @Consumes(MediaType.APPLICATION_JSON) - public Response post(@Context HttpServletRequest request, - final String requestContent) + public Response post( + @Context HttpServletRequest request, + @HeaderParam(HttpHeaders.AUTHORIZATION) String authorization, + @HeaderParam("DPoP") String dpop, + String requestContent) { final AuthleteApi api = AuthleteApiFactory.getDefaultApi(); - // Check request content - final String accessToken = super.checkContentExtractToken(request, requestContent); + // Extract the access token from the request. + String accessToken = extractAccessToken(authorization, null); - // Validate access token - final IntrospectionResponse introspection = introspect(api, accessToken); + // The expected value of the 'htu' claim in the DPoP proof JWT. + String htu = computeHtu(api, dpop, "credential_endpoint"); - // Parse credential and make it an order - final CredentialRequestInfo credential = - credentialSingleParse(api, requestContent, accessToken); + // Validate the access token. + IntrospectionResponse introspection = + introspect(request, api, accessToken, dpop, htu); - final CredentialIssuanceOrder order; - try { - order = CredentialUtil.toOrder(introspection, credential); - } catch (final CredentialUtil.UnknownCredentialFormatException e) { - return ResponseUtil.badRequestJson(e.getJsonError()); - } + // The headers that the response from this endpoint should include. + Map headers = prepareHeaders(introspection); + + // Parse the credential request. + CredentialRequestInfo info = parseRequest( + api, requestContent, accessToken, headers); + + // Prepare a credential issuance order. + CredentialIssuanceOrder order = + prepareOrder(OrderContext.SINGLE, introspection, info, headers); - // Issue - return credentialIssue(api, order, accessToken); + // Issue a credential and return a credential response. + return issue(api, order, accessToken, headers); } - private CredentialRequestInfo credentialSingleParse(final AuthleteApi api, - final String requestContent, - final String accessToken) - throws WebApplicationException + private CredentialRequestInfo parseRequest( + AuthleteApi api, String requestContent, String accessToken, + Map headers) throws WebApplicationException { - final CredentialSingleParseRequest parseRequest = new CredentialSingleParseRequest() - .setRequestContent(requestContent) - .setAccessToken(accessToken); + // Prepare a request to the /vci/single/parse API. + CredentialSingleParseRequest request = + new CredentialSingleParseRequest() + .setRequestContent(requestContent) + .setAccessToken(accessToken); - final CredentialSingleParseResponse response = api.credentialSingleParse(parseRequest); - final String content = response.getResponseContent(); + // Call the /vci/single/parse API and get the response. + CredentialSingleParseResponse response = api.credentialSingleParse(request); + + // The response content. + String content = response.getResponseContent(); switch (response.getAction()) { case BAD_REQUEST: - throw ExceptionUtil.badRequestExceptionJson(content); + throw ExceptionUtil.badRequestExceptionJson(content, headers); case UNAUTHORIZED: - throw ExceptionUtil.unauthorizedException(accessToken, content); + throw ExceptionUtil.unauthorizedException(accessToken, content, headers); case FORBIDDEN: - throw ExceptionUtil.forbiddenExceptionJson(content); + throw ExceptionUtil.forbiddenExceptionJson(content, headers); case OK: return response.getInfo(); case INTERNAL_SERVER_ERROR: default: - throw ExceptionUtil.internalServerErrorExceptionJson(content); + throw ExceptionUtil.internalServerErrorExceptionJson(content, headers); } } - private Response credentialIssue(final AuthleteApi api, - final CredentialIssuanceOrder order, - final String accessToken) + private Response issue( + AuthleteApi api, CredentialIssuanceOrder order, String accessToken, + Map headers) throws WebApplicationException { - final CredentialSingleIssueRequest credentialSingleIssueRequest = + // Prepare a request to the /vci/single/issue API. + CredentialSingleIssueRequest request = new CredentialSingleIssueRequest() .setAccessToken(accessToken) .setOrder(order); - final CredentialSingleIssueResponse response = - api.credentialSingleIssue(credentialSingleIssueRequest); - final String content = response.getResponseContent(); + // Call the /vci/single/issue API and get the response. + CredentialSingleIssueResponse response = api.credentialSingleIssue(request); + + // The response content. + String content = response.getResponseContent(); switch (response.getAction()) { case CALLER_ERROR: - return ResponseUtil.badRequest(content); + return ResponseUtil.internalServerErrorJson(content, headers); case UNAUTHORIZED: - return ResponseUtil.unauthorized(accessToken, content); + return ResponseUtil.unauthorized(accessToken, content, headers); case FORBIDDEN: - return ResponseUtil.forbiddenJson(content); + return ResponseUtil.forbiddenJson(content, headers); case OK: - return ResponseUtil.okJson(content); + return ResponseUtil.okJson(content, headers); case ACCEPTED: - return ResponseUtil.acceptedJson(content); + return ResponseUtil.acceptedJson(content, headers); case INTERNAL_SERVER_ERROR: default: - throw ExceptionUtil.internalServerErrorExceptionJson(content); + return ResponseUtil.internalServerErrorJson(content, headers); } } } diff --git a/src/main/java/com/authlete/jaxrs/server/api/vci/DeferredCredentialEndpoint.java b/src/main/java/com/authlete/jaxrs/server/api/vci/DeferredCredentialEndpoint.java index e93800f..efabb27 100644 --- a/src/main/java/com/authlete/jaxrs/server/api/vci/DeferredCredentialEndpoint.java +++ b/src/main/java/com/authlete/jaxrs/server/api/vci/DeferredCredentialEndpoint.java @@ -17,12 +17,15 @@ package com.authlete.jaxrs.server.api.vci; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; +import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.authlete.common.api.AuthleteApi; @@ -34,10 +37,10 @@ import com.authlete.common.dto.CredentialIssuanceOrder; import com.authlete.common.dto.CredentialRequestInfo; import com.authlete.common.dto.IntrospectionResponse; -import com.authlete.jaxrs.server.util.CredentialUtil; +import com.authlete.common.types.ErrorCode; import com.authlete.jaxrs.server.util.ExceptionUtil; import com.authlete.jaxrs.server.util.ResponseUtil; -import com.authlete.jaxrs.server.util.StringUtil; +import com.authlete.jaxrs.server.vc.OrderContext; @Path("/api/deferred_credential") @@ -45,96 +48,114 @@ public class DeferredCredentialEndpoint extends AbstractCredentialEndpoint { @POST @Consumes(MediaType.APPLICATION_JSON) - public Response post(@Context HttpServletRequest request, - final String requestContent) + public Response post( + @Context HttpServletRequest request, + @HeaderParam(HttpHeaders.AUTHORIZATION) String authorization, + @HeaderParam("DPoP") String dpop, + String requestContent) { final AuthleteApi api = AuthleteApiFactory.getDefaultApi(); - // Check request content - final String accessToken = super.checkContentExtractToken(request, requestContent); + // Extract the access token from the request. + String accessToken = extractAccessToken(authorization, null); - // Validate access token - final IntrospectionResponse introspection = introspect(api, accessToken); + // The expected value of the 'htu' claim in the DPoP proof JWT. + String htu = computeHtu(api, dpop, "deferred_credential_endpoint"); - // Parse credential and make it an order - final CredentialRequestInfo credential = - credentialDeferredParse(api, requestContent, accessToken); + // Validate the access token + IntrospectionResponse introspection = + introspect(request, api, accessToken, dpop, htu); - final CredentialIssuanceOrder order; - try { - order = CredentialUtil.toOrder(introspection, credential); - } catch (final CredentialUtil.UnknownCredentialFormatException e) { - return ResponseUtil.badRequest(e.getJsonError()); - } + // The headers that the response from this endpoint should include. + Map headers = prepareHeaders(introspection); + + // Parse the deferred credential request. + CredentialRequestInfo info = parseRequest( + api, requestContent, accessToken, headers); + + // Prepare a credential issuance order. + CredentialIssuanceOrder order = + prepareOrder(OrderContext.DEFERRED, introspection, info, headers); + // If the requested credential is not ready yet. if (order.isIssuanceDeferred()) { - return ResponseUtil.badRequestJson(StringUtil.toJsonError("issuance_pending")); + // 400 Bad Request + "error":"issuance_pending" + throw ExceptionUtil.badRequestExceptionJson( + errorJson(ErrorCode.issuance_pending, null), headers); } - // Issue - return credentialIssue(api, order); + // Issue a credential and return a deferred credential response. + return issue(api, order, headers); } - private CredentialRequestInfo credentialDeferredParse(final AuthleteApi api, - final String requestContent, - final String accessToken) - throws WebApplicationException + private CredentialRequestInfo parseRequest( + AuthleteApi api, String requestContent, String accessToken, + Map headers) throws WebApplicationException { - final CredentialDeferredParseRequest parseRequest = + // Prepare a request to the /vci/deferred/parse API. + CredentialDeferredParseRequest request = new CredentialDeferredParseRequest() .setRequestContent(requestContent) .setAccessToken(accessToken); - final CredentialDeferredParseResponse response = - api.credentialDeferredParse(parseRequest); - final String content = response.getResponseContent(); + // Call the /vci/deferred/parse API and get the response. + CredentialDeferredParseResponse response = api.credentialDeferredParse(request); + + // The response content. + String content = response.getResponseContent(); switch (response.getAction()) { case BAD_REQUEST: - throw ExceptionUtil.badRequestExceptionJson(content); + throw ExceptionUtil.badRequestExceptionJson(content, headers); case UNAUTHORIZED: - throw ExceptionUtil.unauthorizedException(accessToken, content); + throw ExceptionUtil.unauthorizedException(accessToken, content, headers); case FORBIDDEN: - throw ExceptionUtil.forbiddenExceptionJson(content); + throw ExceptionUtil.forbiddenExceptionJson(content, headers); case OK: return response.getInfo(); case INTERNAL_SERVER_ERROR: default: - throw ExceptionUtil.internalServerErrorExceptionJson(content); + throw ExceptionUtil.internalServerErrorExceptionJson(content, headers); } } - private Response credentialIssue(final AuthleteApi api, - final CredentialIssuanceOrder order) + private Response issue( + AuthleteApi api, CredentialIssuanceOrder order, + Map headers) throws WebApplicationException { - final CredentialDeferredIssueRequest request = new CredentialDeferredIssueRequest() - .setOrder(order); + // Prepare a request to the /vci/deferred/issue API. + CredentialDeferredIssueRequest request = + new CredentialDeferredIssueRequest() + .setOrder(order); + + // Call the /vci/deferred/issue API and get the response. + CredentialDeferredIssueResponse response = api.credentialDeferredIssue(request); - final CredentialDeferredIssueResponse response = api.credentialDeferredIssue(request); - final String content = response.getResponseContent(); + // The response content. + String content = response.getResponseContent(); switch (response.getAction()) { case CALLER_ERROR: - return ResponseUtil.badRequest(content); + return ResponseUtil.internalServerErrorJson(content, headers); case FORBIDDEN: - return ResponseUtil.forbiddenJson(content); + return ResponseUtil.forbiddenJson(content, headers); case OK: - return ResponseUtil.okJson(content); + return ResponseUtil.okJson(content, headers); case INTERNAL_SERVER_ERROR: default: - throw ExceptionUtil.internalServerErrorException(content); + return ResponseUtil.internalServerErrorJson(content, headers); } } } diff --git a/src/main/java/com/authlete/jaxrs/server/api/vci/SdJwtOrderProcessor.java b/src/main/java/com/authlete/jaxrs/server/api/vci/SdJwtOrderProcessor.java deleted file mode 100644 index 3591e9f..0000000 --- a/src/main/java/com/authlete/jaxrs/server/api/vci/SdJwtOrderProcessor.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2023 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.server.api.vci; - - -import com.authlete.common.dto.CredentialIssuanceOrder; -import com.authlete.common.dto.CredentialRequestInfo; -import com.authlete.common.dto.IntrospectionResponse; -import com.authlete.common.types.User; -import com.authlete.jaxrs.server.db.UserDao; -import com.authlete.jaxrs.server.util.ExceptionUtil; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; - - -public class SdJwtOrderProcessor implements IOrderProcessor -{ - public CredentialIssuanceOrder toOrder(final IntrospectionResponse introspection, - final CredentialRequestInfo info) - { - final JsonObject definition = getCredentialDefinition(info.getDetails()); - final String credentialType = getCredentialType(definition); - - // The subject (the identifier of the user) that is associated with the access token. - final String subject = introspection.getSubject(); - - // The information about the user identified by the subject. - final User user = UserDao.getBySubject(subject); - - // Find credentialType - final CredentialDefinitionType definitionType = CredentialDefinitionType.byId(credentialType); - if (definitionType == null) - { - throw ExceptionUtil.badRequestException(String.format("Unknown credential type %s.", credentialType)); - } - - // Prepare the credential payload. - final JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("type", credentialType); - jsonObject.addProperty("sub", subject); - - boolean deferred = false; - for (final String claim : definitionType.getClaims()) - { - String claimValue = (String) user.getClaim(claim, null); - if(claimValue == null) - { - deferred = true; - continue; - } - - jsonObject.addProperty(claim, claimValue); - } - - final String credentialPayload = jsonObject.toString(); - - return new CredentialIssuanceOrder() - .setRequestIdentifier(info.getIdentifier()) - .setCredentialPayload(credentialPayload) - .setIssuanceDeferred(deferred); - } - - - private JsonObject getCredentialDefinition(final String details) - { - final JsonElement request; - try - { - request = JsonParser.parseString(details); - } - catch (JsonSyntaxException e) - { - throw ExceptionUtil.badRequestException("Unreadable credential request details."); - } - - if (!(request instanceof JsonObject)) - { - throw ExceptionUtil.badRequestException("Credential request details should be a JSON object."); - } - - final JsonElement definition = ((JsonObject)request).get("credential_definition"); - if (!(definition instanceof JsonObject)) - { - throw ExceptionUtil.badRequestException("Credential definition should be defined."); - } - - return (JsonObject)definition; - } - - - private String getCredentialType(final JsonObject definition) - { - final JsonElement type = definition.get("type"); - if (type == null) - { - throw ExceptionUtil.badRequestException("Credential type should be defined."); - } - - return type.getAsString(); - } -} diff --git a/src/main/java/com/authlete/jaxrs/server/util/CredentialUtil.java b/src/main/java/com/authlete/jaxrs/server/util/CredentialUtil.java deleted file mode 100644 index e4c295d..0000000 --- a/src/main/java/com/authlete/jaxrs/server/util/CredentialUtil.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.authlete.jaxrs.server.util; - - -import java.util.Collection; -import java.util.LinkedList; -import com.authlete.common.dto.CredentialIssuanceOrder; -import com.authlete.common.dto.CredentialRequestInfo; -import com.authlete.common.dto.IntrospectionResponse; -import com.authlete.jaxrs.server.api.vci.OrderFormat; - - -public class CredentialUtil -{ - public static CredentialIssuanceOrder toOrder(final IntrospectionResponse introspection, - final CredentialRequestInfo info) - throws UnknownCredentialFormatException - { - final String formatId = info.getFormat(); - final OrderFormat format = OrderFormat.byId(formatId); - if (format == null) - { - throw new UnknownCredentialFormatException(String.format("Unsupported credential format %s.", formatId)); - } - - return format.getProcessor() - .toOrder(introspection, info); - } - - - public static CredentialIssuanceOrder[] toOrder(final IntrospectionResponse introspection, - final CredentialRequestInfo[] infos) - throws UnknownCredentialFormatException - { - final Collection orders = new LinkedList<>(); - for(final CredentialRequestInfo info : infos) - { - orders.add(toOrder(introspection, info)); - } - - return orders.toArray(new CredentialIssuanceOrder[0]); - } - - - public static class UnknownCredentialFormatException extends Exception { - UnknownCredentialFormatException(final String message) { - super(message); - } - - public String getJsonError() - { - return StringUtil.toJsonError(this.getMessage()); - } - } -} diff --git a/src/main/java/com/authlete/jaxrs/server/util/ExceptionUtil.java b/src/main/java/com/authlete/jaxrs/server/util/ExceptionUtil.java index 6f9400d..d28cab0 100644 --- a/src/main/java/com/authlete/jaxrs/server/util/ExceptionUtil.java +++ b/src/main/java/com/authlete/jaxrs/server/util/ExceptionUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Authlete, Inc. + * Copyright (C) 2019-2023 Authlete, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,11 @@ import static com.authlete.jaxrs.server.util.ResponseUtil.badRequestJson; import static com.authlete.jaxrs.server.util.ResponseUtil.forbidden; import static com.authlete.jaxrs.server.util.ResponseUtil.forbiddenJson; +import static com.authlete.jaxrs.server.util.ResponseUtil.internalServerError; import static com.authlete.jaxrs.server.util.ResponseUtil.internalServerErrorJson; -import static com.authlete.jaxrs.server.util.ResponseUtil.unauthorized; import static com.authlete.jaxrs.server.util.ResponseUtil.notFound; -import static com.authlete.jaxrs.server.util.ResponseUtil.internalServerError; +import static com.authlete.jaxrs.server.util.ResponseUtil.unauthorized; +import java.util.Map; import javax.ws.rs.WebApplicationException; import org.glassfish.jersey.server.mvc.Viewable; @@ -62,10 +63,15 @@ public static WebApplicationException badRequestException(String entity) */ public static WebApplicationException badRequestExceptionJson(String entity) { - return new WebApplicationException(entity, badRequestJson(entity)); + return badRequestExceptionJson(entity, /* headers */ null); } + public static WebApplicationException badRequestExceptionJson(String entity, Map headers) + { + return new WebApplicationException(entity, badRequestJson(entity, headers)); + } + /** * Create an exception indicating "400 Bad Request". @@ -97,7 +103,13 @@ public static WebApplicationException badRequestException(Viewable entity) */ public static WebApplicationException unauthorizedException(String entity, String challenge) { - return new WebApplicationException(entity, unauthorized(entity, challenge)); + return unauthorizedException(entity, challenge, /* headers */ null); + } + + + public static WebApplicationException unauthorizedException(String entity, String challenge, Map headers) + { + return new WebApplicationException(entity, unauthorized(entity, challenge, headers)); } @@ -145,7 +157,13 @@ public static WebApplicationException forbiddenException(final String entity) */ public static WebApplicationException forbiddenExceptionJson(final String entity) { - return new WebApplicationException(entity, forbiddenJson(entity)); + return forbiddenExceptionJson(entity, /* headers */ null); + } + + + public static WebApplicationException forbiddenExceptionJson(final String entity, Map headers) + { + return new WebApplicationException(entity, forbiddenJson(entity, headers)); } @@ -205,7 +223,13 @@ public static WebApplicationException internalServerErrorException(String entity */ public static WebApplicationException internalServerErrorExceptionJson(String entity) { - return new WebApplicationException(entity, internalServerErrorJson(entity)); + return internalServerErrorExceptionJson(entity, /* headers */ null); + } + + + public static WebApplicationException internalServerErrorExceptionJson(String entity, Map headers) + { + return new WebApplicationException(entity, internalServerErrorJson(entity, headers)); } diff --git a/src/main/java/com/authlete/jaxrs/server/util/ResponseUtil.java b/src/main/java/com/authlete/jaxrs/server/util/ResponseUtil.java index a5a62b2..992ca6f 100644 --- a/src/main/java/com/authlete/jaxrs/server/util/ResponseUtil.java +++ b/src/main/java/com/authlete/jaxrs/server/util/ResponseUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Authlete, Inc. + * Copyright (C) 2019-2023 Authlete, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package com.authlete.jaxrs.server.util; +import java.util.Map; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -63,7 +64,13 @@ public class ResponseUtil */ public static Response ok(String entity) { - return builderForTextPlain(Status.OK, entity).build(); + return ok(entity, /* headers */ null); + } + + + public static Response ok(String entity, Map headers) + { + return builderForTextPlain(Status.OK, entity, headers).build(); } @@ -78,7 +85,13 @@ public static Response ok(String entity) */ public static Response okJson(String entity) { - return builderForJson(Status.OK, entity).build(); + return okJson(entity, /* headers */ null); + } + + + public static Response okJson(String entity, Map headers) + { + return builderForJson(Status.OK, entity, headers).build(); } @@ -93,7 +106,13 @@ public static Response okJson(String entity) */ public static Response ok(Viewable entity) { - return builderForTextHtml(Status.OK, entity).build(); + return ok(entity, /* headers */ null); + } + + + public static Response ok(Viewable entity, Map headers) + { + return builderForTextHtml(Status.OK, entity, headers).build(); } @@ -108,7 +127,13 @@ public static Response ok(Viewable entity) */ public static Response acceptedJson(String entity) { - return builderForJson(Status.ACCEPTED, entity).build(); + return acceptedJson(entity, /* headers */ null); + } + + + public static Response acceptedJson(String entity, Map headers) + { + return builderForJson(Status.ACCEPTED, entity, headers).build(); } @@ -135,7 +160,13 @@ public static Response noContent() */ public static Response badRequest(String entity) { - return builderForTextPlain(Status.BAD_REQUEST, entity).build(); + return badRequest(entity, /* headers */ null); + } + + + public static Response badRequest(String entity, Map headers) + { + return builderForTextPlain(Status.BAD_REQUEST, entity, headers).build(); } @@ -150,7 +181,13 @@ public static Response badRequest(String entity) */ public static Response badRequestJson(String entity) { - return builderForJson(Status.BAD_REQUEST, entity).build(); + return badRequestJson(entity, /* headers */ null); + } + + + public static Response badRequestJson(String entity, Map headers) + { + return builderForJson(Status.BAD_REQUEST, entity, headers).build(); } @@ -165,7 +202,13 @@ public static Response badRequestJson(String entity) */ public static Response badRequest(Viewable entity) { - return builderForTextHtml(Status.BAD_REQUEST, entity).build(); + return badRequest(entity, /* headers */ null); + } + + + public static Response badRequest(Viewable entity, Map headers) + { + return builderForTextHtml(Status.BAD_REQUEST, entity, headers).build(); } @@ -183,7 +226,14 @@ public static Response badRequest(Viewable entity) */ public static Response unauthorized(String entity, String challenge) { - return builderForTextPlain(Status.UNAUTHORIZED, entity) + return unauthorized(entity, challenge, /* headers */ null); + } + + + public static Response unauthorized( + String entity, String challenge, Map headers) + { + return builderForTextPlain(Status.UNAUTHORIZED, entity, headers) .header(HttpHeaders.WWW_AUTHENTICATE, challenge) .build(); } @@ -203,7 +253,14 @@ public static Response unauthorized(String entity, String challenge) */ public static Response unauthorized(Viewable entity, String challenge) { - return builderForTextHtml(Status.UNAUTHORIZED, entity) + return unauthorized(entity, challenge, /* headers */ null); + } + + + public static Response unauthorized( + Viewable entity, String challenge, Map headers) + { + return builderForTextHtml(Status.UNAUTHORIZED, entity, headers) .header(HttpHeaders.WWW_AUTHENTICATE, challenge) .build(); } @@ -216,11 +273,17 @@ public static Response unauthorized(Viewable entity, String challenge) * A string entity to contain in the response. * * @return - * An "text/plain" response of "403 Forbidde". + * An "text/plain" response of "403 Forbidden". */ - public static Response forbidden(final String entity) + public static Response forbidden(String entity) + { + return forbidden(entity, /* headers */ null); + } + + + public static Response forbidden(String entity, Map headers) { - return builderForTextPlain(Status.FORBIDDEN, entity).build(); + return builderForTextPlain(Status.FORBIDDEN, entity, headers).build(); } @@ -231,11 +294,17 @@ public static Response forbidden(final String entity) * A string entity to contain in the response. * * @return - * An "application/json" response of "403 Forbidde". + * An "application/json" response of "403 Forbidden". */ - public static Response forbiddenJson(final String entity) + public static Response forbiddenJson(String entity) + { + return forbiddenJson(entity, /* headers */ null); + } + + + public static Response forbiddenJson(String entity, Map headers) { - return builderForJson(Status.FORBIDDEN, entity).build(); + return builderForJson(Status.FORBIDDEN, entity, headers).build(); } @@ -250,7 +319,13 @@ public static Response forbiddenJson(final String entity) */ public static Response notFound(String entity) { - return builderForTextPlain(Status.NOT_FOUND, entity).build(); + return notFound(entity, /* headers */ null); + } + + + public static Response notFound(String entity, Map headers) + { + return builderForTextPlain(Status.NOT_FOUND, entity, headers).build(); } @@ -265,7 +340,13 @@ public static Response notFound(String entity) */ public static Response notFoundJson(String entity) { - return builderForJson(Status.NOT_FOUND, entity).build(); + return notFoundJson(entity, /* headers */ null); + } + + + public static Response notFoundJson(String entity, Map headers) + { + return builderForJson(Status.NOT_FOUND, entity, headers).build(); } @@ -280,7 +361,13 @@ public static Response notFoundJson(String entity) */ public static Response notFound(Viewable entity) { - return builderForTextHtml(Status.NOT_FOUND, entity).build(); + return notFound(entity, /* headers */ null); + } + + + public static Response notFound(Viewable entity, Map headers) + { + return builderForTextHtml(Status.NOT_FOUND, entity, headers).build(); } @@ -295,7 +382,13 @@ public static Response notFound(Viewable entity) */ public static Response internalServerError(String entity) { - return builderForTextPlain(Status.INTERNAL_SERVER_ERROR, entity).build(); + return internalServerError(entity, /* headers */ null); + } + + + public static Response internalServerError(String entity, Map headers) + { + return builderForTextPlain(Status.INTERNAL_SERVER_ERROR, entity, headers).build(); } @@ -310,7 +403,13 @@ public static Response internalServerError(String entity) */ public static Response internalServerErrorJson(String entity) { - return builderForTextPlain(Status.INTERNAL_SERVER_ERROR, entity).build(); + return internalServerErrorJson(entity, /* headers */ null); + } + + + public static Response internalServerErrorJson(String entity, Map headers) + { + return builderForJson(Status.INTERNAL_SERVER_ERROR, entity, headers).build(); } @@ -325,33 +424,56 @@ public static Response internalServerErrorJson(String entity) */ public static Response internalServerError(Viewable entity) { - return builderForTextHtml(Status.INTERNAL_SERVER_ERROR, entity).build(); + return internalServerError(entity, /* headers */ null); + } + + + public static Response internalServerError(Viewable entity, Map headers) + { + return builderForTextHtml(Status.INTERNAL_SERVER_ERROR, entity, headers).build(); } - private static ResponseBuilder builderForTextPlain(Status status, String entity) + private static ResponseBuilder builderForTextPlain( + Status status, String entity, Map headers) { - return builder(status, entity, MEDIA_TYPE_PLAIN); + return builder(status, entity, MEDIA_TYPE_PLAIN, headers); } - private static ResponseBuilder builderForTextHtml(Status status, Viewable entity) + private static ResponseBuilder builderForTextHtml( + Status status, Viewable entity, Map headers) { - return builder(status, entity, MEDIA_TYPE_HTML); + return builder(status, entity, MEDIA_TYPE_HTML, headers); } - private static ResponseBuilder builderForJson(Status status, String entity) + private static ResponseBuilder builderForJson( + Status status, String entity, Map headers) { - return builder(status, entity, MEDIA_TYPE_JSON); + return builder(status, entity, MEDIA_TYPE_JSON, headers); } - private static ResponseBuilder builder(Status status, Object entity, MediaType type) + private static ResponseBuilder builder( + Status status, Object entity, MediaType type, Map headers) { - return Response + ResponseBuilder builder = Response .status(status) .entity(entity) .type(type); + + // If additional headers are given. + if (headers != null) + { + // For each additional header. + for (Map.Entry header : headers.entrySet()) + { + // Add the header. + builder.header(header.getKey(), header.getValue()); + } + } + + return builder; } } diff --git a/src/main/java/com/authlete/jaxrs/server/util/StringUtil.java b/src/main/java/com/authlete/jaxrs/server/util/StringUtil.java deleted file mode 100644 index d59faf1..0000000 --- a/src/main/java/com/authlete/jaxrs/server/util/StringUtil.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.authlete.jaxrs.server.util; - -public class StringUtil { - - public static String toJsonError(final String error) - { - return String.format("{\"error\": \"%s\"", error); - } - -} diff --git a/src/main/java/com/authlete/jaxrs/server/vc/AbstractOrderProcessor.java b/src/main/java/com/authlete/jaxrs/server/vc/AbstractOrderProcessor.java new file mode 100644 index 0000000..c0dfa75 --- /dev/null +++ b/src/main/java/com/authlete/jaxrs/server/vc/AbstractOrderProcessor.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2023 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.server.vc; + + +import java.util.List; +import java.util.Map; +import com.authlete.common.dto.CredentialIssuanceOrder; +import com.authlete.common.dto.CredentialRequestInfo; +import com.authlete.common.dto.IntrospectionResponse; +import com.authlete.common.types.User; +import com.authlete.jaxrs.server.db.UserDao; +import com.google.gson.Gson; + + +public abstract class AbstractOrderProcessor implements OrderProcessor +{ + @SuppressWarnings("unchecked") + @Override + public CredentialIssuanceOrder toOrder( + OrderContext context, + IntrospectionResponse introspection, + CredentialRequestInfo info) throws VerifiableCredentialException + { + // See "Credential Issuance Order" + // + // 3.5.1. Credential Issuance Order + // https://www.authlete.com/developers/oid4vci/#351-credential-issuance-order + // + + // === Step 1 === + // + // Get the subject (= unique identifier) of the user associated + // with the access token from the access token information. + String subject = introspection.getSubject(); + + // === Step 2 === + // + // Retrieve information about the user identified by the subject + // from the user database. + User user = UserDao.getBySubject(subject); + + // === Step 3 === + // + // Get the information about the issuable credentials associated + // with the access token from the access token information. + List> issuableCredentials = + parseJson(introspection.getIssuableCredentials(), List.class); + + // === Step 4 === + // + // Get the credential information included in the credential request + // from the credential request information. + Map requestedCredential = + parseJson(info.getDetails(), Map.class); + + // === Step 5 === + // + // Confirm that the access token has the necessary permissions for + // the credential request. + checkPermissions(context, issuableCredentials, info.getFormat(), requestedCredential); + + // === Step 6 === + // + // Determine the set of user claims to embed in the VC being issued + // based on the credential information, and get the values of the + // user claims from the dataset retrieved from the user database. + Map claims = + collectClaims(context, user, info.getFormat(), requestedCredential); + + // === Step 7 === + // + // Build a credential issuance order using the collected data. + CredentialIssuanceOrder order = createOrder(info, claims); + + // The credential issuance order. + return order; + } + + + /** + * Convert the given JSON to an instance of the specified Java class. + */ + private T parseJson(String json, Class klass) + { + return new Gson().fromJson(json, klass); + } + + + /** + * Create a credential issuance order. + */ + private CredentialIssuanceOrder createOrder( + CredentialRequestInfo info, Map claims) + { + String payload = (claims != null) ? new Gson().toJson(claims) : null; + boolean deferred = (payload == null); + + return new CredentialIssuanceOrder() + .setRequestIdentifier(info.getIdentifier()) + .setCredentialPayload(payload) + .setIssuanceDeferred(deferred) + ; + } + + + /** + * Check whether the issuable credentials include the requested credential. + * + * @param context + * The context in which this order processor is executed. + * + * @param issuableCredentials + * The issuable credentials associated with the access token. + * + * @param format + * The credential format. + * + * @param requestedCredential + * The requested credential. + * + * @throws InvalidCredentialRequestException + * The issuable credentials do not include the requested credential, + * the content of the requested credential is invalid, or some other + * errors. + */ + protected abstract void checkPermissions( + OrderContext context, + List> issuableCredentials, + String format, Map requestedCredential) + throws InvalidCredentialRequestException; + + + /** + * Collect the requested claims. + * + * @param context + * The context in which this order processor is executed. + * + * @param user + * The user associated with the access token. + * + * @param format + * The credential format. + * + * @param requestedCredential + * The requested credential. + * + * @return + * The key-value pairs representing the requested claims. + * If null is returned, the credential issuance will be deferred. + */ + protected abstract Map collectClaims( + OrderContext context, User user, String format, + Map requestedCredential) throws VerifiableCredentialException; +} diff --git a/src/main/java/com/authlete/jaxrs/server/api/vci/CredentialDefinitionType.java b/src/main/java/com/authlete/jaxrs/server/vc/CredentialDefinitionType.java similarity index 97% rename from src/main/java/com/authlete/jaxrs/server/api/vci/CredentialDefinitionType.java rename to src/main/java/com/authlete/jaxrs/server/vc/CredentialDefinitionType.java index c319b23..135c5c9 100644 --- a/src/main/java/com/authlete/jaxrs/server/api/vci/CredentialDefinitionType.java +++ b/src/main/java/com/authlete/jaxrs/server/vc/CredentialDefinitionType.java @@ -14,7 +14,7 @@ * language governing permissions and limitations under the * License. */ -package com.authlete.jaxrs.server.api.vci; +package com.authlete.jaxrs.server.vc; import java.util.Arrays; diff --git a/src/main/java/com/authlete/jaxrs/server/vc/InvalidCredentialRequestException.java b/src/main/java/com/authlete/jaxrs/server/vc/InvalidCredentialRequestException.java new file mode 100644 index 0000000..fc00be4 --- /dev/null +++ b/src/main/java/com/authlete/jaxrs/server/vc/InvalidCredentialRequestException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 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.server.vc; + + +public class InvalidCredentialRequestException extends VerifiableCredentialException +{ + private static final long serialVersionUID = 1L; + + + public InvalidCredentialRequestException() + { + } + + + public InvalidCredentialRequestException(String message) + { + super(message); + } + + + public InvalidCredentialRequestException(Throwable cause) + { + super(cause); + } + + + public InvalidCredentialRequestException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/src/main/java/com/authlete/jaxrs/server/vc/OrderContext.java b/src/main/java/com/authlete/jaxrs/server/vc/OrderContext.java new file mode 100644 index 0000000..085b9b1 --- /dev/null +++ b/src/main/java/com/authlete/jaxrs/server/vc/OrderContext.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 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.server.vc; + + +public enum OrderContext +{ + SINGLE, + BATCH, + DEFERRED, + ; +} diff --git a/src/main/java/com/authlete/jaxrs/server/api/vci/OrderFormat.java b/src/main/java/com/authlete/jaxrs/server/vc/OrderFormat.java similarity index 77% rename from src/main/java/com/authlete/jaxrs/server/api/vci/OrderFormat.java rename to src/main/java/com/authlete/jaxrs/server/vc/OrderFormat.java index e89d148..605c3bf 100644 --- a/src/main/java/com/authlete/jaxrs/server/api/vci/OrderFormat.java +++ b/src/main/java/com/authlete/jaxrs/server/vc/OrderFormat.java @@ -14,7 +14,7 @@ * language governing permissions and limitations under the * License. */ -package com.authlete.jaxrs.server.api.vci; +package com.authlete.jaxrs.server.vc; import java.util.Arrays; @@ -22,17 +22,17 @@ public enum OrderFormat { - SD_JWT("vc+sd-jwt", new SdJwtOrderProcessor()); + SD_JWT("vc+sd-jwt", new SdJwtOrderProcessor()), + ; - private String id; - private IOrderProcessor processor; + private final String id; + private final OrderProcessor processor; - OrderFormat(final String id, - final IOrderProcessor processor) + private OrderFormat(String id, OrderProcessor processor) { - this.id = id; + this.id = id; this.processor = processor; } @@ -43,7 +43,7 @@ public String getId() } - public IOrderProcessor getProcessor() + public OrderProcessor getProcessor() { return processor; } diff --git a/src/main/java/com/authlete/jaxrs/server/api/vci/IOrderProcessor.java b/src/main/java/com/authlete/jaxrs/server/vc/OrderProcessor.java similarity index 74% rename from src/main/java/com/authlete/jaxrs/server/api/vci/IOrderProcessor.java rename to src/main/java/com/authlete/jaxrs/server/vc/OrderProcessor.java index 4ff6752..6c01d03 100644 --- a/src/main/java/com/authlete/jaxrs/server/api/vci/IOrderProcessor.java +++ b/src/main/java/com/authlete/jaxrs/server/vc/OrderProcessor.java @@ -14,7 +14,7 @@ * language governing permissions and limitations under the * License. */ -package com.authlete.jaxrs.server.api.vci; +package com.authlete.jaxrs.server.vc; import com.authlete.common.dto.CredentialIssuanceOrder; @@ -22,8 +22,10 @@ import com.authlete.common.dto.IntrospectionResponse; -public interface IOrderProcessor +public interface OrderProcessor { - CredentialIssuanceOrder toOrder(final IntrospectionResponse introspection, - final CredentialRequestInfo info); + CredentialIssuanceOrder toOrder( + OrderContext context, + IntrospectionResponse introspection, + CredentialRequestInfo info) throws VerifiableCredentialException; } diff --git a/src/main/java/com/authlete/jaxrs/server/vc/SdJwtOrderProcessor.java b/src/main/java/com/authlete/jaxrs/server/vc/SdJwtOrderProcessor.java new file mode 100644 index 0000000..b0b96b9 --- /dev/null +++ b/src/main/java/com/authlete/jaxrs/server/vc/SdJwtOrderProcessor.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2023 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.server.vc; + + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import com.authlete.common.types.User; + + +public class SdJwtOrderProcessor extends AbstractOrderProcessor +{ + private static final String KEY_CREDENTIAL_DEFINITION = "credential_definition"; + private static final String KEY_FORMAT = "format"; + private static final String KEY_SUB = "sub"; + private static final String KEY_TYPE = "type"; + + + @SuppressWarnings("unchecked") + @Override + protected void checkPermissions( + OrderContext context, + List> issuableCredentials, + String format, Map requestedCredential) + throws InvalidCredentialRequestException + { + // As explained in https://www.authlete.com/developers/oid4vci/, + // it is challenging to implement this step in a manner consistent + // across all implementations due to the flaws of the OID4VCI spec. + + // The implementation here follows "SD-JWT-based Verifiable Credentials" + // as much as possible. + // + // https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/ + + // The requested credential must contain "credential_definition". + Map credentialDefinition = + extractCredentialDefinition(requestedCredential); + + // The "credential_definition" object must contain "type". + String type = extractType(credentialDefinition); + + // For each issuable credential. + for (Map issuableCredential : issuableCredentials) + { + // The format of the issuable credential. + String issuableCredentialFormat = (String)issuableCredential.get(KEY_FORMAT); + + // If the format of the requested credential is different from + // the format of the issuable credential + if (!format.equals(issuableCredentialFormat)) + { + continue; + } + + // The "credential_definition" in the issuable credential. + Object value = issuableCredential.get(KEY_CREDENTIAL_DEFINITION); + + // If the "credential_definition" property is not available as a JSON object. + if (!(value instanceof Map)) + { + continue; + } + + // The "type" in the "credential_definition" object of the issuable credential. + value = ((Map)value).get(KEY_TYPE); + + // If the "type" property is not available as a string. + if (!(value instanceof String)) + { + continue; + } + + // This implementation of the checkPermissions method is simple. + // If "credential_definition.type" of the requested credential + // matches "credential_definition.type" of any of the issuable + // credentials, it is regarded that the credential request is + // permitted. + if (type.equals(value)) + { + // The credential request is permitted. + return; + } + } + + throw new InvalidCredentialRequestException( + "The access token does not have permissions to request the credential."); + } + + + @SuppressWarnings("unchecked") + private Map extractCredentialDefinition( + Map requestedCredential) throws InvalidCredentialRequestException + { + // If the requested credential does not contain "credential_definition". + if (!requestedCredential.containsKey(KEY_CREDENTIAL_DEFINITION)) + { + throw new InvalidCredentialRequestException( + "The credential request does not contain 'credential_definition'."); + } + + // The value of the "credential_definition" property. + Object value = requestedCredential.get(KEY_CREDENTIAL_DEFINITION); + + // If the value of the "credential_definition" property is not a JSON object. + if (!(value instanceof Map)) + { + throw new InvalidCredentialRequestException( + "The value of the 'credential_definition' property in the credential request is not a JSON object."); + } + + return (Map)value; + } + + + private String extractType( + Map credentialDefinition) throws InvalidCredentialRequestException + { + // If the "credential_definition" does not contain "type". + if (!credentialDefinition.containsKey(KEY_TYPE)) + { + throw new InvalidCredentialRequestException( + "The 'credential_definition' object does not contain 'type'."); + } + + // The value of the "type" property. + Object value = credentialDefinition.get(KEY_TYPE); + + // If the value of the "type" property is not a string. + if (!(value instanceof String)) + { + throw new InvalidCredentialRequestException( + "The value of the 'type' property in the 'credential_definition' object is not a string."); + } + + return (String)value; + } + + + @Override + protected Map collectClaims( + OrderContext context, User user, String format, + Map requestedCredential) throws VerifiableCredentialException + { + // The "credential_definition.type" in the requested credential. + @SuppressWarnings("unchecked") + String type = (String)((Map)requestedCredential.get(KEY_CREDENTIAL_DEFINITION)).get(KEY_TYPE); + + // Find a CredentialDefinitionType having the type. + CredentialDefinitionType cdType = CredentialDefinitionType.byId(type); + + if (cdType == null) + { + // The credential type is not supported. + throw new UnsupportedCredentialTypeException(String.format( + "The credential type '%s' is not supported.", type)); + } + + // For testing purposes, the credential issuance for a certain user + // (subject = "1003", loginId = "max") is intentionally deferred. + if (context != OrderContext.DEFERRED && user.getSubject().equals("1003")) + { + // Returning null from the collectClaims() method will result in + // issuing a transaction ID instead of a verifiable credential. + return null; + } + + // Claims. + Map claims = new LinkedHashMap<>(); + + // "type" + claims.put(KEY_TYPE, type); + + // "sub" + claims.put(KEY_SUB, user.getSubject()); + + // The CredentialDefinitionType has a set of claims. + // For each claim in the set. + for (String claimName : cdType.getClaims()) + { + // The value of the claim. + Object claimValue = user.getClaim(claimName, null); + + // If the value of the claim is available. + if (claimValue != null) + { + claims.put(claimName, claimValue); + } + } + + return claims; + } +} diff --git a/src/main/java/com/authlete/jaxrs/server/vc/UnsupportedCredentialFormatException.java b/src/main/java/com/authlete/jaxrs/server/vc/UnsupportedCredentialFormatException.java new file mode 100644 index 0000000..3c599b0 --- /dev/null +++ b/src/main/java/com/authlete/jaxrs/server/vc/UnsupportedCredentialFormatException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 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.server.vc; + + +public class UnsupportedCredentialFormatException extends VerifiableCredentialException +{ + private static final long serialVersionUID = 1L; + + + public UnsupportedCredentialFormatException() + { + } + + + public UnsupportedCredentialFormatException(String message) + { + super(message); + } + + + public UnsupportedCredentialFormatException(Throwable cause) + { + super(cause); + } + + + public UnsupportedCredentialFormatException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/src/main/java/com/authlete/jaxrs/server/vc/UnsupportedCredentialTypeException.java b/src/main/java/com/authlete/jaxrs/server/vc/UnsupportedCredentialTypeException.java new file mode 100644 index 0000000..ede80fe --- /dev/null +++ b/src/main/java/com/authlete/jaxrs/server/vc/UnsupportedCredentialTypeException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 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.server.vc; + + +public class UnsupportedCredentialTypeException extends VerifiableCredentialException +{ + private static final long serialVersionUID = 1L; + + + public UnsupportedCredentialTypeException() + { + } + + + public UnsupportedCredentialTypeException(String message) + { + super(message); + } + + + public UnsupportedCredentialTypeException(Throwable cause) + { + super(cause); + } + + + public UnsupportedCredentialTypeException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/src/main/java/com/authlete/jaxrs/server/vc/VerifiableCredentialException.java b/src/main/java/com/authlete/jaxrs/server/vc/VerifiableCredentialException.java new file mode 100644 index 0000000..fd0c2fa --- /dev/null +++ b/src/main/java/com/authlete/jaxrs/server/vc/VerifiableCredentialException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 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.server.vc; + + +public class VerifiableCredentialException extends Exception +{ + private static final long serialVersionUID = 1L; + + + public VerifiableCredentialException() + { + } + + + public VerifiableCredentialException(String message) + { + super(message); + } + + + public VerifiableCredentialException(Throwable cause) + { + super(cause); + } + + + public VerifiableCredentialException(String message, Throwable cause) + { + super(message, cause); + } +}