From b45d59a718a344b73b52272edf3cce5655b28e3a Mon Sep 17 00:00:00 2001 From: irotech Date: Wed, 1 Mar 2023 09:09:11 +0000 Subject: [PATCH] SDK-2256: Add Digital Identity Session QR code retrieval service --- .../api/client/DigitalIdentityClient.java | 11 +++- .../client/identity/ShareSessionQrCode.java | 49 +++++++++++++++++- .../call/factory/UnsignedPathFactory.java | 5 ++ .../call/identity/DigitalIdentityService.java | 50 ++++++++++++++---- .../api/client/DigitalIdentityClientTest.java | 10 ++-- .../identity/DigitalIdentityServiceTest.java | 51 ++++++++++++++++--- .../springboot/DigitalIdentityController.java | 22 ++++++-- .../templates/digital-identity-share.html | 12 ++++- 8 files changed, 182 insertions(+), 28 deletions(-) diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/DigitalIdentityClient.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/DigitalIdentityClient.java index c5d80c91..7a62b27f 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/DigitalIdentityClient.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/DigitalIdentityClient.java @@ -61,8 +61,15 @@ public ShareSessionQrCode createShareQrCode(String sessionId) throws DigitalIden return identityService.createShareQrCode(sdkId, keyPair, sessionId); } - public Object fetchShareQrCode(String qrCodeId) { - return identityService.fetchShareQrCode(qrCodeId); + /** + * Retrieve the sharing session QR code + * + * @param qrCodeId ID of the QR code to retrieve + * @return The content of the Share Session QR code + * @throws DigitalIdentityException Thrown if the QR code retrieval is unsuccessful + */ + public ShareSessionQrCode fetchShareQrCode(String qrCodeId) throws DigitalIdentityException { + return identityService.fetchShareQrCode(sdkId, keyPair, qrCodeId); } public Object fetchShareReceipt(String receiptId) { diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionQrCode.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionQrCode.java index c18908ac..30c44a27 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionQrCode.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionQrCode.java @@ -1,5 +1,10 @@ package com.yoti.api.client.identity; +import java.net.URI; +import java.util.List; + +import com.yoti.api.client.identity.extension.Extension; + import com.fasterxml.jackson.annotation.JsonProperty; public class ShareSessionQrCode { @@ -8,20 +13,60 @@ public class ShareSessionQrCode { private String id; @JsonProperty(Property.URI) - private String uri; + private URI uri; + + @JsonProperty(Property.EXPIRY) + private String expiry; + + @JsonProperty(Property.POLICY) + private String policy; + + @JsonProperty(Property.EXTENSIONS) + private List> extensions; + + @JsonProperty(Property.SESSION) + private ShareSession session; + + @JsonProperty(Property.REDIRECT_URI) + private URI redirectUri; public String getId() { return id; } - public String getUri() { + public URI getUri() { return uri; } + public String getExpiry() { + return expiry; + } + + public String getPolicy() { + return policy; + } + + public List> getExtensions() { + return extensions; + } + + public ShareSession getSession() { + return session; + } + + public URI getRedirectUri() { + return redirectUri; + } + private static final class Property { private static final String ID = "id"; private static final String URI = "uri"; + private static final String EXPIRY = "expiry"; + private static final String POLICY = "policy"; + private static final String EXTENSIONS = "extensions"; + private static final String SESSION = "session"; + private static final String REDIRECT_URI = "redirectUri"; } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/factory/UnsignedPathFactory.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/factory/UnsignedPathFactory.java index 41c473ff..fe31d055 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/factory/UnsignedPathFactory.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/factory/UnsignedPathFactory.java @@ -10,6 +10,7 @@ public class UnsignedPathFactory { // Share V2 private static final String IDENTITY_SESSION_CREATION = "/v2/sessions"; private static final String IDENTITY_SESSION_QR_CODE_CREATION = "/v2/sessions/%s/qr-codes"; + private static final String IDENTITY_SESSION_QR_CODE_RETRIEVAL_TEMPLATE = "/v2/qr-codes/%s"; // Share V1 private static final String PROFILE = "/profile/%s?appId=%s"; @@ -41,6 +42,10 @@ public String createIdentitySessionQrCodePath(String sessionId) { return format(IDENTITY_SESSION_QR_CODE_CREATION, sessionId); } + public String createIdentitySessionQrCodeRetrievalPath(String qrCodeId) { + return format(IDENTITY_SESSION_QR_CODE_RETRIEVAL_TEMPLATE, qrCodeId); + } + public String createProfilePath(String appId, String connectToken) { return format(PROFILE, connectToken, appId); } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityService.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityService.java index a687542c..c6620cb9 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityService.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityService.java @@ -1,5 +1,6 @@ package com.yoti.api.client.spi.remote.call.identity; +import static com.yoti.api.client.spi.remote.call.HttpMethod.HTTP_GET; import static com.yoti.api.client.spi.remote.call.HttpMethod.HTTP_POST; import static com.yoti.api.client.spi.remote.call.YotiConstants.AUTH_ID_HEADER; import static com.yoti.api.client.spi.remote.call.YotiConstants.CONTENT_TYPE; @@ -15,12 +16,14 @@ import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyPair; +import java.util.Optional; import com.yoti.api.client.identity.ShareSession; import com.yoti.api.client.identity.ShareSessionQrCode; import com.yoti.api.client.identity.ShareSessionRequest; import com.yoti.api.client.spi.remote.call.ResourceException; import com.yoti.api.client.spi.remote.call.SignedRequest; +import com.yoti.api.client.spi.remote.call.SignedRequestBuilder; import com.yoti.api.client.spi.remote.call.SignedRequestBuilderFactory; import com.yoti.api.client.spi.remote.call.factory.UnsignedPathFactory; import com.yoti.json.ResourceMapper; @@ -32,6 +35,8 @@ public class DigitalIdentityService { private static final Logger LOG = LoggerFactory.getLogger(DigitalIdentityService.class); + private static final byte[] EMPTY_JSON = "{}".getBytes(StandardCharsets.UTF_8); + private final UnsignedPathFactory pathFactory; private final SignedRequestBuilderFactory requestBuilderFactory; @@ -60,7 +65,7 @@ public ShareSession createShareSession(String sdkId, KeyPair keyPair, ShareSessi try { byte[] payload = ResourceMapper.writeValueAsString(shareSessionRequest); - SignedRequest request = createSignedRequest(sdkId, keyPair, path, payload); + SignedRequest request = createSignedRequest(sdkId, keyPair, path, HTTP_POST, payload); return request.execute(ShareSession.class); } catch (IOException ex) { @@ -89,7 +94,7 @@ public ShareSessionQrCode createShareQrCode(String sdkId, KeyPair keyPair, Strin LOG.debug("Requesting Share Session QR code Creation for session ID '{}' at '{}'", sessionId, path); try { - SignedRequest request = createSignedRequest(sdkId, keyPair, path, "{}".getBytes(StandardCharsets.UTF_8)); + SignedRequest request = createSignedRequest(sdkId, keyPair, path, HTTP_POST, EMPTY_JSON); return request.execute(ShareSessionQrCode.class); } catch (GeneralSecurityException ex) { @@ -99,25 +104,50 @@ public ShareSessionQrCode createShareQrCode(String sdkId, KeyPair keyPair, Strin } } - public Object fetchShareQrCode(String qrCodeId) { - return null; + public ShareSessionQrCode fetchShareQrCode(String sdkId, KeyPair keyPair, String qrCodeId) + throws DigitalIdentityException { + notNullOrEmpty(sdkId, "SDK ID"); + notNull(keyPair, "Application Key Pair"); + notNullOrEmpty(qrCodeId, "QR Code ID"); + + String path = pathFactory.createIdentitySessionQrCodeRetrievalPath(qrCodeId); + + LOG.info("Requesting Share Session QR code with ID '{} at '{}'", qrCodeId, path); + + try { + SignedRequest request = createSignedRequest(sdkId, keyPair, path); + + return request.execute(ShareSessionQrCode.class); + } catch (GeneralSecurityException ex) { + throw new DigitalIdentityException("Error while signing the share QR code fetch request ", ex); + } catch (IOException | URISyntaxException | ResourceException ex) { + throw new DigitalIdentityException("Error while executing the share QR code fetch request ", ex); + } } public Object fetchShareReceipt(String receiptId) { return null; } - SignedRequest createSignedRequest(String sdkId, KeyPair keyPair, String path, byte[] payload) + SignedRequest createSignedRequest(String sdkId, KeyPair keyPair, String path) throws GeneralSecurityException, UnsupportedEncodingException, URISyntaxException { - return requestBuilderFactory.create() + return createSignedRequest(sdkId, keyPair, path, HTTP_GET, null); + } + + SignedRequest createSignedRequest(String sdkId, KeyPair keyPair, String path, String method, byte[] payload) + throws GeneralSecurityException, UnsupportedEncodingException, URISyntaxException { + SignedRequestBuilder requestBuilder = requestBuilderFactory.create() .withKeyPair(keyPair) .withBaseUrl(apiUrl) .withEndpoint(path) .withHeader(AUTH_ID_HEADER, sdkId) - .withHttpMethod(HTTP_POST) - .withHeader(CONTENT_TYPE, CONTENT_TYPE_JSON) - .withPayload(payload) - .build(); + .withHttpMethod(method); + + Optional.ofNullable(payload).map(v -> + requestBuilder.withPayload(v).withHeader(CONTENT_TYPE, CONTENT_TYPE_JSON) + ); + + return requestBuilder.build(); } } diff --git a/yoti-sdk-api/src/test/java/com/yoti/api/client/DigitalIdentityClientTest.java b/yoti-sdk-api/src/test/java/com/yoti/api/client/DigitalIdentityClientTest.java index 30f3e0fb..3970d738 100644 --- a/yoti-sdk-api/src/test/java/com/yoti/api/client/DigitalIdentityClientTest.java +++ b/yoti-sdk-api/src/test/java/com/yoti/api/client/DigitalIdentityClientTest.java @@ -77,6 +77,7 @@ public void builderWithKeyPairSource_NullKeyPairSource_IllegalArgumentException( assertThat(ex.getMessage(), containsString("Key Pair Source")); } + @Test public void build_MissingSdkId_IllegalArgumentException() { DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder().withKeyPairSource(validKeyPairSource); @@ -177,15 +178,18 @@ public void client_CreateShareQrCodeException_DigitalIdentityException() throws } @Test - public void client_FetchShareQrCodeException_DigitalIdentityException() { + public void client_FetchShareQrCodeException_DigitalIdentityException() throws IOException { + when(keyPairSource.getFromStream(any(KeyStreamVisitor.class))).thenReturn(keyPair); + DigitalIdentityClient identityClient = new DigitalIdentityClient( AN_SDK_ID, - validKeyPairSource, + keyPairSource, identityService ); String exMessage = "Fetch Share QR Error"; - when(identityService.fetchShareQrCode(A_QR_CODE_ID)).thenThrow(new DigitalIdentityException(exMessage)); + when(identityService.fetchShareQrCode(AN_SDK_ID, keyPair, A_QR_CODE_ID)) + .thenThrow(new DigitalIdentityException(exMessage)); DigitalIdentityException ex = assertThrows( DigitalIdentityException.class, diff --git a/yoti-sdk-api/src/test/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityServiceTest.java b/yoti-sdk-api/src/test/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityServiceTest.java index 5b23b4c3..dd458dfb 100644 --- a/yoti-sdk-api/src/test/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityServiceTest.java +++ b/yoti-sdk-api/src/test/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityServiceTest.java @@ -33,7 +33,9 @@ public class DigitalIdentityServiceTest { private static final String SDK_ID = "anSdkId"; private static final String SESSION_ID = "aSessionId"; + private static final String QR_CODE_ID = "aQrCodeId"; private static final String SESSION_CREATION_PATH = "aSessionCreationPath"; + private static final String POST = "POST"; private static final byte[] A_BODY_BYTES = "aBody".getBytes(StandardCharsets.UTF_8); @Spy @InjectMocks DigitalIdentityService identityService; @@ -111,15 +113,14 @@ public void createShareSession_SerializingWrongPayload_Exception() { } @Test - public void createShareSession_BuildingRequestWithWrongUri_Exception() - throws GeneralSecurityException, UnsupportedEncodingException, URISyntaxException { + public void createShareSession_BuildingRequestWithWrongUri_Exception() throws Exception { try (MockedStatic mapper = Mockito.mockStatic(ResourceMapper.class)) { mapper.when(() -> ResourceMapper.writeValueAsString(shareSessionRequest)).thenReturn(A_BODY_BYTES); when(requestBuilderFactory.create()).thenReturn(signedRequestBuilder); String exMessage = "URI wrong format"; URISyntaxException causeEx = new URISyntaxException("", exMessage); - when(identityService.createSignedRequest(SDK_ID, keyPair, SESSION_CREATION_PATH, A_BODY_BYTES)) + when(identityService.createSignedRequest(SDK_ID, keyPair, SESSION_CREATION_PATH, POST, A_BODY_BYTES)) .thenThrow(causeEx); DigitalIdentityException ex = assertThrows( @@ -142,7 +143,7 @@ public void createShareSession_BuildingRequestWithWrongQueryParams_Exception() t String exMessage = "Wrong query params format"; UnsupportedEncodingException causeEx = new UnsupportedEncodingException(exMessage); - when(identityService.createSignedRequest(SDK_ID, keyPair, SESSION_CREATION_PATH, A_BODY_BYTES)) + when(identityService.createSignedRequest(SDK_ID, keyPair, SESSION_CREATION_PATH, POST, A_BODY_BYTES)) .thenThrow(causeEx); DigitalIdentityException ex = assertThrows( @@ -165,7 +166,7 @@ public void createShareSession_BuildingRequestWithWrongDigest_Exception() throws String exMessage = "Wrong digest"; GeneralSecurityException causeEx = new GeneralSecurityException(exMessage); - when(identityService.createSignedRequest(SDK_ID, keyPair, SESSION_CREATION_PATH, A_BODY_BYTES)) + when(identityService.createSignedRequest(SDK_ID, keyPair, SESSION_CREATION_PATH, POST, A_BODY_BYTES)) .thenThrow(causeEx); DigitalIdentityException ex = assertThrows( @@ -186,7 +187,7 @@ public void createShareSession_SessionRequest_exception() throws Exception { when(requestBuilderFactory.create()).thenReturn(signedRequestBuilder); - when(identityService.createSignedRequest(SDK_ID, keyPair, SESSION_CREATION_PATH, A_BODY_BYTES)) + when(identityService.createSignedRequest(SDK_ID, keyPair, SESSION_CREATION_PATH, POST, A_BODY_BYTES)) .thenReturn(signedRequest); when(signedRequest.execute(ShareSession.class)).thenReturn(shareSession); @@ -236,6 +237,44 @@ public void createShareQrCode_NullSessionId_Exception() { assertThat(ex.getMessage(), containsString("Session ID")); } + @Test + public void fetchShareQrCode_NullSdkId_Exception() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> identityService.fetchShareQrCode(null, keyPair, QR_CODE_ID) + ); + + assertThat(ex.getMessage(), containsString("SDK ID")); + } + + @Test + public void fetchShareQrCode_EmptySdkId_Exception() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> identityService.fetchShareQrCode("", keyPair, QR_CODE_ID) + ); + + assertThat(ex.getMessage(), containsString("SDK ID")); + } + @Test + public void fetchShareQrCode_NullKeyPair_Exception() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> identityService.fetchShareQrCode(SDK_ID, null, QR_CODE_ID) + ); + + assertThat(ex.getMessage(), containsString("Application Key Pair")); + } + + @Test + public void fetchShareQrCode_NullSessionId_Exception() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> identityService.fetchShareQrCode(SDK_ID, keyPair, null) + ); + + assertThat(ex.getMessage(), containsString("QR Code ID")); + } } diff --git a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DigitalIdentityController.java b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DigitalIdentityController.java index d6ca1483..3ecc6afb 100644 --- a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DigitalIdentityController.java +++ b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DigitalIdentityController.java @@ -71,18 +71,32 @@ public String identityShare(Model model) throws URISyntaxException { String sessionId = session.getId(); + model.addAttribute("session_id", sessionId); + model.addAttribute("session_status", session.getStatus()); + model.addAttribute("session_expiry", session.getExpiry()); + ShareSessionQrCode sessionQrCode = execute(() -> client.createShareQrCode(sessionId), model); if (sessionQrCode == null) { return "error"; } - model.addAttribute("session_id", sessionId); - model.addAttribute("session_status", session.getStatus()); - model.addAttribute("session_expiry", session.getExpiry()); + String qrCodeId = sessionQrCode.getId(); - model.addAttribute("session_qrcode_id", sessionQrCode.getId()); + model.addAttribute("session_qrcode_id", qrCodeId); model.addAttribute("session_qrcode_uri", sessionQrCode.getUri()); + ShareSessionQrCode fetchQrCode = execute(() -> client.fetchShareQrCode(qrCodeId), model); + if (fetchQrCode == null) { + return "error"; + } + + model.addAttribute("qrcode_expiry", fetchQrCode.getExpiry()); + model.addAttribute("qrcode_extensions", fetchQrCode.getExtensions()); + model.addAttribute("qrcode_redirect_uri", fetchQrCode.getRedirectUri()); + model.addAttribute("qrcode_session_id", fetchQrCode.getSession().getId()); + model.addAttribute("qrcode_session_status", fetchQrCode.getSession().getStatus()); + model.addAttribute("qrcode_session_expiry", fetchQrCode.getSession().getExpiry()); + return "digital-identity-share"; } diff --git a/yoti-sdk-spring-boot-example/src/main/resources/templates/digital-identity-share.html b/yoti-sdk-spring-boot-example/src/main/resources/templates/digital-identity-share.html index d7d9ba56..bf327469 100644 --- a/yoti-sdk-spring-boot-example/src/main/resources/templates/digital-identity-share.html +++ b/yoti-sdk-spring-boot-example/src/main/resources/templates/digital-identity-share.html @@ -7,7 +7,7 @@ - +>
@@ -29,6 +29,16 @@

Digital Identity Share Example page

Id:

URI:

+ +
+

Fetched Session QR Code

+

Expiry:

+

Extensions:

+

Redirect URI:

+

Session ID:

+

Session Status:

+

Session Expiry:

+