Skip to content

Commit fa54dba

Browse files
Merge pull request #50 from authlete/misc/vci-endpoints-refactoring
[misc] OID4VCI Endpoints Refactoring
2 parents d49cfb7 + 0bf7130 commit fa54dba

20 files changed

+1229
-394
lines changed

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
<properties>
1313
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1414

15-
<authlete.java.common.version>3.81</authlete.java.common.version>
16-
<authlete.java.jaxrs.version>2.65</authlete.java.jaxrs.version>
15+
<authlete.java.common.version>3.83</authlete.java.common.version>
16+
<authlete.java.jaxrs.version>2.66</authlete.java.jaxrs.version>
1717
<javax.servlet-api.version>3.0.1</javax.servlet-api.version>
1818
<jersey.version>2.30.1</jersey.version>
1919
<jetty.version>9.4.27.v20200227</jetty.version>

src/main/java/com/authlete/jaxrs/server/api/vci/AbstractCredentialEndpoint.java

Lines changed: 259 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,72 +17,299 @@
1717
package com.authlete.jaxrs.server.api.vci;
1818

1919

20+
import java.util.Arrays;
21+
import java.util.LinkedHashMap;
22+
import java.util.Map;
23+
import java.util.stream.Collectors;
2024
import javax.servlet.http.HttpServletRequest;
2125
import javax.ws.rs.WebApplicationException;
22-
import javax.ws.rs.core.HttpHeaders;
2326
import com.authlete.common.api.AuthleteApi;
27+
import com.authlete.common.dto.CredentialIssuanceOrder;
28+
import com.authlete.common.dto.CredentialIssuerMetadataRequest;
29+
import com.authlete.common.dto.CredentialIssuerMetadataResponse;
30+
import com.authlete.common.dto.CredentialRequestInfo;
2431
import com.authlete.common.dto.IntrospectionRequest;
2532
import com.authlete.common.dto.IntrospectionResponse;
33+
import com.authlete.common.types.ErrorCode;
2634
import com.authlete.jaxrs.BaseResourceEndpoint;
2735
import com.authlete.jaxrs.server.util.ExceptionUtil;
36+
import com.authlete.jaxrs.server.vc.InvalidCredentialRequestException;
37+
import com.authlete.jaxrs.server.vc.OrderContext;
38+
import com.authlete.jaxrs.server.vc.OrderFormat;
39+
import com.authlete.jaxrs.server.vc.UnsupportedCredentialFormatException;
40+
import com.authlete.jaxrs.server.vc.UnsupportedCredentialTypeException;
41+
import com.google.gson.Gson;
42+
import com.google.gson.GsonBuilder;
2843

2944

3045
public abstract class AbstractCredentialEndpoint extends BaseResourceEndpoint
3146
{
32-
protected String checkContentExtractToken(final HttpServletRequest request,
33-
final String requestContent)
47+
/**
48+
* Get the configured value of the endpoint of the credential issuer.
49+
* The value is used as the expected value of the {@code htu} claim
50+
* in the DPoP proof JWT.
51+
*
52+
* <p>
53+
* When {@code dpop} is null, this method returns null. Otherwise, this
54+
* method calls the {@code /vci/metadata} API to get the metadata of the
55+
* credential issuer, and extracts the value of the specified endpoint
56+
* from the metadata.
57+
* </p>
58+
*
59+
* @param api
60+
* An instance of the {@link AuthleteApi} instance.
61+
*
62+
* @param dpop
63+
* A DPoP proof JWT, specified by the {@code DPoP} HTTP header.
64+
*
65+
* @param endpointName
66+
* The name of an endpoint, such as "{@code credential_endpoint}".
67+
*
68+
* @return
69+
* The configured value of the endpoint. If {@code dpop} is null,
70+
* this method returns null.
71+
*/
72+
protected String computeHtu(AuthleteApi api, String dpop, String endpointName)
3473
{
35-
if (requestContent == null)
74+
if (dpop == null)
3675
{
37-
throw ExceptionUtil.badRequestException("Missing request content.");
76+
// When a DPoP proof JWT is not available, computing the value
77+
// of "htu" is meaningless. We skip the computation to avoid
78+
// making a call to the /vci/metadata API.
79+
return null;
3880
}
3981

40-
final String accessToken = processAccessToken(request);
41-
if (accessToken == null)
82+
// Get the credential issuer metadata and extract the value of the
83+
// endpoint from the metadata.
84+
return (String)getCredentialIssuerMetadata(api).get(endpointName);
85+
}
86+
87+
88+
/**
89+
* Get the credential issuer metadata by calling the {@code /vci/metadata} API.
90+
*
91+
* @param api
92+
* An instance of the {@link AuthleteApi} instance.
93+
*
94+
* @return
95+
* The credential issuer metadata.
96+
*/
97+
@SuppressWarnings("unchecked")
98+
private Map<String, Object> getCredentialIssuerMetadata(AuthleteApi api)
99+
{
100+
// Call the /vci/metadata API to get the metadata of the credential issuer.
101+
CredentialIssuerMetadataResponse response =
102+
api.credentialIssuerMetadata(new CredentialIssuerMetadataRequest());
103+
104+
// The response content.
105+
String content = response.getResponseContent();
106+
107+
// If something wrong was reported by the /vci/metadata API.
108+
if (response.getAction() != CredentialIssuerMetadataResponse.Action.OK)
42109
{
43-
throw ExceptionUtil.badRequestException("Missing access token.");
110+
// 500 Internal Server Error + application/json
111+
throw ExceptionUtil.internalServerErrorExceptionJson(content);
44112
}
45113

46-
return accessToken;
114+
// Convert the credential issuer metadata into a Map instance.
115+
return new Gson().fromJson(content, Map.class);
47116
}
48117

49118

50-
private String processAccessToken(final HttpServletRequest request)
119+
/**
120+
* Validate the access token and get the information about it.
121+
*
122+
* @param req
123+
* The HTTP request that this endpoint has received.
124+
*
125+
* @param api
126+
* An instance of the {@link AuthleteApi} interface.
127+
*
128+
* @param at
129+
* The access token.
130+
*
131+
* @param dpop
132+
* A DPoP proof JWT, specified by the {@code DPoP} HTTP header.
133+
*
134+
* @param htu
135+
* The URL of this endpoint, the expected value of the {@code htu}
136+
* claim in the DPoP proof JWT.
137+
*
138+
* @return
139+
* The response from the {@code /auth/introspection} API.
140+
*/
141+
protected IntrospectionResponse introspect(
142+
HttpServletRequest req, AuthleteApi api,
143+
String at, String dpop, String htu)
51144
{
52-
// The value of the "Authorization" header.
53-
final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
145+
// The client certificate. This is needed for certificate-bound
146+
// access tokens. See RFC 8705 for details.
147+
String certificate = extractClientCertificate(req);
54148

55-
return super.extractAccessToken(authorization, null);
149+
// The request to the /auth/introspection API.
150+
IntrospectionRequest request =
151+
new IntrospectionRequest()
152+
.setToken(at)
153+
.setClientCertificate(certificate)
154+
.setDpop(dpop)
155+
.setHtm("POST")
156+
.setHtu(htu)
157+
;
158+
159+
// Validate the access token.
160+
return validateAccessToken(api, request);
56161
}
57162

58163

59-
protected IntrospectionResponse introspect(final AuthleteApi api,
60-
final String accessToken)
61-
throws WebApplicationException
164+
/**
165+
* Prepare additional HTTP headers that the response from this endpoint
166+
* should include.
167+
*
168+
* @param introspection
169+
* The response from the {@code /auth/introspection} API.
170+
*
171+
* @return
172+
* A map including pairs of a header name and a header value.
173+
*/
174+
protected Map<String, Object> prepareHeaders(IntrospectionResponse introspection)
62175
{
63-
final IntrospectionRequest introspectionRequest = new IntrospectionRequest()
64-
.setToken(accessToken);
176+
Map<String, Object> headers = new LinkedHashMap<>();
65177

66-
final IntrospectionResponse response = api.introspection(introspectionRequest);
67-
final String content = response.getResponseContent();
178+
// The expected nonce value for DPoP proof JWT.
179+
String dpopNonce = introspection.getDpopNonce();
180+
if (dpopNonce != null)
181+
{
182+
headers.put("DPoP-Nonce", dpopNonce);
183+
}
184+
185+
return headers;
186+
}
68187

69-
switch (response.getAction())
188+
189+
/**
190+
* Prepare a credential issuance order.
191+
*
192+
* @param context
193+
* The context in which this method is called.
194+
*
195+
* @param introspection
196+
* The response from the {@code /auth/introspection} API.
197+
*
198+
* @param info
199+
* The information about the credential request.
200+
*
201+
* @param headers
202+
* The additional headers that should be included in the response
203+
* from this endpoint.
204+
*
205+
* @return
206+
* A credential issuance order.
207+
*/
208+
protected CredentialIssuanceOrder prepareOrder(
209+
OrderContext context,
210+
IntrospectionResponse introspection, CredentialRequestInfo info,
211+
Map<String, Object> headers)
212+
{
213+
try
70214
{
71-
case BAD_REQUEST:
72-
throw ExceptionUtil.badRequestException(content);
215+
// Get an OrderFormat instance corresponding to the credential format.
216+
OrderFormat format = getOrderFormat(info);
217+
218+
// Let the processor for the format create a credential issuance
219+
// order based on the credential request.
220+
return format.getProcessor().toOrder(context, introspection, info);
221+
}
222+
catch (UnsupportedCredentialFormatException cause)
223+
{
224+
// 400 Bad Request + "error":"unsupported_credential_format"
225+
throw ExceptionUtil.badRequestExceptionJson(
226+
errorJson(ErrorCode.unsupported_credential_format, cause), headers);
227+
}
228+
catch (UnsupportedCredentialTypeException cause)
229+
{
230+
// 400 Bad Request + "error":"unsupported_credential_type"
231+
throw ExceptionUtil.badRequestExceptionJson(
232+
errorJson(ErrorCode.unsupported_credential_type, cause), headers);
233+
}
234+
catch (InvalidCredentialRequestException cause)
235+
{
236+
// 400 Bad Request + "error":"invalid_credential_request"
237+
throw ExceptionUtil.badRequestExceptionJson(
238+
errorJson(ErrorCode.invalid_credential_request, cause), headers);
239+
}
240+
catch (WebApplicationException cause)
241+
{
242+
throw cause;
243+
}
244+
catch (Exception cause)
245+
{
246+
// 500 Internal Server Error + "error":"server_error"
247+
throw ExceptionUtil.internalServerErrorExceptionJson(
248+
errorJson(ErrorCode.server_error, cause), headers);
249+
}
250+
}
251+
252+
253+
/**
254+
* Prepare credential issuance orders. The method is supposed to be called
255+
* from the implementation of the batch credential endpoint.
256+
*
257+
* @param introspection
258+
* The response from the {@code /auth/introspection} API.
259+
*
260+
* @param infos
261+
* The list of credential requests.
262+
*
263+
* @param headers
264+
* The additional headers that should be included in the response
265+
* from this endpoint.
266+
*
267+
* @return
268+
* The list of credential issuance orders.
269+
*/
270+
protected CredentialIssuanceOrder[] prepareOrders(
271+
IntrospectionResponse introspection, CredentialRequestInfo[] infos,
272+
Map<String, Object> headers)
273+
{
274+
// Convert the array of CredentialRequestInfo instances
275+
// into an array of CredentialIssuanceOrder instances.
276+
return Arrays.stream(infos)
277+
.map(info -> prepareOrder(OrderContext.BATCH, introspection, info, headers))
278+
.collect(Collectors.toList())
279+
.toArray(new CredentialIssuanceOrder[infos.length]);
280+
}
281+
282+
283+
private OrderFormat getOrderFormat(CredentialRequestInfo info) throws UnsupportedCredentialFormatException
284+
{
285+
// Get an OrderFormat instance that corresponds to the credential format.
286+
OrderFormat format = OrderFormat.byId(info.getFormat());
287+
288+
// If the format is not supported.
289+
if (format == null)
290+
{
291+
throw new UnsupportedCredentialFormatException(String.format(
292+
"The credential format '%s' is not supported.", info.getFormat()));
293+
}
294+
295+
return format;
296+
}
73297

74-
case UNAUTHORIZED:
75-
throw ExceptionUtil.unauthorizedException(accessToken, content);
76298

77-
case FORBIDDEN:
78-
throw ExceptionUtil.forbiddenException(content);
299+
protected String errorJson(ErrorCode errorCode, Throwable cause)
300+
{
301+
Map<String, Object> map = new LinkedHashMap<>();
79302

80-
case OK:
81-
return response;
303+
// "error"
304+
map.put("error", errorCode.name());
82305

83-
case INTERNAL_SERVER_ERROR:
84-
default:
85-
throw ExceptionUtil.internalServerErrorException(content);
306+
if (cause != null)
307+
{
308+
// "error_description"
309+
map.put("error_description", cause.getMessage());
86310
}
311+
312+
// The content of the error response.
313+
return new GsonBuilder().setPrettyPrinting().create().toJson(map);
87314
}
88315
}

0 commit comments

Comments
 (0)