From 1290a0f55af47924b3963da96ec6d4676e8333aa Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 18 Apr 2023 16:40:41 +0100 Subject: [PATCH 01/41] =?UTF-8?q?Bump=20maven-compiler-plugin=20version=20?= =?UTF-8?q?(3.10.1=20=E2=86=92=203.11.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 49eadfa6..474f9e90 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -97,7 +97,7 @@ 3.12.0 - 3.10.1 + 3.11.0 3.0.5 1.12.0 From 6b5d12ab82a6a76f35609876a3ae80c9e9a0c92b Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 18 Apr 2023 16:41:00 +0100 Subject: [PATCH 02/41] =?UTF-8?q?Bump=20animal-sniffer-maven-plugin=20vers?= =?UTF-8?q?ion=20(1.21=20=E2=86=92=201.23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 474f9e90..393e3234 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -106,7 +106,7 @@ 4 12 - 1.21 + 1.23 java18 1.0 From ee2b5d9da8edf9d18dcfd9fef89d2ee9808805c6 Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 18 Apr 2023 16:41:15 +0100 Subject: [PATCH 03/41] =?UTF-8?q?Bump=20dependency-check-maven=20version?= =?UTF-8?q?=20(8.0.2=20=E2=86=92=208.2.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 393e3234..5b14c2aa 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -102,7 +102,7 @@ 3.0.5 1.12.0 - 8.0.2 + 8.2.1 4 12 From 295ea256a1f8d1d05ec6a6d0267411f11f304880 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 20 Apr 2023 09:47:26 +0100 Subject: [PATCH 04/41] =?UTF-8?q?Bump=20spring-boot-dependencies=20version?= =?UTF-8?q?=20(2.7.8=20=E2=86=92=202.7.10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 5b14c2aa..e4c602e1 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -87,7 +87,7 @@ - 2.7.8 + 2.7.10 4.0.1 From 2569b58651319c7346e379520243cad5358e2ab4 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 20 Apr 2023 09:48:04 +0100 Subject: [PATCH 05/41] =?UTF-8?q?Bump=20spring-boot-starter-parent=20versi?= =?UTF-8?q?on=20(2.7.1=20=E2=86=92=202.7.10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/doc-scan/pom.xml | 4 +++- yoti-sdk-spring-boot-example/pom.xml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/doc-scan/pom.xml b/examples/doc-scan/pom.xml index f31f5c38..40421207 100644 --- a/examples/doc-scan/pom.xml +++ b/examples/doc-scan/pom.xml @@ -2,12 +2,14 @@ 4.0.0 + org.springframework.boot spring-boot-starter-parent - 2.7.1 + 2.7.10 + com.yoti.docscan.demo doc-scan-demo 1.0.0 diff --git a/yoti-sdk-spring-boot-example/pom.xml b/yoti-sdk-spring-boot-example/pom.xml index 0cd044e2..048b11a4 100644 --- a/yoti-sdk-spring-boot-example/pom.xml +++ b/yoti-sdk-spring-boot-example/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.1 + 2.7.10 From 390e282c50f3a89620524ef5ed1dc7ef4478f7c6 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 20 Apr 2023 13:33:24 +0100 Subject: [PATCH 06/41] NA: Drop dependency-check-maven fail build config --- yoti-sdk-parent/pom.xml | 3 --- yoti-sdk-parent/suppressed-cves.xml | 7 ------- 2 files changed, 10 deletions(-) delete mode 100644 yoti-sdk-parent/suppressed-cves.xml diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index e4c602e1..cb378073 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -103,7 +103,6 @@ 1.12.0 8.2.1 - 4 12 1.23 @@ -259,10 +258,8 @@ dependency-check-maven ${dependency-check-maven.version} - ${dependency-check-maven.cvss.limit} ${dependency-check-maven.cve.update.limit} false - ${basedir}/../yoti-sdk-parent/suppressed-cves.xml diff --git a/yoti-sdk-parent/suppressed-cves.xml b/yoti-sdk-parent/suppressed-cves.xml deleted file mode 100644 index cd78d73e..00000000 --- a/yoti-sdk-parent/suppressed-cves.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - CVE-2020-5408 - CVE-2016-1000027 - - From 6304eabd4b5039121b5d2cde8c277fdd30c9ab58 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 27 Apr 2023 14:42:16 +0100 Subject: [PATCH 07/41] =?UTF-8?q?Bump=20maven-deploy-plugin=20version=20(3?= =?UTF-8?q?.0.0-M2=20=E2=86=92=203.1.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9c8ef62f..b306207b 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ - 3.0.0-M2 + 3.1.1 1.1.0 From 89a644972f4fba350f74f66ae869135832a78586 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 27 Apr 2023 14:42:59 +0100 Subject: [PATCH 08/41] =?UTF-8?q?Bump=20jacoco-maven-plugin=20version=20(0?= =?UTF-8?q?.8.8=20=E2=86=92=200.8.10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index cb378073..73f884e5 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -125,7 +125,7 @@ 4.3.0 - 0.8.8 + 0.8.10 From e467c92c6619bc6eef8f6e8f81c6fac94b8a8d80 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 27 Apr 2023 14:43:30 +0100 Subject: [PATCH 09/41] =?UTF-8?q?Bump=20maven-project-info-reports-plugin?= =?UTF-8?q?=20version=20(3.4.2=20=E2=86=92=203.4.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 73f884e5..05cc234d 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -121,7 +121,7 @@ 3.0.1 - 3.4.2 + 3.4.3 4.3.0 From 6160675907c3bc5a4e57393003f44b34020aaa4f Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 27 Apr 2023 14:43:56 +0100 Subject: [PATCH 10/41] =?UTF-8?q?Bump=20maven-javadoc-plugin=20version=20(?= =?UTF-8?q?3.4.0=20=E2=86=92=203.5.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 05cc234d..d2dc318a 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -116,7 +116,7 @@ 3.2.1 - 3.4.0 + 3.5.0 8 3.0.1 From 98d31a8569a44d8b8a37d090b34f43c114692319 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 27 Apr 2023 14:44:21 +0100 Subject: [PATCH 11/41] =?UTF-8?q?Bump=20maven-enforcer-plugin=20version=20?= =?UTF-8?q?(3.1.0=20=E2=86=92=203.3.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index d2dc318a..156d6542 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -109,7 +109,7 @@ java18 1.0 - 3.1.0 + 3.3.0 1.6.1 1.6.13 From 6cbd78c718ccc0e2ecde58f0492137390d8f766c Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 27 Apr 2023 14:45:20 +0100 Subject: [PATCH 12/41] =?UTF-8?q?Bump=20jackson=20version=20(2.14.2=20?= =?UTF-8?q?=E2=86=92=202.15.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 156d6542..e74c8e33 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -81,7 +81,7 @@ 2.0.6 1.70 - 2.14.2 + 2.15.0 3.21.12 4.5.14 From 019e0d7c5ed822e1fe038ecd34c0ffdf637d0df4 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 27 Apr 2023 14:45:52 +0100 Subject: [PATCH 13/41] =?UTF-8?q?Bump=20protobuf-java=20version=20(3.21.12?= =?UTF-8?q?=20=E2=86=92=203.22.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index e74c8e33..cc7b244a 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -82,7 +82,7 @@ 2.0.6 1.70 2.15.0 - 3.21.12 + 3.22.3 4.5.14 From 39ac65a1b238f5aa703e4a156a762dc17f4b783a Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 27 Apr 2023 14:46:17 +0100 Subject: [PATCH 14/41] =?UTF-8?q?Bump=20slf4j=20version=20(2.0.6=20?= =?UTF-8?q?=E2=86=92=202.0.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index cc7b244a..f6822a1b 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -79,7 +79,7 @@ ${supported.java.version} - 2.0.6 + 2.0.7 1.70 2.15.0 3.22.3 From fe83d281605df88841618bc5b96a13b5694c281f Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 2 May 2023 16:20:13 +0100 Subject: [PATCH 15/41] =?UTF-8?q?SDK-2306:=20Migrate=20findbugs=20(3.0.5)?= =?UTF-8?q?=20=E2=86=92=20spotbugs=20(4.7.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-api/findbugs-rules.xml | 94 ------------------- yoti-sdk-api/pom.xml | 8 +- yoti-sdk-api/spotbugs/exclude-filter.xml | 47 ++++++++++ yoti-sdk-parent/pom.xml | 24 +++-- .../findbugs-rules.xml | 13 --- yoti-sdk-spring-boot-auto-config/pom.xml | 8 +- .../spotbugs/exclude-filter.xml | 9 ++ yoti-sdk-spring-security/pom.xml | 8 +- .../exclude-filter.xml} | 9 +- 9 files changed, 89 insertions(+), 131 deletions(-) delete mode 100644 yoti-sdk-api/findbugs-rules.xml create mode 100644 yoti-sdk-api/spotbugs/exclude-filter.xml delete mode 100644 yoti-sdk-spring-boot-auto-config/findbugs-rules.xml create mode 100644 yoti-sdk-spring-boot-auto-config/spotbugs/exclude-filter.xml rename yoti-sdk-spring-security/{findbugs-rules.xml => spotbugs/exclude-filter.xml} (72%) diff --git a/yoti-sdk-api/findbugs-rules.xml b/yoti-sdk-api/findbugs-rules.xml deleted file mode 100644 index e7bdf885..00000000 --- a/yoti-sdk-api/findbugs-rules.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/yoti-sdk-api/pom.xml b/yoti-sdk-api/pom.xml index be1e2cc9..1ff8bda0 100644 --- a/yoti-sdk-api/pom.xml +++ b/yoti-sdk-api/pom.xml @@ -82,10 +82,6 @@ org.owasp dependency-check-maven - - org.codehaus.mojo - findbugs-maven-plugin - org.codehaus.mojo animal-sniffer-maven-plugin @@ -115,6 +111,10 @@ + + com.github.spotbugs + spotbugs-maven-plugin + diff --git a/yoti-sdk-api/spotbugs/exclude-filter.xml b/yoti-sdk-api/spotbugs/exclude-filter.xml new file mode 100644 index 00000000..8b106198 --- /dev/null +++ b/yoti-sdk-api/spotbugs/exclude-filter.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index f6822a1b..f174ab76 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -99,7 +99,8 @@ 3.11.0 - 3.0.5 + 4.7.3 + 4.7.3.4 1.12.0 8.2.1 @@ -225,16 +226,18 @@ ${supported.java.version} - + - org.codehaus.mojo - findbugs-maven-plugin - ${findbugs-maven-plugin.version} + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs-maven-plugin.version} + true + ${project.basedir}/target/spotbugs + report.xml + ${project.basedir}/spotbugs/exclude-filter.xml Max Low - findbugs-rules.xml - true com.h3xstream.findsecbugs @@ -243,6 +246,13 @@ + + + com.github.spotbugs + spotbugs + ${spotbugs.version} + + diff --git a/yoti-sdk-spring-boot-auto-config/findbugs-rules.xml b/yoti-sdk-spring-boot-auto-config/findbugs-rules.xml deleted file mode 100644 index b43947ad..00000000 --- a/yoti-sdk-spring-boot-auto-config/findbugs-rules.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/yoti-sdk-spring-boot-auto-config/pom.xml b/yoti-sdk-spring-boot-auto-config/pom.xml index 544ccbc7..a950744a 100644 --- a/yoti-sdk-spring-boot-auto-config/pom.xml +++ b/yoti-sdk-spring-boot-auto-config/pom.xml @@ -60,10 +60,6 @@ org.owasp dependency-check-maven - - org.codehaus.mojo - findbugs-maven-plugin - org.codehaus.mojo animal-sniffer-maven-plugin @@ -72,6 +68,10 @@ org.apache.maven.plugins maven-enforcer-plugin + + com.github.spotbugs + spotbugs-maven-plugin + diff --git a/yoti-sdk-spring-boot-auto-config/spotbugs/exclude-filter.xml b/yoti-sdk-spring-boot-auto-config/spotbugs/exclude-filter.xml new file mode 100644 index 00000000..b5a52881 --- /dev/null +++ b/yoti-sdk-spring-boot-auto-config/spotbugs/exclude-filter.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/yoti-sdk-spring-security/pom.xml b/yoti-sdk-spring-security/pom.xml index 4821e92e..58c61424 100644 --- a/yoti-sdk-spring-security/pom.xml +++ b/yoti-sdk-spring-security/pom.xml @@ -74,10 +74,6 @@ org.owasp dependency-check-maven - - org.codehaus.mojo - findbugs-maven-plugin - org.codehaus.mojo animal-sniffer-maven-plugin @@ -86,6 +82,10 @@ org.apache.maven.plugins maven-enforcer-plugin + + com.github.spotbugs + spotbugs-maven-plugin + diff --git a/yoti-sdk-spring-security/findbugs-rules.xml b/yoti-sdk-spring-security/spotbugs/exclude-filter.xml similarity index 72% rename from yoti-sdk-spring-security/findbugs-rules.xml rename to yoti-sdk-spring-security/spotbugs/exclude-filter.xml index d59c5533..150c01cb 100644 --- a/yoti-sdk-spring-security/findbugs-rules.xml +++ b/yoti-sdk-spring-security/spotbugs/exclude-filter.xml @@ -1,15 +1,14 @@ - + - + + - + From cd96dbdc3bc74ece4aeca7321fbfa96e3e5adf95 Mon Sep 17 00:00:00 2001 From: irotech Date: Wed, 3 May 2023 08:39:43 +0100 Subject: [PATCH 16/41] =?UTF-8?q?Bump=20extra-enforcer-rules=20version=20(?= =?UTF-8?q?1.6.1=20=E2=86=92=201.6.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index f174ab76..445b89f6 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -111,7 +111,7 @@ 1.0 3.3.0 - 1.6.1 + 1.6.2 1.6.13 From fb743bb65767a6b0c9d35030292e010c5c1d408f Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Tue, 25 Jul 2023 11:09:04 +0100 Subject: [PATCH 17/41] DEVELOPMENT: Set version to 3.8.0-SNAPSHOT --- README.md | 4 ++-- examples/doc-scan/pom.xml | 2 +- pom.xml | 2 +- yoti-sdk-api/pom.xml | 2 +- .../com/yoti/api/client/spi/remote/call/YotiConstants.java | 2 +- yoti-sdk-parent/pom.xml | 2 +- yoti-sdk-sandbox/pom.xml | 2 +- yoti-sdk-spring-boot-auto-config/README.md | 4 ++-- yoti-sdk-spring-boot-auto-config/pom.xml | 2 +- yoti-sdk-spring-boot-example/README.md | 6 +++--- yoti-sdk-spring-boot-example/pom.xml | 2 +- yoti-sdk-spring-security/README.md | 4 ++-- yoti-sdk-spring-security/pom.xml | 2 +- 13 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9ff72c73..ed0f2db1 100644 --- a/README.md +++ b/README.md @@ -101,13 +101,13 @@ If you are using Maven, you need to add the following dependency: com.yoti yoti-sdk-api - 3.7.0 + 3.8.0-SNAPSHOT ``` If you are using Gradle, here is the dependency to add: -`compile group: 'com.yoti', name: 'yoti-sdk-api', version: '3.7.0'` +`compile group: 'com.yoti', name: 'yoti-sdk-api', version: '3.8.0-SNAPSHOT'` You will find all classes packaged under `com.yoti.api` diff --git a/examples/doc-scan/pom.xml b/examples/doc-scan/pom.xml index 40421207..d15f1fda 100644 --- a/examples/doc-scan/pom.xml +++ b/examples/doc-scan/pom.xml @@ -53,7 +53,7 @@ com.yoti yoti-sdk-api - 3.7.0 + 3.8.0-SNAPSHOT diff --git a/pom.xml b/pom.xml index b306207b..4d97749d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.yoti yoti-sdk pom - 3.7.0 + 3.8.0-SNAPSHOT Yoti SDK Java SDK for simple integration with the Yoti platform https://github.com/getyoti/yoti-java-sdk diff --git a/yoti-sdk-api/pom.xml b/yoti-sdk-api/pom.xml index 1ff8bda0..d3af7f84 100644 --- a/yoti-sdk-api/pom.xml +++ b/yoti-sdk-api/pom.xml @@ -11,7 +11,7 @@ com.yoti yoti-sdk-parent - 3.7.0 + 3.8.0-SNAPSHOT ../yoti-sdk-parent diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java index e303258a..e12607f1 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java @@ -25,7 +25,7 @@ private YotiConstants() {} public static final String CONTENT_TYPE_JPEG = "image/jpeg"; public static final String JAVA = "Java"; - public static final String SDK_VERSION = JAVA + "-3.7.0"; + public static final String SDK_VERSION = JAVA + "-3.8.0-SNAPSHOT"; public static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; public static final String ASYMMETRIC_CIPHER = "RSA/NONE/PKCS1Padding"; public static final String SYMMETRIC_CIPHER = "AES/CBC/PKCS7Padding"; diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 445b89f6..e6233c84 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -5,7 +5,7 @@ com.yoti yoti-sdk-parent pom - 3.7.0 + 3.8.0-SNAPSHOT Yoti SDK Parent Pom Parent pom for the Java SDK projects https://github.com/getyoti/yoti-java-sdk diff --git a/yoti-sdk-sandbox/pom.xml b/yoti-sdk-sandbox/pom.xml index 9a1c1dd4..6c7d5883 100644 --- a/yoti-sdk-sandbox/pom.xml +++ b/yoti-sdk-sandbox/pom.xml @@ -11,7 +11,7 @@ com.yoti yoti-sdk-parent - 3.7.0 + 3.8.0-SNAPSHOT ../yoti-sdk-parent diff --git a/yoti-sdk-spring-boot-auto-config/README.md b/yoti-sdk-spring-boot-auto-config/README.md index 51664523..06230787 100644 --- a/yoti-sdk-spring-boot-auto-config/README.md +++ b/yoti-sdk-spring-boot-auto-config/README.md @@ -18,7 +18,7 @@ If you are using Maven, you need to add the following dependencies: com.yoti yoti-sdk-spring-boot-auto-config - 3.7.0 + 3.8.0-SNAPSHOT ``` @@ -26,7 +26,7 @@ If you are using Maven, you need to add the following dependencies: If you are using Gradle, here is the dependency to add: ``` -compile group: 'com.yoti', name: 'yoti-sdk-spring-boot-auto-config', version: '3.7.0' +compile group: 'com.yoti', name: 'yoti-sdk-spring-boot-auto-config', version: '3.8.0-SNAPSHOT' ``` diff --git a/yoti-sdk-spring-boot-auto-config/pom.xml b/yoti-sdk-spring-boot-auto-config/pom.xml index a950744a..a632ca5e 100644 --- a/yoti-sdk-spring-boot-auto-config/pom.xml +++ b/yoti-sdk-spring-boot-auto-config/pom.xml @@ -12,7 +12,7 @@ com.yoti yoti-sdk-parent - 3.7.0 + 3.8.0-SNAPSHOT ../yoti-sdk-parent diff --git a/yoti-sdk-spring-boot-example/README.md b/yoti-sdk-spring-boot-example/README.md index d4291554..4556d125 100644 --- a/yoti-sdk-spring-boot-example/README.md +++ b/yoti-sdk-spring-boot-example/README.md @@ -16,7 +16,7 @@ Before you start, you'll need to create an Application in [Yoti Hub](https://hub com.yoti yoti-sdk-api - 3.7.0 + 3.8.0-SNAPSHOT ``` @@ -29,8 +29,8 @@ Before you start, you'll need to create an Application in [Yoti Hub](https://hub 1. Run `mvn clean package` to build the project. ## Running -* You can run your server-app by executing `java -jar target/yoti-sdk-spring-boot-example-3.7.0.jar` - * If you are using Java 9, you can run the server-app as follows `java -jar target/yoti-sdk-spring-boot-example-3.7.0.jar --add-exports java.base/jdk.internal.ref=ALL-UNNAMED` +* You can run your server-app by executing `java -jar target/yoti-sdk-spring-boot-example-3.8.0-SNAPSHOT.jar` + * If you are using Java 9, you can run the server-app as follows `java -jar target/yoti-sdk-spring-boot-example-3.8.0-SNAPSHOT.jar --add-exports java.base/jdk.internal.ref=ALL-UNNAMED` * Navigate to: * `https://localhost:8443` to initiate a login using Yoti. The Spring demo is listening for the response on `https://localhost:8443/login`. * `https://localhost:8443/dynamic-share` to initiate a dynamic share with location with result displayed in profile page. diff --git a/yoti-sdk-spring-boot-example/pom.xml b/yoti-sdk-spring-boot-example/pom.xml index 048b11a4..215e56ab 100644 --- a/yoti-sdk-spring-boot-example/pom.xml +++ b/yoti-sdk-spring-boot-example/pom.xml @@ -6,7 +6,7 @@ com.yoti yoti-sdk-spring-boot-example Yoti Spring Boot Example - 3.7.0 + 3.8.0-SNAPSHOT org.springframework.boot diff --git a/yoti-sdk-spring-security/README.md b/yoti-sdk-spring-security/README.md index 1a1673e4..2fc11090 100644 --- a/yoti-sdk-spring-security/README.md +++ b/yoti-sdk-spring-security/README.md @@ -25,14 +25,14 @@ If you are using Maven, you need to add the following dependencies: com.yoti yoti-sdk-spring-security - 3.7.0 + 3.8.0-SNAPSHOT ``` If you are using Gradle, here is the dependency to add: ``` -compile group: 'com.yoti', name: 'yoti-sdk-spring-security', version: '3.7.0' +compile group: 'com.yoti', name: 'yoti-sdk-spring-security', version: '3.8.0-SNAPSHOT' ``` ### Provide a `YotiClient` instance diff --git a/yoti-sdk-spring-security/pom.xml b/yoti-sdk-spring-security/pom.xml index 58c61424..9ce80ac9 100644 --- a/yoti-sdk-spring-security/pom.xml +++ b/yoti-sdk-spring-security/pom.xml @@ -12,7 +12,7 @@ com.yoti yoti-sdk-parent - 3.7.0 + 3.8.0-SNAPSHOT ../yoti-sdk-parent From 231c983e6e1931d35fb863b4e7de17d598711f9b Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 25 Jul 2023 16:57:43 +0100 Subject: [PATCH 18/41] =?UTF-8?q?Bump=20protobuf-java=20version=20(3.22.3?= =?UTF-8?q?=20=E2=86=92=203.22.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index e6233c84..889102e8 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -82,7 +82,7 @@ 2.0.7 1.70 2.15.0 - 3.22.3 + 3.22.4 4.5.14 From 3155849915bd5c244c167b385942ce8ef2a41658 Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 25 Jul 2023 17:00:00 +0100 Subject: [PATCH 19/41] =?UTF-8?q?Bump=20guava=20version=20(31.1-jre=20?= =?UTF-8?q?=E2=86=92=2032.1.1-jre)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-spring-boot-example/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-spring-boot-example/pom.xml b/yoti-sdk-spring-boot-example/pom.xml index 215e56ab..0936a764 100644 --- a/yoti-sdk-spring-boot-example/pom.xml +++ b/yoti-sdk-spring-boot-example/pom.xml @@ -19,7 +19,7 @@ UTF-8 1.8 - 31.1-jre + 32.1.1-jre From 917fce0821e4f16417b8f9a3fccb45755788ee38 Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 25 Jul 2023 17:10:22 +0100 Subject: [PATCH 20/41] =?UTF-8?q?Bump=20maven-gpg-plugin=20version=20(3.0.?= =?UTF-8?q?1=20=E2=86=92=203.1.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 889102e8..b08e81ea 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -120,7 +120,7 @@ 3.5.0 8 - 3.0.1 + 3.1.0 3.4.3 From 1c6360d9f7a7530d150eca7107edb8fd8973aaa8 Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 25 Jul 2023 17:11:39 +0100 Subject: [PATCH 21/41] =?UTF-8?q?Bump=20dependency-check-maven=20version?= =?UTF-8?q?=20(8.2.1=20=E2=86=92=208.3.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index b08e81ea..8130629d 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -103,7 +103,7 @@ 4.7.3.4 1.12.0 - 8.2.1 + 8.3.1 12 1.23 From 82d578a30a40614cac799349a5800041c07fa805 Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 25 Jul 2023 17:24:35 +0100 Subject: [PATCH 22/41] =?UTF-8?q?Bump=20jackson=20version=20(2.15.0=20?= =?UTF-8?q?=E2=86=92=202.15.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 8130629d..b8f5f89e 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -81,7 +81,7 @@ 2.0.7 1.70 - 2.15.0 + 2.15.2 3.22.4 4.5.14 From 4f28dd5734e4bfe9f077256a7adc8f3391b807a1 Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 7 Mar 2023 15:39:28 +0000 Subject: [PATCH 23/41] SDK-2231: Add Maven profile to set java release build version for JDK 9+ --- yoti-sdk-parent/pom.xml | 32 ++++++++++++++++++------- yoti-sdk-spring-boot-example/pom.xml | 35 +++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index b8f5f89e..4a296a86 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -30,6 +30,25 @@ + + java-8 + + 1.8 + + + ${supported.java.version} + ${supported.java.version} + + + + java-9+ + + [9,) + + + ${supported.java.release.version} + + sonatype @@ -74,10 +93,10 @@ ${project.version} + 1.8 - ${supported.java.version} - ${supported.java.version} - + 8 + 2.0.7 1.70 @@ -216,15 +235,10 @@ - org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} - - ${supported.java.version} - ${supported.java.version} - @@ -262,7 +276,7 @@ - + org.owasp dependency-check-maven diff --git a/yoti-sdk-spring-boot-example/pom.xml b/yoti-sdk-spring-boot-example/pom.xml index 0936a764..0b6e7a9d 100644 --- a/yoti-sdk-spring-boot-example/pom.xml +++ b/yoti-sdk-spring-boot-example/pom.xml @@ -15,9 +15,33 @@ + + + java-8 + + 1.8 + + + ${supported.java.version} + ${supported.java.version} + + + + java-9+ + + [9,) + + + ${supported.java.release.version} + + + + UTF-8 - 1.8 + + 1.8 + 8 32.1.1-jre @@ -68,6 +92,15 @@ + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + org.springframework.boot From b07c43c335c8b3b9daf61f99efba945e9881a7e8 Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 7 Mar 2023 15:42:22 +0000 Subject: [PATCH 24/41] SDK-2231: Add Digital Identity Client --- .../api/client/DigitalIdentityClient.java | 108 +++++++++ .../api/client/InitialisationException.java | 1 + .../com/yoti/api/client/KeyPairSource.java | 3 +- .../java/com/yoti/api/client/YotiClient.java | 1 - .../identity/DigitalIdentityException.java | 21 ++ .../call/identity/DigitalIdentityService.java | 29 +++ .../client/spi/remote/util/Validation.java | 4 + .../java/com/yoti/validation/Validation.java | 77 ++++++ .../api/client/DigitalIdentityClientTest.java | 229 ++++++++++++++++++ yoti-sdk-spring-boot-auto-config/pom.xml | 7 +- ...tProperties.java => ClientProperties.java} | 45 +--- ...igitalIdentityClientAutoConfiguration.java | 49 ++++ .../api/spring/DigitalIdentityProperties.java | 18 ++ .../spring/SpringResourceKeyPairSource.java | 40 +-- .../spring/YotiClientAutoConfiguration.java | 33 +-- .../com/yoti/api/spring/YotiProperties.java | 63 +---- .../main/resources/META-INF/spring.factories | 4 +- ...iesTest.java => ClientPropertiesTest.java} | 35 ++- .../spring/DigitalIdentityPropertiesTest.java | 33 +++ .../yoti/api/spring/YotiPropertiesTest.java | 17 +- .../springboot/DigitalIdentityController.java | 54 +++++ .../springboot/YotiLoginController.java | 12 +- .../templates/digital-identity-share.html | 22 ++ 23 files changed, 717 insertions(+), 188 deletions(-) create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/DigitalIdentityClient.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityException.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityService.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/validation/Validation.java create mode 100644 yoti-sdk-api/src/test/java/com/yoti/api/client/DigitalIdentityClientTest.java rename yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/{YotiClientProperties.java => ClientProperties.java} (52%) create mode 100644 yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/DigitalIdentityClientAutoConfiguration.java create mode 100644 yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/DigitalIdentityProperties.java rename yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/{YotiClientPropertiesTest.java => ClientPropertiesTest.java} (54%) create mode 100644 yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/DigitalIdentityPropertiesTest.java create mode 100644 yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DigitalIdentityController.java create mode 100644 yoti-sdk-spring-boot-example/src/main/resources/templates/digital-identity-share.html 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 new file mode 100644 index 00000000..362aa41f --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/DigitalIdentityClient.java @@ -0,0 +1,108 @@ +package com.yoti.api.client; + +import static com.yoti.validation.Validation.notNull; +import static com.yoti.validation.Validation.notNullOrEmpty; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.Security; + +import com.yoti.api.client.spi.remote.KeyStreamVisitor; +import com.yoti.api.client.spi.remote.call.identity.DigitalIdentityService; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +public class DigitalIdentityClient { + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + private final String sdkId; + private final KeyPair keyPair; + private final DigitalIdentityService identityService; + + DigitalIdentityClient(String sdkId, KeyPairSource keyPair, DigitalIdentityService identityService) { + this.sdkId = sdkId; + this.keyPair = loadKeyPair(keyPair); + this.identityService = identityService; + } + + public Object createShareSession() { + notNullOrEmpty(sdkId, "SDK ID"); + notNull(keyPair, "Application Key Pair"); + + return identityService.createShareSession(); + } + + public Object fetchShareSession(String sessionId) { + notNullOrEmpty(sdkId, "SDK ID"); + notNull(keyPair, "Application Key Pair"); + + return identityService.fetchShareSession(sessionId); + } + + public Object createShareQrCode(String sessionId) { + notNullOrEmpty(sdkId, "SDK ID"); + notNull(keyPair, "Application Key Pair"); + + return identityService.createShareQrCode(sessionId); + } + + public Object fetchShareQrCode(String qrCodeId) { + notNullOrEmpty(sdkId, "SDK ID"); + notNull(keyPair, "Application Key Pair"); + + return identityService.fetchShareQrCode(qrCodeId); + } + + public Object fetchShareReceipt(String receiptId) { + notNullOrEmpty(sdkId, "SDK ID"); + notNull(keyPair, "Application Key Pair"); + + return identityService.fetchShareReceipt(receiptId); + } + + private KeyPair loadKeyPair(KeyPairSource keyPair) throws InitialisationException { + try { + return keyPair.getFromStream(new KeyStreamVisitor()); + } catch (IOException ex) { + throw new InitialisationException("Cannot load Key Pair", ex); + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String sdkId; + private KeyPairSource keyPairSource; + + private Builder() { } + + public Builder withClientSdkId(String sdkId) { + notNullOrEmpty(sdkId, "SDK ID"); + + this.sdkId = sdkId; + return this; + } + + public Builder withKeyPairSource(KeyPairSource keyPairSource) { + notNull(keyPairSource, "Key Pair Source"); + + this.keyPairSource = keyPairSource; + return this; + } + + public DigitalIdentityClient build() { + notNull(sdkId, "SDK ID"); + notNull(keyPairSource, "Key Pair Source"); + + return new DigitalIdentityClient(sdkId, keyPairSource, DigitalIdentityService.newInstance()); + } + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/InitialisationException.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/InitialisationException.java index b535832a..2112b96a 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/InitialisationException.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/InitialisationException.java @@ -12,4 +12,5 @@ public InitialisationException(String message) { public InitialisationException(String message, Throwable cause) { super(message, cause); } + } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/KeyPairSource.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/KeyPairSource.java index eaf15a4c..8880c24b 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/KeyPairSource.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/KeyPairSource.java @@ -30,7 +30,8 @@ public interface KeyPairSource { */ KeyPair getFromStream(StreamVisitor streamVisitor) throws IOException, InitialisationException; - public static interface StreamVisitor { + interface StreamVisitor { KeyPair accept(InputStream stream) throws IOException, InitialisationException; } + } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/YotiClient.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/YotiClient.java index 5a6c4e9f..c4dfce3a 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/YotiClient.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/YotiClient.java @@ -118,7 +118,6 @@ public ShareUrlResult createShareUrl(DynamicScenario dynamicScenario) throws Dyn private KeyPair loadKeyPair(KeyPairSource kpSource) throws InitialisationException { try { - LOG.debug("Loading key pair from '{}'", kpSource); return kpSource.getFromStream(new KeyStreamVisitor()); } catch (IOException e) { throw new InitialisationException("Cannot load key pair", e); diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityException.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityException.java new file mode 100644 index 00000000..c0c191ba --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityException.java @@ -0,0 +1,21 @@ +package com.yoti.api.client.spi.remote.call.identity; + +public class DigitalIdentityException extends RuntimeException { + + public DigitalIdentityException() { + super(); + } + + public DigitalIdentityException(String message) { + super(message); + } + + public DigitalIdentityException(String message, Throwable cause) { + super(message, cause); + } + + public DigitalIdentityException(Throwable cause) { + super(cause); + } + +} 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 new file mode 100644 index 00000000..5aefc1a1 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityService.java @@ -0,0 +1,29 @@ +package com.yoti.api.client.spi.remote.call.identity; + +public class DigitalIdentityService { + + public static DigitalIdentityService newInstance() { + return new DigitalIdentityService(); + } + + public Object createShareSession() { + return null; + } + + public Object fetchShareSession(String sessionId) { + return null; + } + + public Object createShareQrCode(String sessionId) { + return null; + } + + public Object fetchShareQrCode(String qrCodeId) { + return null; + } + + public Object fetchShareReceipt(String receiptId) { + return null; + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/util/Validation.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/util/Validation.java index 7cc078b0..b3b72e9b 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/util/Validation.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/util/Validation.java @@ -5,6 +5,10 @@ import java.util.Collection; import java.util.List; +/** + * @deprecated replaced by {@link com.yoti.validation.Validation} + */ +@Deprecated public class Validation { public static T notNull(T value, String name) { diff --git a/yoti-sdk-api/src/main/java/com/yoti/validation/Validation.java b/yoti-sdk-api/src/main/java/com/yoti/validation/Validation.java new file mode 100644 index 00000000..0eedb2ac --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/validation/Validation.java @@ -0,0 +1,77 @@ +package com.yoti.validation; + +import static java.lang.String.format; + +import java.util.Collection; +import java.util.List; + +public final class Validation { + + private Validation() { } + + public static T notNull(T value, String name) { + if (value == null) { + throw new IllegalArgumentException(format("'%s' must not be null", name)); + } + return value; + } + + public static boolean isNullOrEmpty(String value) { + return value == null || value.isEmpty(); + } + + public static void notNullOrEmpty(String value, String name) { + if (isNullOrEmpty(value)) { + throw new IllegalArgumentException(format("'%s' must not be empty or null", name)); + } + } + + public static void notNullOrEmpty(Collection value, String name) { + if (value == null || value.isEmpty()) { + throw new IllegalArgumentException(format("'%s' must not be empty or null", name)); + } + } + + public static void notEqualTo(Object value, Object notAllowed, String name) { + if (notEqualTo(value, notAllowed)) { + throw new IllegalArgumentException(format("'%s' value may not be equalTo '%s'", name, notAllowed)); + } + } + + private static boolean notEqualTo(Object value, Object notAllowed) { + if (notAllowed == null) { + return value == null; + } + return notAllowed.equals(value); + } + + public static void notGreaterThan(T value, T limit, String name) { + if (value.compareTo(limit) > 0) { + throw new IllegalArgumentException(format("'%s' value '%s' is greater than '%s'", name, value, limit)); + } + } + + public static void notLessThan(T value, T limit, String name) { + if (value.compareTo(limit) < 0) { + throw new IllegalArgumentException(format("'%s' value '%s' is less than '%s'", name, value, limit)); + } + } + + public static void withinRange(T value, T minLimit, T maxLimit, String name) { + notLessThan(value, minLimit, name); + notGreaterThan(value, maxLimit, name); + } + + public static void matchesPattern(String value, String regex, String name) { + if (value == null || ! value.matches(regex)) { + throw new IllegalArgumentException(format("'%s' value '%s' does not match format '%s'", name, value, regex)); + } + } + + public static void withinList(T value, List allowedValues) { + if (!allowedValues.contains(value)) { + throw new IllegalArgumentException(format("value '%s' is not in the list '%s'", value, allowedValues)); + } + } + +} 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 new file mode 100644 index 00000000..af3fb56d --- /dev/null +++ b/yoti-sdk-api/src/test/java/com/yoti/api/client/DigitalIdentityClientTest.java @@ -0,0 +1,229 @@ +package com.yoti.api.client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyPair; + +import com.yoti.api.client.spi.remote.call.identity.DigitalIdentityException; +import com.yoti.api.client.spi.remote.call.identity.DigitalIdentityService; +import com.yoti.api.client.spi.remote.util.CryptoUtil; + +import org.bouncycastle.openssl.PEMException; +import org.junit.*; +import org.junit.runner.RunWith; +import org.mockito.*; +import org.mockito.junit.*; + +@RunWith(MockitoJUnitRunner.class) +public class DigitalIdentityClientTest { + + private static final String AN_APP_ID = "anAppId"; + private static final String A_QR_CODE_ID = "aQrCodeId"; + private static final String A_SESSION_ID = "aSessionId"; + private static final String A_RECEIPT_ID = "aReceiptId"; + + @Mock DigitalIdentityService identityService; + + private KeyPairSource validKeyPairSource; + + @Before + public void setUp() throws Exception { + validKeyPairSource = new KeyPairSourceTest(CryptoUtil.KEY_PAIR_PEM); + } + + @Test + public void builderWithClientSdkId_NullSdkId_IllegalArgumentException() { + DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder() + .withKeyPairSource(validKeyPairSource); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> builder.withClientSdkId(null)); + + assertThat(ex.getMessage(), containsString("SDK ID")); + } + + @Test + public void builderWithClientSdkId_EmptySdkId_IllegalArgumentException() { + DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder() + .withKeyPairSource(validKeyPairSource); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> builder.withClientSdkId("")); + + assertThat(ex.getMessage(), containsString("SDK ID")); + } + + @Test + public void builderWithKeyPairSource_NullKeyPairSource_IllegalArgumentException() { + DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder() + .withClientSdkId(AN_APP_ID); + + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> builder.withKeyPairSource(null) + ); + + assertThat(ex.getMessage(), containsString("Key Pair Source")); + } + @Test + public void build_MissingSdkId_IllegalArgumentException() { + DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder().withKeyPairSource(validKeyPairSource); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, builder::build); + + assertThat(ex.getMessage(), containsString("SDK ID")); + } + + @Test + public void build_MissingKeyPairSource_IllegalArgumentException() { + DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder().withClientSdkId(AN_APP_ID); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, builder::build); + + assertThat(ex.getMessage(), containsString("Key Pair Source")); + } + + @Test + public void build_NoKeyPairInFile_InitialisationException() { + KeyPairSource invalidKeyPairSource = new KeyPairSourceTest("no-key-pair-in-file"); + + DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder() + .withClientSdkId(AN_APP_ID) + .withKeyPairSource(invalidKeyPairSource); + + InitialisationException ex = assertThrows(InitialisationException.class, builder::build); + + assertThat(ex.getMessage(), containsString("No key pair found in the provided source")); + } + + @Test + public void build_InvalidKeyPair_InitialisationException() { + KeyPairSource invalidKeyPairSource = new KeyPairSourceTest(CryptoUtil.INVALID_KEY_PAIR_PEM); + + DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder() + .withClientSdkId(AN_APP_ID) + .withKeyPairSource(invalidKeyPairSource); + + InitialisationException ex = assertThrows(InitialisationException.class, builder::build); + + assertThat(ex.getMessage(), equalTo("Cannot load Key Pair")); + assertTrue(ex.getCause() instanceof PEMException); + } + + @Test + public void client_CreateShareSessionException_DigitalIdentityException() { + DigitalIdentityClient identityClient = new DigitalIdentityClient( + AN_APP_ID, + validKeyPairSource, + identityService + ); + + String exMessage = "Create Share Session Error"; + when(identityService.createShareSession()).thenThrow(new DigitalIdentityException(exMessage)); + + DigitalIdentityException ex = assertThrows( + DigitalIdentityException.class, + identityClient::createShareSession + ); + + assertThat(ex.getMessage(), equalTo(exMessage)); + } + + @Test + public void client_FetchShareSessionException_DigitalIdentityException() { + DigitalIdentityClient identityClient = new DigitalIdentityClient( + AN_APP_ID, + validKeyPairSource, + identityService + ); + + String exMessage = "Fetch Share Session Error"; + when(identityService.fetchShareSession(A_SESSION_ID)).thenThrow(new DigitalIdentityException(exMessage)); + + DigitalIdentityException ex = assertThrows( + DigitalIdentityException.class, + () -> identityClient.fetchShareSession(A_SESSION_ID) + ); + + assertThat(ex.getMessage(), equalTo(exMessage)); + } + + @Test + public void client_CreateShareQrCodeException_DigitalIdentityException() { + DigitalIdentityClient identityClient = new DigitalIdentityClient( + AN_APP_ID, + validKeyPairSource, + identityService + ); + + String exMessage = "Create Share QR Error"; + when(identityService.createShareQrCode(A_SESSION_ID)).thenThrow(new DigitalIdentityException(exMessage)); + + DigitalIdentityException ex = assertThrows( + DigitalIdentityException.class, + () -> identityClient.createShareQrCode(A_SESSION_ID) + ); + + assertThat(ex.getMessage(), equalTo(exMessage)); + } + + @Test + public void client_FetchShareQrCodeException_DigitalIdentityException() { + DigitalIdentityClient identityClient = new DigitalIdentityClient( + AN_APP_ID, + validKeyPairSource, + identityService + ); + + String exMessage = "Fetch Share QR Error"; + when(identityService.fetchShareQrCode(A_QR_CODE_ID)).thenThrow(new DigitalIdentityException(exMessage)); + + DigitalIdentityException ex = assertThrows( + DigitalIdentityException.class, + () -> identityClient.fetchShareQrCode(A_QR_CODE_ID) + ); + + assertThat(ex.getMessage(), equalTo(exMessage)); + } + + @Test + public void client_FetchShareReceiptException_DigitalIdentityException() { + DigitalIdentityClient identityClient = new DigitalIdentityClient( + AN_APP_ID, + validKeyPairSource, + identityService + ); + + String exMessage = "Fetch Share Receipt Error"; + when(identityService.fetchShareReceipt(A_RECEIPT_ID)).thenThrow(new DigitalIdentityException(exMessage)); + + DigitalIdentityException ex = assertThrows( + DigitalIdentityException.class, + () -> identityClient.fetchShareReceipt(A_RECEIPT_ID) + ); + + assertThat(ex.getMessage(), equalTo(exMessage)); + } + + private static class KeyPairSourceTest implements KeyPairSource { + + private final String keyPair; + + public KeyPairSourceTest(String keyPair) { + this.keyPair = keyPair; + } + + @Override + public KeyPair getFromStream(StreamVisitor streamVisitor) throws IOException, InitialisationException { + InputStream is = new ByteArrayInputStream(keyPair.getBytes()); + return streamVisitor.accept(is); + } + + } + +} diff --git a/yoti-sdk-spring-boot-auto-config/pom.xml b/yoti-sdk-spring-boot-auto-config/pom.xml index a632ca5e..6b8cf9dc 100644 --- a/yoti-sdk-spring-boot-auto-config/pom.xml +++ b/yoti-sdk-spring-boot-auto-config/pom.xml @@ -17,11 +17,15 @@ - org.springframework.boot spring-boot-autoconfigure + + org.springframework.boot + spring-boot-configuration-processor + true + com.yoti yoti-sdk-api @@ -43,7 +47,6 @@ hamcrest-library test - diff --git a/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/YotiClientProperties.java b/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/ClientProperties.java similarity index 52% rename from yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/YotiClientProperties.java rename to yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/ClientProperties.java index 5eb10a9a..e0ae440d 100644 --- a/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/YotiClientProperties.java +++ b/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/ClientProperties.java @@ -3,10 +3,10 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** - * Allows properties to the Yoti client to be supplied via spring properties (e.g. YAML or properties file). + * Allows properties to the clients to be supplied via spring properties (e.g. YAML or properties file). */ @ConfigurationProperties(prefix = "com.yoti.client") -public class YotiClientProperties { +public class ClientProperties { /** * The SDK client ID provided by Yoti Hub. @@ -37,7 +37,7 @@ public String getClientSdkId() { * * @param clientSdkId the Yoti client SDK ID. */ - public void setClientSdkId(final String clientSdkId) { + public void setClientSdkId(String clientSdkId) { this.clientSdkId = clientSdkId; } @@ -46,14 +46,18 @@ public void setClientSdkId(final String clientSdkId) { * * @return the Yoti scenario ID. */ - public String getScenarioId() { return scenarioId; }; + public String getScenarioId() { + return scenarioId; + } /** * Sets the Yoti scenario ID that is provided to the client developer via Yoti Hub. * * @param scenarioId the Yoti scenario ID. */ - public void setScenarioId(final String scenarioId) { this.scenarioId = scenarioId; } + public void setScenarioId(String scenarioId) { + this.scenarioId = scenarioId; + } /** * Gets the location for the key pair. @@ -70,37 +74,8 @@ public String getAccessSecurityKey() { * @param accessSecurityKey the key pair location in spring resource format e.g. classpath: or file: or as a URL. * @see org.springframework.core.io.Resource */ - public void setAccessSecurityKey(final String accessSecurityKey) { + public void setAccessSecurityKey(String accessSecurityKey) { this.accessSecurityKey = accessSecurityKey; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - YotiClientProperties that = (YotiClientProperties) o; - if (scenarioId != null ? !scenarioId.equals(that.scenarioId) : that.scenarioId != null) return false; - if (clientSdkId != null ? !clientSdkId.equals(that.clientSdkId) : that.clientSdkId != null) return false; - - return accessSecurityKey != null ? accessSecurityKey.equals(that.accessSecurityKey) : that.accessSecurityKey == null; - } - - @Override - public int hashCode() { - int result = scenarioId != null ? scenarioId.hashCode() : 0; - result = 31 * result + (clientSdkId != null ? clientSdkId.hashCode() : 0); - result = 31 * result + (accessSecurityKey != null ? accessSecurityKey.hashCode() : 0); - return result; - } - - @Override - public String toString() { - final StringBuffer sb = new StringBuffer("YotiClientProperties{"); - sb.append("scenarioId='").append(scenarioId).append('\''); - sb.append(", clientSdkId='").append(clientSdkId).append('\''); - sb.append(", accessSecurityKey='").append(accessSecurityKey).append('\''); - sb.append('}'); - return sb.toString(); - } } diff --git a/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/DigitalIdentityClientAutoConfiguration.java b/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/DigitalIdentityClientAutoConfiguration.java new file mode 100644 index 00000000..56ac4844 --- /dev/null +++ b/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/DigitalIdentityClientAutoConfiguration.java @@ -0,0 +1,49 @@ +package com.yoti.api.spring; + +import com.yoti.api.client.DigitalIdentityClient; +import com.yoti.api.client.KeyPairSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +@Configuration +@ConditionalOnClass(DigitalIdentityClient.class) +@EnableConfigurationProperties({ ClientProperties.class, DigitalIdentityProperties.class }) +public class DigitalIdentityClientAutoConfiguration { + + private final ClientProperties properties; + private final ResourceLoader resourceLoader; + + @Autowired + public DigitalIdentityClientAutoConfiguration(ClientProperties properties, ResourceLoader resourceLoader) { + this.properties = properties; + this.resourceLoader = resourceLoader; + } + + @Bean + @ConditionalOnMissingBean(DigitalIdentityClient.class) + public DigitalIdentityClient digitalIdentityClient(KeyPairSource keyPairSource) { + return DigitalIdentityClient.builder() + .withClientSdkId(properties.getClientSdkId()) + .withKeyPairSource(keyPairSource) + .build(); + } + + @Bean + @ConditionalOnMissingBean(KeyPairSource.class) + public KeyPairSource keyPairSource() { + return loadAsSpringResource(properties); + } + + private KeyPairSource loadAsSpringResource(ClientProperties properties) { + final Resource keyPairResource = resourceLoader.getResource(properties.getAccessSecurityKey()); + return new SpringResourceKeyPairSource(keyPairResource); + } + +} diff --git a/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/DigitalIdentityProperties.java b/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/DigitalIdentityProperties.java new file mode 100644 index 00000000..2a72e4de --- /dev/null +++ b/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/DigitalIdentityProperties.java @@ -0,0 +1,18 @@ +package com.yoti.api.spring; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "com.yoti.identity") +public class DigitalIdentityProperties { + + private String applicationId; + + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String id) { + applicationId = id; + } + +} diff --git a/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/SpringResourceKeyPairSource.java b/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/SpringResourceKeyPairSource.java index 37258e8b..a7bf2fe5 100644 --- a/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/SpringResourceKeyPairSource.java +++ b/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/SpringResourceKeyPairSource.java @@ -1,50 +1,26 @@ package com.yoti.api.spring; -import com.yoti.api.client.KeyPairSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.core.io.Resource; - import java.io.IOException; import java.io.InputStream; import java.security.KeyPair; -public class SpringResourceKeyPairSource implements KeyPairSource { +import com.yoti.api.client.KeyPairSource; - private static final Logger LOGGER = LoggerFactory.getLogger(SpringResourceKeyPairSource.class); +import org.springframework.core.io.Resource; + +public class SpringResourceKeyPairSource implements KeyPairSource { private final Resource resource; - public SpringResourceKeyPairSource(final Resource resource) { + public SpringResourceKeyPairSource(Resource resource) { this.resource = resource; } @Override - public KeyPair getFromStream(final StreamVisitor streamVisitor) throws IOException { - InputStream in = null; - try { - LOGGER.debug("Found key pair source {}", resource); - in = resource.getInputStream(); - return streamVisitor.accept(in); - } finally { - closeQuietly(in); - } - } - - private void closeQuietly(final InputStream in) { - if (in != null) { - try { - in.close(); - } catch (final IOException ioe) { - LOGGER.error("Unable to close key pair source input stream.", ioe); - } + public KeyPair getFromStream(StreamVisitor stream) throws IOException { + try (InputStream is = resource.getInputStream()) { + return stream.accept(is); } } - @Override - public String toString() { - return "SpringResourceKeyPairSource{" + - "resource=" + resource + - '}'; - } } diff --git a/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/YotiClientAutoConfiguration.java b/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/YotiClientAutoConfiguration.java index 9dcdbef3..9d40c624 100644 --- a/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/YotiClientAutoConfiguration.java +++ b/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/YotiClientAutoConfiguration.java @@ -3,8 +3,6 @@ import com.yoti.api.client.KeyPairSource; import com.yoti.api.client.YotiClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -14,34 +12,23 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -/** - * Automatically configures a Yoti Client instance based on the {@link YotiClientProperties} - * provided as long as a bean of the same type doesn't already exist. - */ @Configuration @ConditionalOnClass(YotiClient.class) -@EnableConfigurationProperties({YotiClientProperties.class, YotiProperties.class}) +@EnableConfigurationProperties({ ClientProperties.class, YotiProperties.class}) public class YotiClientAutoConfiguration { - private static final Logger LOGGER = LoggerFactory.getLogger(SpringResourceKeyPairSource.class); + private final ClientProperties properties; + private final ResourceLoader resourceLoader; @Autowired - private YotiClientProperties properties; + public YotiClientAutoConfiguration(ClientProperties properties, ResourceLoader resourceLoader) { + this.properties = properties; + this.resourceLoader = resourceLoader; + } - @Autowired - private ResourceLoader resourceLoader; - - /** - * Configures a Yoti client if a bean of this type does not already exist. - * - * @param keyPairSource the instance of a key pair source configured separately as another bean. - * @return the configured client. - * @throws Exception if the client could not be created. - */ @Bean @ConditionalOnMissingBean(YotiClient.class) - public YotiClient yotiClient(final KeyPairSource keyPairSource) throws Exception { - LOGGER.info("Configuring Yoti client with {} and {}.", properties, keyPairSource); + public YotiClient yotiClient(KeyPairSource keyPairSource) { return YotiClient.builder() .withClientSdkId(properties.getClientSdkId()) .withKeyPair(keyPairSource) @@ -51,12 +38,12 @@ public YotiClient yotiClient(final KeyPairSource keyPairSource) throws Exception @Bean @ConditionalOnMissingBean(KeyPairSource.class) public KeyPairSource keyPairSource() { - LOGGER.info("Configuring key pair source based on {}.", properties); return loadAsSpringResource(properties); } - private KeyPairSource loadAsSpringResource(final YotiClientProperties properties) { + private KeyPairSource loadAsSpringResource(ClientProperties properties) { final Resource keyPairResource = resourceLoader.getResource(properties.getAccessSecurityKey()); return new SpringResourceKeyPairSource(keyPairResource); } + } diff --git a/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/YotiProperties.java b/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/YotiProperties.java index 180b6439..ef4b2965 100644 --- a/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/YotiProperties.java +++ b/yoti-sdk-spring-boot-auto-config/src/main/java/com/yoti/api/spring/YotiProperties.java @@ -2,82 +2,27 @@ import org.springframework.boot.context.properties.ConfigurationProperties; -/** - * Allows properties for Yoti configuration to be supplied via spring properties (e.g. YAML or properties file). - */ @ConfigurationProperties(prefix = "com.yoti") public class YotiProperties { - /** - * The Yoti Hub Application ID. - */ private String applicationId; - /** - * The Yoti Hub Scenario ID. - */ private String scenarioId; - /** - * Gets the Yoti Hub Application ID. - * - * @return the Application ID. - */ public String getApplicationId() { return applicationId; } - /** - * Sets the Yoti Hub Application ID. - * - * @param applicationId the new Application ID. - */ - public void setApplicationId(final String applicationId) { - this.applicationId = applicationId; + public void setApplicationId(String id) { + applicationId = id; } - /** - * Gets the Yoti Hub Scenario ID. - * - * @return the scenario ID. - */ public String getScenarioId() { return scenarioId; } - /** - * Sets the scenario ID given to you by Yoti Hub. - * - * @param scenarioId the scenario ID. - */ - public void setScenarioId(final String scenarioId) { - this.scenarioId = scenarioId; + public void setScenarioId(String id) { + scenarioId = id; } - @Override - public String toString() { - return "YotiProperties{" + - "applicationId='" + applicationId + '\'' + - ", scenarioId='" + scenarioId + '\'' + - '}'; - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final YotiProperties that = (YotiProperties) o; - - if (applicationId != null ? !applicationId.equals(that.applicationId) : that.applicationId != null) - return false; - return scenarioId != null ? scenarioId.equals(that.scenarioId) : that.scenarioId == null; - } - - @Override - public int hashCode() { - int result = applicationId != null ? applicationId.hashCode() : 0; - result = 31 * result + (scenarioId != null ? scenarioId.hashCode() : 0); - return result; - } } diff --git a/yoti-sdk-spring-boot-auto-config/src/main/resources/META-INF/spring.factories b/yoti-sdk-spring-boot-auto-config/src/main/resources/META-INF/spring.factories index 98f14c09..d3e2e9f0 100644 --- a/yoti-sdk-spring-boot-auto-config/src/main/resources/META-INF/spring.factories +++ b/yoti-sdk-spring-boot-auto-config/src/main/resources/META-INF/spring.factories @@ -1 +1,3 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.yoti.api.spring.YotiClientAutoConfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.yoti.api.spring.YotiClientAutoConfiguration,\ + com.yoti.api.spring.DigitalIdentityClientAutoConfiguration diff --git a/yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/YotiClientPropertiesTest.java b/yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/ClientPropertiesTest.java similarity index 54% rename from yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/YotiClientPropertiesTest.java rename to yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/ClientPropertiesTest.java index ad2f472e..382fac42 100644 --- a/yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/YotiClientPropertiesTest.java +++ b/yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/ClientPropertiesTest.java @@ -1,13 +1,12 @@ package com.yoti.api.spring; -import org.junit.Test; -import org.springframework.boot.context.properties.ConfigurationProperties; - import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.*; -public class YotiClientPropertiesTest { +import org.junit.*; +import org.springframework.boot.context.properties.ConfigurationProperties; + +public class ClientPropertiesTest { private static final String SCENARIO_ID = "scenario-id-123"; private static final String CLIENT_SDK_ID = "abc-123"; @@ -15,40 +14,38 @@ public class YotiClientPropertiesTest { private static final String YOTI_PREFIX = "com.yoti.client"; @Test - public void scenarioIdShouldBeSet() throws Exception { - final YotiClientProperties properties = new YotiClientProperties(); + public void scenarioIdShouldBeSet() { + ClientProperties properties = new ClientProperties(); properties.setScenarioId(SCENARIO_ID); assertThat(properties.getScenarioId(), is(SCENARIO_ID)); } @Test - public void clientSdkIdShouldBeSet() throws Exception { - final YotiClientProperties properties = new YotiClientProperties(); + public void clientSdkIdShouldBeSet() { + ClientProperties properties = new ClientProperties(); properties.setClientSdkId(CLIENT_SDK_ID); assertThat(properties.getClientSdkId(), is(CLIENT_SDK_ID)); } @Test - public void accessSecurityKeyShouldBeSet() throws Exception { - final YotiClientProperties properties = new YotiClientProperties(); + public void accessSecurityKeyShouldBeSet() { + ClientProperties properties = new ClientProperties(); properties.setAccessSecurityKey(ACCESS_SECURITY_KEY); assertThat(properties.getAccessSecurityKey(), is(ACCESS_SECURITY_KEY)); } @Test - public void defaultValuesShouldBeNull() throws Exception { - final YotiClientProperties properties = new YotiClientProperties(); + public void defaultValuesShouldBeNull() { + ClientProperties properties = new ClientProperties(); assertThat(properties.getAccessSecurityKey(), nullValue()); assertThat(properties.getClientSdkId(), nullValue()); assertThat(properties.getScenarioId(), nullValue()); } - /** - * Just here as a guard against someone changing the prefix as this would cause any existing client applications to break. - */ @Test - public void configurationPrefixShouldBeAsExpected() throws Exception { - final ConfigurationProperties annotation = YotiClientProperties.class.getAnnotation(ConfigurationProperties.class); + public void configurationPrefixShouldBeAsExpected() { + final ConfigurationProperties annotation = ClientProperties.class.getAnnotation(ConfigurationProperties.class); assertThat(annotation.prefix(), is(YOTI_PREFIX)); } + } diff --git a/yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/DigitalIdentityPropertiesTest.java b/yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/DigitalIdentityPropertiesTest.java new file mode 100644 index 00000000..2de55c13 --- /dev/null +++ b/yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/DigitalIdentityPropertiesTest.java @@ -0,0 +1,33 @@ +package com.yoti.api.spring; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import org.junit.*; +import org.springframework.boot.context.properties.ConfigurationProperties; + +public class DigitalIdentityPropertiesTest { + + private static final String APPLICATION_ID = "anAppId"; + private static final String IDENTITY_PREFIX = "com.yoti.identity"; + + @Test + public void applicationIdShouldBeSet() { + DigitalIdentityProperties properties = new DigitalIdentityProperties(); + properties.setApplicationId(APPLICATION_ID); + assertThat(properties.getApplicationId(), is(APPLICATION_ID)); + } + + @Test + public void defaultValuesShouldBeNull() { + DigitalIdentityProperties properties = new DigitalIdentityProperties(); + assertThat(properties.getApplicationId(), nullValue()); + } + + @Test + public void configurationPrefixShouldBeAsExpected() { + ConfigurationProperties annotation = DigitalIdentityProperties.class.getAnnotation(ConfigurationProperties.class); + assertThat(annotation.prefix(), is(IDENTITY_PREFIX)); + } + +} diff --git a/yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/YotiPropertiesTest.java b/yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/YotiPropertiesTest.java index 465e09c2..8fb3736b 100644 --- a/yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/YotiPropertiesTest.java +++ b/yoti-sdk-spring-boot-auto-config/src/test/java/com/yoti/api/spring/YotiPropertiesTest.java @@ -1,11 +1,10 @@ package com.yoti.api.spring; -import org.junit.Test; -import org.springframework.boot.context.properties.ConfigurationProperties; - import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.*; + +import org.junit.*; +import org.springframework.boot.context.properties.ConfigurationProperties; public class YotiPropertiesTest { @@ -14,21 +13,21 @@ public class YotiPropertiesTest { private static final String YOTI_PREFIX = "com.yoti"; @Test - public void applicationIdShouldBeSet() throws Exception { + public void applicationIdShouldBeSet() { final YotiProperties properties = new YotiProperties(); properties.setApplicationId(APPLICATION_ID); assertThat(properties.getApplicationId(), is(APPLICATION_ID)); } @Test - public void scenarioIdShouldBeSet() throws Exception { + public void scenarioIdShouldBeSet() { final YotiProperties properties = new YotiProperties(); properties.setScenarioId(SCENARIO_ID); assertThat(properties.getScenarioId(), is(SCENARIO_ID)); } @Test - public void defaultValuesShouldBeNull() throws Exception { + public void defaultValuesShouldBeNull() { final YotiProperties properties = new YotiProperties(); assertThat(properties.getApplicationId(), nullValue()); assertThat(properties.getScenarioId(), nullValue()); @@ -38,7 +37,7 @@ public void defaultValuesShouldBeNull() throws Exception { * Just here as a guard against someone changing the prefix as this would cause any existing client applications to break. */ @Test - public void configurationPrefixShouldBeAsExpected() throws Exception { + public void configurationPrefixShouldBeAsExpected() { final ConfigurationProperties annotation = YotiProperties.class.getAnnotation(ConfigurationProperties.class); assertThat(annotation.prefix(), is(YOTI_PREFIX)); } 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 new file mode 100644 index 00000000..8be54ca6 --- /dev/null +++ b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DigitalIdentityController.java @@ -0,0 +1,54 @@ +package com.yoti.api.examples.springboot; + +import com.yoti.api.client.DigitalIdentityClient; +import com.yoti.api.spring.ClientProperties; +import com.yoti.api.spring.DigitalIdentityProperties; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@ConditionalOnClass(DigitalIdentityClient.class) +@EnableConfigurationProperties({ ClientProperties.class, DigitalIdentityProperties.class }) +@Controller +@EnableWebMvc +@RequestMapping("/v2") +public class DigitalIdentityController implements WebMvcConfigurer { + + private final DigitalIdentityClient client; + private final ClientProperties properties; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); + } + + @Autowired + public DigitalIdentityController(DigitalIdentityClient client, ClientProperties properties) { + this.client = client; + this.properties = properties; + } + + @RequestMapping("/") + public String home(final Model model) { + model.addAttribute("clientSdkId", properties.getClientSdkId()); + model.addAttribute("scenarioId", properties.getScenarioId()); + return "index"; + } + + @RequestMapping("/digital-identity-share") + public String identityShare(final Model model) { + model.addAttribute("message", "Example page for identity share using Yoti web-share"); + + return "digital-identity-share"; + } + +} diff --git a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/YotiLoginController.java b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/YotiLoginController.java index d5382b0f..0e27105a 100644 --- a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/YotiLoginController.java +++ b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/YotiLoginController.java @@ -22,7 +22,7 @@ import com.yoti.api.client.shareurl.extension.LocationConstraintExtensionBuilder; import com.yoti.api.client.shareurl.policy.DynamicPolicy; import com.yoti.api.client.shareurl.policy.WantedAttribute; -import com.yoti.api.spring.YotiClientProperties; +import com.yoti.api.spring.ClientProperties; import com.yoti.api.spring.YotiProperties; import com.fasterxml.jackson.databind.ObjectMapper; @@ -38,20 +38,20 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.thymeleaf.util.StringUtils; @Configuration @ConditionalOnClass(YotiClient.class) -@EnableConfigurationProperties({ YotiClientProperties.class, YotiProperties.class }) +@EnableConfigurationProperties({ ClientProperties.class, YotiProperties.class }) @Controller @EnableWebMvc -public class YotiLoginController extends WebMvcConfigurerAdapter { +public class YotiLoginController implements WebMvcConfigurer { private static final Logger LOG = LoggerFactory.getLogger(YotiLoginController.class); private final YotiClient client; - private final YotiClientProperties properties; + private final ClientProperties properties; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { @@ -59,7 +59,7 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) { } @Autowired - public YotiLoginController(final YotiClient client, YotiClientProperties properties) { + public YotiLoginController(final YotiClient client, ClientProperties properties) { this.client = client; this.properties = properties; } 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 new file mode 100644 index 00000000..4418c754 --- /dev/null +++ b/yoti-sdk-spring-boot-example/src/main/resources/templates/digital-identity-share.html @@ -0,0 +1,22 @@ + + + + + + Digital Identity Example + + + + +
+
+
+ Yoti +
+ +

Digital Identity Share Example page

+
+
+ + + From 64ddcda38a3151959b4c4b53f3baef6d315f7026 Mon Sep 17 00:00:00 2001 From: irotech Date: Mon, 27 Feb 2023 19:01:18 +0000 Subject: [PATCH 25/41] SDK-2231: Add Digital Identity Session creation representation layer --- .../api/client/identity/ShareSession.java | 36 ++ .../identity/ShareSessionNotification.java | 104 ++++ .../client/identity/ShareSessionRequest.java | 127 +++++ .../identity/constraint/Constraint.java | 7 + .../identity/constraint/PreferredSources.java | 55 ++ .../identity/constraint/SourceConstraint.java | 92 ++++ .../extension/BasicExtensionBuilder.java | 31 ++ .../client/identity/extension/Extension.java | 33 ++ .../identity/extension/ExtensionBuilder.java | 7 + .../extension/ExtensionBuilderFactory.java | 27 + .../extension/LocationConstraintContent.java | 64 +++ .../LocationConstraintExtensionBuilder.java | 57 ++ .../extension/ThirdPartyAttributeContent.java | 44 ++ .../ThirdPartyAttributeExtensionBuilder.java | 58 +++ .../TransactionalFlowExtensionBuilder.java | 29 ++ .../api/client/identity/policy/Policy.java | 286 ++++++++++ .../client/identity/policy/WantedAnchor.java | 64 +++ .../identity/policy/WantedAttribute.java | 122 +++++ .../client/identity/policy/PolicyTest.java | 492 ++++++++++++++++++ 19 files changed, 1735 insertions(+) create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSession.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/constraint/Constraint.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/constraint/PreferredSources.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/constraint/SourceConstraint.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/BasicExtensionBuilder.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ExtensionBuilder.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ExtensionBuilderFactory.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintContent.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/Policy.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java create mode 100644 yoti-sdk-api/src/test/java/com/yoti/api/client/identity/policy/PolicyTest.java diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSession.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSession.java new file mode 100644 index 00000000..55bcde75 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSession.java @@ -0,0 +1,36 @@ +package com.yoti.api.client.identity; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ShareSession { + + @JsonProperty(Property.ID) + private String id; + + @JsonProperty(Property.STATUS) + private String status; + + @JsonProperty(Property.EXPIRY) + private String expiry; + + public String getId() { + return id; + } + + public String getStatus() { + return status; + } + + public String getExpiry() { + return expiry; + } + + private static final class Property { + + private static final String ID = "id"; + private static final String STATUS = "status"; + private static final String EXPIRY = "expiry"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java new file mode 100644 index 00000000..99aaada1 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java @@ -0,0 +1,104 @@ +package com.yoti.api.client.identity; + +import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public final class ShareSessionNotification { + + @JsonProperty(Property.URL) + private final String url; + + @JsonProperty(Property.METHOD) + private final String method; + + @JsonProperty(Property.VERIFY_TLS) + private final boolean verifyTls; + + @JsonProperty(Property.HEADERS) + private final Map headers; + + private ShareSessionNotification(Builder builder) { + url = builder.url; + method = builder.method; + verifyTls = builder.verifyTls; + headers = builder.headers; + } + + public String getUrl() { + return url; + } + + public String getMethod() { + return method; + } + + public boolean isVerifyTls() { + return verifyTls; + } + + public Map getHeaders() { + return headers; + } + + public static Builder builder(URI uri) { + return new Builder(uri); + } + + public static final class Builder { + + private final String url; + private final Map headers; + + private String method; + private boolean verifyTls; + + private Builder(URI uri) { + url = uri.toString(); + method = "POST"; + verifyTls = true; + headers = new HashMap<>(); + } + + public Builder withMethod(String method) { + this.method = method; + return this; + } + + public Builder withVerifyTls(boolean verifyTls) { + this.verifyTls = verifyTls; + return this; + } + + public Builder withHeaders(Map headers) { + this.headers.putAll(headers); + return this; + } + + public Builder withHeader(String key, String value) { + headers.put(key, value); + return this; + } + + public ShareSessionNotification build() { + notNullOrEmpty(url, Property.URL); + + return new ShareSessionNotification(this); + } + + } + + private static final class Property { + + private static final String URL = "url"; + private static final String METHOD = "method"; + private static final String VERIFY_TLS = "verifyTls"; + private static final String HEADERS = "headers"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java new file mode 100644 index 00000000..ca50dc5a --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java @@ -0,0 +1,127 @@ +package com.yoti.api.client.identity; + +import static com.yoti.api.client.spi.remote.util.Validation.notNull; +import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.yoti.api.client.identity.extension.Extension; +import com.yoti.api.client.identity.policy.Policy; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ShareSessionRequest { + + @JsonProperty(Property.SUBJECT) + private final Map subject; + + @JsonProperty(Property.POLICY) + private final Policy policy; + + @JsonProperty(Property.EXTENSIONS) + private final List extensions; + + @JsonProperty(Property.REDIRECT_URI) + private final String redirectUri; + + @JsonProperty(Property.NOTIFICATION) + private final ShareSessionNotification notification; + + private ShareSessionRequest(Builder builder) { + subject = builder.subject; + policy = builder.policy; + extensions = builder.extensions; + redirectUri = builder.redirectUri; + notification = builder.notification; + } + + public Map getSubject() { + return subject; + } + + public Policy getPolicy() { + return policy; + } + + public List getExtensions() { + return extensions; + } + + public String getRedirectUri() { + return redirectUri; + } + + public ShareSessionNotification getNotification() { + return notification; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + private Map subject; + private Policy policy; + private List extensions; + private String redirectUri; + private ShareSessionNotification notification; + + private Builder() { + extensions = new ArrayList<>(); + } + + public Builder withSubject(Map subject) { + this.subject = subject; + return this; + } + + public Builder withPolicy(Policy policy) { + this.policy = policy; + return this; + } + + public Builder withExtensions(List extensions) { + this.extensions = Collections.unmodifiableList(extensions); + return this; + } + + public Builder withExtension(Extension extension) { + extensions.add(extension); + return this; + } + + public Builder withRedirectUri(URI redirectUri) { + this.redirectUri = redirectUri.toString(); + return this; + } + + public Builder withNotification(ShareSessionNotification notification) { + this.notification = notification; + return this; + } + + public ShareSessionRequest build() { + notNull(policy, Property.POLICY); + notNullOrEmpty(redirectUri, Property.REDIRECT_URI); + + return new ShareSessionRequest(this); + } + + } + + private static final class Property { + + private static final String SUBJECT = "subject"; + private static final String POLICY = "policy"; + private static final String EXTENSIONS = "extensions"; + private static final String REDIRECT_URI = "redirectUri"; + private static final String NOTIFICATION = "notification"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/constraint/Constraint.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/constraint/Constraint.java new file mode 100644 index 00000000..c86d866f --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/constraint/Constraint.java @@ -0,0 +1,7 @@ +package com.yoti.api.client.identity.constraint; + +public interface Constraint { + + String getType(); + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/constraint/PreferredSources.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/constraint/PreferredSources.java new file mode 100644 index 00000000..eaf043b9 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/constraint/PreferredSources.java @@ -0,0 +1,55 @@ +package com.yoti.api.client.identity.constraint; + +import java.util.List; +import java.util.Objects; + +import com.yoti.api.client.identity.policy.WantedAnchor; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class PreferredSources { + + @JsonProperty(Property.ANCHORS) + private final List wantedAnchors; + + @JsonProperty(Property.SOFT_PREFERENCE) + private final boolean softPreference; + + PreferredSources(List wantedAnchors, boolean softPreference) { + this.wantedAnchors = wantedAnchors; + this.softPreference = softPreference; + } + + public List getWantedAnchors() { + return wantedAnchors; + } + + public boolean isSoftPreference() { + return softPreference; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PreferredSources that = (PreferredSources) o; + return softPreference == that.softPreference && Objects.equals(wantedAnchors, that.wantedAnchors); + } + + @Override + public int hashCode() { + return Objects.hash(softPreference, wantedAnchors); + } + + private static final class Property { + + private static final String ANCHORS = "anchors"; + private static final String SOFT_PREFERENCE = "soft_preference"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/constraint/SourceConstraint.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/constraint/SourceConstraint.java new file mode 100644 index 00000000..69279fe8 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/constraint/SourceConstraint.java @@ -0,0 +1,92 @@ +package com.yoti.api.client.identity.constraint; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import com.yoti.api.client.identity.policy.WantedAnchor; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SourceConstraint implements Constraint { + + @JsonProperty(Property.TYPE) + private final String type; + + @JsonProperty(Property.PREFERRED_SOURCES) + private final PreferredSources preferredSources; + + SourceConstraint(List wantedAnchors, boolean softPreference) { + type = "SOURCE"; + preferredSources = new PreferredSources(wantedAnchors, softPreference); + } + + @Override + public String getType() { + return type; + } + + public PreferredSources getPreferredSources() { + return preferredSources; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SourceConstraint that = (SourceConstraint) o; + return Objects.equals(type, that.type) && Objects.equals(preferredSources, that.preferredSources); + } + + @Override + public int hashCode() { + return Objects.hash(type, preferredSources); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private List wantedAnchors; + private boolean softPreference; + + private Builder() { + this.wantedAnchors = new ArrayList<>(); + } + + public Builder withWantedAnchors(List wantedAnchors) { + this.wantedAnchors = Collections.unmodifiableList(wantedAnchors); + return this; + } + + public Builder withWantedAnchor(WantedAnchor wantedAnchor) { + this.wantedAnchors.add(wantedAnchor); + return this; + } + + public Builder withSoftPreference(boolean softPreference) { + this.softPreference = softPreference; + return this; + } + + public SourceConstraint build() { + return new SourceConstraint(Collections.unmodifiableList(wantedAnchors), softPreference); + } + + } + + private static final class Property { + + private static final String TYPE = "type"; + private static final String PREFERRED_SOURCES = "preferred_sources"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/BasicExtensionBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/BasicExtensionBuilder.java new file mode 100644 index 00000000..20e38dd3 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/BasicExtensionBuilder.java @@ -0,0 +1,31 @@ +package com.yoti.api.client.identity.extension; + +public class BasicExtensionBuilder implements ExtensionBuilder { + + private String type; + private Object content; + + public BasicExtensionBuilder withType(String type) { + this.type = type; + return this; + } + + public BasicExtensionBuilder withContent(Object content) { + this.content = content; + return this; + } + + public String getType() { + return type; + } + + public Object getContent() { + return content; + } + + @Override + public Extension build() { + return new Extension<>(type, content); + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java new file mode 100644 index 00000000..b7ee1b2d --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java @@ -0,0 +1,33 @@ +package com.yoti.api.client.identity.extension; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Extension { + + @JsonProperty(Property.TYPE) + private final String type; + + @JsonProperty(Property.CONTENT) + private final T content; + + Extension(String type, T content) { + this.type = type; + this.content = content; + } + + public String getType() { + return type; + } + + public T getContent() { + return content; + } + + private static final class Property { + + private static final String TYPE = "type"; + private static final String CONTENT = "content"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ExtensionBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ExtensionBuilder.java new file mode 100644 index 00000000..7c38e848 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ExtensionBuilder.java @@ -0,0 +1,7 @@ +package com.yoti.api.client.identity.extension; + +public interface ExtensionBuilder { + + Extension build(); + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ExtensionBuilderFactory.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ExtensionBuilderFactory.java new file mode 100644 index 00000000..eff7b487 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ExtensionBuilderFactory.java @@ -0,0 +1,27 @@ +package com.yoti.api.client.identity.extension; + +public class ExtensionBuilderFactory { + + private ExtensionBuilderFactory() { } + + public static ExtensionBuilderFactory newInstance() { + return new ExtensionBuilderFactory(); + } + + public BasicExtensionBuilder createExtensionBuilder() { + return new BasicExtensionBuilder(); + } + + public LocationConstraintExtensionBuilder createLocationConstraintExtensionBuilder() { + return new LocationConstraintExtensionBuilder(); + } + + public TransactionalFlowExtensionBuilder createTransactionalFlowExtensionBuilder() { + return new TransactionalFlowExtensionBuilder(); + } + + public ThirdPartyAttributeExtensionBuilder createThirdPartyAttributeExtensionBuilder() { + return new ThirdPartyAttributeExtensionBuilder(); + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintContent.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintContent.java new file mode 100644 index 00000000..09839f88 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintContent.java @@ -0,0 +1,64 @@ +package com.yoti.api.client.identity.extension; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class LocationConstraintContent { + + private final DeviceLocation expectedDeviceLocation; + + LocationConstraintContent(double latitude, double longitude, double radius, double maxUncertainty) { + this.expectedDeviceLocation = new DeviceLocation(latitude, longitude, radius, maxUncertainty); + } + + @JsonProperty(Property.EXPECTED_DEVICE_LOCATION) + public DeviceLocation getExpectedDeviceLocation() { + return expectedDeviceLocation; + } + + public static final class DeviceLocation { + + private final double latitude; + private final double longitude; + private final double radius; + private final double maxUncertainty; + + DeviceLocation(double latitude, double longitude, double radius, double maxUncertainty) { + this.latitude = latitude; + this.longitude = longitude; + this.radius = radius; + this.maxUncertainty = maxUncertainty; + } + + @JsonProperty(Property.LATITUDE) + public double getLatitude() { + return latitude; + } + + @JsonProperty(Property.LONGITUDE) + public double getLongitude() { + return longitude; + } + + @JsonProperty(Property.RADIUS) + public double getRadius() { + return radius; + } + + @JsonProperty(Property.MAX_UNCERTAINTY_RADIUS) + public double getMaxUncertainty() { + return maxUncertainty; + } + + } + + private static final class Property { + + private static final String EXPECTED_DEVICE_LOCATION = "expected_device_location"; + private static final String LATITUDE = "latitude"; + private static final String LONGITUDE = "longitude"; + private static final String RADIUS = "radius"; + private static final String MAX_UNCERTAINTY_RADIUS = "max_uncertainty_radius"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java new file mode 100644 index 00000000..4ec60494 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java @@ -0,0 +1,57 @@ +package com.yoti.api.client.identity.extension; + +import com.yoti.api.client.spi.remote.util.Validation; + +public class LocationConstraintExtensionBuilder implements ExtensionBuilder { + + public static final String TYPE = "LOCATION_CONSTRAINT"; + + private double latitude; + private double longitude; + private double radius = 150d; + private double maxUncertainty = 150d; + + public LocationConstraintExtensionBuilder withLatitude(double latitude) { + Validation.withinRange(latitude, -90d, 90d, Property.LATITUDE); + + this.latitude = latitude; + return this; + } + + public LocationConstraintExtensionBuilder withLongitude(double longitude) { + Validation.withinRange(longitude, -180d, 180d, Property.LONGITUDE); + + this.longitude = longitude; + return this; + } + + public LocationConstraintExtensionBuilder withRadius(double radius) { + Validation.notLessThan(radius, 0d, Property.RADIUS); + + this.radius = radius; + return this; + } + + public LocationConstraintExtensionBuilder withMaxUncertainty(double maxUncertainty) { + Validation.notLessThan(maxUncertainty, 0d, Property.MAX_UNCERTAINTY); + + this.maxUncertainty = maxUncertainty; + return this; + } + + @Override + public Extension build() { + LocationConstraintContent content = new LocationConstraintContent(latitude, longitude, radius, maxUncertainty); + return new Extension<>(TYPE, content); + } + + private static final class Property { + + private static final String LATITUDE = "latitude"; + private static final String LONGITUDE = "longitude"; + private static final String RADIUS = "radius"; + private static final String MAX_UNCERTAINTY = "maxUncertainty"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java new file mode 100644 index 00000000..6948afb8 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java @@ -0,0 +1,44 @@ +package com.yoti.api.client.identity.extension; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import com.yoti.api.client.AttributeDefinition; +import com.yoti.api.client.spi.remote.call.YotiConstants; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ThirdPartyAttributeContent { + + private final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(YotiConstants.RFC3339_PATTERN_MILLIS); + + private final Date expiryDate; + + @JsonProperty(Property.DEFINITIONS) + private final List definitions; + + ThirdPartyAttributeContent(Date expiryDate, List definitions) { + this.expiryDate = expiryDate; + this.definitions = definitions; + } + + @JsonProperty(Property.EXPIRY_DATE) + public String getExpiryDate() { + DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + return DATE_FORMAT.format(expiryDate.getTime()); + } + + public List getDefinitions() { + return definitions; + } + + private static final class Property { + + private static final String DEFINITIONS = "definitions"; + private static final String EXPIRY_DATE = "expiry_date"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java new file mode 100644 index 00000000..614a13ab --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java @@ -0,0 +1,58 @@ +package com.yoti.api.client.identity.extension; + +import static com.yoti.api.client.spi.remote.util.Validation.notNull; +import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.yoti.api.client.AttributeDefinition; + +public class ThirdPartyAttributeExtensionBuilder implements ExtensionBuilder { + + public static final String TYPE = "THIRD_PARTY_ATTRIBUTE"; + + private Date expiryDate; + private List definitions; + + public ThirdPartyAttributeExtensionBuilder() { + this.definitions = new ArrayList<>(); + } + + public ThirdPartyAttributeExtensionBuilder withExpiryDate(Date expiryDate) { + notNull(expiryDate, Property.EXPIRY_DATE); + + this.expiryDate = new Date(expiryDate.getTime()); + return this; + } + + public ThirdPartyAttributeExtensionBuilder withDefinition(String definition) { + notNullOrEmpty(definition, Property.DEFINITION); + + this.definitions.add(new AttributeDefinition(definition)); + return this; + } + + public ThirdPartyAttributeExtensionBuilder withDefinitions(List definitions) { + List attributeDefinitions = new ArrayList<>(); + for (String definition : definitions) { + attributeDefinitions.add(new AttributeDefinition(definition)); + } + this.definitions = attributeDefinitions; + return this; + } + + public Extension build() { + ThirdPartyAttributeContent thirdPartyAttributeContent = new ThirdPartyAttributeContent(expiryDate, definitions); + return new Extension<>(TYPE, thirdPartyAttributeContent); + } + + private static final class Property { + + private static final String EXPIRY_DATE = "expiryDate"; + private static final String DEFINITION = "definition"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java new file mode 100644 index 00000000..8723c30c --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java @@ -0,0 +1,29 @@ +package com.yoti.api.client.identity.extension; + +import com.yoti.api.client.spi.remote.util.Validation; + +public class TransactionalFlowExtensionBuilder implements ExtensionBuilder { + + public static final String TYPE = "TRANSACTIONAL_FLOW"; + + private Object content; + + public TransactionalFlowExtensionBuilder withContent(Object content) { + Validation.notNull(content, Property.CONTENT); + + this.content = content; + return this; + } + + @Override + public Extension build() { + return new Extension<>(TYPE, content); + } + + private static final class Property { + + private static final String CONTENT = "content"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/Policy.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/Policy.java new file mode 100644 index 00000000..06d8bad2 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/Policy.java @@ -0,0 +1,286 @@ +package com.yoti.api.client.identity.policy; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import com.yoti.api.attributes.AttributeConstants; +import com.yoti.api.client.identity.constraint.Constraint; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Policy { + + @JsonProperty(Property.WANTED) + private final Collection wantedAttributes; + + @JsonProperty(Property.WANTED_AUTH_TYPES) + private final Set wantedAuthTypes; + + @JsonProperty(Property.WANTED_REMEMBER_ME) + private final boolean wantedRememberMe; + + @JsonProperty(Property.WANTED_REMEMBER_ME_OPTIONAL) + private final boolean wantedRememberMeOptional; + + @JsonProperty(Property.IDENTITY_PROFILE_REQUIREMENTS) + private final Object identityProfile; + + private Policy(Builder builder) { + wantedAttributes = builder.wantedAttributes.values(); + wantedAuthTypes = builder.wantedAuthTypes; + wantedRememberMe = builder.wantedRememberMe; + wantedRememberMeOptional = builder.wantedRememberMeOptional; + identityProfile = builder.identityProfile; + } + + public Collection getWantedAttributes() { + return wantedAttributes; + } + + public Set getWantedAuthTypes() { + return wantedAuthTypes; + } + + public boolean isWantedRememberMe() { + return wantedRememberMe; + } + + public boolean isWantedRememberMeOptional() { + return wantedRememberMeOptional; + } + + public Object getIdentityProfile() { + return identityProfile; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private static final int SELFIE_AUTH_TYPE = 1; + private static final int PIN_AUTH_TYPE = 2; + + private final Map wantedAttributes; + private final Set wantedAuthTypes; + + private boolean wantedRememberMe; + private boolean wantedRememberMeOptional; + private Object identityProfile; + + private Builder() { + wantedAttributes = new HashMap<>(); + wantedAuthTypes = new HashSet<>(); + } + + public Builder withWantedAttribute(WantedAttribute wanted) { + String key = Optional.ofNullable(wanted.getDerivation()).orElseGet(wanted::getName); + + if (!wanted.hasConstraints()) { + wantedAttributes.put(key, wanted); + return this; + } + + if (!hasDuplicatedConstraints(wantedAttributes, wanted)) { + wantedAttributes.put(key + "-" + UUID.randomUUID(), wanted); + } + + return this; + } + + private static boolean hasDuplicatedConstraints(Map attributes, WantedAttribute wanted) { + return attributes.values().stream() + .filter(attribute -> attribute.getName().equals(wanted.getName())) + .map(WantedAttribute::getConstraints) + .anyMatch(c -> c.equals(wanted.getConstraints())); + } + + public Builder withWantedAttribute(boolean optional, String name, List constraints) { + return withWantedAttribute( + WantedAttribute.builder() + .withName(name) + .withOptional(optional) + .withConstraints(constraints) + .build() + ); + } + + public Builder withWantedAttribute(boolean optional, String name) { + return withWantedAttribute(optional, name, Collections.emptyList()); + } + + public Builder withFamilyName() { + return withFamilyName(false); + } + + public Builder withFamilyName(boolean optional) { + return withWantedAttribute(optional, AttributeConstants.HumanProfileAttributes.FAMILY_NAME); + } + + public Builder withGivenNames() { + return withGivenNames(false); + } + + public Builder withGivenNames(boolean optional) { + return withWantedAttribute(optional, AttributeConstants.HumanProfileAttributes.GIVEN_NAMES); + } + + public Builder withFullName() { + return withFullName(false); + } + + public Builder withFullName(boolean optional) { + return withWantedAttribute(optional, AttributeConstants.HumanProfileAttributes.FULL_NAME); + } + + public Builder withDateOfBirth() { + return withDateOfBirth(false); + } + + public Builder withDateOfBirth(boolean optional) { + return withWantedAttribute(optional, AttributeConstants.HumanProfileAttributes.DATE_OF_BIRTH); + } + + public Builder withAgeOver(int age) { + return withAgeOver(false, age); + } + + public Builder withAgeOver(boolean optional, int age) { + return withAgeDerivedAttribute(optional, AttributeConstants.HumanProfileAttributes.AGE_OVER + age); + } + + public Builder withAgeUnder(int age) { + return withAgeUnder(false, age); + } + + public Builder withAgeUnder(boolean optional, int age) { + return withAgeDerivedAttribute(optional, AttributeConstants.HumanProfileAttributes.AGE_UNDER + age); + } + + private Builder withAgeDerivedAttribute(boolean optional, String derivation) { + WantedAttribute wantedAttribute = WantedAttribute.builder() + .withName(AttributeConstants.HumanProfileAttributes.DATE_OF_BIRTH) + .withDerivation(derivation) + .withOptional(optional) + .build(); + return withWantedAttribute(wantedAttribute); + } + + public Builder withGender() { + return withGender(false); + } + + public Builder withGender(boolean optional) { + return withWantedAttribute(optional, AttributeConstants.HumanProfileAttributes.GENDER); + } + + public Builder withPostalAddress() { + return withPostalAddress(false); + } + + public Builder withPostalAddress(boolean optional) { + return withWantedAttribute(optional, AttributeConstants.HumanProfileAttributes.POSTAL_ADDRESS); + } + + public Builder withStructuredPostalAddress() { + return withStructuredPostalAddress(false); + } + + public Builder withStructuredPostalAddress(boolean optional) { + return withWantedAttribute(optional, AttributeConstants.HumanProfileAttributes.STRUCTURED_POSTAL_ADDRESS); + } + + public Builder withNationality() { + return withNationality(false); + } + + public Builder withNationality(boolean optional) { + return withWantedAttribute(optional, AttributeConstants.HumanProfileAttributes.NATIONALITY); + } + + public Builder withPhoneNumber() { + return withPhoneNumber(false); + } + + public Builder withPhoneNumber(boolean optional) { + return withWantedAttribute(optional, AttributeConstants.HumanProfileAttributes.PHONE_NUMBER); + } + + public Builder withSelfie() { + return withSelfie(false); + } + + public Builder withSelfie(boolean optional) { + return withWantedAttribute(optional, AttributeConstants.HumanProfileAttributes.SELFIE); + } + + public Builder withEmail() { + return withEmail(false); + } + + public Builder withEmail(boolean optional) { + return withWantedAttribute(optional, AttributeConstants.HumanProfileAttributes.EMAIL_ADDRESS); + } + + public Builder withSelfieAuthentication(boolean enabled) { + return withWantedAuthType(SELFIE_AUTH_TYPE, enabled); + } + + public Builder withPinAuthentication(boolean enabled) { + return withWantedAuthType(PIN_AUTH_TYPE, enabled); + } + + public Builder withWantedAuthType(int wantedAuthType) { + this.wantedAuthTypes.add(wantedAuthType); + return this; + } + + public Builder withWantedAuthType(int wantedAuthType, boolean enabled) { + if (enabled) { + return withWantedAuthType(wantedAuthType); + } else { + this.wantedAuthTypes.remove(wantedAuthType); + return this; + } + } + + public Builder withWantedRememberMe(boolean wantedRememberMe) { + this.wantedRememberMe = wantedRememberMe; + return this; + } + + public Builder withWantedRememberMeOptional(boolean wantedRememberMeOptional) { + this.wantedRememberMeOptional = wantedRememberMeOptional; + return this; + } + + public Builder withIdentityProfile(Object identityProfile) { + this.identityProfile = identityProfile; + return this; + } + + public Policy build() { + return new Policy(this); + } + + } + + private static final class Property { + + private static final String WANTED = "wanted"; + private static final String WANTED_AUTH_TYPES = "wanted_auth_types"; + private static final String WANTED_REMEMBER_ME = "wanted_remember_me"; + private static final String WANTED_REMEMBER_ME_OPTIONAL = "wanted_remember_me_optional"; + private static final String IDENTITY_PROFILE_REQUIREMENTS = "identity_profile_requirements"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java new file mode 100644 index 00000000..d8cc1f5f --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java @@ -0,0 +1,64 @@ +package com.yoti.api.client.identity.policy; + +import static com.yoti.api.client.spi.remote.util.Validation.notNull; +import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class WantedAnchor { + + @JsonProperty(Property.NAME) + private final String value; + + @JsonProperty(Property.SUB_TYPE) + private final String subType; + + private WantedAnchor(Builder builder) { + value = builder.value; + subType = builder.subType; + } + + public String getValue() { + return value; + } + + public String getSubType() { + return subType; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String value; + private String subType; + + public Builder withValue(String value) { + this.value = value; + return this; + } + + public Builder withSubType(String subType) { + this.subType = subType; + return this; + } + + public WantedAnchor build() { + notNullOrEmpty(value, Property.NAME); + notNull(subType, Property.SUB_TYPE); + + return new WantedAnchor(this); + } + + } + + private static final class Property { + + private static final String NAME = "name"; + private static final String SUB_TYPE = "sub_type"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java new file mode 100644 index 00000000..a2cd32f5 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java @@ -0,0 +1,122 @@ +package com.yoti.api.client.identity.policy; + +import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.yoti.api.client.identity.constraint.Constraint; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class WantedAttribute { + + @JsonProperty(Property.NAME) + private final String name; + + @JsonProperty(Property.DERIVATION) + private final String derivation; + + @JsonProperty(Property.OPTIONAL) + private final boolean optional; + + @JsonProperty(Property.ACCEPT_SELF_ASSERTED) + private final Boolean acceptSelfAsserted; + + @JsonProperty(Property.CONSTRAINTS) + private final List constraints; + + private WantedAttribute(Builder builder) { + name = builder.name; + derivation = builder.derivation; + optional = builder.optional; + acceptSelfAsserted = builder.acceptSelfAsserted; + constraints = builder.constraints; + } + + public String getName() { + return name; + } + + public String getDerivation() { + return derivation; + } + + public boolean isOptional() { + return optional; + } + + public Boolean getAcceptSelfAsserted() { + return acceptSelfAsserted; + } + + public List getConstraints() { + return constraints; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String name; + private String derivation; + private boolean optional; + private Boolean acceptSelfAsserted; + private List constraints; + + private Builder() { + this.constraints = new ArrayList<>(); + } + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Builder withDerivation(String derivation) { + this.derivation = derivation; + return this; + } + + public Builder withOptional(boolean optional) { + this.optional = optional; + return this; + } + + public Builder withAcceptSelfAsserted(boolean acceptSelfAsserted) { + this.acceptSelfAsserted = acceptSelfAsserted; + return this; + } + + public Builder withConstraints(List constraints) { + this.constraints = Collections.unmodifiableList(constraints); + return this; + } + + public Builder withConstraint(Constraint constraint) { + this.constraints.add(constraint); + return this; + } + + public WantedAttribute build() { + notNullOrEmpty(name, Property.NAME); + + return new WantedAttribute(this); + } + + } + + private static final class Property { + + private static final String NAME = "name"; + private static final String DERIVATION = "derivation"; + private static final String OPTIONAL = "optional"; + private static final String ACCEPT_SELF_ASSERTED = "accept_self_asserted"; + private static final String CONSTRAINTS = "constraints"; + + } + +} diff --git a/yoti-sdk-api/src/test/java/com/yoti/api/client/identity/policy/PolicyTest.java b/yoti-sdk-api/src/test/java/com/yoti/api/client/identity/policy/PolicyTest.java new file mode 100644 index 00000000..035128b4 --- /dev/null +++ b/yoti-sdk-api/src/test/java/com/yoti/api/client/identity/policy/PolicyTest.java @@ -0,0 +1,492 @@ +package com.yoti.api.client.identity.policy; + +import static java.lang.String.format; + +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.AGE_OVER; +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.AGE_UNDER; +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.DATE_OF_BIRTH; +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.EMAIL_ADDRESS; +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.FAMILY_NAME; +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.FULL_NAME; +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.GENDER; +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.GIVEN_NAMES; +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.NATIONALITY; +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.PHONE_NUMBER; +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.POSTAL_ADDRESS; +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.SELFIE; +import static com.yoti.api.attributes.AttributeConstants.HumanProfileAttributes.STRUCTURED_POSTAL_ADDRESS; +import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_CHARSET; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.core.IsNot.not; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.yoti.api.client.common.IdentityProfile; +import com.yoti.api.client.common.IdentityProfileScheme; +import com.yoti.api.client.identity.constraint.Constraint; +import com.yoti.api.client.identity.constraint.SourceConstraint; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.junit.*; + +public class PolicyTest { + + private static final int EXPECTED_SELFIE_AUTH_TYPE = 1; + private static final int EXPECTED_PIN_AUTH_TYPE = 2; + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + public void ensuresOnlyOneAttributeWithTheSameName() { + WantedAttribute wantedAttribute = WantedAttribute.builder() + .withName("anAttributeName") + .build(); + + Policy result = Policy.builder() + .withWantedAttribute(wantedAttribute) + .withWantedAttribute(wantedAttribute) + .build(); + + assertThat(result.getWantedAttributes(), hasSize(1)); + assertThat(result.getWantedAttributes(), hasItem(wantedAttribute)); + } + + @Test + public void builderSimpleAttributeWithers() { + Policy result = Policy.builder() + .withFamilyName() + .withGivenNames() + .withFullName() + .withDateOfBirth() + .withGender() + .withPostalAddress() + .withStructuredPostalAddress() + .withNationality() + .withPhoneNumber() + .withSelfie() + .withEmail() + .build(); + + Collection wanted = result.getWantedAttributes(); + assertThat(wanted, hasSize(11)); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(FAMILY_NAME, false))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(GIVEN_NAMES, false))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(FULL_NAME, false))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(DATE_OF_BIRTH, false))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(GENDER, false))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(POSTAL_ADDRESS, false))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(STRUCTURED_POSTAL_ADDRESS, false))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(NATIONALITY, false))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(PHONE_NUMBER, false))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(SELFIE, false))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(EMAIL_ADDRESS, false))); + } + + @Test + public void builderOptionalAttributeWithers() { + Policy result = Policy.builder() + .withFamilyName(true) + .withGivenNames(true) + .withFullName(true) + .withDateOfBirth(true) + .withGender(true) + .withPostalAddress(true) + .withStructuredPostalAddress(true) + .withNationality(true) + .withPhoneNumber(true) + .withSelfie(true) + .withEmail(true) + .build(); + + Collection wanted = result.getWantedAttributes(); + assertThat(wanted, hasSize(11)); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(FAMILY_NAME, true))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(GIVEN_NAMES, true))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(FULL_NAME, true))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(DATE_OF_BIRTH, true))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(GENDER, true))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(POSTAL_ADDRESS, true))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(STRUCTURED_POSTAL_ADDRESS, true))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(NATIONALITY, true))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(PHONE_NUMBER, true))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(SELFIE, true))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(EMAIL_ADDRESS, true))); + } + + @Test + public void ensureMultipleAgeDerivedAttributes() { + Policy result = Policy.builder() + .withDateOfBirth() + .withAgeOver(18) + .withAgeUnder(30) + .withAgeUnder(40) + .build(); + + Collection wanted = result.getWantedAttributes(); + assertThat(wanted, hasSize(4)); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(DATE_OF_BIRTH, false))); + assertThat( + wanted, + hasItem(WantedAttributeMatcher.forAttribute(DATE_OF_BIRTH, AGE_OVER + 18, false)) + ); + assertThat( + wanted, + hasItem(WantedAttributeMatcher.forAttribute(DATE_OF_BIRTH, AGE_UNDER + 30, false)) + ); + assertThat( + wanted, + hasItem(WantedAttributeMatcher.forAttribute(DATE_OF_BIRTH, AGE_UNDER + 40, false)) + ); + } + + @Test + public void ensureMultipleOptionalAgeDerivedAttributes() { + Policy result = Policy.builder() + .withDateOfBirth(true) + .withAgeOver(true, 18) + .withAgeUnder(true, 30) + .withAgeUnder(true, 40) + .build(); + + Collection wanted = result.getWantedAttributes(); + assertThat(wanted, hasSize(4)); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(DATE_OF_BIRTH, true))); + assertThat( + wanted, + hasItem(WantedAttributeMatcher.forAttribute(DATE_OF_BIRTH, AGE_OVER + 18, true)) + ); + assertThat( + wanted, + hasItem(WantedAttributeMatcher.forAttribute(DATE_OF_BIRTH, AGE_UNDER + 30, true)) + ); + assertThat( + wanted, + hasItem(WantedAttributeMatcher.forAttribute(DATE_OF_BIRTH, AGE_UNDER + 40, true)) + ); + } + + @Test + public void shouldOverwriteIdenticalAgeVerificationToEnsureItOnlyExistsOnce() { + Policy result = Policy.builder() + .withAgeUnder(true, 30) + .withAgeUnder(false, 30) + .build(); + + Collection wanted = result.getWantedAttributes(); + assertThat(wanted, hasSize(1)); + assertThat( + wanted, + hasItem(WantedAttributeMatcher.forAttribute(DATE_OF_BIRTH, AGE_UNDER + 30, false)) + ); + } + + @Test + public void ensureUniqueConstraintsAttribute() { + SourceConstraint constraint = SourceConstraint.builder() + .withSoftPreference(true) + .withWantedAnchor( + WantedAnchor.builder() + .withValue("aValue") + .withSubType("aSubType") + .build() + ).build(); + + List constraints = Collections.singletonList(constraint); + Policy result = Policy.builder() + .withWantedAttribute(false, NATIONALITY, constraints) + .withWantedAttribute(false, NATIONALITY, constraints) + .build(); + + Collection wanted = result.getWantedAttributes(); + assertThat(wanted, hasSize(1)); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(NATIONALITY, false, constraints))); + } + + @Test + public void ensureMultipleConstraintsAttribute() { + WantedAnchor wantedAnchor = WantedAnchor.builder() + .withValue("aValue") + .withSubType("aSubType") + .build(); + + List softPresenceConstraints = Collections.singletonList( + SourceConstraint.builder() + .withSoftPreference(true) + .withWantedAnchor(wantedAnchor) + .build() + ); + + List nonSoftPresentConstraints = Collections.singletonList( + SourceConstraint.builder() + .withWantedAnchor(wantedAnchor) + .build() + ); + + Policy result = Policy.builder() + .withWantedAttribute(true, NATIONALITY, softPresenceConstraints) + .withWantedAttribute(true, NATIONALITY, nonSoftPresentConstraints) + .withWantedAttribute(true, NATIONALITY, softPresenceConstraints) + .withWantedAttribute(true, FULL_NAME, softPresenceConstraints) + .build(); + + Collection wanted = result.getWantedAttributes(); + assertThat(wanted, hasSize(3)); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(NATIONALITY, true, softPresenceConstraints))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(NATIONALITY, true, nonSoftPresentConstraints))); + assertThat(wanted, hasItem(WantedAttributeMatcher.forAttribute(FULL_NAME, true, softPresenceConstraints))); + } + + @Test + public void builderWithAuthTypes() { + Policy result = Policy.builder() + .withSelfieAuthentication(true) + .withPinAuthentication(true) + .withWantedAuthType(99) + .build(); + + assertThat(result.getWantedAuthTypes(), hasSize(3)); + assertThat(result.getWantedAuthTypes(), hasItems(EXPECTED_SELFIE_AUTH_TYPE, EXPECTED_PIN_AUTH_TYPE, 99)); + } + + @Test + public void builderWithDisabledAuthTypes() { + Policy result = Policy.builder() + .withSelfieAuthentication(false) + .withPinAuthentication(false) + .build(); + + assertThat(result.getWantedAuthTypes(), hasSize(0)); + } + + @Test + public void builderWithRememberMeFlags() { + Policy result = Policy.builder() + .withWantedRememberMe(true) + .withWantedRememberMeOptional(true) + .build(); + + assertTrue(result.isWantedRememberMe()); + assertTrue(result.isWantedRememberMeOptional()); + } + + @Test + public void builderWithSelfieAuthorisationEnabledThenDisabled() { + Policy result = Policy.builder() + .withSelfieAuthentication(true) + .withSelfieAuthentication(false) + .build(); + + assertThat(result.getWantedAuthTypes(), not(hasItem(EXPECTED_SELFIE_AUTH_TYPE))); + assertThat(result.getWantedAuthTypes(), hasSize(0)); + } + + @Test + public void builderWithSelfieAuthorisationDisabledThenEnabled() { + Policy result = Policy.builder() + .withSelfieAuthentication(false) + .withSelfieAuthentication(true) + .build(); + + assertThat(result.getWantedAuthTypes(), hasItem(EXPECTED_SELFIE_AUTH_TYPE)); + assertThat(result.getWantedAuthTypes(), hasSize(1)); + } + + @Test + public void builderWithPinAuthorisationEnabledThenDisabled() { + Policy result = Policy.builder() + .withPinAuthentication(true) + .withPinAuthentication(false) + .build(); + + assertThat(result.getWantedAuthTypes(), not(hasItem(EXPECTED_PIN_AUTH_TYPE))); + assertThat(result.getWantedAuthTypes(), hasSize(0)); + } + + @Test + public void builderWithPinAuthorisationDisabledThenEnabled() { + Policy result = Policy.builder() + .withPinAuthentication(false) + .withPinAuthentication(true) + .build(); + + assertThat(result.getWantedAuthTypes(), hasItem(EXPECTED_PIN_AUTH_TYPE)); + assertThat(result.getWantedAuthTypes(), hasSize(1)); + } + + @Test + public void builderWithSelfieAuthorisationDisabled() { + Policy result = Policy.builder() + .withSelfieAuthentication(false) + .build(); + + assertThat(result.getWantedAuthTypes(), not(hasItem(EXPECTED_SELFIE_AUTH_TYPE))); + assertThat(result.getWantedAuthTypes(), hasSize(0)); + } + + @Test + public void builderWithPinAuthorisationDisabled() { + Policy result = Policy.builder() + .withPinAuthentication(false) + .build(); + + assertThat(result.getWantedAuthTypes(), not(hasItem(EXPECTED_PIN_AUTH_TYPE))); + assertThat(result.getWantedAuthTypes(), hasSize(0)); + } + + @Test + public void builderWithWantedAttributeByNameWithOptionalTrue() { + Policy result = Policy.builder() + .withWantedAttribute(true, GIVEN_NAMES) + .build(); + + assertThat(result.getWantedAttributes(), hasSize(1)); + assertThat(result.getWantedAttributes(), hasItem(WantedAttributeMatcher.forAttribute(GIVEN_NAMES, true))); + } + + @Test + public void builderWithWantedAttributeByNameWithOptionalFalse() { + Policy result = Policy.builder() + .withWantedAttribute(false, GIVEN_NAMES) + .build(); + + assertThat(result.getWantedAttributes(), hasSize(1)); + assertThat(result.getWantedAttributes(), hasItem(WantedAttributeMatcher.forAttribute(GIVEN_NAMES, false))); + } + + @Test + public void buildWithIdentityProfile() throws IOException { + IdentityProfileScheme scheme = new IdentityProfileScheme("A_TYPE", "AN_OBJECTIVE"); + + IdentityProfile identityProfile = new IdentityProfile("A_FRAMEWORK", scheme); + + JsonNode json = toJson(identityProfile); + + assertThat(json.get(Property.TRUST_FRAMEWORK).asText(), is(equalTo(identityProfile.getFramework()))); + + JsonNode schemeJsonNode = json.get(Property.SCHEME); + assertThat(schemeJsonNode.get(Property.TYPE).asText(), is(equalTo(scheme.getType()))); + assertThat(schemeJsonNode.get(Property.OBJECTIVE).asText(), is(equalTo(scheme.getObjective()))); + } + + @Test + public void buildWithIdentityProfileMap() throws IOException { + Map scheme = new HashMap<>(); + scheme.put(Property.TYPE, "A_TYPE"); + scheme.put(Property.OBJECTIVE, "AN_OBJECTIVE"); + + Map identityProfile = new HashMap<>(); + identityProfile.put(Property.TRUST_FRAMEWORK, "A_FRAMEWORK"); + identityProfile.put(Property.SCHEME, scheme); + + JsonNode json = toJson(identityProfile); + + assertThat( + json.get(Property.TRUST_FRAMEWORK).asText(), + is(equalTo(identityProfile.get(Property.TRUST_FRAMEWORK))) + ); + + JsonNode schemeJsonNode = json.get(Property.SCHEME); + assertThat(schemeJsonNode.get(Property.TYPE).asText(), is(equalTo(scheme.get(Property.TYPE)))); + assertThat(schemeJsonNode.get(Property.OBJECTIVE).asText(), is(equalTo(scheme.get( + Property.OBJECTIVE)))); + } + + private static JsonNode toJson(Object obj) throws IOException { + Policy policy = Policy.builder() + .withIdentityProfile(obj) + .build(); + + return MAPPER.readTree(MAPPER.writeValueAsString(policy.getIdentityProfile()).getBytes(DEFAULT_CHARSET)); + } + + private static final class Property { + + private Property() { } + + private static final String TYPE = "type"; + private static final String SCHEME = "scheme"; + private static final String OBJECTIVE = "objective"; + private static final String TRUST_FRAMEWORK = "trust_framework"; + + } + + private static class WantedAttributeMatcher extends TypeSafeDiagnosingMatcher { + + private static final String TEMPLATE = "{ name: '%s', derivation: '%s', optional: '%s', constraints: %s }"; + + private final String name; + private final String derivation; + private final boolean optional; + private final List constraints; + + private WantedAttributeMatcher(String name, String derivation, boolean optional) { + this.name = name; + this.derivation = derivation; + this.optional = optional; + constraints = new ArrayList<>(); + } + + private WantedAttributeMatcher(String name, boolean optional, List constraints) { + this.name = name; + derivation = null; + this.optional = optional; + this.constraints = constraints; + } + + @Override + protected boolean matchesSafely(WantedAttribute attribute, Description description) { + description.appendText(format(TEMPLATE, + attribute.getName(), + attribute.getDerivation(), + attribute.isOptional(), + attribute.getConstraints() + ) + ); + return optional == attribute.isOptional() + && nullSafeEquals(name, attribute.getName()) + && nullSafeEquals(derivation, attribute.getDerivation()) + && nullSafeEquals(constraints, attribute.getConstraints()); + } + + @Override + public void describeTo(Description description) { + description.appendText(format(TEMPLATE, name, derivation, optional, constraints)); + } + + private static boolean nullSafeEquals(Object o, Object other) { + if (o == null) { + return other == null; + } + return o.equals(other); + } + + private static WantedAttributeMatcher forAttribute(String name, boolean optional) { + return new WantedAttributeMatcher(name, null, optional); + } + + private static WantedAttributeMatcher forAttribute(String name, String derivation, boolean optional) { + return new WantedAttributeMatcher(name, derivation, optional); + } + + private static WantedAttributeMatcher forAttribute( + String name, + boolean optional, + List constraints) { + return new WantedAttributeMatcher(name, optional, constraints); + } + + } + +} From ca7b48ce42b6d49152189b49fccbf8e127c0a0d3 Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 7 Mar 2023 16:27:46 +0000 Subject: [PATCH 26/41] SDK-2231: Add Digital Identity Session creation service --- .../api/client/DigitalIdentityClient.java | 21 +- .../client/spi/remote/call/YotiConstants.java | 4 + .../call/factory/UnsignedPathFactory.java | 35 +- .../call/identity/DigitalIdentityService.java | 81 ++++- .../java/com/yoti/json/ResourceMapper.java | 39 ++ .../api/client/DigitalIdentityClientTest.java | 38 +- .../identity/DigitalIdentityServiceTest.java | 344 ++++++++++++++++++ .../springboot/DigitalIdentityController.java | 36 +- .../templates/digital-identity-share.html | 9 + 9 files changed, 569 insertions(+), 38 deletions(-) create mode 100644 yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java create mode 100644 yoti-sdk-api/src/test/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityServiceTest.java 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 362aa41f..8353a4c1 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 @@ -7,7 +7,10 @@ import java.security.KeyPair; import java.security.Security; +import com.yoti.api.client.identity.ShareSession; +import com.yoti.api.client.identity.ShareSessionRequest; import com.yoti.api.client.spi.remote.KeyStreamVisitor; +import com.yoti.api.client.spi.remote.call.identity.DigitalIdentityException; import com.yoti.api.client.spi.remote.call.identity.DigitalIdentityService; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -28,11 +31,21 @@ public class DigitalIdentityClient { this.identityService = identityService; } - public Object createShareSession() { + /** + * Create a sharing session to initiate a sharing process based on a policy + * + * @param request + * Details of the request like policy, extensions and push notification for the application + * @return an {@link ShareSession} + * Id, status and expiry of the newly created Share Session + * @throws DigitalIdentityException + * Aggregate exception signalling issues during the call + */ + public ShareSession createShareSession(ShareSessionRequest request) throws DigitalIdentityException { notNullOrEmpty(sdkId, "SDK ID"); notNull(keyPair, "Application Key Pair"); - return identityService.createShareSession(); + return identityService.createShareSession(sdkId, keyPair, request); } public Object fetchShareSession(String sessionId) { @@ -63,9 +76,9 @@ public Object fetchShareReceipt(String receiptId) { return identityService.fetchShareReceipt(receiptId); } - private KeyPair loadKeyPair(KeyPairSource keyPair) throws InitialisationException { + private KeyPair loadKeyPair(KeyPairSource keyPairSource) throws InitialisationException { try { - return keyPair.getFromStream(new KeyStreamVisitor()); + return keyPairSource.getFromStream(new KeyStreamVisitor()); } catch (IOException ex) { throw new InitialisationException("Cannot load Key Pair", ex); } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java index e12607f1..c0af35a4 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java @@ -14,6 +14,10 @@ private YotiConstants() {} public static final String YOTI_DOCS_PATH_PREFIX = "/idverify/v1"; public static final String DEFAULT_YOTI_DOCS_URL = DEFAULT_YOTI_HOST + YOTI_DOCS_PATH_PREFIX; + public static final String IDENTITY_PREFIX = "/share"; + public static final String DEFAULT_IDENTITY_URL = DEFAULT_YOTI_HOST + IDENTITY_PREFIX; + + public static final String AUTH_ID_HEADER = "X-Yoti-Auth-Id"; public static final String AUTH_KEY_HEADER = "X-Yoti-Auth-Key"; public static final String DIGEST_HEADER = "X-Yoti-Auth-Digest"; public static final String YOTI_SDK_HEADER = "X-Yoti-SDK"; 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 0a98e315..a0a9fe4e 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 @@ -4,21 +4,22 @@ public class UnsignedPathFactory { - static final String PROFILE_PATH_TEMPLATE = "/profile/%s?appId=%s"; - static final String AML_PATH_TEMPLATE = "/aml-check?appId=%s"; - static final String QR_CODE_PATH_TEMPLATE = "/qrcodes/apps/%s"; - static final String DOCS_CREATE_SESSION_PATH_TEMPLATE = "/sessions?sdkId=%s"; - static final String DOCS_SESSION_PATH_TEMPLATE = "/sessions/%s?sdkId=%s"; - static final String DOCS_MEDIA_CONTENT_PATH_TEMPLATE = "/sessions/%s/media/%s/content?sdkId=%s"; - static final String DOCS_PUT_IBV_INSTRUCTIONS_PATH_TEMPLATE = "/sessions/%s/instructions?sdkId=%s"; - static final String DOCS_FETCH_IBV_INSTRUCTIONS_PATH_TEMPLATE = "/sessions/%s/instructions?sdkId=%s"; - static final String DOCS_FETCH_IBV_INSTRUCTIONS_PDF_PATH_TEMPLATE = "/sessions/%s/instructions/pdf?sdkId=%s"; - static final String DOCS_SUPPORTED_DOCUMENTS_PATH = "/supported-documents?includeNonLatin=%s"; - static final String DOCS_FETCH_INSTRUCTION_CONTACT_PROFILE_PATH_TEMPLATE = "/sessions/%s/instructions/contact-profile?sdkId=%s"; - static final String DOCS_FETCH_SESSION_CONFIGURATION_PATH_TEMPLATE = "/sessions/%s/configuration?sdkId=%s"; - static final String DOCS_NEW_FACE_CAPTURE_RESOURCE_PATH_TEMPLATE = "/sessions/%s/resources/face-capture?sdkId=%s"; - static final String DOCS_UPLOAD_FACE_CAPTURE_IMAGE_PATH_TEMPLATE = "/sessions/%s/resources/face-capture/%s/image?sdkId=%s"; - static final String DOCS_TRIGGER_IBV_NOTIFICATION_PATH_TEMPLATE = "/sessions/%s/instructions/email?sdkId=%s"; + private static final String PROFILE_PATH_TEMPLATE = "/profile/%s?appId=%s"; + private static final String AML_PATH_TEMPLATE = "/aml-check?appId=%s"; + private static final String QR_CODE_PATH_TEMPLATE = "/qrcodes/apps/%s"; + private static final String DOCS_CREATE_SESSION_PATH_TEMPLATE = "/sessions?sdkId=%s"; + private static final String DOCS_SESSION_PATH_TEMPLATE = "/sessions/%s?sdkId=%s"; + private static final String DOCS_MEDIA_CONTENT_PATH_TEMPLATE = "/sessions/%s/media/%s/content?sdkId=%s"; + private static final String DOCS_PUT_IBV_INSTRUCTIONS_PATH_TEMPLATE = "/sessions/%s/instructions?sdkId=%s"; + private static final String DOCS_FETCH_IBV_INSTRUCTIONS_PATH_TEMPLATE = "/sessions/%s/instructions?sdkId=%s"; + private static final String DOCS_FETCH_IBV_INSTRUCTIONS_PDF_PATH_TEMPLATE = "/sessions/%s/instructions/pdf?sdkId=%s"; + private static final String DOCS_SUPPORTED_DOCUMENTS_PATH = "/supported-documents?includeNonLatin=%s"; + private static final String DOCS_FETCH_INSTRUCTION_CONTACT_PROFILE_PATH_TEMPLATE = "/sessions/%s/instructions/contact-profile?sdkId=%s"; + private static final String DOCS_FETCH_SESSION_CONFIGURATION_PATH_TEMPLATE = "/sessions/%s/configuration?sdkId=%s"; + private static final String DOCS_NEW_FACE_CAPTURE_RESOURCE_PATH_TEMPLATE = "/sessions/%s/resources/face-capture?sdkId=%s"; + private static final String DOCS_UPLOAD_FACE_CAPTURE_IMAGE_PATH_TEMPLATE = "/sessions/%s/resources/face-capture/%s/image?sdkId=%s"; + private static final String DOCS_TRIGGER_IBV_NOTIFICATION_PATH_TEMPLATE = "/sessions/%s/instructions/email?sdkId=%s"; + private static final String IDENTITY_SESSION_CREATION_TEMPLATE = "/v2/sessions"; public String createProfilePath(String appId, String connectToken) { return format(PROFILE_PATH_TEMPLATE, connectToken, appId); @@ -80,4 +81,8 @@ public String createTriggerIbvEmailNotificationPath(String sdkId, String session return format(DOCS_TRIGGER_IBV_NOTIFICATION_PATH_TEMPLATE, sessionId, sdkId); } + public String createIdentitySessionPath() { + return IDENTITY_SESSION_CREATION_TEMPLATE; + } + } 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 5aefc1a1..6fae678c 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,13 +1,88 @@ package com.yoti.api.client.spi.remote.call.identity; +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; +import static com.yoti.api.client.spi.remote.call.YotiConstants.CONTENT_TYPE_JSON; +import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_IDENTITY_URL; +import static com.yoti.api.client.spi.remote.call.YotiConstants.PROPERTY_YOTI_API_URL; +import static com.yoti.validation.Validation.notNull; +import static com.yoti.validation.Validation.notNullOrEmpty; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; + +import com.yoti.api.client.identity.ShareSession; +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.SignedRequestBuilderFactory; +import com.yoti.api.client.spi.remote.call.factory.UnsignedPathFactory; +import com.yoti.json.ResourceMapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class DigitalIdentityService { + private static final Logger LOG = LoggerFactory.getLogger(DigitalIdentityService.class); + + private final UnsignedPathFactory pathFactory; + private final SignedRequestBuilderFactory requestBuilderFactory; + + private final String apiUrl; + + public DigitalIdentityService(UnsignedPathFactory pathFactory, SignedRequestBuilderFactory requestBuilderFactory) { + this.pathFactory = pathFactory; + this.requestBuilderFactory = requestBuilderFactory; + + this.apiUrl = System.getProperty(PROPERTY_YOTI_API_URL, DEFAULT_IDENTITY_URL); + } + public static DigitalIdentityService newInstance() { - return new DigitalIdentityService(); + return new DigitalIdentityService(new UnsignedPathFactory(), new SignedRequestBuilderFactory()); } - public Object createShareSession() { - return null; + public ShareSession createShareSession(String sdkId, KeyPair keyPair, ShareSessionRequest shareSessionRequest) + throws DigitalIdentityException { + notNullOrEmpty(sdkId, "SDK ID"); + notNull(keyPair, "Application Key Pair"); + notNull(shareSessionRequest, "Share Session request"); + + LOG.debug("Requesting Share Session Creation for SDK ID '{}", sdkId); + + String path = pathFactory.createIdentitySessionPath(); + + try { + byte[] payload = ResourceMapper.writeValueAsString(shareSessionRequest); + SignedRequest request = createSignedRequest(sdkId, keyPair, path, payload); + + return request.execute(ShareSession.class); + } catch (IOException ex) { + throw new DigitalIdentityException("Error parsing the request: ", ex); + } catch (URISyntaxException ex) { + throw new DigitalIdentityException("Error building the request: ", ex); + } catch (GeneralSecurityException ex) { + throw new DigitalIdentityException("Error signing the request: ", ex); + } catch (ResourceException ex) { + throw new DigitalIdentityException("Error posting the request: ", ex); + } + } + + SignedRequest createSignedRequest(String appId, KeyPair keyPair, String path, byte[] payload) + throws GeneralSecurityException, UnsupportedEncodingException, URISyntaxException { + return requestBuilderFactory.create() + .withKeyPair(keyPair) + .withBaseUrl(apiUrl) + .withEndpoint(path) + .withHeader(AUTH_ID_HEADER, appId) + .withHttpMethod(HTTP_POST) + .withHeader(CONTENT_TYPE, CONTENT_TYPE_JSON) + .withPayload(payload) + .build(); } public Object fetchShareSession(String sessionId) { diff --git a/yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java b/yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java new file mode 100644 index 00000000..d3f8088b --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java @@ -0,0 +1,39 @@ +package com.yoti.json; + +import java.nio.charset.StandardCharsets; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.introspect.VisibilityChecker; + +public final class ResourceMapper { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + static { + MAPPER.disable(DeserializationFeature.WRAP_EXCEPTIONS) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) + .setVisibility(configureVisibility(MAPPER.getDeserializationConfig())) + .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SKIP)); + } + + private static VisibilityChecker configureVisibility(MapperConfig config) { + return config.getDefaultVisibilityChecker() + .withFieldVisibility(JsonAutoDetect.Visibility.NONE) + .withGetterVisibility(JsonAutoDetect.Visibility.NONE) + .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE) + .withSetterVisibility(JsonAutoDetect.Visibility.NONE) + .withCreatorVisibility(JsonAutoDetect.Visibility.NONE); + } + + public static byte[] writeValueAsString(Object object) throws JsonProcessingException { + return MAPPER.writeValueAsString(object).getBytes(StandardCharsets.UTF_8); + } + +} 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 af3fb56d..868bf6fd 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 @@ -4,6 +4,8 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.Answers.RETURNS_DEEP_STUBS; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import java.io.ByteArrayInputStream; @@ -11,6 +13,8 @@ import java.io.InputStream; import java.security.KeyPair; +import com.yoti.api.client.identity.ShareSessionRequest; +import com.yoti.api.client.spi.remote.KeyStreamVisitor; import com.yoti.api.client.spi.remote.call.identity.DigitalIdentityException; import com.yoti.api.client.spi.remote.call.identity.DigitalIdentityService; import com.yoti.api.client.spi.remote.util.CryptoUtil; @@ -24,12 +28,15 @@ @RunWith(MockitoJUnitRunner.class) public class DigitalIdentityClientTest { - private static final String AN_APP_ID = "anAppId"; + private static final String AN_SDK_ID = "anSdkId"; private static final String A_QR_CODE_ID = "aQrCodeId"; private static final String A_SESSION_ID = "aSessionId"; private static final String A_RECEIPT_ID = "aReceiptId"; + @Mock KeyPairSource keyPairSource; + @Mock(answer = RETURNS_DEEP_STUBS) KeyPair keyPair; @Mock DigitalIdentityService identityService; + @Mock ShareSessionRequest shareSessionRequest; private KeyPairSource validKeyPairSource; @@ -61,7 +68,7 @@ public void builderWithClientSdkId_EmptySdkId_IllegalArgumentException() { @Test public void builderWithKeyPairSource_NullKeyPairSource_IllegalArgumentException() { DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder() - .withClientSdkId(AN_APP_ID); + .withClientSdkId(AN_SDK_ID); IllegalArgumentException ex = assertThrows( IllegalArgumentException.class, @@ -81,7 +88,7 @@ public void build_MissingSdkId_IllegalArgumentException() { @Test public void build_MissingKeyPairSource_IllegalArgumentException() { - DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder().withClientSdkId(AN_APP_ID); + DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder().withClientSdkId(AN_SDK_ID); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, builder::build); @@ -93,7 +100,7 @@ public void build_NoKeyPairInFile_InitialisationException() { KeyPairSource invalidKeyPairSource = new KeyPairSourceTest("no-key-pair-in-file"); DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder() - .withClientSdkId(AN_APP_ID) + .withClientSdkId(AN_SDK_ID) .withKeyPairSource(invalidKeyPairSource); InitialisationException ex = assertThrows(InitialisationException.class, builder::build); @@ -106,7 +113,7 @@ public void build_InvalidKeyPair_InitialisationException() { KeyPairSource invalidKeyPairSource = new KeyPairSourceTest(CryptoUtil.INVALID_KEY_PAIR_PEM); DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder() - .withClientSdkId(AN_APP_ID) + .withClientSdkId(AN_SDK_ID) .withKeyPairSource(invalidKeyPairSource); InitialisationException ex = assertThrows(InitialisationException.class, builder::build); @@ -116,19 +123,22 @@ public void build_InvalidKeyPair_InitialisationException() { } @Test - public void client_CreateShareSessionException_DigitalIdentityException() { + public void client_CreateShareSessionException_DigitalIdentityException() throws IOException { + when(keyPairSource.getFromStream(any(KeyStreamVisitor.class))).thenReturn(keyPair); + DigitalIdentityClient identityClient = new DigitalIdentityClient( - AN_APP_ID, - validKeyPairSource, + AN_SDK_ID, + keyPairSource, identityService ); String exMessage = "Create Share Session Error"; - when(identityService.createShareSession()).thenThrow(new DigitalIdentityException(exMessage)); + when(identityService.createShareSession(AN_SDK_ID, keyPair, shareSessionRequest)) + .thenThrow(new DigitalIdentityException(exMessage)); DigitalIdentityException ex = assertThrows( DigitalIdentityException.class, - identityClient::createShareSession + () -> identityClient.createShareSession(shareSessionRequest) ); assertThat(ex.getMessage(), equalTo(exMessage)); @@ -137,7 +147,7 @@ public void client_CreateShareSessionException_DigitalIdentityException() { @Test public void client_FetchShareSessionException_DigitalIdentityException() { DigitalIdentityClient identityClient = new DigitalIdentityClient( - AN_APP_ID, + AN_SDK_ID, validKeyPairSource, identityService ); @@ -156,7 +166,7 @@ public void client_FetchShareSessionException_DigitalIdentityException() { @Test public void client_CreateShareQrCodeException_DigitalIdentityException() { DigitalIdentityClient identityClient = new DigitalIdentityClient( - AN_APP_ID, + AN_SDK_ID, validKeyPairSource, identityService ); @@ -175,7 +185,7 @@ public void client_CreateShareQrCodeException_DigitalIdentityException() { @Test public void client_FetchShareQrCodeException_DigitalIdentityException() { DigitalIdentityClient identityClient = new DigitalIdentityClient( - AN_APP_ID, + AN_SDK_ID, validKeyPairSource, identityService ); @@ -194,7 +204,7 @@ public void client_FetchShareQrCodeException_DigitalIdentityException() { @Test public void client_FetchShareReceiptException_DigitalIdentityException() { DigitalIdentityClient identityClient = new DigitalIdentityClient( - AN_APP_ID, + AN_SDK_ID, validKeyPairSource, identityService ); 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 new file mode 100644 index 00000000..d942ed7d --- /dev/null +++ b/yoti-sdk-api/src/test/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityServiceTest.java @@ -0,0 +1,344 @@ +package com.yoti.api.client.spi.remote.call.identity; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Answers.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.*; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import com.yoti.api.client.identity.ShareSession; +import com.yoti.api.client.identity.ShareSessionNotification; +import com.yoti.api.client.identity.ShareSessionRequest; +import com.yoti.api.client.identity.constraint.SourceConstraint; +import com.yoti.api.client.identity.extension.Extension; +import com.yoti.api.client.identity.extension.ThirdPartyAttributeContent; +import com.yoti.api.client.identity.extension.ThirdPartyAttributeExtensionBuilder; +import com.yoti.api.client.identity.policy.Policy; +import com.yoti.api.client.identity.policy.WantedAnchor; +import com.yoti.api.client.identity.policy.WantedAttribute; +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; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.*; +import org.junit.runner.RunWith; +import org.mockito.*; +import org.mockito.junit.*; + +@RunWith(MockitoJUnitRunner.class) +public class DigitalIdentityServiceTest { + + private static final String SDK_ID = "anSdkId"; + private static final String SESSION_CREATION_PATH = "aSessionCreationPath"; + private static final Date DATE = new Date(); + private static final byte[] A_BODY_BYTES = "aBody".getBytes(StandardCharsets.UTF_8); + + @Spy @InjectMocks DigitalIdentityService identityService; + + @Mock UnsignedPathFactory unsignedPathFactory; + @Mock(answer = RETURNS_DEEP_STUBS) SignedRequestBuilder signedRequestBuilder; + @Mock SignedRequestBuilderFactory requestBuilderFactory; + + @Mock ShareSessionRequest shareSessionRequest; + @Mock SignedRequest signedRequest; + @Mock(answer = RETURNS_DEEP_STUBS) KeyPair keyPair; + @Mock ShareSession shareSession; + + @Before + public void setUp() { + when(unsignedPathFactory.createIdentitySessionPath()).thenReturn(SESSION_CREATION_PATH); + } + + @Test + public void createShareSession_NullSdkId_Exception() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> identityService.createShareSession(null, keyPair, shareSessionRequest) + ); + + assertThat(ex.getMessage(), containsString("SDK ID")); + } + + @Test + public void createShareSession_EmptySdkId_Exception() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> identityService.createShareSession("", keyPair, shareSessionRequest) + ); + + assertThat(ex.getMessage(), containsString("SDK ID")); + } + + @Test + public void createShareSession_NullKeyPair_Exception() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> identityService.createShareSession(SDK_ID, null, shareSessionRequest) + ); + + assertThat(ex.getMessage(), containsString("Application Key Pair")); + } + + @Test + public void createShareSession_NullShareSessionRequest_Exception() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> identityService.createShareSession(SDK_ID, keyPair, null) + ); + + assertThat(ex.getMessage(), containsString("Share Session request")); + } + + @Test + public void createShareSession_SerializingWrongPayload_Exception() { + JsonProcessingException causeEx = new JsonProcessingException("serialization error") {}; + + try (MockedStatic mapper = Mockito.mockStatic(ResourceMapper.class)) { + mapper.when(() -> ResourceMapper.writeValueAsString(shareSessionRequest)).thenThrow(causeEx); + + DigitalIdentityException ex = assertThrows( + DigitalIdentityException.class, + () -> identityService.createShareSession(SDK_ID, keyPair, shareSessionRequest) + ); + + Throwable cause = ex.getCause(); + assertTrue(cause instanceof JsonProcessingException); + assertThat(cause.getMessage(), containsString("serialization error")); + } + } + + @Test + public void createShareSession_BuildingRequestWithWrongUri_Exception() + throws GeneralSecurityException, UnsupportedEncodingException, URISyntaxException { + 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)) + .thenThrow(causeEx); + + DigitalIdentityException ex = assertThrows( + DigitalIdentityException.class, + () -> identityService.createShareSession(SDK_ID, keyPair, shareSessionRequest) + ); + + Throwable cause = ex.getCause(); + assertTrue(cause instanceof URISyntaxException); + assertThat(cause.getMessage(), containsString(exMessage)); + } + } + + @Test + public void createShareSession_BuildingRequestWithWrongQueryParams_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 = "Wrong query params format"; + UnsupportedEncodingException causeEx = new UnsupportedEncodingException(exMessage); + when(identityService.createSignedRequest(SDK_ID, keyPair, SESSION_CREATION_PATH, A_BODY_BYTES)) + .thenThrow(causeEx); + + DigitalIdentityException ex = assertThrows( + DigitalIdentityException.class, + () -> identityService.createShareSession(SDK_ID, keyPair, shareSessionRequest) + ); + + Throwable cause = ex.getCause(); + assertTrue(cause instanceof UnsupportedEncodingException); + assertThat(cause.getMessage(), containsString(exMessage)); + } + } + + @Test + public void createShareSession_BuildingRequestWithWrongDigest_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 = "Wrong digest"; + GeneralSecurityException causeEx = new GeneralSecurityException(exMessage); + when(identityService.createSignedRequest(SDK_ID, keyPair, SESSION_CREATION_PATH, A_BODY_BYTES)) + .thenThrow(causeEx); + + DigitalIdentityException ex = assertThrows( + DigitalIdentityException.class, + () -> identityService.createShareSession(SDK_ID, keyPair, shareSessionRequest) + ); + + Throwable cause = ex.getCause(); + assertTrue(cause instanceof GeneralSecurityException); + assertThat(cause.getMessage(), containsString(exMessage)); + } + } + + @Test + public void createShareSession_SessionRequest_exception() throws Exception { + try (MockedStatic mapper = Mockito.mockStatic(ResourceMapper.class)) { + mapper.when(() -> ResourceMapper.writeValueAsString(shareSessionRequest)).thenReturn(A_BODY_BYTES); + + when(requestBuilderFactory.create()).thenReturn(signedRequestBuilder); + + when(identityService.createSignedRequest(SDK_ID, keyPair, SESSION_CREATION_PATH, A_BODY_BYTES)) + .thenReturn(signedRequest); + + when(signedRequest.execute(ShareSession.class)).thenReturn(shareSession); + + ShareSession result = identityService.createShareSession(SDK_ID, keyPair, shareSessionRequest); + assertSame(shareSession, result); + } + } + + @Test + public void serializePayload_CreatesValidShareSessionRequestJson() throws Exception { + String subjectId = "subject_id"; + String subjectIdValue = "00000000-1111-2222-3333-444444444444"; + + Map subject = new HashMap<>(); + subject.put(subjectId, subjectIdValue); + + String wantedAnchorValue = "aAnchorValue"; + String wantedAnchorSubType = "aAnchorSubType"; + SourceConstraint constraint = SourceConstraint.builder() + .withWantedAnchor( + WantedAnchor.builder() + .withValue(wantedAnchorValue) + .withSubType(wantedAnchorSubType) + .build() + ) + .withSoftPreference(true) + .build(); + + String wantedAttributeName = "aWantedAttributeName"; + String wantedAttributeDerivation = "aWantedAttributeDerivation"; + + String trustFramework = "trust_framework"; + String trustFrameworkValue = "aTrustFramework"; + String scheme = "scheme"; + String schemeValue = "aScheme"; + Map identityProfile = new HashMap<>(); + identityProfile.put(trustFramework, trustFrameworkValue); + identityProfile.put(scheme, schemeValue); + + Policy policy = Policy.builder() + .withWantedAttribute( + WantedAttribute.builder() + .withName(wantedAttributeName) + .withDerivation(wantedAttributeDerivation) + .withOptional(true) + .withAcceptSelfAsserted(true) + .withConstraint(constraint) + .build() + ) + .withWantedAuthType(1) + .withWantedRememberMe(true) + .withWantedRememberMeOptional(true) + .withIdentityProfile(identityProfile) + .build(); + + String thirdPartyAttributeDefinition = "aDefinition"; + Extension extension = new ThirdPartyAttributeExtensionBuilder() + .withDefinition(thirdPartyAttributeDefinition) + .withExpiryDate(DATE) + .build(); + + String redirectUriValue = "aRedirectUri"; + String notificationUriValue = "aNotificationUri"; + ShareSessionRequest request = ShareSessionRequest.builder() + .withSubject(subject) + .withPolicy(policy) + .withExtension(extension) + .withRedirectUri(new URI(redirectUriValue)) + .withNotification(ShareSessionNotification.builder(new URI(notificationUriValue)).build()) + .build(); + + byte[] payload = ResourceMapper.writeValueAsString(request); + + JsonNode json = new ObjectMapper().readTree(new String(payload)); + + JsonNode subjectNode = json.get("subject"); + assertThat(subjectNode.get(subjectId).asText(), equalTo(subjectIdValue)); + + JsonNode policyNode = json.get("policy"); + + JsonNode policyWantedNode = policyNode.get("wanted"); + assertThat(policyWantedNode.size(), equalTo(1)); + + JsonNode wantedAttributeNode = policyWantedNode.get(0); + assertThat(wantedAttributeNode.get("name").asText(), equalTo(wantedAttributeName)); + assertThat(wantedAttributeNode.get("derivation").asText(), equalTo(wantedAttributeDerivation)); + assertTrue(wantedAttributeNode.get("optional").asBoolean()); + assertTrue(wantedAttributeNode.get("accept_self_asserted").asBoolean()); + + JsonNode wantedAttributeConstraintsNode = wantedAttributeNode.get("constraints"); + assertThat(wantedAttributeConstraintsNode.size(), equalTo(1)); + + JsonNode wantedAttributeConstraintNode = wantedAttributeConstraintsNode.get(0); + assertThat(wantedAttributeConstraintNode.get("type").asText(), equalTo("SOURCE")); + JsonNode preferredSourcesNode = wantedAttributeConstraintNode.get("preferred_sources"); + assertTrue(preferredSourcesNode.get("soft_preference").asBoolean()); + + JsonNode preferredSourceAnchorsNode = preferredSourcesNode.get("anchors"); + assertThat(preferredSourceAnchorsNode.size(), equalTo(1)); + + JsonNode preferredSourceAnchorNode = preferredSourceAnchorsNode.get(0); + assertThat(preferredSourceAnchorNode.get("name").asText(), equalTo(wantedAnchorValue)); + assertThat(preferredSourceAnchorNode.get("sub_type").asText(), equalTo(wantedAnchorSubType)); + + JsonNode wantedAuthTypes = policyNode.get("wanted_auth_types"); + assertThat(wantedAuthTypes.size(), equalTo(1)); + assertThat(wantedAuthTypes.get(0).asInt(), equalTo(1)); + + assertTrue(policyNode.get("wanted_remember_me").asBoolean()); + assertTrue(policyNode.get("wanted_remember_me_optional").asBoolean()); + + JsonNode wantedAttributeIdentityProfile = policyNode.get("identity_profile_requirements"); + assertThat(wantedAttributeIdentityProfile.get(trustFramework).asText(), equalTo(trustFrameworkValue)); + assertThat(wantedAttributeIdentityProfile.get(scheme).asText(), equalTo(schemeValue)); + + JsonNode extensionsNode = json.get("extensions"); + assertThat(extensionsNode.size(), equalTo(1)); + + JsonNode extensionNode = extensionsNode.get(0); + assertThat(extensionNode.get("type").asText(), equalTo("THIRD_PARTY_ATTRIBUTE")); + + JsonNode extensionContent = extensionNode.get("content"); + assertThat(extensionContent.get("expiry_date").asText(), notNullValue()); + + JsonNode extensionDefinitions = extensionContent.get("definitions"); + assertThat(extensionDefinitions.size(), equalTo(1)); + + JsonNode extensionDefinition = extensionDefinitions.get(0); + assertThat(extensionDefinition.get("name").asText(), equalTo(thirdPartyAttributeDefinition)); + + assertThat(json.get("redirectUri").asText(), equalTo(redirectUriValue)); + + JsonNode notificationNode = json.get("notification"); + assertThat(notificationNode.get("url").asText(), equalTo(notificationUriValue)); + assertThat(notificationNode.get("method").asText(), equalTo("POST")); + assertTrue(notificationNode.get("verifyTls").asBoolean()); + assertThat(notificationNode.get("headers").size(), equalTo(0)); + } + +} 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 8be54ca6..cd9db508 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 @@ -1,9 +1,17 @@ package com.yoti.api.examples.springboot; +import java.net.URI; +import java.net.URISyntaxException; + import com.yoti.api.client.DigitalIdentityClient; +import com.yoti.api.client.identity.ShareSession; +import com.yoti.api.client.identity.ShareSessionRequest; +import com.yoti.api.client.identity.policy.Policy; import com.yoti.api.spring.ClientProperties; import com.yoti.api.spring.DigitalIdentityProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -23,6 +31,8 @@ @RequestMapping("/v2") public class DigitalIdentityController implements WebMvcConfigurer { + private static final Logger LOG = LoggerFactory.getLogger(DigitalIdentityController.class); + private final DigitalIdentityClient client; private final ClientProperties properties; @@ -38,16 +48,38 @@ public DigitalIdentityController(DigitalIdentityClient client, ClientProperties } @RequestMapping("/") - public String home(final Model model) { + public String home(Model model) { model.addAttribute("clientSdkId", properties.getClientSdkId()); model.addAttribute("scenarioId", properties.getScenarioId()); return "index"; } @RequestMapping("/digital-identity-share") - public String identityShare(final Model model) { + public String identityShare(Model model) throws URISyntaxException { model.addAttribute("message", "Example page for identity share using Yoti web-share"); + Policy policy = Policy.builder().build(); + + ShareSessionRequest shareSessionRequest = ShareSessionRequest.builder() + .withPolicy(policy) + .withRedirectUri(new URI("https://host/redirect/")) + .build(); + + ShareSession result = null; + try { + result = client.createShareSession(shareSessionRequest); + } catch (Exception ex) { + LOG.error(ex.getMessage()); + } + + model.addAttribute("message", "Identity creation example"); + + model.addAttribute("sdkId", properties.getClientSdkId()); + + model.addAttribute("session_id", result.getId()); + model.addAttribute("session_status", result.getStatus()); + model.addAttribute("session_expiry", result.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 4418c754..3a614ecb 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 @@ -15,6 +15,15 @@

Digital Identity Share Example page

+ +

SdkId:

+ +
+

Id:

+

Status:

+

Expiry:

+
+ From d887552fb89cf6a48ea560c88f6998f5ecaedad1 Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 28 Feb 2023 16:32:31 +0000 Subject: [PATCH 27/41] SDK-2249: Add Digital Identity Session QR code creation service --- .../api/client/DigitalIdentityClient.java | 42 ++-- .../client/identity/ShareSessionQrCode.java | 28 +++ .../call/factory/UnsignedPathFactory.java | 5 + .../call/identity/DigitalIdentityService.java | 61 ++++-- .../api/client/DigitalIdentityClientTest.java | 18 +- .../identity/DigitalIdentityServiceTest.java | 183 ++++-------------- .../com/yoti/json/ResourceMapperTest.java | 159 +++++++++++++++ .../springboot/DigitalIdentityController.java | 42 ++-- .../templates/digital-identity-share.html | 7 +- 9 files changed, 327 insertions(+), 218 deletions(-) create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionQrCode.java create mode 100644 yoti-sdk-api/src/test/java/com/yoti/json/ResourceMapperTest.java 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 8353a4c1..c5d80c91 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 @@ -8,6 +8,7 @@ import java.security.Security; 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.KeyStreamVisitor; import com.yoti.api.client.spi.remote.call.identity.DigitalIdentityException; @@ -26,6 +27,9 @@ public class DigitalIdentityClient { private final DigitalIdentityService identityService; DigitalIdentityClient(String sdkId, KeyPairSource keyPair, DigitalIdentityService identityService) { + notNullOrEmpty(sdkId, "SDK ID"); + notNull(keyPair, "Application Key Pair"); + this.sdkId = sdkId; this.keyPair = loadKeyPair(keyPair); this.identityService = identityService; @@ -34,45 +38,34 @@ public class DigitalIdentityClient { /** * Create a sharing session to initiate a sharing process based on a policy * - * @param request - * Details of the request like policy, extensions and push notification for the application - * @return an {@link ShareSession} - * Id, status and expiry of the newly created Share Session - * @throws DigitalIdentityException - * Aggregate exception signalling issues during the call + * @param request Details of the request like policy, extensions and push notification for the application + * @return an {@link ShareSession} ID, status and expiry of the newly created Share Session + * @throws DigitalIdentityException Thrown if the session creation is unsuccessful */ public ShareSession createShareSession(ShareSessionRequest request) throws DigitalIdentityException { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); - return identityService.createShareSession(sdkId, keyPair, request); } public Object fetchShareSession(String sessionId) { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); - return identityService.fetchShareSession(sessionId); } - public Object createShareQrCode(String sessionId) { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); - - return identityService.createShareQrCode(sessionId); + /** + * Create a sharing session QR code to initiate a sharing process based on a policy + * + * @param sessionId Session ID the QR code will belong to + * @return ID and URI of the newly created Share Session QR code + * @throws DigitalIdentityException Thrown if the QR code creation is unsuccessful + */ + public ShareSessionQrCode createShareQrCode(String sessionId) throws DigitalIdentityException { + return identityService.createShareQrCode(sdkId, keyPair, sessionId); } public Object fetchShareQrCode(String qrCodeId) { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); - return identityService.fetchShareQrCode(qrCodeId); } public Object fetchShareReceipt(String receiptId) { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); - return identityService.fetchShareReceipt(receiptId); } @@ -110,9 +103,6 @@ public Builder withKeyPairSource(KeyPairSource keyPairSource) { } public DigitalIdentityClient build() { - notNull(sdkId, "SDK ID"); - notNull(keyPairSource, "Key Pair Source"); - return new DigitalIdentityClient(sdkId, keyPairSource, DigitalIdentityService.newInstance()); } 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 new file mode 100644 index 00000000..c18908ac --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionQrCode.java @@ -0,0 +1,28 @@ +package com.yoti.api.client.identity; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ShareSessionQrCode { + + @JsonProperty(Property.ID) + private String id; + + @JsonProperty(Property.URI) + private String uri; + + public String getId() { + return id; + } + + public String getUri() { + return uri; + } + + private static final class Property { + + private static final String ID = "id"; + private static final String URI = "uri"; + + } + +} 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 a0a9fe4e..c4fd7ee7 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 @@ -20,6 +20,7 @@ public class UnsignedPathFactory { private static final String DOCS_UPLOAD_FACE_CAPTURE_IMAGE_PATH_TEMPLATE = "/sessions/%s/resources/face-capture/%s/image?sdkId=%s"; private static final String DOCS_TRIGGER_IBV_NOTIFICATION_PATH_TEMPLATE = "/sessions/%s/instructions/email?sdkId=%s"; private static final String IDENTITY_SESSION_CREATION_TEMPLATE = "/v2/sessions"; + private static final String IDENTITY_SESSION_QR_CODE_CREATION_TEMPLATE = "/v2/sessions/%s/qr-codes"; public String createProfilePath(String appId, String connectToken) { return format(PROFILE_PATH_TEMPLATE, connectToken, appId); @@ -85,4 +86,8 @@ public String createIdentitySessionPath() { return IDENTITY_SESSION_CREATION_TEMPLATE; } + public String createIdentitySessionQrCodePath(String sessionId) { + return format(IDENTITY_SESSION_QR_CODE_CREATION_TEMPLATE, sessionId); + } + } 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 6fae678c..a687542c 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 @@ -12,10 +12,12 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyPair; 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; @@ -52,45 +54,49 @@ public ShareSession createShareSession(String sdkId, KeyPair keyPair, ShareSessi notNull(keyPair, "Application Key Pair"); notNull(shareSessionRequest, "Share Session request"); - LOG.debug("Requesting Share Session Creation for SDK ID '{}", sdkId); - String path = pathFactory.createIdentitySessionPath(); + LOG.debug("Requesting Share Session Creation for SDK ID '{}' at '{}'", sdkId, path); + try { byte[] payload = ResourceMapper.writeValueAsString(shareSessionRequest); SignedRequest request = createSignedRequest(sdkId, keyPair, path, payload); return request.execute(ShareSession.class); } catch (IOException ex) { - throw new DigitalIdentityException("Error parsing the request: ", ex); + throw new DigitalIdentityException("Error while parsing the share session creation request ", ex); } catch (URISyntaxException ex) { - throw new DigitalIdentityException("Error building the request: ", ex); + throw new DigitalIdentityException("Error while building the share session creation request ", ex); } catch (GeneralSecurityException ex) { - throw new DigitalIdentityException("Error signing the request: ", ex); + throw new DigitalIdentityException("Error while signing the share session creation request ", ex); } catch (ResourceException ex) { - throw new DigitalIdentityException("Error posting the request: ", ex); + throw new DigitalIdentityException("Error while executing the share session creation request ", ex); } } - SignedRequest createSignedRequest(String appId, KeyPair keyPair, String path, byte[] payload) - throws GeneralSecurityException, UnsupportedEncodingException, URISyntaxException { - return requestBuilderFactory.create() - .withKeyPair(keyPair) - .withBaseUrl(apiUrl) - .withEndpoint(path) - .withHeader(AUTH_ID_HEADER, appId) - .withHttpMethod(HTTP_POST) - .withHeader(CONTENT_TYPE, CONTENT_TYPE_JSON) - .withPayload(payload) - .build(); - } - public Object fetchShareSession(String sessionId) { return null; } - public Object createShareQrCode(String sessionId) { - return null; + public ShareSessionQrCode createShareQrCode(String sdkId, KeyPair keyPair, String sessionId) + throws DigitalIdentityException { + notNullOrEmpty(sdkId, "SDK ID"); + notNull(keyPair, "Application Key Pair"); + notNullOrEmpty(sessionId, "Session ID"); + + String path = pathFactory.createIdentitySessionQrCodePath(sessionId); + + 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)); + + return request.execute(ShareSessionQrCode.class); + } catch (GeneralSecurityException ex) { + throw new DigitalIdentityException("Error while signing the share QR code creation request ", ex); + } catch (IOException | URISyntaxException | ResourceException ex) { + throw new DigitalIdentityException("Error while executing the share QR code creation request ", ex); + } } public Object fetchShareQrCode(String qrCodeId) { @@ -101,4 +107,17 @@ public Object fetchShareReceipt(String receiptId) { return null; } + SignedRequest createSignedRequest(String sdkId, KeyPair keyPair, String path, byte[] payload) + throws GeneralSecurityException, UnsupportedEncodingException, URISyntaxException { + return 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(); + } + } 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 868bf6fd..30f3e0fb 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 @@ -86,15 +86,6 @@ public void build_MissingSdkId_IllegalArgumentException() { assertThat(ex.getMessage(), containsString("SDK ID")); } - @Test - public void build_MissingKeyPairSource_IllegalArgumentException() { - DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder().withClientSdkId(AN_SDK_ID); - - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, builder::build); - - assertThat(ex.getMessage(), containsString("Key Pair Source")); - } - @Test public void build_NoKeyPairInFile_InitialisationException() { KeyPairSource invalidKeyPairSource = new KeyPairSourceTest("no-key-pair-in-file"); @@ -164,15 +155,18 @@ public void client_FetchShareSessionException_DigitalIdentityException() { } @Test - public void client_CreateShareQrCodeException_DigitalIdentityException() { + public void client_CreateShareQrCodeException_DigitalIdentityException() throws IOException { + when(keyPairSource.getFromStream(any(KeyStreamVisitor.class))).thenReturn(keyPair); + DigitalIdentityClient identityClient = new DigitalIdentityClient( AN_SDK_ID, - validKeyPairSource, + keyPairSource, identityService ); String exMessage = "Create Share QR Error"; - when(identityService.createShareQrCode(A_SESSION_ID)).thenThrow(new DigitalIdentityException(exMessage)); + when(identityService.createShareQrCode(AN_SDK_ID, keyPair, A_SESSION_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 d942ed7d..5b23b4c3 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 @@ -9,25 +9,13 @@ import static org.mockito.Mockito.*; import java.io.UnsupportedEncodingException; -import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyPair; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; import com.yoti.api.client.identity.ShareSession; -import com.yoti.api.client.identity.ShareSessionNotification; import com.yoti.api.client.identity.ShareSessionRequest; -import com.yoti.api.client.identity.constraint.SourceConstraint; -import com.yoti.api.client.identity.extension.Extension; -import com.yoti.api.client.identity.extension.ThirdPartyAttributeContent; -import com.yoti.api.client.identity.extension.ThirdPartyAttributeExtensionBuilder; -import com.yoti.api.client.identity.policy.Policy; -import com.yoti.api.client.identity.policy.WantedAnchor; -import com.yoti.api.client.identity.policy.WantedAttribute; 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; @@ -35,8 +23,6 @@ import com.yoti.json.ResourceMapper; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.*; import org.junit.runner.RunWith; import org.mockito.*; @@ -46,8 +32,8 @@ public class DigitalIdentityServiceTest { private static final String SDK_ID = "anSdkId"; + private static final String SESSION_ID = "aSessionId"; private static final String SESSION_CREATION_PATH = "aSessionCreationPath"; - private static final Date DATE = new Date(); private static final byte[] A_BODY_BYTES = "aBody".getBytes(StandardCharsets.UTF_8); @Spy @InjectMocks DigitalIdentityService identityService; @@ -211,134 +197,45 @@ public void createShareSession_SessionRequest_exception() throws Exception { } @Test - public void serializePayload_CreatesValidShareSessionRequestJson() throws Exception { - String subjectId = "subject_id"; - String subjectIdValue = "00000000-1111-2222-3333-444444444444"; - - Map subject = new HashMap<>(); - subject.put(subjectId, subjectIdValue); - - String wantedAnchorValue = "aAnchorValue"; - String wantedAnchorSubType = "aAnchorSubType"; - SourceConstraint constraint = SourceConstraint.builder() - .withWantedAnchor( - WantedAnchor.builder() - .withValue(wantedAnchorValue) - .withSubType(wantedAnchorSubType) - .build() - ) - .withSoftPreference(true) - .build(); - - String wantedAttributeName = "aWantedAttributeName"; - String wantedAttributeDerivation = "aWantedAttributeDerivation"; - - String trustFramework = "trust_framework"; - String trustFrameworkValue = "aTrustFramework"; - String scheme = "scheme"; - String schemeValue = "aScheme"; - Map identityProfile = new HashMap<>(); - identityProfile.put(trustFramework, trustFrameworkValue); - identityProfile.put(scheme, schemeValue); - - Policy policy = Policy.builder() - .withWantedAttribute( - WantedAttribute.builder() - .withName(wantedAttributeName) - .withDerivation(wantedAttributeDerivation) - .withOptional(true) - .withAcceptSelfAsserted(true) - .withConstraint(constraint) - .build() - ) - .withWantedAuthType(1) - .withWantedRememberMe(true) - .withWantedRememberMeOptional(true) - .withIdentityProfile(identityProfile) - .build(); - - String thirdPartyAttributeDefinition = "aDefinition"; - Extension extension = new ThirdPartyAttributeExtensionBuilder() - .withDefinition(thirdPartyAttributeDefinition) - .withExpiryDate(DATE) - .build(); - - String redirectUriValue = "aRedirectUri"; - String notificationUriValue = "aNotificationUri"; - ShareSessionRequest request = ShareSessionRequest.builder() - .withSubject(subject) - .withPolicy(policy) - .withExtension(extension) - .withRedirectUri(new URI(redirectUriValue)) - .withNotification(ShareSessionNotification.builder(new URI(notificationUriValue)).build()) - .build(); - - byte[] payload = ResourceMapper.writeValueAsString(request); - - JsonNode json = new ObjectMapper().readTree(new String(payload)); - - JsonNode subjectNode = json.get("subject"); - assertThat(subjectNode.get(subjectId).asText(), equalTo(subjectIdValue)); - - JsonNode policyNode = json.get("policy"); - - JsonNode policyWantedNode = policyNode.get("wanted"); - assertThat(policyWantedNode.size(), equalTo(1)); - - JsonNode wantedAttributeNode = policyWantedNode.get(0); - assertThat(wantedAttributeNode.get("name").asText(), equalTo(wantedAttributeName)); - assertThat(wantedAttributeNode.get("derivation").asText(), equalTo(wantedAttributeDerivation)); - assertTrue(wantedAttributeNode.get("optional").asBoolean()); - assertTrue(wantedAttributeNode.get("accept_self_asserted").asBoolean()); - - JsonNode wantedAttributeConstraintsNode = wantedAttributeNode.get("constraints"); - assertThat(wantedAttributeConstraintsNode.size(), equalTo(1)); - - JsonNode wantedAttributeConstraintNode = wantedAttributeConstraintsNode.get(0); - assertThat(wantedAttributeConstraintNode.get("type").asText(), equalTo("SOURCE")); - JsonNode preferredSourcesNode = wantedAttributeConstraintNode.get("preferred_sources"); - assertTrue(preferredSourcesNode.get("soft_preference").asBoolean()); - - JsonNode preferredSourceAnchorsNode = preferredSourcesNode.get("anchors"); - assertThat(preferredSourceAnchorsNode.size(), equalTo(1)); - - JsonNode preferredSourceAnchorNode = preferredSourceAnchorsNode.get(0); - assertThat(preferredSourceAnchorNode.get("name").asText(), equalTo(wantedAnchorValue)); - assertThat(preferredSourceAnchorNode.get("sub_type").asText(), equalTo(wantedAnchorSubType)); - - JsonNode wantedAuthTypes = policyNode.get("wanted_auth_types"); - assertThat(wantedAuthTypes.size(), equalTo(1)); - assertThat(wantedAuthTypes.get(0).asInt(), equalTo(1)); - - assertTrue(policyNode.get("wanted_remember_me").asBoolean()); - assertTrue(policyNode.get("wanted_remember_me_optional").asBoolean()); - - JsonNode wantedAttributeIdentityProfile = policyNode.get("identity_profile_requirements"); - assertThat(wantedAttributeIdentityProfile.get(trustFramework).asText(), equalTo(trustFrameworkValue)); - assertThat(wantedAttributeIdentityProfile.get(scheme).asText(), equalTo(schemeValue)); - - JsonNode extensionsNode = json.get("extensions"); - assertThat(extensionsNode.size(), equalTo(1)); - - JsonNode extensionNode = extensionsNode.get(0); - assertThat(extensionNode.get("type").asText(), equalTo("THIRD_PARTY_ATTRIBUTE")); - - JsonNode extensionContent = extensionNode.get("content"); - assertThat(extensionContent.get("expiry_date").asText(), notNullValue()); - - JsonNode extensionDefinitions = extensionContent.get("definitions"); - assertThat(extensionDefinitions.size(), equalTo(1)); - - JsonNode extensionDefinition = extensionDefinitions.get(0); - assertThat(extensionDefinition.get("name").asText(), equalTo(thirdPartyAttributeDefinition)); - - assertThat(json.get("redirectUri").asText(), equalTo(redirectUriValue)); - - JsonNode notificationNode = json.get("notification"); - assertThat(notificationNode.get("url").asText(), equalTo(notificationUriValue)); - assertThat(notificationNode.get("method").asText(), equalTo("POST")); - assertTrue(notificationNode.get("verifyTls").asBoolean()); - assertThat(notificationNode.get("headers").size(), equalTo(0)); + public void createShareQrCode_NullSdkId_Exception() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> identityService.createShareQrCode(null, keyPair, SESSION_ID) + ); + + assertThat(ex.getMessage(), containsString("SDK ID")); + } + + @Test + public void createShareQrCode_EmptySdkId_Exception() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> identityService.createShareQrCode("", keyPair, SESSION_ID) + ); + + assertThat(ex.getMessage(), containsString("SDK ID")); + } + + @Test + public void createShareQrCode_NullKeyPair_Exception() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> identityService.createShareQrCode(SDK_ID, null, SESSION_ID) + ); + + assertThat(ex.getMessage(), containsString("Application Key Pair")); } + @Test + public void createShareQrCode_NullSessionId_Exception() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> identityService.createShareQrCode(SDK_ID, keyPair, null) + ); + + assertThat(ex.getMessage(), containsString("Session ID")); + } + + + } diff --git a/yoti-sdk-api/src/test/java/com/yoti/json/ResourceMapperTest.java b/yoti-sdk-api/src/test/java/com/yoti/json/ResourceMapperTest.java new file mode 100644 index 00000000..9f09ade3 --- /dev/null +++ b/yoti-sdk-api/src/test/java/com/yoti/json/ResourceMapperTest.java @@ -0,0 +1,159 @@ +package com.yoti.json; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.net.URI; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import com.yoti.api.client.identity.ShareSessionNotification; +import com.yoti.api.client.identity.ShareSessionRequest; +import com.yoti.api.client.identity.constraint.SourceConstraint; +import com.yoti.api.client.identity.extension.Extension; +import com.yoti.api.client.identity.extension.ThirdPartyAttributeContent; +import com.yoti.api.client.identity.extension.ThirdPartyAttributeExtensionBuilder; +import com.yoti.api.client.identity.policy.Policy; +import com.yoti.api.client.identity.policy.WantedAnchor; +import com.yoti.api.client.identity.policy.WantedAttribute; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.*; + +public class ResourceMapperTest { + + @Test + public void mapper_CreatesValidShareSessionRequestJson() throws Exception { + String subjectId = "subject_id"; + String subjectIdValue = "00000000-1111-2222-3333-444444444444"; + + Map subject = new HashMap<>(); + subject.put(subjectId, subjectIdValue); + + String wantedAnchorValue = "aAnchorValue"; + String wantedAnchorSubType = "aAnchorSubType"; + SourceConstraint constraint = SourceConstraint.builder() + .withWantedAnchor( + WantedAnchor.builder() + .withValue(wantedAnchorValue) + .withSubType(wantedAnchorSubType) + .build() + ) + .withSoftPreference(true) + .build(); + + String wantedAttributeName = "aWantedAttributeName"; + String wantedAttributeDerivation = "aWantedAttributeDerivation"; + + String trustFramework = "trust_framework"; + String trustFrameworkValue = "aTrustFramework"; + String scheme = "scheme"; + String schemeValue = "aScheme"; + Map identityProfile = new HashMap<>(); + identityProfile.put(trustFramework, trustFrameworkValue); + identityProfile.put(scheme, schemeValue); + + Policy policy = Policy.builder() + .withWantedAttribute( + WantedAttribute.builder() + .withName(wantedAttributeName) + .withDerivation(wantedAttributeDerivation) + .withOptional(true) + .withAcceptSelfAsserted(true) + .withConstraint(constraint) + .build() + ) + .withWantedAuthType(1) + .withWantedRememberMe(true) + .withWantedRememberMeOptional(true) + .withIdentityProfile(identityProfile) + .build(); + + String thirdPartyAttributeDefinition = "aDefinition"; + Extension extension = new ThirdPartyAttributeExtensionBuilder() + .withDefinition(thirdPartyAttributeDefinition) + .withExpiryDate(new Date()) + .build(); + + String redirectUriValue = "aRedirectUri"; + String notificationUriValue = "aNotificationUri"; + ShareSessionRequest request = ShareSessionRequest.builder() + .withSubject(subject) + .withPolicy(policy) + .withExtension(extension) + .withRedirectUri(new URI(redirectUriValue)) + .withNotification(ShareSessionNotification.builder(new URI(notificationUriValue)).build()) + .build(); + + byte[] payload = ResourceMapper.writeValueAsString(request); + + JsonNode json = new ObjectMapper().readTree(new String(payload)); + + JsonNode subjectNode = json.get("subject"); + assertThat(subjectNode.get(subjectId).asText(), equalTo(subjectIdValue)); + + JsonNode policyNode = json.get("policy"); + + JsonNode policyWantedNode = policyNode.get("wanted"); + assertThat(policyWantedNode.size(), equalTo(1)); + + JsonNode wantedAttributeNode = policyWantedNode.get(0); + assertThat(wantedAttributeNode.get("name").asText(), equalTo(wantedAttributeName)); + assertThat(wantedAttributeNode.get("derivation").asText(), equalTo(wantedAttributeDerivation)); + assertTrue(wantedAttributeNode.get("optional").asBoolean()); + assertTrue(wantedAttributeNode.get("accept_self_asserted").asBoolean()); + + JsonNode wantedAttributeConstraintsNode = wantedAttributeNode.get("constraints"); + assertThat(wantedAttributeConstraintsNode.size(), equalTo(1)); + + JsonNode wantedAttributeConstraintNode = wantedAttributeConstraintsNode.get(0); + assertThat(wantedAttributeConstraintNode.get("type").asText(), equalTo("SOURCE")); + JsonNode preferredSourcesNode = wantedAttributeConstraintNode.get("preferred_sources"); + assertTrue(preferredSourcesNode.get("soft_preference").asBoolean()); + + JsonNode preferredSourceAnchorsNode = preferredSourcesNode.get("anchors"); + assertThat(preferredSourceAnchorsNode.size(), equalTo(1)); + + JsonNode preferredSourceAnchorNode = preferredSourceAnchorsNode.get(0); + assertThat(preferredSourceAnchorNode.get("name").asText(), equalTo(wantedAnchorValue)); + assertThat(preferredSourceAnchorNode.get("sub_type").asText(), equalTo(wantedAnchorSubType)); + + JsonNode wantedAuthTypes = policyNode.get("wanted_auth_types"); + assertThat(wantedAuthTypes.size(), equalTo(1)); + assertThat(wantedAuthTypes.get(0).asInt(), equalTo(1)); + + assertTrue(policyNode.get("wanted_remember_me").asBoolean()); + assertTrue(policyNode.get("wanted_remember_me_optional").asBoolean()); + + JsonNode wantedAttributeIdentityProfile = policyNode.get("identity_profile_requirements"); + assertThat(wantedAttributeIdentityProfile.get(trustFramework).asText(), equalTo(trustFrameworkValue)); + assertThat(wantedAttributeIdentityProfile.get(scheme).asText(), equalTo(schemeValue)); + + JsonNode extensionsNode = json.get("extensions"); + assertThat(extensionsNode.size(), equalTo(1)); + + JsonNode extensionNode = extensionsNode.get(0); + assertThat(extensionNode.get("type").asText(), equalTo("THIRD_PARTY_ATTRIBUTE")); + + JsonNode extensionContent = extensionNode.get("content"); + assertThat(extensionContent.get("expiry_date").asText(), notNullValue()); + + JsonNode extensionDefinitions = extensionContent.get("definitions"); + assertThat(extensionDefinitions.size(), equalTo(1)); + + JsonNode extensionDefinition = extensionDefinitions.get(0); + assertThat(extensionDefinition.get("name").asText(), equalTo(thirdPartyAttributeDefinition)); + + assertThat(json.get("redirectUri").asText(), equalTo(redirectUriValue)); + + JsonNode notificationNode = json.get("notification"); + assertThat(notificationNode.get("url").asText(), equalTo(notificationUriValue)); + assertThat(notificationNode.get("method").asText(), equalTo("POST")); + assertTrue(notificationNode.get("verifyTls").asBoolean()); + assertThat(notificationNode.get("headers").size(), equalTo(0)); + } + +} 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 cd9db508..d6ca1483 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 @@ -2,16 +2,16 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.function.Supplier; import com.yoti.api.client.DigitalIdentityClient; 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.identity.policy.Policy; import com.yoti.api.spring.ClientProperties; import com.yoti.api.spring.DigitalIdentityProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -31,8 +31,6 @@ @RequestMapping("/v2") public class DigitalIdentityController implements WebMvcConfigurer { - private static final Logger LOG = LoggerFactory.getLogger(DigitalIdentityController.class); - private final DigitalIdentityClient client; private final ClientProperties properties; @@ -56,7 +54,8 @@ public String home(Model model) { @RequestMapping("/digital-identity-share") public String identityShare(Model model) throws URISyntaxException { - model.addAttribute("message", "Example page for identity share using Yoti web-share"); + model.addAttribute("sdkId", properties.getClientSdkId()); + model.addAttribute("message", "Example page for identity share"); Policy policy = Policy.builder().build(); @@ -65,22 +64,35 @@ public String identityShare(Model model) throws URISyntaxException { .withRedirectUri(new URI("https://host/redirect/")) .build(); - ShareSession result = null; - try { - result = client.createShareSession(shareSessionRequest); - } catch (Exception ex) { - LOG.error(ex.getMessage()); + ShareSession session = execute(() -> client.createShareSession(shareSessionRequest), model); + if (session == null) { + return "error"; } - model.addAttribute("message", "Identity creation example"); + String sessionId = session.getId(); - model.addAttribute("sdkId", properties.getClientSdkId()); + ShareSessionQrCode sessionQrCode = execute(() -> client.createShareQrCode(sessionId), model); + if (sessionQrCode == null) { + return "error"; + } - model.addAttribute("session_id", result.getId()); - model.addAttribute("session_status", result.getStatus()); - model.addAttribute("session_expiry", result.getExpiry()); + model.addAttribute("session_id", sessionId); + model.addAttribute("session_status", session.getStatus()); + model.addAttribute("session_expiry", session.getExpiry()); + + model.addAttribute("session_qrcode_id", sessionQrCode.getId()); + model.addAttribute("session_qrcode_uri", sessionQrCode.getUri()); return "digital-identity-share"; } + private static T execute(Supplier supplier, Model model) { + try { + return supplier.get(); + } catch (Exception ex) { + model.addAttribute("error", ex.getMessage()); + return null; + } + } + } 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 3a614ecb..d7d9ba56 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 @@ -23,7 +23,12 @@

Digital Identity Share Example page

Status:

Expiry:

- + +
+

Session QR Code

+

Id:

+

URI:

+
From bc72c96a1aaa081f82688fb1c1e4a1d3a9752beb Mon Sep 17 00:00:00 2001 From: irotech Date: Tue, 7 Mar 2023 17:09:26 +0000 Subject: [PATCH 28/41] SDK-2249: Reduce verbosity in path factory --- .../call/factory/UnsignedPathFactory.java | 101 ++++++++++-------- 1 file changed, 54 insertions(+), 47 deletions(-) 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 c4fd7ee7..41c473ff 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 @@ -4,90 +4,97 @@ public class UnsignedPathFactory { - private static final String PROFILE_PATH_TEMPLATE = "/profile/%s?appId=%s"; - private static final String AML_PATH_TEMPLATE = "/aml-check?appId=%s"; - private static final String QR_CODE_PATH_TEMPLATE = "/qrcodes/apps/%s"; - private static final String DOCS_CREATE_SESSION_PATH_TEMPLATE = "/sessions?sdkId=%s"; - private static final String DOCS_SESSION_PATH_TEMPLATE = "/sessions/%s?sdkId=%s"; - private static final String DOCS_MEDIA_CONTENT_PATH_TEMPLATE = "/sessions/%s/media/%s/content?sdkId=%s"; - private static final String DOCS_PUT_IBV_INSTRUCTIONS_PATH_TEMPLATE = "/sessions/%s/instructions?sdkId=%s"; - private static final String DOCS_FETCH_IBV_INSTRUCTIONS_PATH_TEMPLATE = "/sessions/%s/instructions?sdkId=%s"; - private static final String DOCS_FETCH_IBV_INSTRUCTIONS_PDF_PATH_TEMPLATE = "/sessions/%s/instructions/pdf?sdkId=%s"; - private static final String DOCS_SUPPORTED_DOCUMENTS_PATH = "/supported-documents?includeNonLatin=%s"; - private static final String DOCS_FETCH_INSTRUCTION_CONTACT_PROFILE_PATH_TEMPLATE = "/sessions/%s/instructions/contact-profile?sdkId=%s"; - private static final String DOCS_FETCH_SESSION_CONFIGURATION_PATH_TEMPLATE = "/sessions/%s/configuration?sdkId=%s"; - private static final String DOCS_NEW_FACE_CAPTURE_RESOURCE_PATH_TEMPLATE = "/sessions/%s/resources/face-capture?sdkId=%s"; - private static final String DOCS_UPLOAD_FACE_CAPTURE_IMAGE_PATH_TEMPLATE = "/sessions/%s/resources/face-capture/%s/image?sdkId=%s"; - private static final String DOCS_TRIGGER_IBV_NOTIFICATION_PATH_TEMPLATE = "/sessions/%s/instructions/email?sdkId=%s"; - private static final String IDENTITY_SESSION_CREATION_TEMPLATE = "/v2/sessions"; - private static final String IDENTITY_SESSION_QR_CODE_CREATION_TEMPLATE = "/v2/sessions/%s/qr-codes"; + // AML + private static final String AML = "/aml-check?appId=%s"; + + // 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"; + + // Share V1 + private static final String PROFILE = "/profile/%s?appId=%s"; + private static final String QR_CODE = "/qrcodes/apps/%s"; + + // Docs + private static final String DOCS_CREATE_SESSION = "/sessions?sdkId=%s"; + private static final String DOCS_SESSION = "/sessions/%s?sdkId=%s"; + private static final String DOCS_MEDIA_CONTENT = "/sessions/%s/media/%s/content?sdkId=%s"; + private static final String DOCS_PUT_IBV_INSTRUCTIONS = "/sessions/%s/instructions?sdkId=%s"; + private static final String DOCS_FETCH_IBV_INSTRUCTIONS = "/sessions/%s/instructions?sdkId=%s"; + private static final String DOCS_FETCH_IBV_INSTRUCTIONS_PDF = "/sessions/%s/instructions/pdf?sdkId=%s"; + private static final String DOCS_SUPPORTED_DOCUMENTS = "/supported-documents?includeNonLatin=%s"; + private static final String DOCS_FETCH_INSTRUCTION_CONTACT_PROFILE = "/sessions/%s/instructions/contact-profile?sdkId=%s"; + private static final String DOCS_FETCH_SESSION_CONFIGURATION = "/sessions/%s/configuration?sdkId=%s"; + private static final String DOCS_NEW_FACE_CAPTURE_RESOURCE = "/sessions/%s/resources/face-capture?sdkId=%s"; + private static final String DOCS_UPLOAD_FACE_CAPTURE_IMAGE = "/sessions/%s/resources/face-capture/%s/image?sdkId=%s"; + private static final String DOCS_TRIGGER_IBV_NOTIFICATION = "/sessions/%s/instructions/email?sdkId=%s"; - public String createProfilePath(String appId, String connectToken) { - return format(PROFILE_PATH_TEMPLATE, connectToken, appId); + public String createAmlPath(String appId) { + return format(AML, appId); } - public String createAmlPath(String appId) { - return format(AML_PATH_TEMPLATE, appId); + public String createIdentitySessionPath() { + return IDENTITY_SESSION_CREATION; + } + + public String createIdentitySessionQrCodePath(String sessionId) { + return format(IDENTITY_SESSION_QR_CODE_CREATION, sessionId); + } + + public String createProfilePath(String appId, String connectToken) { + return format(PROFILE, connectToken, appId); } public String createDynamicSharingPath(String appId) { - return format(QR_CODE_PATH_TEMPLATE, appId); + return format(QR_CODE, appId); } public String createNewYotiDocsSessionPath(String appId) { - return format(DOCS_CREATE_SESSION_PATH_TEMPLATE, appId); + return format(DOCS_CREATE_SESSION, appId); } public String createYotiDocsSessionPath(String appId, String sessionId) { - return format(DOCS_SESSION_PATH_TEMPLATE, sessionId, appId); + return format(DOCS_SESSION, sessionId, appId); } public String createMediaContentPath(String appId, String sessionId, String mediaId) { - return format(DOCS_MEDIA_CONTENT_PATH_TEMPLATE, sessionId, mediaId, appId); + return format(DOCS_MEDIA_CONTENT, sessionId, mediaId, appId); } public String createPutIbvInstructionsPath(String appId, String sessionId) { - return format(DOCS_PUT_IBV_INSTRUCTIONS_PATH_TEMPLATE, sessionId, appId); + return format(DOCS_PUT_IBV_INSTRUCTIONS, sessionId, appId); } public String createFetchIbvInstructionsPath(String appId, String sessionId) { - return format(DOCS_FETCH_IBV_INSTRUCTIONS_PATH_TEMPLATE, sessionId, appId); - } - - public String createFetchInstructionsContactProfilePath(String appId, String sessionId) { - return format(DOCS_FETCH_INSTRUCTION_CONTACT_PROFILE_PATH_TEMPLATE, sessionId, appId); - } - - public String createGetSupportedDocumentsPath(boolean includeNonLatin) { - return format(DOCS_SUPPORTED_DOCUMENTS_PATH, includeNonLatin); + return format(DOCS_FETCH_IBV_INSTRUCTIONS, sessionId, appId); } public String createFetchIbvInstructionsPdfPath(String sdkId, String sessionId) { - return format(DOCS_FETCH_IBV_INSTRUCTIONS_PDF_PATH_TEMPLATE, sessionId, sdkId); + return format(DOCS_FETCH_IBV_INSTRUCTIONS_PDF, sessionId, sdkId); } - public String createNewFaceCaptureResourcePath(String sdkId, String sessionId) { - return format(DOCS_NEW_FACE_CAPTURE_RESOURCE_PATH_TEMPLATE, sessionId, sdkId); + public String createGetSupportedDocumentsPath(boolean includeNonLatin) { + return format(DOCS_SUPPORTED_DOCUMENTS, includeNonLatin); } - public String createUploadFaceCaptureImagePath(String sdkId, String sessionId, String resourceId) { - return format(DOCS_UPLOAD_FACE_CAPTURE_IMAGE_PATH_TEMPLATE, sessionId, resourceId, sdkId); + public String createFetchInstructionsContactProfilePath(String appId, String sessionId) { + return format(DOCS_FETCH_INSTRUCTION_CONTACT_PROFILE, sessionId, appId); } public String createFetchSessionConfigurationPath(String sdkId, String sessionId) { - return format(DOCS_FETCH_SESSION_CONFIGURATION_PATH_TEMPLATE, sessionId, sdkId); + return format(DOCS_FETCH_SESSION_CONFIGURATION, sessionId, sdkId); } - public String createTriggerIbvEmailNotificationPath(String sdkId, String sessionId) { - return format(DOCS_TRIGGER_IBV_NOTIFICATION_PATH_TEMPLATE, sessionId, sdkId); + public String createNewFaceCaptureResourcePath(String sdkId, String sessionId) { + return format(DOCS_NEW_FACE_CAPTURE_RESOURCE, sessionId, sdkId); } - public String createIdentitySessionPath() { - return IDENTITY_SESSION_CREATION_TEMPLATE; + public String createUploadFaceCaptureImagePath(String sdkId, String sessionId, String resourceId) { + return format(DOCS_UPLOAD_FACE_CAPTURE_IMAGE, sessionId, resourceId, sdkId); } - public String createIdentitySessionQrCodePath(String sessionId) { - return format(IDENTITY_SESSION_QR_CODE_CREATION_TEMPLATE, sessionId); + public String createTriggerIbvEmailNotificationPath(String sdkId, String sessionId) { + return format(DOCS_TRIGGER_IBV_NOTIFICATION, sessionId, sdkId); } } From 3cccf126c23c5b250e999de347492873c793d004 Mon Sep 17 00:00:00 2001 From: irotech Date: Wed, 1 Mar 2023 09:09:11 +0000 Subject: [PATCH 29/41] 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:

+
From 2d68c99213a08260beb78665d0c7dd20daffe710 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 9 Mar 2023 10:25:10 +0000 Subject: [PATCH 30/41] SDK-2248: Add Digital Identity Session retrieval service --- .../api/client/DigitalIdentityClient.java | 17 +++-- .../api/client/identity/ShareSession.java | 66 +++++++++++++++++-- .../call/factory/UnsignedPathFactory.java | 9 ++- .../call/identity/DigitalIdentityService.java | 37 ++++++++--- .../api/client/DigitalIdentityClientTest.java | 9 ++- .../springboot/DigitalIdentityController.java | 13 ++++ .../templates/digital-identity-share.html | 18 +++-- 7 files changed, 141 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 7a62b27f..3729aaff 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 @@ -39,22 +39,29 @@ public class DigitalIdentityClient { * Create a sharing session to initiate a sharing process based on a policy * * @param request Details of the request like policy, extensions and push notification for the application - * @return an {@link ShareSession} ID, status and expiry of the newly created Share Session + * @return ID, status and expiry of the newly created share session * @throws DigitalIdentityException Thrown if the session creation is unsuccessful */ public ShareSession createShareSession(ShareSessionRequest request) throws DigitalIdentityException { return identityService.createShareSession(sdkId, keyPair, request); } - public Object fetchShareSession(String sessionId) { - return identityService.fetchShareSession(sessionId); + /** + * Retrieve the sharing session + * + * @param sessionId ID of the session to retrieve + * @return ID, status and expiry of the share session + * @throws DigitalIdentityException Thrown if the session retrieval is unsuccessful + */ + public ShareSession fetchShareSession(String sessionId) throws DigitalIdentityException { + return identityService.fetchShareSession(sdkId, keyPair, sessionId); } /** * Create a sharing session QR code to initiate a sharing process based on a policy * * @param sessionId Session ID the QR code will belong to - * @return ID and URI of the newly created Share Session QR code + * @return ID and URI of the newly created share session QR code * @throws DigitalIdentityException Thrown if the QR code creation is unsuccessful */ public ShareSessionQrCode createShareQrCode(String sessionId) throws DigitalIdentityException { @@ -65,7 +72,7 @@ public ShareSessionQrCode createShareQrCode(String sessionId) throws DigitalIden * Retrieve the sharing session QR code * * @param qrCodeId ID of the QR code to retrieve - * @return The content of the Share Session QR code + * @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 { diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSession.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSession.java index 55bcde75..8273fa40 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSession.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSession.java @@ -1,17 +1,18 @@ package com.yoti.api.client.identity; +import java.util.Map; + import com.fasterxml.jackson.annotation.JsonProperty; public class ShareSession { - @JsonProperty(Property.ID) private String id; - - @JsonProperty(Property.STATUS) private String status; - - @JsonProperty(Property.EXPIRY) + private String created; + private String updated; private String expiry; + private String qrCodeId; + private String receiptId; public String getId() { return id; @@ -21,15 +22,70 @@ public String getStatus() { return status; } + public String getCreated() { + return created; + } + + public String getUpdated() { + return updated; + } + public String getExpiry() { return expiry; } + public String getQrCodeId() { + return qrCodeId; + } + + public String getReceiptId() { + return receiptId; + } + + @JsonProperty(Property.ID) + public void setId(String id) { + this.id = id; + } + + @JsonProperty(Property.STATUS) + public void setStatus(String status) { + this.status = status; + } + + @JsonProperty(Property.CREATED) + public void setCreated(String created) { + this.created = created; + } + + @JsonProperty(Property.UPDATED) + public void setUpdated(String updated) { + this.updated = updated; + } + + @JsonProperty(Property.EXPIRY) + public void setExpiry(String expiry) { + this.expiry = expiry; + } + + @JsonProperty(Property.QR_CODE) + public void setQrCodeId(Map qrCode) { + this.qrCodeId = (String) qrCode.get(Property.ID); + } + + @JsonProperty(Property.RECEIPT) + public void setReceiptId(Map receipt) { + this.receiptId = (String) receipt.get(Property.ID); + } + private static final class Property { private static final String ID = "id"; + private static final String CREATED = "created"; + private static final String UPDATED = "updated"; private static final String STATUS = "status"; private static final String EXPIRY = "expiry"; + private static final String QR_CODE = "qrCode"; + private static final String RECEIPT = "receipt"; } 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 fe31d055..07bffa47 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 @@ -9,8 +9,9 @@ public class UnsignedPathFactory { // Share V2 private static final String IDENTITY_SESSION_CREATION = "/v2/sessions"; + private static final String IDENTITY_SESSION_RETRIEVAL = "/v2/sessions/%s"; 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"; + private static final String IDENTITY_SESSION_QR_CODE_RETRIEVAL = "/v2/qr-codes/%s"; // Share V1 private static final String PROFILE = "/profile/%s?appId=%s"; @@ -38,12 +39,16 @@ public String createIdentitySessionPath() { return IDENTITY_SESSION_CREATION; } + public String createIdentitySessionRetrievalPath(String sessionId) { + return format(IDENTITY_SESSION_RETRIEVAL, sessionId); + } + 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); + return format(IDENTITY_SESSION_QR_CODE_RETRIEVAL, qrCodeId); } public String createProfilePath(String appId, String connectToken) { 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 c6620cb9..99661718 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 @@ -61,7 +61,7 @@ public ShareSession createShareSession(String sdkId, KeyPair keyPair, ShareSessi String path = pathFactory.createIdentitySessionPath(); - LOG.debug("Requesting Share Session Creation for SDK ID '{}' at '{}'", sdkId, path); + LOG.debug("Requesting share session creation for SDK ID '{}' at '{}'", sdkId, path); try { byte[] payload = ResourceMapper.writeValueAsString(shareSessionRequest); @@ -79,8 +79,26 @@ public ShareSession createShareSession(String sdkId, KeyPair keyPair, ShareSessi } } - public Object fetchShareSession(String sessionId) { - return null; + public ShareSession fetchShareSession(String sdkId, KeyPair keyPair, String sessionId) + throws DigitalIdentityException { + notNullOrEmpty(sdkId, "SDK ID"); + notNull(keyPair, "Application Key Pair"); + notNull(sessionId, "Session ID"); + + String path = pathFactory.createIdentitySessionRetrievalPath(sessionId); + + LOG.debug("Requesting share session with ID '{}' at '{}'", sessionId, path); + + try { + SignedRequest request = createSignedRequest(sdkId, keyPair, path); + + return request.execute(ShareSession.class); + } catch (Exception ex) { + throw new DigitalIdentityException( + String.format("Error while fetching the share session with ID '{%s}' ", sessionId), + ex + ); + } } public ShareSessionQrCode createShareQrCode(String sdkId, KeyPair keyPair, String sessionId) @@ -91,7 +109,7 @@ public ShareSessionQrCode createShareQrCode(String sdkId, KeyPair keyPair, Strin String path = pathFactory.createIdentitySessionQrCodePath(sessionId); - LOG.debug("Requesting Share Session QR code Creation for session ID '{}' at '{}'", sessionId, path); + LOG.debug("Requesting share session '{}' QR code creation at '{}'", sessionId, path); try { SignedRequest request = createSignedRequest(sdkId, keyPair, path, HTTP_POST, EMPTY_JSON); @@ -112,16 +130,17 @@ public ShareSessionQrCode fetchShareQrCode(String sdkId, KeyPair keyPair, String String path = pathFactory.createIdentitySessionQrCodeRetrievalPath(qrCodeId); - LOG.info("Requesting Share Session QR code with ID '{} at '{}'", qrCodeId, path); + 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); + } catch (Exception ex) { + throw new DigitalIdentityException( + String.format("Error while fetching the share session QR code with ID '{%s}' ", qrCodeId), + ex + ); } } 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 3970d738..7cb69fda 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 @@ -137,15 +137,18 @@ public void client_CreateShareSessionException_DigitalIdentityException() throws } @Test - public void client_FetchShareSessionException_DigitalIdentityException() { + public void client_FetchShareSessionException_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 Session Error"; - when(identityService.fetchShareSession(A_SESSION_ID)).thenThrow(new DigitalIdentityException(exMessage)); + when(identityService.fetchShareSession(AN_SDK_ID, keyPair, A_SESSION_ID)) + .thenThrow(new DigitalIdentityException(exMessage)); DigitalIdentityException ex = assertThrows( DigitalIdentityException.class, 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 3ecc6afb..f8a2c087 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 @@ -97,6 +97,19 @@ public String identityShare(Model model) throws URISyntaxException { model.addAttribute("qrcode_session_status", fetchQrCode.getSession().getStatus()); model.addAttribute("qrcode_session_expiry", fetchQrCode.getSession().getExpiry()); + ShareSession fetchSession = execute(() -> client.fetchShareSession(sessionId), model); + if (fetchSession == null) { + return "error"; + } + + model.addAttribute("fetch_session_id", fetchSession.getId()); + model.addAttribute("fetch_session_created", fetchSession.getCreated()); + model.addAttribute("fetch_session_updated", fetchSession.getUpdated()); + model.addAttribute("fetch_session_expiry", fetchSession.getExpiry()); + model.addAttribute("fetch_session_status", fetchSession.getStatus()); + model.addAttribute("fetch_session_qrcode_id", fetchSession.getQrCodeId()); + model.addAttribute("fetch_session_receipt_id", fetchSession.getReceiptId()); + 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 bf327469..9c8baccf 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 @@ -19,14 +19,15 @@

Digital Identity Share Example page

SdkId:

-

Id:

+

Created Session

+

ID:

Status:

Expiry:

-

Session QR Code

-

Id:

+

Created Session QR Code

+

ID:

URI:

@@ -35,10 +36,19 @@

Digital Identity Share Example page

Expiry:

Extensions:

Redirect URI:

-

Session ID:

Session Status:

Session Expiry:

+ +
+

Fetched Session

+

Created:

+

Updated:

+

Expiry:

+

Status:

+

QR Code ID:

+

Receipt ID:

+
From fc514b2608309b3682384acd25fe6b8b59727151 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 9 Mar 2023 13:51:34 +0000 Subject: [PATCH 31/41] SDK-2262: Add Digital Identity Session Receipt retrieval service --- .../api/client/DigitalIdentityClient.java | 45 +--- .../identity/ShareSessionNotification.java | 6 +- .../client/identity/ShareSessionRequest.java | 18 +- .../client/identity/extension/Extension.java | 4 +- .../LocationConstraintExtensionBuilder.java | 6 +- .../extension/ThirdPartyAttributeContent.java | 18 +- .../ThirdPartyAttributeExtensionBuilder.java | 22 +- .../TransactionalFlowExtensionBuilder.java | 6 +- .../client/identity/policy/WantedAnchor.java | 26 +- .../identity/policy/WantedAttribute.java | 9 +- .../spi/remote/AttributeListReader.java | 11 +- .../client/spi/remote/ExtraDataReader.java | 5 +- .../spi/remote/call/SignedRequestBuilder.java | 2 +- .../call/factory/UnsignedPathFactory.java | 20 +- .../call/identity/DigitalIdentityService.java | 119 +++++++--- .../spi/remote/call/identity/Receipt.java | 173 ++++++++++++++ .../remote/call/identity/ReceiptItemKey.java | 79 +++++++ .../remote/call/identity/ReceiptParser.java | 114 +++++++++ .../remote/call/identity/WrappedReceipt.java | 222 ++++++++++++++++++ .../main/java/com/yoti/crypto/Algorithm.java | 17 ++ .../main/java/com/yoti/crypto/CipherType.java | 28 +++ .../src/main/java/com/yoti/crypto/Crypto.java | 68 ++++++ .../java/com/yoti/crypto/CryptoException.java | 17 ++ .../java/com/yoti/json/ResourceMapper.java | 17 +- .../api/client/DigitalIdentityClientTest.java | 9 +- .../com/yoti/json/ResourceMapperTest.java | 6 +- 26 files changed, 919 insertions(+), 148 deletions(-) create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/Receipt.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptItemKey.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptParser.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/WrappedReceipt.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/crypto/Algorithm.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/crypto/CipherType.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/crypto/Crypto.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/crypto/CryptoException.java 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 3729aaff..2aa6f5c5 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 @@ -1,8 +1,5 @@ package com.yoti.api.client; -import static com.yoti.validation.Validation.notNull; -import static com.yoti.validation.Validation.notNullOrEmpty; - import java.io.IOException; import java.security.KeyPair; import java.security.Security; @@ -13,6 +10,8 @@ import com.yoti.api.client.spi.remote.KeyStreamVisitor; import com.yoti.api.client.spi.remote.call.identity.DigitalIdentityException; import com.yoti.api.client.spi.remote.call.identity.DigitalIdentityService; +import com.yoti.api.client.spi.remote.call.identity.Receipt; +import com.yoti.validation.Validation; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -27,60 +26,32 @@ public class DigitalIdentityClient { private final DigitalIdentityService identityService; DigitalIdentityClient(String sdkId, KeyPairSource keyPair, DigitalIdentityService identityService) { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); this.sdkId = sdkId; this.keyPair = loadKeyPair(keyPair); this.identityService = identityService; } - /** - * Create a sharing session to initiate a sharing process based on a policy - * - * @param request Details of the request like policy, extensions and push notification for the application - * @return ID, status and expiry of the newly created share session - * @throws DigitalIdentityException Thrown if the session creation is unsuccessful - */ public ShareSession createShareSession(ShareSessionRequest request) throws DigitalIdentityException { return identityService.createShareSession(sdkId, keyPair, request); } - /** - * Retrieve the sharing session - * - * @param sessionId ID of the session to retrieve - * @return ID, status and expiry of the share session - * @throws DigitalIdentityException Thrown if the session retrieval is unsuccessful - */ public ShareSession fetchShareSession(String sessionId) throws DigitalIdentityException { return identityService.fetchShareSession(sdkId, keyPair, sessionId); } - /** - * Create a sharing session QR code to initiate a sharing process based on a policy - * - * @param sessionId Session ID the QR code will belong to - * @return ID and URI of the newly created share session QR code - * @throws DigitalIdentityException Thrown if the QR code creation is unsuccessful - */ public ShareSessionQrCode createShareQrCode(String sessionId) throws DigitalIdentityException { return identityService.createShareQrCode(sdkId, keyPair, sessionId); } - /** - * 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) { - return identityService.fetchShareReceipt(receiptId); + public Receipt fetchShareReceipt(String receiptId) throws DigitalIdentityException { + return identityService.fetchShareReceipt(sdkId, keyPair, receiptId); } private KeyPair loadKeyPair(KeyPairSource keyPairSource) throws InitialisationException { @@ -103,14 +74,14 @@ public static class Builder { private Builder() { } public Builder withClientSdkId(String sdkId) { - notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNullOrEmpty(sdkId, "SDK ID"); this.sdkId = sdkId; return this; } public Builder withKeyPairSource(KeyPairSource keyPairSource) { - notNull(keyPairSource, "Key Pair Source"); + Validation.notNull(keyPairSource, "Key Pair Source"); this.keyPairSource = keyPairSource; return this; diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java index 99aaada1..3c57702c 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java @@ -1,11 +1,11 @@ package com.yoti.api.client.identity; -import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; - import java.net.URI; import java.util.HashMap; import java.util.Map; +import com.yoti.validation.Validation; + import com.fasterxml.jackson.annotation.JsonProperty; public final class ShareSessionNotification { @@ -85,7 +85,7 @@ public Builder withHeader(String key, String value) { } public ShareSessionNotification build() { - notNullOrEmpty(url, Property.URL); + Validation.notNullOrEmpty(url, Property.URL); return new ShareSessionNotification(this); } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java index ca50dc5a..7f8db0eb 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java @@ -1,8 +1,5 @@ package com.yoti.api.client.identity; -import static com.yoti.api.client.spi.remote.util.Validation.notNull; -import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; - import java.net.URI; import java.util.ArrayList; import java.util.Collections; @@ -11,6 +8,7 @@ import com.yoti.api.client.identity.extension.Extension; import com.yoti.api.client.identity.policy.Policy; +import com.yoti.validation.Validation; import com.fasterxml.jackson.annotation.JsonProperty; @@ -23,7 +21,7 @@ public class ShareSessionRequest { private final Policy policy; @JsonProperty(Property.EXTENSIONS) - private final List extensions; + private final List> extensions; @JsonProperty(Property.REDIRECT_URI) private final String redirectUri; @@ -47,7 +45,7 @@ public Policy getPolicy() { return policy; } - public List getExtensions() { + public List> getExtensions() { return extensions; } @@ -67,7 +65,7 @@ public static final class Builder { private Map subject; private Policy policy; - private List extensions; + private List> extensions; private String redirectUri; private ShareSessionNotification notification; @@ -85,12 +83,12 @@ public Builder withPolicy(Policy policy) { return this; } - public Builder withExtensions(List extensions) { + public Builder withExtensions(List> extensions) { this.extensions = Collections.unmodifiableList(extensions); return this; } - public Builder withExtension(Extension extension) { + public Builder withExtension(Extension extension) { extensions.add(extension); return this; } @@ -106,8 +104,8 @@ public Builder withNotification(ShareSessionNotification notification) { } public ShareSessionRequest build() { - notNull(policy, Property.POLICY); - notNullOrEmpty(redirectUri, Property.REDIRECT_URI); + Validation.notNull(policy, Property.POLICY); + Validation.notNullOrEmpty(redirectUri, Property.REDIRECT_URI); return new ShareSessionRequest(this); } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java index b7ee1b2d..83e8e8f5 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java @@ -1,5 +1,6 @@ package com.yoti.api.client.identity.extension; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; public class Extension { @@ -10,7 +11,8 @@ public class Extension { @JsonProperty(Property.CONTENT) private final T content; - Extension(String type, T content) { + @JsonCreator + Extension(@JsonProperty(Property.TYPE) String type, @JsonProperty(Property.CONTENT) T content) { this.type = type; this.content = content; } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java index 4ec60494..81248840 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java @@ -1,11 +1,9 @@ package com.yoti.api.client.identity.extension; -import com.yoti.api.client.spi.remote.util.Validation; +import com.yoti.validation.Validation; public class LocationConstraintExtensionBuilder implements ExtensionBuilder { - public static final String TYPE = "LOCATION_CONSTRAINT"; - private double latitude; private double longitude; private double radius = 150d; @@ -42,7 +40,7 @@ public LocationConstraintExtensionBuilder withMaxUncertainty(double maxUncertain @Override public Extension build() { LocationConstraintContent content = new LocationConstraintContent(latitude, longitude, radius, maxUncertainty); - return new Extension<>(TYPE, content); + return new Extension<>("LOCATION_CONSTRAINT", content); } private static final class Property { diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java index 6948afb8..05eb6c7a 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java @@ -1,33 +1,27 @@ package com.yoti.api.client.identity.extension; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.time.OffsetDateTime; import java.util.List; -import java.util.TimeZone; import com.yoti.api.client.AttributeDefinition; -import com.yoti.api.client.spi.remote.call.YotiConstants; import com.fasterxml.jackson.annotation.JsonProperty; public class ThirdPartyAttributeContent { - private final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(YotiConstants.RFC3339_PATTERN_MILLIS); - - private final Date expiryDate; + private final OffsetDateTime expiry; @JsonProperty(Property.DEFINITIONS) private final List definitions; - ThirdPartyAttributeContent(Date expiryDate, List definitions) { - this.expiryDate = expiryDate; + ThirdPartyAttributeContent(OffsetDateTime expiry, List definitions) { + this.expiry = expiry; this.definitions = definitions; } @JsonProperty(Property.EXPIRY_DATE) - public String getExpiryDate() { - DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); - return DATE_FORMAT.format(expiryDate.getTime()); + public OffsetDateTime getExpiryDate() { + return expiry; } public List getDefinitions() { diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java index 614a13ab..fadba855 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java @@ -1,34 +1,30 @@ package com.yoti.api.client.identity.extension; -import static com.yoti.api.client.spi.remote.util.Validation.notNull; -import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; - +import java.time.OffsetDateTime; import java.util.ArrayList; -import java.util.Date; import java.util.List; import com.yoti.api.client.AttributeDefinition; +import com.yoti.validation.Validation; public class ThirdPartyAttributeExtensionBuilder implements ExtensionBuilder { - public static final String TYPE = "THIRD_PARTY_ATTRIBUTE"; - - private Date expiryDate; + private OffsetDateTime expiryDate; private List definitions; public ThirdPartyAttributeExtensionBuilder() { this.definitions = new ArrayList<>(); } - public ThirdPartyAttributeExtensionBuilder withExpiryDate(Date expiryDate) { - notNull(expiryDate, Property.EXPIRY_DATE); + public ThirdPartyAttributeExtensionBuilder withExpiryDate(OffsetDateTime expiryDate) { + Validation.notNull(expiryDate, Property.EXPIRY_DATE); - this.expiryDate = new Date(expiryDate.getTime()); + this.expiryDate = expiryDate; return this; } public ThirdPartyAttributeExtensionBuilder withDefinition(String definition) { - notNullOrEmpty(definition, Property.DEFINITION); + Validation.notNullOrEmpty(definition, Property.DEFINITION); this.definitions.add(new AttributeDefinition(definition)); return this; @@ -45,12 +41,12 @@ public ThirdPartyAttributeExtensionBuilder withDefinitions(List definiti public Extension build() { ThirdPartyAttributeContent thirdPartyAttributeContent = new ThirdPartyAttributeContent(expiryDate, definitions); - return new Extension<>(TYPE, thirdPartyAttributeContent); + return new Extension<>("THIRD_PARTY_ATTRIBUTE", thirdPartyAttributeContent); } private static final class Property { - private static final String EXPIRY_DATE = "expiryDate"; + private static final String EXPIRY_DATE = "expiry_date"; private static final String DEFINITION = "definition"; } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java index 8723c30c..913630ca 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java @@ -1,11 +1,9 @@ package com.yoti.api.client.identity.extension; -import com.yoti.api.client.spi.remote.util.Validation; +import com.yoti.validation.Validation; public class TransactionalFlowExtensionBuilder implements ExtensionBuilder { - public static final String TYPE = "TRANSACTIONAL_FLOW"; - private Object content; public TransactionalFlowExtensionBuilder withContent(Object content) { @@ -17,7 +15,7 @@ public TransactionalFlowExtensionBuilder withContent(Object content) { @Override public Extension build() { - return new Extension<>(TYPE, content); + return new Extension<>("TRANSACTIONAL_FLOW", content); } private static final class Property { diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java index d8cc1f5f..86bf6845 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java @@ -1,7 +1,8 @@ package com.yoti.api.client.identity.policy; -import static com.yoti.api.client.spi.remote.util.Validation.notNull; -import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; +import java.util.Objects; + +import com.yoti.validation.Validation; import com.fasterxml.jackson.annotation.JsonProperty; @@ -26,6 +27,23 @@ public String getSubType() { return subType; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + WantedAnchor that = (WantedAnchor) o; + return Objects.equals(value, that.value) && Objects.equals(subType, that.subType); + } + + @Override + public int hashCode() { + return Objects.hash(value, subType); + } + public static Builder builder() { return new Builder(); } @@ -46,8 +64,8 @@ public Builder withSubType(String subType) { } public WantedAnchor build() { - notNullOrEmpty(value, Property.NAME); - notNull(subType, Property.SUB_TYPE); + Validation.notNullOrEmpty(value, Property.NAME); + Validation.notNull(subType, Property.SUB_TYPE); return new WantedAnchor(this); } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java index a2cd32f5..010b0dc0 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java @@ -1,12 +1,11 @@ package com.yoti.api.client.identity.policy; -import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; - import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.yoti.api.client.identity.constraint.Constraint; +import com.yoti.validation.Validation; import com.fasterxml.jackson.annotation.JsonProperty; @@ -55,6 +54,10 @@ public List getConstraints() { return constraints; } + public boolean hasConstraints() { + return !constraints.isEmpty(); + } + public static Builder builder() { return new Builder(); } @@ -102,7 +105,7 @@ public Builder withConstraint(Constraint constraint) { } public WantedAttribute build() { - notNullOrEmpty(name, Property.NAME); + Validation.notNullOrEmpty(name, Property.NAME); return new WantedAttribute(this); } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/AttributeListReader.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/AttributeListReader.java index 778048d8..5960f0ce 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/AttributeListReader.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/AttributeListReader.java @@ -7,25 +7,26 @@ import com.yoti.api.client.Attribute; import com.yoti.api.client.ProfileException; -class AttributeListReader { +public class AttributeListReader { private final EncryptedDataReader encryptedDataReader; private final AttributeListConverter attributeListConverter; - private AttributeListReader(EncryptedDataReader encryptedDataReader, - AttributeListConverter attributeListConverter) { + private AttributeListReader( + EncryptedDataReader encryptedDataReader, + AttributeListConverter attributeListConverter) { this.encryptedDataReader = encryptedDataReader; this.attributeListConverter = attributeListConverter; } - static AttributeListReader newInstance() { + public static AttributeListReader newInstance() { return new AttributeListReader( EncryptedDataReader.newInstance(), AttributeListConverter.newInstance() ); } - List> read(byte[] encryptedProfileBytes, Key secretKey) throws ProfileException { + public List> read(byte[] encryptedProfileBytes, Key secretKey) throws ProfileException { List> attributeList = new ArrayList<>(); if (encryptedProfileBytes != null && encryptedProfileBytes.length > 0) { byte[] profileData = encryptedDataReader.decryptBytes(encryptedProfileBytes, secretKey); diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/ExtraDataReader.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/ExtraDataReader.java index 9bc1a069..0c8e7137 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/ExtraDataReader.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/ExtraDataReader.java @@ -18,14 +18,14 @@ private ExtraDataReader( this.extraDataConverter = extraDataConverter; } - static ExtraDataReader newInstance() { + public static ExtraDataReader newInstance() { return new ExtraDataReader( EncryptedDataReader.newInstance(), ExtraDataConverter.newInstance() ); } - ExtraData read(byte[] encryptedBytes, Key secretKey) throws ProfileException, ExtraDataException { + public ExtraData read(byte[] encryptedBytes, Key secretKey) throws ProfileException, ExtraDataException { ExtraData extraData; if (encryptedBytes != null && encryptedBytes.length > 0) { byte[] extraDataBytes = encryptedDataReader.decryptBytes(encryptedBytes, secretKey); @@ -36,4 +36,5 @@ ExtraData read(byte[] encryptedBytes, Key secretKey) throws ProfileException, Ex return extraData; } + } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilder.java index f073fa21..64da8584 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilder.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilder.java @@ -19,7 +19,7 @@ import com.yoti.api.client.spi.remote.call.factory.HeadersFactory; import com.yoti.api.client.spi.remote.call.factory.PathFactory; import com.yoti.api.client.spi.remote.call.factory.SignedMessageFactory; -import com.yoti.api.client.spi.remote.util.Validation; +import com.yoti.validation.Validation; import org.apache.http.HttpEntity; import org.apache.http.entity.ContentType; 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 07bffa47..a8a57f58 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 @@ -12,6 +12,8 @@ public class UnsignedPathFactory { private static final String IDENTITY_SESSION_RETRIEVAL = "/v2/sessions/%s"; private static final String IDENTITY_SESSION_QR_CODE_CREATION = "/v2/sessions/%s/qr-codes"; private static final String IDENTITY_SESSION_QR_CODE_RETRIEVAL = "/v2/qr-codes/%s"; + private static final String IDENTITY_SESSION_RECEIPT_RETRIEVAL = "/v2/receipts/%s"; + private static final String IDENTITY_SESSION_RECEIPT_KEY_RETRIEVAL = "/v2/wrapped-item-keys/%s"; // Share V1 private static final String PROFILE = "/profile/%s?appId=%s"; @@ -40,15 +42,27 @@ public String createIdentitySessionPath() { } public String createIdentitySessionRetrievalPath(String sessionId) { - return format(IDENTITY_SESSION_RETRIEVAL, sessionId); + return format(IDENTITY_SESSION_RETRIEVAL, base64ToBase64url(sessionId)); } public String createIdentitySessionQrCodePath(String sessionId) { - return format(IDENTITY_SESSION_QR_CODE_CREATION, sessionId); + return format(IDENTITY_SESSION_QR_CODE_CREATION, base64ToBase64url(sessionId)); } public String createIdentitySessionQrCodeRetrievalPath(String qrCodeId) { - return format(IDENTITY_SESSION_QR_CODE_RETRIEVAL, qrCodeId); + return format(IDENTITY_SESSION_QR_CODE_RETRIEVAL, base64ToBase64url(qrCodeId)); + } + + public String createIdentitySessionReceiptRetrievalPath(String receiptId) { + return format(IDENTITY_SESSION_RECEIPT_RETRIEVAL, base64ToBase64url(receiptId)); + } + + public String createIdentitySessionReceiptKeyRetrievalPath(String wrappedItemKeyId) { + return format(IDENTITY_SESSION_RECEIPT_KEY_RETRIEVAL, base64ToBase64url(wrappedItemKeyId)); + } + + private static String base64ToBase64url(String value) { + return value.replace('+', '-').replace('/', '_'); } public String createProfilePath(String appId, String connectToken) { 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 99661718..f2d9d642 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 @@ -7,8 +7,6 @@ import static com.yoti.api.client.spi.remote.call.YotiConstants.CONTENT_TYPE_JSON; import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_IDENTITY_URL; import static com.yoti.api.client.spi.remote.call.YotiConstants.PROPERTY_YOTI_API_URL; -import static com.yoti.validation.Validation.notNull; -import static com.yoti.validation.Validation.notNullOrEmpty; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -27,6 +25,7 @@ import com.yoti.api.client.spi.remote.call.SignedRequestBuilderFactory; import com.yoti.api.client.spi.remote.call.factory.UnsignedPathFactory; import com.yoti.json.ResourceMapper; +import com.yoti.validation.Validation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,25 +38,34 @@ public class DigitalIdentityService { private final UnsignedPathFactory pathFactory; private final SignedRequestBuilderFactory requestBuilderFactory; + private final ReceiptParser receiptParser; private final String apiUrl; - public DigitalIdentityService(UnsignedPathFactory pathFactory, SignedRequestBuilderFactory requestBuilderFactory) { + public DigitalIdentityService( + UnsignedPathFactory pathFactory, + SignedRequestBuilderFactory requestBuilderFactory, + ReceiptParser receiptParser) { this.pathFactory = pathFactory; this.requestBuilderFactory = requestBuilderFactory; + this.receiptParser = receiptParser; this.apiUrl = System.getProperty(PROPERTY_YOTI_API_URL, DEFAULT_IDENTITY_URL); } public static DigitalIdentityService newInstance() { - return new DigitalIdentityService(new UnsignedPathFactory(), new SignedRequestBuilderFactory()); + return new DigitalIdentityService( + new UnsignedPathFactory(), + new SignedRequestBuilderFactory(), + ReceiptParser.newInstance() + ); } public ShareSession createShareSession(String sdkId, KeyPair keyPair, ShareSessionRequest shareSessionRequest) throws DigitalIdentityException { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); - notNull(shareSessionRequest, "Share Session request"); + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); + Validation.notNull(shareSessionRequest, "Share Session request"); String path = pathFactory.createIdentitySessionPath(); @@ -65,9 +73,7 @@ public ShareSession createShareSession(String sdkId, KeyPair keyPair, ShareSessi try { byte[] payload = ResourceMapper.writeValueAsString(shareSessionRequest); - SignedRequest request = createSignedRequest(sdkId, keyPair, path, HTTP_POST, payload); - - return request.execute(ShareSession.class); + return createSignedRequest(sdkId, keyPair, path, HTTP_POST, payload).execute(ShareSession.class); } catch (IOException ex) { throw new DigitalIdentityException("Error while parsing the share session creation request ", ex); } catch (URISyntaxException ex) { @@ -81,21 +87,19 @@ public ShareSession createShareSession(String sdkId, KeyPair keyPair, ShareSessi public ShareSession fetchShareSession(String sdkId, KeyPair keyPair, String sessionId) throws DigitalIdentityException { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); - notNull(sessionId, "Session ID"); + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); + Validation.notNull(sessionId, "Session ID"); String path = pathFactory.createIdentitySessionRetrievalPath(sessionId); - LOG.debug("Requesting share session with ID '{}' at '{}'", sessionId, path); + LOG.debug("Requesting share session '{}' at '{}'", sessionId, path); try { - SignedRequest request = createSignedRequest(sdkId, keyPair, path); - - return request.execute(ShareSession.class); + return createSignedRequest(sdkId, keyPair, path).execute(ShareSession.class); } catch (Exception ex) { throw new DigitalIdentityException( - String.format("Error while fetching the share session with ID '{%s}' ", sessionId), + String.format("Error while fetching the share session '{%s}' ", sessionId), ex ); } @@ -103,18 +107,16 @@ public ShareSession fetchShareSession(String sdkId, KeyPair keyPair, String sess public ShareSessionQrCode createShareQrCode(String sdkId, KeyPair keyPair, String sessionId) throws DigitalIdentityException { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); - notNullOrEmpty(sessionId, "Session ID"); + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); + Validation.notNullOrEmpty(sessionId, "Session ID"); String path = pathFactory.createIdentitySessionQrCodePath(sessionId); LOG.debug("Requesting share session '{}' QR code creation at '{}'", sessionId, path); try { - SignedRequest request = createSignedRequest(sdkId, keyPair, path, HTTP_POST, EMPTY_JSON); - - return request.execute(ShareSessionQrCode.class); + return createSignedRequest(sdkId, keyPair, path, HTTP_POST, EMPTY_JSON).execute(ShareSessionQrCode.class); } catch (GeneralSecurityException ex) { throw new DigitalIdentityException("Error while signing the share QR code creation request ", ex); } catch (IOException | URISyntaxException | ResourceException ex) { @@ -124,28 +126,67 @@ public ShareSessionQrCode createShareQrCode(String sdkId, KeyPair keyPair, Strin 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"); + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); + Validation.notNullOrEmpty(qrCodeId, "QR Code ID"); String path = pathFactory.createIdentitySessionQrCodeRetrievalPath(qrCodeId); - LOG.info("Requesting share session QR code with ID '{} at '{}'", qrCodeId, path); + LOG.debug("Requesting share session QR code '{} at '{}'", qrCodeId, path); try { - SignedRequest request = createSignedRequest(sdkId, keyPair, path); + return createSignedRequest(sdkId, keyPair, path).execute(ShareSessionQrCode.class); + } catch (Exception ex) { + throw new DigitalIdentityException( + String.format("Error while fetching the share session QR code '{%s}' ", qrCodeId), + ex + ); + } + } + + public Receipt fetchShareReceipt(String sdkId, KeyPair keyPair, String receiptId) throws DigitalIdentityException { + WrappedReceipt wrappedReceipt = doFetchShareReceipt(sdkId, keyPair, receiptId); + + return Optional.ofNullable(wrappedReceipt.getError()) + .map(ignored -> receiptParser.create(wrappedReceipt)) + .orElseGet(() -> { + ReceiptItemKey receiptKey = fetchShareReceiptKey(sdkId, keyPair, wrappedReceipt); + + return receiptParser.create(wrappedReceipt, receiptKey, keyPair.getPrivate()); + }); + } + + private WrappedReceipt doFetchShareReceipt(String sdkId, KeyPair keyPair, String receiptId) { + String path = pathFactory.createIdentitySessionReceiptRetrievalPath(receiptId); - return request.execute(ShareSessionQrCode.class); + LOG.debug("Requesting share session receipt '{}' at '{}'", receiptId, path); + + try { + return createSignedRequest(sdkId, keyPair, path).execute(WrappedReceipt.class); } catch (Exception ex) { throw new DigitalIdentityException( - String.format("Error while fetching the share session QR code with ID '{%s}' ", qrCodeId), + String.format("Error while fetching the share session QR code '{%s}' ", receiptId), ex ); } } - public Object fetchShareReceipt(String receiptId) { - return null; + private ReceiptItemKey fetchShareReceiptKey(String sdkId, KeyPair keyPair, WrappedReceipt wrappedReceipt) + throws DigitalIdentityException { + String wrappedItemKeyId = wrappedReceipt.getWrappedItemKeyId(); + + String path = pathFactory.createIdentitySessionReceiptKeyRetrievalPath(wrappedItemKeyId); + + LOG.debug("Requesting share session receipt item key '{}' at '{}'", wrappedItemKeyId, path); + + try { + return createSignedRequest(sdkId, keyPair, path).execute(ReceiptItemKey.class); + } catch (Exception ex) { + throw new DigitalIdentityException( + String.format("Error while fetching the share session receipt key '{%s}' ", wrappedItemKeyId), + ex + ); + } } SignedRequest createSignedRequest(String sdkId, KeyPair keyPair, String path) @@ -155,18 +196,18 @@ SignedRequest createSignedRequest(String sdkId, KeyPair keyPair, String path) SignedRequest createSignedRequest(String sdkId, KeyPair keyPair, String path, String method, byte[] payload) throws GeneralSecurityException, UnsupportedEncodingException, URISyntaxException { - SignedRequestBuilder requestBuilder = requestBuilderFactory.create() + SignedRequestBuilder request = requestBuilderFactory.create() .withKeyPair(keyPair) .withBaseUrl(apiUrl) .withEndpoint(path) .withHeader(AUTH_ID_HEADER, sdkId) .withHttpMethod(method); - Optional.ofNullable(payload).map(v -> - requestBuilder.withPayload(v).withHeader(CONTENT_TYPE, CONTENT_TYPE_JSON) - ); - - return requestBuilder.build(); + return Optional.ofNullable(payload) + .map(request::withPayload) + .map(r -> r.withHeader(CONTENT_TYPE, CONTENT_TYPE_JSON)) + .orElse(request) + .build(); } } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/Receipt.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/Receipt.java new file mode 100644 index 00000000..13709fc6 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/Receipt.java @@ -0,0 +1,173 @@ +package com.yoti.api.client.spi.remote.call.identity; + +import java.time.OffsetDateTime; +import java.util.Optional; + +import com.yoti.api.client.ApplicationProfile; +import com.yoti.api.client.ExtraData; +import com.yoti.api.client.HumanProfile; + +public final class Receipt { + + private final String id; + private final String sessionId; + private final String rememberMeId; + private final String parentRememberMeId; + private final OffsetDateTime timestamp; + private final ApplicationContent applicationContent; + private final UserContent userContent; + private final String error; + + private Receipt(Builder builder) { + id = builder.id; + sessionId = builder.sessionId; + rememberMeId = builder.rememberMeId; + parentRememberMeId = builder.parentRememberMeId; + timestamp = builder.timestamp; + applicationContent = builder.applicationContent; + userContent = builder.userContent; + error = builder.error; + } + + public String getId() { + return id; + } + + public String getSessionId() { + return sessionId; + } + + public String getRememberMeId() { + return rememberMeId; + } + + public String getParentRememberMeId() { + return parentRememberMeId; + } + + public OffsetDateTime getTimestamp() { + return timestamp; + } + + public Optional getProfile() { + return userContent.getProfile(); + } + + public Optional getExtraData() { + return userContent.getExtraData(); + } + + public ApplicationContent getApplicationContent() { + return applicationContent; + } + + public UserContent getUserContent() { + return userContent; + } + + public Optional getError() { + return Optional.ofNullable(error); + } + + public static Builder forReceipt(String id) { + return new Builder(id); + } + + public static final class Builder { + + private final String id; + + private String sessionId; + private String rememberMeId; + private String parentRememberMeId; + private OffsetDateTime timestamp; + private ApplicationContent applicationContent; + private UserContent userContent; + private String error; + + private Builder(String id) { + this.id = id; + } + + public Builder sessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + public Builder rememberMeId(String rememberMeId) { + this.rememberMeId = rememberMeId; + return this; + } + + public Builder parentRememberMeId(String parentRememberMeId) { + this.parentRememberMeId = parentRememberMeId; + return this; + } + + public Builder timestamp(OffsetDateTime timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder applicationContent(ApplicationProfile profile, ExtraData extraData) { + this.applicationContent = new ApplicationContent(profile, extraData); + return this; + } + + public Builder userContent(HumanProfile profile, ExtraData extraData) { + this.userContent = new UserContent(profile, extraData); + return this; + } + + public Builder error(String error) { + this.error = error; + return this; + } + + public Receipt build() { + return new Receipt(this); + } + + } + + public static class ApplicationContent { + + private final ApplicationProfile profile; + private final ExtraData extraData; + + private ApplicationContent(ApplicationProfile profile, ExtraData extraData) { + this.profile = profile; + this.extraData = extraData; + } + + public ApplicationProfile getProfile() { + return profile; + } + + public Optional getExtraData() { + return Optional.ofNullable(extraData); + } + + } + + public static class UserContent { + + private final HumanProfile profile; + private final ExtraData extraData; + + private UserContent(HumanProfile profile, ExtraData extraData) { + this.profile = profile; + this.extraData = extraData; + } + + public Optional getProfile() { + return Optional.ofNullable(profile); + } + + public Optional getExtraData() { + return Optional.ofNullable(extraData); + } + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptItemKey.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptItemKey.java new file mode 100644 index 00000000..4f624193 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptItemKey.java @@ -0,0 +1,79 @@ +package com.yoti.api.client.spi.remote.call.identity; + +import java.util.Base64; + +import javax.crypto.spec.IvParameterSpec; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(builder = ReceiptItemKey.Builder.class) +public final class ReceiptItemKey { + + private final String id; + private final IvParameterSpec iv; + private final byte[] value; + + private ReceiptItemKey(Builder builder) { + id = builder.id; + iv = builder.iv; + value = builder.value; + } + + public String getId() { + return id; + } + + public IvParameterSpec getIv() { + return iv; + } + + public byte[] getValue() { + return value.clone(); + } + + public static final class Builder { + + private String id; + private IvParameterSpec iv; + private byte[] value; + + private Builder() { } + + @JsonProperty(Property.ID) + public Builder id(String id) { + this.id = id; + return this; + } + + @JsonProperty(Property.IV) + public Builder iv(String iv) { + this.iv = new IvParameterSpec(decode(iv)); + return this; + } + + @JsonProperty(Property.VALUE) + public Builder value(String value) { + this.value = decode(value); + return this; + } + + public ReceiptItemKey build() { + return new ReceiptItemKey(this); + } + + private static byte[] decode(String value) { + return Base64.getDecoder().decode(value); + } + + } + + private static class Property { + + private static final String ID = "id"; + private static final String IV = "iv"; + private static final String VALUE = "value"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptParser.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptParser.java new file mode 100644 index 00000000..9b3a1e99 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptParser.java @@ -0,0 +1,114 @@ +package com.yoti.api.client.spi.remote.call.identity; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.PrivateKey; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import javax.crypto.spec.SecretKeySpec; + +import com.yoti.api.client.ApplicationProfile; +import com.yoti.api.client.Attribute; +import com.yoti.api.client.ExtraData; +import com.yoti.api.client.ExtraDataException; +import com.yoti.api.client.HumanProfile; +import com.yoti.api.client.ProfileException; +import com.yoti.api.client.spi.remote.AttributeListReader; +import com.yoti.api.client.spi.remote.ExtraDataReader; +import com.yoti.crypto.CipherType; +import com.yoti.crypto.Crypto; +import com.yoti.validation.Validation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReceiptParser { + + private static final Logger LOG = LoggerFactory.getLogger(ReceiptParser.class); + + private final AttributeListReader attributeListReader; + private final ExtraDataReader extraDataReader; + + private ReceiptParser(AttributeListReader attributeListReader, ExtraDataReader extraDataReader) { + this.attributeListReader = Validation.notNull(attributeListReader, "profileReader"); + this.extraDataReader = Validation.notNull(extraDataReader, "extraDataReader"); + } + + public static ReceiptParser newInstance() { + return new ReceiptParser( + AttributeListReader.newInstance(), + ExtraDataReader.newInstance() + ); + } + + public Receipt create(WrappedReceipt wrappedReceipt, ReceiptItemKey wrappedItemKey, PrivateKey privateKey) + throws DigitalIdentityException { + Key receiptKey = decryptReceiptKey(wrappedReceipt.getWrappedKey(), wrappedItemKey, privateKey); + + Receipt.Builder receipt = Receipt.forReceipt(wrappedReceipt.getId()) + .sessionId(wrappedReceipt.getSessionId()) + .timestamp(wrappedReceipt.getTimestamp()) + .applicationContent( + new ApplicationProfile(parseProfileAttr(wrappedReceipt.getProfile(), receiptKey)), + wrappedReceipt.getExtraData().map(data -> parseExtraData(data, receiptKey)).orElse(null) + ); + + receipt.userContent( + wrappedReceipt.getOtherPartyProfile() + .map(profile -> parseProfileAttr(profile, receiptKey)) + .map(HumanProfile::new) + .orElse(null), + wrappedReceipt.getOtherPartyExtraData() + .map(data -> parseExtraData(data, receiptKey)) + .orElse(null) + ); + + wrappedReceipt.getRememberMeId().ifPresent(id -> receipt.rememberMeId(parseRememberMeId(id))); + wrappedReceipt.getParentRememberMeId().ifPresent(id -> receipt.parentRememberMeId(parseRememberMeId(id))); + + return receipt.build(); + } + + public Receipt create(WrappedReceipt failureReceipt) { + return Receipt.forReceipt(failureReceipt.getId()) + .sessionId(failureReceipt.getSessionId()) + .timestamp(failureReceipt.getTimestamp()) + .error(failureReceipt.getError()) + .build(); + } + + private static Key decryptReceiptKey(byte[] wrappedKey, ReceiptItemKey wrappedItemKey, PrivateKey privateKey) { + byte[] itemKey = Crypto.decrypt(wrappedItemKey.getValue(), privateKey, CipherType.RSA_NONE_PKCS1); + byte[] receiptKey = Crypto.decrypt(wrappedKey, itemKey, wrappedItemKey.getIv(), CipherType.AES_GCM); + return new SecretKeySpec(Objects.requireNonNull(receiptKey), CipherType.AES_CBC.value()); + } + + private List> parseProfileAttr(byte[] profile, Key key) { + try { + return attributeListReader.read(profile, key); + } catch (ProfileException ex) { + throw new DigitalIdentityException(ex); + } + } + + private ExtraData parseExtraData(byte[] extraData, Key key) { + try { + return extraDataReader.read(extraData, key); + } catch (ExtraDataException ex) { + LOG.error("Failed to parse extra data from receipt"); + return new ExtraData(); + } catch (ProfileException ex) { + throw new DigitalIdentityException(ex); + } + } + + private static String parseRememberMeId(byte[] id) { + return Optional.ofNullable(id) + .map(v -> new String(Base64.getEncoder().encode(v), StandardCharsets.UTF_8)) + .orElse(null); + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/WrappedReceipt.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/WrappedReceipt.java new file mode 100644 index 00000000..32d8d94e --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/WrappedReceipt.java @@ -0,0 +1,222 @@ +package com.yoti.api.client.spi.remote.call.identity; + +import java.time.OffsetDateTime; +import java.util.Base64; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(builder = WrappedReceipt.Builder.class) +public final class WrappedReceipt { + + private final String id; + private final String sessionId; + private final OffsetDateTime timestamp; + private final Content content; + private final Content otherPartyContent; + private final String wrappedItemKeyId; + private final byte[] rememberMeId; + private final byte[] parentRememberMeId; + private final byte[] wrappedKey; + private final String error; + + private WrappedReceipt(Builder builder) { + id = builder.id; + sessionId = builder.sessionId; + timestamp = builder.timestamp; + content = builder.content; + otherPartyContent = builder.otherPartyContent; + wrappedItemKeyId = builder.wrappedItemKeyId; + rememberMeId = builder.rememberMeId; + parentRememberMeId = builder.parentRememberMeId; + wrappedKey = builder.wrappedKey; + error = builder.error; + } + + public String getId() { + return id; + } + + public String getSessionId() { + return sessionId; + } + + public OffsetDateTime getTimestamp() { + return timestamp; + } + + public byte[] getProfile() { + return content.profile() + .orElseThrow(() -> new DigitalIdentityException("Application profile should not be missing")); + } + + public Optional getExtraData() { + return content.extraData(); + } + + public Optional getOtherPartyProfile() { + return Optional.ofNullable(otherPartyContent).flatMap(Content::profile); + } + + public Optional getOtherPartyExtraData() { + return Optional.ofNullable(otherPartyContent).flatMap(Content::extraData); + } + + public String getWrappedItemKeyId() { + return wrappedItemKeyId; + } + + public Optional getRememberMeId() { + return Optional.ofNullable(rememberMeId).map(byte[]::clone); + } + + public Optional getParentRememberMeId() { + return Optional.ofNullable(parentRememberMeId).map(byte[]::clone); + } + + public byte[] getWrappedKey() { + return wrappedKey.clone(); + } + + public String getError() { + return error; + } + + public static final class Builder { + + private String id; + private String sessionId; + private OffsetDateTime timestamp; + private Content content; + private Content otherPartyContent; + private String wrappedItemKeyId; + private byte[] rememberMeId; + private byte[] parentRememberMeId; + private byte[] wrappedKey; + private String error; + + private Builder() { } + + @JsonProperty(Property.ID) + public Builder id(String id) { + this.id = id; + return this; + } + + @JsonProperty(Property.SESSION_ID) + public Builder sessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + @JsonProperty(Property.TIMESTAMP) + public Builder timestamp(String timestamp) { + this.timestamp = OffsetDateTime.parse(timestamp); + return this; + } + + @JsonProperty(Property.CONTENT) + public Builder content(Content content) { + this.content = content; + return this; + } + + @JsonProperty(Property.OTHER_PARTY_CONTENT) + public Builder otherPartyContent(Content otherPartyContent) { + this.otherPartyContent = otherPartyContent; + return this; + } + + @JsonProperty(Property.WRAPPED_ITEM_KEY_ID) + public Builder wrappedItemKeyId(String wrappedItemKeyId) { + this.wrappedItemKeyId = wrappedItemKeyId; + return this; + } + + @JsonProperty(Property.REMEMBER_ME_ID) + public Builder rememberMeId(String rememberMeId) { + this.rememberMeId = decode(rememberMeId); + return this; + } + + @JsonProperty(Property.PARENT_REMEMBER_ME_ID) + public Builder parentRememberMeId(String parentRememberMeId) { + this.parentRememberMeId = decode(parentRememberMeId); + return this; + } + + @JsonProperty(Property.WRAPPED_KEY) + public Builder wrappedKey(String wrappedKey) { + this.wrappedKey = decode(wrappedKey); + return this; + } + + @JsonProperty(Property.ERROR) + public Builder error(String error) { + this.error = error; + return this; + } + + public WrappedReceipt build() { + return new WrappedReceipt(this); + } + + private static byte[] decode(String value) { + return Base64.getDecoder().decode(value); + } + + } + + private static class Content { + + private byte[] profile; + private byte[] extraData; + + public Optional profile() { + return Optional.ofNullable(profile).map(byte[]::clone); + } + + public Optional extraData() { + return Optional.ofNullable(extraData).map(byte[]::clone); + } + + @JsonProperty(Property.Content.PROFILE) + public void setProfile(String profile) { + this.profile = decode(profile); + } + + @JsonProperty(Property.Content.EXTRA_DATA) + public void setExtraData(String extraData) { + this.extraData = decode(extraData); + } + + private static byte[] decode(String value) { + return Base64.getDecoder().decode(value); + } + + } + + private static class Property { + + private static final String ID = "id"; + private static final String SESSION_ID = "sessionId"; + private static final String TIMESTAMP = "timestamp"; + private static final String REMEMBER_ME_ID = "rememberMeId"; + private static final String PARENT_REMEMBER_ME_ID = "parentRememberMeId"; + private static final String CONTENT = "content"; + private static final String OTHER_PARTY_CONTENT = "otherPartyContent"; + private static final String WRAPPED_ITEM_KEY_ID = "wrappedItemKeyId"; + private static final String WRAPPED_KEY = "wrappedKey"; + private static final String ERROR = "error"; + + private static class Content { + + private static final String PROFILE = "profile"; + private static final String EXTRA_DATA = "extraData"; + + } + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/crypto/Algorithm.java b/yoti-sdk-api/src/main/java/com/yoti/crypto/Algorithm.java new file mode 100644 index 00000000..28827d2e --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/crypto/Algorithm.java @@ -0,0 +1,17 @@ +package com.yoti.crypto; + +public enum Algorithm { + + AES("AES"); + + private final String value; + + Algorithm(String value) { + this.value = value; + } + + public String value() { + return value; + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/crypto/CipherType.java b/yoti-sdk-api/src/main/java/com/yoti/crypto/CipherType.java new file mode 100644 index 00000000..21ce85a0 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/crypto/CipherType.java @@ -0,0 +1,28 @@ +package com.yoti.crypto; + +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; + +public enum CipherType { + + AES_GCM("AES/GCM/NoPadding"), + AES_CBC("AES/CBC/PKCS7Padding"), + RSA_NONE_PKCS1("RSA/NONE/PKCS1Padding"); + + private final String value; + + CipherType(String value) { + this.value = value; + } + + public String value() { + return value; + } + + Cipher getInstance() throws NoSuchPaddingException, NoSuchAlgorithmException { + return Cipher.getInstance(value); + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/crypto/Crypto.java b/yoti-sdk-api/src/main/java/com/yoti/crypto/Crypto.java new file mode 100644 index 00000000..acc1399a --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/crypto/Crypto.java @@ -0,0 +1,68 @@ +package com.yoti.crypto; + +import java.security.Key; +import java.security.PrivateKey; +import java.util.function.Supplier; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public final class Crypto { + + private static final String DECRYPT_ERROR_TEMPLATE = "Failed to decrypt message using '%s' transformation"; + + private Crypto() { } + + public static byte[] decrypt(byte[] msg, PrivateKey key, CipherType transformation) { + return wrapOnException( + () -> { + try { + return doCipher(msg, key, transformation); + } catch (Exception ex) { + throw new CryptoException(ex); + } + }, + String.format(DECRYPT_ERROR_TEMPLATE, transformation.value()) + ); + } + + private static byte[] doCipher(byte[] msg, Key key, CipherType transformation) throws Exception { + Cipher cipher = transformation.getInstance(); + cipher.init(Cipher.DECRYPT_MODE, key); + return cipher.doFinal(msg); + } + + public static byte[] decrypt(byte[] msg, byte[] key, IvParameterSpec iv, CipherType transformation) { + return decrypt(msg, new SecretKeySpec(key, Algorithm.AES.value()), iv, transformation); + } + + public static byte[] decrypt(byte[] msg, SecretKeySpec key, IvParameterSpec iv, CipherType transformation) { + return wrapOnException( + () -> { + try { + return doCipher(msg, key, iv, transformation); + } catch (Exception ex) { + throw new CryptoException(ex); + } + }, + String.format(DECRYPT_ERROR_TEMPLATE, transformation.value()) + ); + } + + private static byte[] doCipher(byte[] msg, SecretKeySpec key, IvParameterSpec iv, CipherType transformation) + throws Exception { + Cipher cipher = transformation.getInstance(); + cipher.init(Cipher.DECRYPT_MODE, key, iv); + return cipher.doFinal(msg); + } + + private static T wrapOnException(Supplier op, String msg) { + try { + return op.get(); + } catch (Exception ex) { + throw new CryptoException(msg, ex); + } + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/crypto/CryptoException.java b/yoti-sdk-api/src/main/java/com/yoti/crypto/CryptoException.java new file mode 100644 index 00000000..1e99a37a --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/crypto/CryptoException.java @@ -0,0 +1,17 @@ +package com.yoti.crypto; + +public class CryptoException extends RuntimeException { + + public CryptoException(String message) { + super(message); + } + + public CryptoException(Throwable cause) { + super(cause); + } + + public CryptoException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java b/yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java index d3f8088b..97548582 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java +++ b/yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java @@ -1,6 +1,9 @@ package com.yoti.json; import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.TimeZone; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonSetter; @@ -10,6 +13,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.introspect.VisibilityChecker; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public final class ResourceMapper { @@ -20,9 +24,20 @@ public final class ResourceMapper { .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) .setVisibility(configureVisibility(MAPPER.getDeserializationConfig())) - .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SKIP)); + .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SKIP)) + .setDateFormat(utc()) + .registerModule(new JavaTimeModule()); } + private static SimpleDateFormat utc() { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + + return format; + } + + private ResourceMapper() { } + private static VisibilityChecker configureVisibility(MapperConfig config) { return config.getDefaultVisibilityChecker() .withFieldVisibility(JsonAutoDetect.Visibility.NONE) 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 7cb69fda..eaeb6abe 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 @@ -203,15 +203,18 @@ public void client_FetchShareQrCodeException_DigitalIdentityException() throws I } @Test - public void client_FetchShareReceiptException_DigitalIdentityException() { + public void client_FetchShareReceiptException_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 Receipt Error"; - when(identityService.fetchShareReceipt(A_RECEIPT_ID)).thenThrow(new DigitalIdentityException(exMessage)); + when(identityService.fetchShareReceipt(AN_SDK_ID, keyPair, A_RECEIPT_ID)) + .thenThrow(new DigitalIdentityException(exMessage)); DigitalIdentityException ex = assertThrows( DigitalIdentityException.class, diff --git a/yoti-sdk-api/src/test/java/com/yoti/json/ResourceMapperTest.java b/yoti-sdk-api/src/test/java/com/yoti/json/ResourceMapperTest.java index 9f09ade3..23e19e6e 100644 --- a/yoti-sdk-api/src/test/java/com/yoti/json/ResourceMapperTest.java +++ b/yoti-sdk-api/src/test/java/com/yoti/json/ResourceMapperTest.java @@ -2,10 +2,10 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.junit.Assert.assertTrue; import java.net.URI; -import java.util.Date; +import java.time.OffsetDateTime; import java.util.HashMap; import java.util.Map; @@ -75,7 +75,7 @@ public void mapper_CreatesValidShareSessionRequestJson() throws Exception { String thirdPartyAttributeDefinition = "aDefinition"; Extension extension = new ThirdPartyAttributeExtensionBuilder() .withDefinition(thirdPartyAttributeDefinition) - .withExpiryDate(new Date()) + .withExpiryDate(OffsetDateTime.now()) .build(); String redirectUriValue = "aRedirectUri"; From 8d59320df5eba592696cbd6462de022c4f42f72d Mon Sep 17 00:00:00 2001 From: irotech Date: Wed, 3 May 2023 12:13:27 +0100 Subject: [PATCH 32/41] SDK-2262: Update spotbugs config --- pom.xml | 4 ++++ yoti-sdk-api/spotbugs/exclude-filter.xml | 6 ++++++ .../spotbugs/exclude-filter.xml | 4 ++++ yoti-sdk-spring-boot-example/pom.xml | 6 ++++++ 4 files changed, 20 insertions(+) diff --git a/pom.xml b/pom.xml index 4d97749d..e7ed2d68 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,10 @@ + + com.github.spotbugs + spotbugs-maven-plugin + diff --git a/yoti-sdk-api/spotbugs/exclude-filter.xml b/yoti-sdk-api/spotbugs/exclude-filter.xml index 8b106198..c1a507e9 100644 --- a/yoti-sdk-api/spotbugs/exclude-filter.xml +++ b/yoti-sdk-api/spotbugs/exclude-filter.xml @@ -11,6 +11,12 @@ + + + + + + diff --git a/yoti-sdk-spring-boot-auto-config/spotbugs/exclude-filter.xml b/yoti-sdk-spring-boot-auto-config/spotbugs/exclude-filter.xml index b5a52881..551c7d03 100644 --- a/yoti-sdk-spring-boot-auto-config/spotbugs/exclude-filter.xml +++ b/yoti-sdk-spring-boot-auto-config/spotbugs/exclude-filter.xml @@ -2,6 +2,10 @@ + + + + diff --git a/yoti-sdk-spring-boot-example/pom.xml b/yoti-sdk-spring-boot-example/pom.xml index 0b6e7a9d..f2d3d9d6 100644 --- a/yoti-sdk-spring-boot-example/pom.xml +++ b/yoti-sdk-spring-boot-example/pom.xml @@ -106,6 +106,7 @@ org.springframework.boot spring-boot-maven-plugin + org.apache.maven.plugins maven-deploy-plugin @@ -113,6 +114,11 @@ true + + + com.github.spotbugs + spotbugs-maven-plugin + From cc02d84b46a69a28798dbaa6966e836f128093dd Mon Sep 17 00:00:00 2001 From: irotech Date: Wed, 26 Jul 2023 14:12:09 +0100 Subject: [PATCH 33/41] SDK-2230: Add example for share v2 --- pom.xml | 2 + yoti-sdk-spring-boot-example/README.md | 58 ++--- yoti-sdk-spring-boot-example/pom.xml | 3 + .../springboot/DigitalIdentityController.java | 125 ---------- .../examples/springboot/DisplayAttribute.java | 46 ---- .../springboot/IdentityLoginController.java | 111 +++++++++ .../springboot/IdentitySessionController.java | 124 ++++++++++ .../springboot/YotiLoginController.java | 72 +----- .../attribute/AttributeDisplayProperty.java | 59 +++++ .../springboot/attribute/AttributeMapper.java | 51 ++++ .../attribute/DisplayAttribute.java | 49 ++++ .../src/main/resources/application.properties | 1 + .../static/digital-identity-share.css | 79 +++++++ .../src/main/resources/static/error.css | 28 +++ .../main/resources/templates/dbs-check.html | 145 ++++++------ .../templates/digital-identity-share.html | 121 ++++++---- .../resources/templates/dynamic-share.html | 145 ++++++------ .../src/main/resources/templates/error.html | 35 ++- .../src/main/resources/templates/index.html | 145 ++++++------ .../src/main/resources/templates/profile.html | 218 +++++++++--------- 20 files changed, 969 insertions(+), 648 deletions(-) delete mode 100644 yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DigitalIdentityController.java delete mode 100644 yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DisplayAttribute.java create mode 100644 yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/IdentityLoginController.java create mode 100644 yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/IdentitySessionController.java create mode 100644 yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/attribute/AttributeDisplayProperty.java create mode 100644 yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/attribute/AttributeMapper.java create mode 100644 yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/attribute/DisplayAttribute.java create mode 100644 yoti-sdk-spring-boot-example/src/main/resources/application.properties create mode 100644 yoti-sdk-spring-boot-example/src/main/resources/static/digital-identity-share.css create mode 100644 yoti-sdk-spring-boot-example/src/main/resources/static/error.css diff --git a/pom.xml b/pom.xml index e7ed2d68..65a31096 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ 3.1.1 1.1.0 + 4.7.3.4 @@ -59,6 +60,7 @@ com.github.spotbugs spotbugs-maven-plugin + ${spotbugs-maven-plugin.version} diff --git a/yoti-sdk-spring-boot-example/README.md b/yoti-sdk-spring-boot-example/README.md index 4556d125..494171be 100644 --- a/yoti-sdk-spring-boot-example/README.md +++ b/yoti-sdk-spring-boot-example/README.md @@ -1,16 +1,17 @@ -# Spring Boot Yoti SDK Example +# Spring Boot Yoti SDK Examples +This project shows example implementations of a server-app with an endpoint which will be called by Yoti -This project shows an example implementation of a server-app with an endpoint which will be called by Yoti with a `token`. -You will need to pass this token to Yoti-SDK in order to retrieve the profile of a user which has been logged in by Yoti. +# Prerequisites +Before you start, you'll need to create an Application in [Yoti Hub](https://hub.yoti.com) and set the domain to `https://localhost:8443/` -Before you start, you'll need to create an Application in [Yoti Hub](https://hub.yoti.com) and verify the domain. - -**NOTE: While creating Application in Yoti Hub, some of the attributes (except phone number and selfie) require users to have a Yoti with a verified passport. If your application, for instance, requires the user's date of birth and she/he has not added their passport to their Yoti account, this will lead to a failed login.** +Note that: +- Your endpoint must be accessible from the machine that is displaying the QR code.** +- In order to receive calls on endpoint, you need to expose your server-app to the outside world. We require that you use the domain from the Callback URL and HTTPS +- While creating Application in Yoti Hub, some of the attributes (except phone number and selfie) require users to have a Yoti with a verified passport. If your application, for instance, requires the user's date of birth and she/he has not added their passport to their Yoti account, this will lead to a failed login ## Project Structure -* The logic for retrieving the profile can be found in `com.yoti.api.examples.springboot.YotiLoginController#doLogin`. * `resources/app-keypair.pem` is the keypair you can get from Yoti Hub. -* `resource/application.yml` contains the configuration that enforces SSL usage by your server-app (in case you are not using a proxy server like NGINX). Make sure that you update the SDK Application ID and the configuration points to the right path to the java keystore with an SSL key (there is an already one included in the project ``` server.keystore.jks ```). +* `resources/application.yml` contains the configuration that enforces SSL usage by your server-app (in case you are not using a proxy server like NGINX). Make sure that you update the SDK Application ID and the configuration points to the right path to the java keystore with an SSL key (there is an already one included in the project ``` server.keystore.jks ```). * This project contains a Spring-boot server application. In this example we used the current SDK version by including the specific Maven dependency with its repository: ```xml @@ -20,26 +21,31 @@ Before you start, you'll need to create an Application in [Yoti Hub](https://hub ``` -## Building your example server-app -1. In the [Yoti Hub](https://hub.yoti.com) set the application domain of your app to `https://localhost:8443/`. Note that your endpoint must be accessible from the machine that is displaying the QR code. -1. Still in the Hub, set the scenario callback URL to `/login`. -1. Copy the [resources/application.yml.example](src/main/resources/application.yml.example) and create a new file `resources/application.yml` -1. Edit the `resources/application.yml` and replace the `yoti-client-sdk-id-from-hub` value with the `Yoti client SDK ID` you can find in Yoti Hub. -1. Download your Application's key pair from Yoti Hub and copy it to `resources/app-keypair.pem`. -1. Run `mvn clean package` to build the project. - -## Running -* You can run your server-app by executing `java -jar target/yoti-sdk-spring-boot-example-3.8.0-SNAPSHOT.jar` - * If you are using Java 9, you can run the server-app as follows `java -jar target/yoti-sdk-spring-boot-example-3.8.0-SNAPSHOT.jar --add-exports java.base/jdk.internal.ref=ALL-UNNAMED` +## Building your server-app and run the example +* Copy the [application.yml.example](src/main/resources/application.yml.example) and rename it to `application.yml` +* Edit the newly renamed file and replace `yoti-client-sdk-id-from-hub` value with `Yoti client SDK ID` you can find in Yoti Hub +* Download your Application's key pair from Yoti Hub and save it to `resources/app-keypair.pem` +* Run `mvn clean package` to build the project + +### Share v1 +* In the Hub, set the scenario callback URL to `/login`. + * In order to receive calls on your /login endpoint, you need to expose your server-app to the outside world. + * You **must** use the domain from the Callback URL and HTTPS +* You can run your server-app by executing `java -jar target/yoti-sdk-spring-boot-example.jar` * Navigate to: - * `https://localhost:8443` to initiate a login using Yoti. The Spring demo is listening for the response on `https://localhost:8443/login`. - * `https://localhost:8443/dynamic-share` to initiate a dynamic share with location with result displayed in profile page. - * `https://localhost:8443/dbs-check` to initiate a BDS standard check with location with result displayed in profile page. + * [https://localhost:8443](https://localhost:8443) to initiate a login using Yoti. The Spring demo is listening for the response on `https://localhost:8443/login` + * [https://localhost:8443/dynamic-share](https://localhost:8443/dynamic-share) to initiate a dynamic share with location with result displayed in profile page. + * [https://localhost:8443/dbs-check](https://localhost:8443/dbs-check) to initiate a BDS standard check with location with result displayed in profile page. -In order to receive calls on your /login endpoint, you need to expose your server-app to the outside world. We require that you use the domain from the Callback URL and HTTPS. +The logic for all the v1 share examples can be found in the `YotiLoginController` -## Requirements for running the application -* Java 8 or above -* If you are using Oracle JDK/JRE you need to install JCE extension in your server's Java to allow strong encryption (http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html). This is not a requirement if you are using OpenJDK. +### Share v2 +* You can run your server-app by executing `java -jar -Dyoti.api.url="https://api.yoti.com/share" target/yoti-sdk-spring-boot-example.jar`. The JVM argument is required to override the default `https://api.yoti.com/api/v1` +* Navigate to: + * [https://localhost:8443/v2/digital-identity-share](https://localhost:8443/v2/digital-identity-share) to initiate a login using the Yoti share v2 +The logic for the v2 share example session creation and receipt can be found respectively in the `IdentitySessionController` and `IdentityLoginController` +## Requirements for running the application +* Java 8 or above +* If you are using Oracle JDK/JRE you need to install JCE extension in your server's Java to allow strong encryption (http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html). This is not a requirement if you are using OpenJDK diff --git a/yoti-sdk-spring-boot-example/pom.xml b/yoti-sdk-spring-boot-example/pom.xml index f2d3d9d6..941cd07b 100644 --- a/yoti-sdk-spring-boot-example/pom.xml +++ b/yoti-sdk-spring-boot-example/pom.xml @@ -44,6 +44,7 @@ 8 32.1.1-jre + 4.7.3.4 @@ -92,6 +93,7 @@ + ${project.artifactId} @@ -118,6 +120,7 @@ com.github.spotbugs spotbugs-maven-plugin + ${spotbugs-maven-plugin.version} 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 deleted file mode 100644 index f8a2c087..00000000 --- a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DigitalIdentityController.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.yoti.api.examples.springboot; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.function.Supplier; - -import com.yoti.api.client.DigitalIdentityClient; -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.identity.policy.Policy; -import com.yoti.api.spring.ClientProperties; -import com.yoti.api.spring.DigitalIdentityProperties; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -@ConditionalOnClass(DigitalIdentityClient.class) -@EnableConfigurationProperties({ ClientProperties.class, DigitalIdentityProperties.class }) -@Controller -@EnableWebMvc -@RequestMapping("/v2") -public class DigitalIdentityController implements WebMvcConfigurer { - - private final DigitalIdentityClient client; - private final ClientProperties properties; - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); - } - - @Autowired - public DigitalIdentityController(DigitalIdentityClient client, ClientProperties properties) { - this.client = client; - this.properties = properties; - } - - @RequestMapping("/") - public String home(Model model) { - model.addAttribute("clientSdkId", properties.getClientSdkId()); - model.addAttribute("scenarioId", properties.getScenarioId()); - return "index"; - } - - @RequestMapping("/digital-identity-share") - public String identityShare(Model model) throws URISyntaxException { - model.addAttribute("sdkId", properties.getClientSdkId()); - model.addAttribute("message", "Example page for identity share"); - - Policy policy = Policy.builder().build(); - - ShareSessionRequest shareSessionRequest = ShareSessionRequest.builder() - .withPolicy(policy) - .withRedirectUri(new URI("https://host/redirect/")) - .build(); - - ShareSession session = execute(() -> client.createShareSession(shareSessionRequest), model); - if (session == null) { - return "error"; - } - - 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"; - } - - String qrCodeId = 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()); - - ShareSession fetchSession = execute(() -> client.fetchShareSession(sessionId), model); - if (fetchSession == null) { - return "error"; - } - - model.addAttribute("fetch_session_id", fetchSession.getId()); - model.addAttribute("fetch_session_created", fetchSession.getCreated()); - model.addAttribute("fetch_session_updated", fetchSession.getUpdated()); - model.addAttribute("fetch_session_expiry", fetchSession.getExpiry()); - model.addAttribute("fetch_session_status", fetchSession.getStatus()); - model.addAttribute("fetch_session_qrcode_id", fetchSession.getQrCodeId()); - model.addAttribute("fetch_session_receipt_id", fetchSession.getReceiptId()); - - return "digital-identity-share"; - } - - private static T execute(Supplier supplier, Model model) { - try { - return supplier.get(); - } catch (Exception ex) { - model.addAttribute("error", ex.getMessage()); - return null; - } - } - -} diff --git a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DisplayAttribute.java b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DisplayAttribute.java deleted file mode 100644 index 85f174e5..00000000 --- a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DisplayAttribute.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.yoti.api.examples.springboot; - -import com.yoti.api.client.Attribute; - -public class DisplayAttribute { - - private final String displayName; - private final String preValue; - private final String icon; - private final Attribute attribute; - - public DisplayAttribute(String preValue, String displayName, Attribute attribute, String icon) { - this.displayName = displayName; - this.preValue = preValue; - this.icon = icon; - this.attribute = attribute; - } - - public DisplayAttribute(String displayName, Attribute attribute, String icon) { - this.displayName = displayName; - this.preValue = ""; - this.icon = icon; - this.attribute = attribute; - } - - public String getDisplayName() { - return displayName; - } - - public String getPreValue() { - return preValue; - } - - public String getIcon() { - return icon; - } - - public Attribute getAttribute() { - return attribute; - } - - public String getDisplayValue() { - return this.preValue + this.attribute.getValue(); - } - -} diff --git a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/IdentityLoginController.java b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/IdentityLoginController.java new file mode 100644 index 00000000..68438b53 --- /dev/null +++ b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/IdentityLoginController.java @@ -0,0 +1,111 @@ +package com.yoti.api.examples.springboot; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import com.yoti.api.client.ApplicationProfile; +import com.yoti.api.client.Attribute; +import com.yoti.api.client.DigitalIdentityClient; +import com.yoti.api.client.HumanProfile; +import com.yoti.api.client.spi.remote.call.identity.Receipt; +import com.yoti.api.examples.springboot.attribute.AttributeMapper; +import com.yoti.api.examples.springboot.attribute.DisplayAttribute; +import com.yoti.api.spring.ClientProperties; +import com.yoti.api.spring.DigitalIdentityProperties; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@ConditionalOnClass(DigitalIdentityClient.class) +@EnableConfigurationProperties({ ClientProperties.class, DigitalIdentityProperties.class }) +@Controller +@EnableWebMvc +@RequestMapping("/v2") +public class IdentityLoginController implements WebMvcConfigurer { + + private final DigitalIdentityClient client; + private final ClientProperties properties; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); + } + + @Autowired + public IdentityLoginController(DigitalIdentityClient client, ClientProperties properties) { + this.client = client; + this.properties = properties; + } + + @RequestMapping("/") + public String home(Model model) { + model.addAttribute("clientSdkId", properties.getClientSdkId()); + model.addAttribute("scenarioId", properties.getScenarioId()); + return "index"; + } + + @RequestMapping("/digital-identity-share") + public String identityShare(Model model) { + model.addAttribute("sdkId", properties.getClientSdkId()); + model.addAttribute("message", "Example page for identity share"); + + return "digital-identity-share"; + } + + @GetMapping(value = "/receipt") + public String receipt(@RequestParam("receiptId") String receiptId, Model model) { + Receipt receipt = execute(() -> client.fetchShareReceipt(receiptId), model); + + if (receipt == null || receipt.getError().isPresent()) { + model.addAttribute("error", receipt.getError().get()); + return "error"; + } + + Receipt.ApplicationContent applicationContent = receipt.getApplicationContent(); + + Optional.ofNullable(applicationContent.getProfile()) + .map(ApplicationProfile::getApplicationLogo) + .map(attr -> model.addAttribute("appLogo", attr.getValue().getBase64Content())); + + receipt.getProfile().map(HumanProfile::getSelfie) + .map(attr -> model.addAttribute("base64Selfie", attr.getValue().getBase64Content())); + receipt.getProfile().map(HumanProfile::getFullName) + .map(attr -> model.addAttribute("fullName", attr.getValue())); + receipt.getProfile().map(HumanProfile::getAttributes) + .map(attr -> model.addAttribute("displayAttributes", mapAttributes(attr))); + + return "profile"; + } + + private List mapAttributes(Collection> attributes) { + return attributes.stream() + .map(AttributeMapper::mapToDisplayAttribute) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static T execute(Supplier supplier, Model model) { + try { + return supplier.get(); + } catch (Exception ex) { + model.addAttribute("error", ex.getMessage()); + return null; + } + } + +} diff --git a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/IdentitySessionController.java b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/IdentitySessionController.java new file mode 100644 index 00000000..e037e484 --- /dev/null +++ b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/IdentitySessionController.java @@ -0,0 +1,124 @@ +package com.yoti.api.examples.springboot; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import com.yoti.api.client.DigitalIdentityClient; +import com.yoti.api.client.identity.ShareSession; +import com.yoti.api.client.identity.ShareSessionRequest; +import com.yoti.api.client.identity.extension.Extension; +import com.yoti.api.client.identity.extension.LocationConstraintContent; +import com.yoti.api.client.identity.extension.LocationConstraintExtensionBuilder; +import com.yoti.api.client.identity.policy.Policy; +import com.yoti.api.client.identity.policy.WantedAttribute; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v2") +public class IdentitySessionController { + + private static final URI REDIRECT_URI = URI.create("https://localhost:8443/v2/receipt"); + + private final DigitalIdentityClient client; + + @Autowired + public IdentitySessionController(DigitalIdentityClient client) { + this.client = client; + } + + @GetMapping("/digital-identity-session") + public String identityShareSession() { + ShareSession session = client.createShareSession( + // forMinimalShare() + forDynamicScenarioShare() + // forIdentityProfileShare() + // forLocationExtensionShare() + ); + + Map response = new HashMap<>(); + response.put("sessionId", session.getId()); + + return toJson(response); + } + + private static String toJson(Map map) { + try { + return new ObjectMapper().writeValueAsString(map); + } catch (JsonProcessingException e) { + return "error"; + } + } + + private static ShareSessionRequest forMinimalShare() { + return ShareSessionRequest.builder() + .withPolicy(Policy.builder().build()) + .withRedirectUri(REDIRECT_URI) + .build(); + } + + private static ShareSessionRequest forDynamicScenarioShare() { + WantedAttribute givenNamesWantedAttribute = WantedAttribute.builder() + .withName("given_names") + .build(); + + WantedAttribute emailAddressWantedAttribute = WantedAttribute.builder() + .withName("email_address") + .build(); + + Policy policy = Policy.builder() + .withWantedAttribute(givenNamesWantedAttribute) + .withWantedAttribute(emailAddressWantedAttribute) + .withFullName() + .withSelfie() + .withPhoneNumber() + .withAgeOver(18) + .build(); + + return ShareSessionRequest.builder() + .withPolicy(policy) + .withRedirectUri(REDIRECT_URI) + .build(); + } + + private static ShareSessionRequest forIdentityProfileShare() { + Map scheme = new HashMap<>(); + scheme.put("type", "DBS"); + scheme.put("objective", "BASIC"); + + Map identityProfile = new HashMap<>(); + identityProfile.put("trust_framework", "UK_TFIDA"); + identityProfile.put("scheme", scheme); + + Policy policy = Policy.builder() + .withWantedRememberMe(true) + .withIdentityProfile(identityProfile) + .build(); + + return ShareSessionRequest.builder() + .withPolicy(policy) + .withRedirectUri(REDIRECT_URI) + .build(); + } + + private static ShareSessionRequest forLocationExtensionShare() { + Extension locationExtension = new LocationConstraintExtensionBuilder() + .withLatitude(51.5074) + .withLongitude(-0.1278) + .withRadius(6000) + .build(); + + return ShareSessionRequest.builder() + .withPolicy(Policy.builder().withWantedRememberMe(true).build()) + .withExtension(locationExtension) + .withRedirectUri(REDIRECT_URI) + .build(); + } + +} diff --git a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/YotiLoginController.java b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/YotiLoginController.java index 0e27105a..124bef78 100644 --- a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/YotiLoginController.java +++ b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/YotiLoginController.java @@ -1,13 +1,11 @@ package com.yoti.api.examples.springboot; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; -import com.yoti.api.attributes.AttributeConstants; import com.yoti.api.client.ActivityDetails; import com.yoti.api.client.Attribute; import com.yoti.api.client.HumanProfile; @@ -22,10 +20,11 @@ import com.yoti.api.client.shareurl.extension.LocationConstraintExtensionBuilder; import com.yoti.api.client.shareurl.policy.DynamicPolicy; import com.yoti.api.client.shareurl.policy.WantedAttribute; +import com.yoti.api.examples.springboot.attribute.AttributeMapper; +import com.yoti.api.examples.springboot.attribute.DisplayAttribute; import com.yoti.api.spring.ClientProperties; import com.yoti.api.spring.YotiProperties; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -39,7 +38,6 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.thymeleaf.util.StringUtils; @Configuration @ConditionalOnClass(YotiClient.class) @@ -149,8 +147,8 @@ public String doLogin(@RequestParam("token") final String token, final Model mod } List displayAttributes = humanProfile.getAttributes().stream() - .map(this::mapToDisplayAttribute) - .filter(displayAttribute -> displayAttribute != null) + .map(AttributeMapper::mapToDisplayAttribute) + .filter(Objects::nonNull) .collect(Collectors.toList()); model.addAttribute("displayAttributes", displayAttributes); @@ -195,64 +193,4 @@ public String dbsCheck(final Model model) { return "dbs-check"; } - private DisplayAttribute mapToDisplayAttribute(Attribute attribute) { - switch (attribute.getName()) { - case AttributeConstants.HumanProfileAttributes.FULL_NAME: - return new DisplayAttribute("Full name", attribute, "yoti-icon-profile"); - case AttributeConstants.HumanProfileAttributes.GIVEN_NAMES: - return new DisplayAttribute("Given names", attribute, "yoti-icon-profile"); - case AttributeConstants.HumanProfileAttributes.FAMILY_NAME: - return new DisplayAttribute("Family name", attribute, "yoti-icon-profile"); - case AttributeConstants.HumanProfileAttributes.NATIONALITY: - return new DisplayAttribute("Nationality", attribute, "yoti-icon-nationality"); - case AttributeConstants.HumanProfileAttributes.POSTAL_ADDRESS: - return new DisplayAttribute("Address", attribute, "yoti-icon-address"); - case AttributeConstants.HumanProfileAttributes.STRUCTURED_POSTAL_ADDRESS: - return new DisplayAttribute("Structured Postal Address", attribute, "yoti-icon-address"); - case AttributeConstants.HumanProfileAttributes.PHONE_NUMBER: - return new DisplayAttribute("Mobile number", attribute, "yoti-icon-phone"); - case AttributeConstants.HumanProfileAttributes.EMAIL_ADDRESS: - return new DisplayAttribute("Email address", attribute, "yoti-icon-email"); - case AttributeConstants.HumanProfileAttributes.DATE_OF_BIRTH: - return new DisplayAttribute("Date of birth", attribute, "yoti-icon-calendar"); - case AttributeConstants.HumanProfileAttributes.SELFIE: - return null; // Do nothing - we already display the selfie - case AttributeConstants.HumanProfileAttributes.GENDER: - return new DisplayAttribute("Gender", attribute, "yoti-icon-gender"); - case AttributeConstants.HumanProfileAttributes.IDENTITY_PROFILE_REPORT: - return new DisplayAttribute("Identity Profile Report", toJsonAttribute(attribute), "yoti-icon-document"); - - default: - if (attribute.getName().contains(":")) { - return handleAgeVerification(attribute); - } else { - return handleProfileAttribute(attribute); - } - } - } - - private Attribute toJsonAttribute(Attribute attribute) { - ObjectMapper MAPPER = new ObjectMapper(); - - String json; - - try { - json = MAPPER.readTree(MAPPER.writeValueAsString(attribute.getValue()).getBytes(StandardCharsets.UTF_8)) - .toString(); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - - return new Attribute<>(attribute.getName(), json); - } - - private DisplayAttribute handleAgeVerification(Attribute attribute) { - return new DisplayAttribute("Age Verification/", "Age verified", attribute, "yoti-icon-verified"); - } - - private DisplayAttribute handleProfileAttribute(Attribute attribute) { - String attributeName = StringUtils.capitalize(attribute.getName()); - return new DisplayAttribute(attributeName, attribute, "yoti-icon-profile"); - } - } diff --git a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/attribute/AttributeDisplayProperty.java b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/attribute/AttributeDisplayProperty.java new file mode 100644 index 00000000..80790f5b --- /dev/null +++ b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/attribute/AttributeDisplayProperty.java @@ -0,0 +1,59 @@ +package com.yoti.api.examples.springboot.attribute; + +import java.util.EnumSet; +import java.util.Optional; + +import com.yoti.api.client.Attribute; + +public enum AttributeDisplayProperty { + + FAMILY_NAME("family_name", "Family name", "yoti-icon-profile"), + GIVEN_NAMES("given_names", "Given names", "yoti-icon-profile"), + FULL_NAME("full_name", "Full name", "yoti-icon-profile"), + DATE_OF_BIRTH("date_of_birth", "Date of birth", "yoti-icon-calendar"), + GENDER("gender", "Gender", "yoti-icon-gender"), + POSTAL_ADDRESS("postal_address", "Address", "yoti-icon-address"), + STRUCTURED_POSTAL_ADDRESS("structured_postal_address", "Structured address", "yoti-icon-address"), + NATIONALITY("nationality", "Nationality", "yoti-icon-nationality"), + PHONE_NUMBER("phone_number", "Mobile number", "yoti-icon-phone"), + EMAIL_ADDRESS("email_address", "Email address", "yoti-icon-email"), + IDENTITY_PROFILE_REPORT("identity_profile_report", "Identity Profile Report", "yoti-icon-document", true); + + private final String name; + private final String label; + private final String icon; + private final boolean isJson; + + AttributeDisplayProperty(String name, String label, String icon) { + this.name = name; + this.label = label; + this.icon = icon; + isJson = false; + } + + AttributeDisplayProperty(String name, String label, String icon, boolean isJson) { + this.name = name; + this.label = label; + this.icon = icon; + this.isJson = isJson; + } + + public static Optional fromAttribute(Attribute attribute) { + return EnumSet.allOf(AttributeDisplayProperty.class).stream() + .filter(adp -> adp.name.equals(attribute.getName())) + .findFirst(); + } + + public String label() { + return label; + } + + public String icon() { + return icon; + } + + public boolean isJson() { + return isJson; + } + +} diff --git a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/attribute/AttributeMapper.java b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/attribute/AttributeMapper.java new file mode 100644 index 00000000..e9bfc3d0 --- /dev/null +++ b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/attribute/AttributeMapper.java @@ -0,0 +1,51 @@ +package com.yoti.api.examples.springboot.attribute; + +import java.io.IOException; + +import com.yoti.api.client.Attribute; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.thymeleaf.util.StringUtils; + +public class AttributeMapper { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public static DisplayAttribute mapToDisplayAttribute(Attribute attribute) { + return AttributeDisplayProperty.fromAttribute(attribute) + .map(property -> property.isJson() + ? new DisplayAttribute(toJson(attribute), property) + : new DisplayAttribute(attribute, property) + ) + .orElseGet(() -> { + if (attribute.getName().contains(":")) { + return handleAgeVerification(attribute); + } else { + return handleProfileAttribute(attribute); + } + }); + } + + private static Attribute toJson(Attribute attribute) { + String json; + + try { + json = MAPPER.writeValueAsString(attribute.getValue()); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + + return new Attribute<>(attribute.getName(), json); + } + + private static DisplayAttribute handleAgeVerification(Attribute attribute) { + return new DisplayAttribute("Age Verification/", "Age verified", attribute, "yoti-icon-verified"); + } + + private static DisplayAttribute handleProfileAttribute(Attribute attribute) { + return attribute.getName().equalsIgnoreCase("selfie") + ? null + : new DisplayAttribute(StringUtils.capitalize(attribute.getName()), attribute, "yoti-icon-profile"); + } + +} diff --git a/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/attribute/DisplayAttribute.java b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/attribute/DisplayAttribute.java new file mode 100644 index 00000000..cfeb457d --- /dev/null +++ b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/attribute/DisplayAttribute.java @@ -0,0 +1,49 @@ +package com.yoti.api.examples.springboot.attribute; + +import com.yoti.api.client.Attribute; + +public class DisplayAttribute { + + private final String label; + private final String preValue; + private final String icon; + private final Attribute attribute; + + public DisplayAttribute(String preValue, String label, Attribute attribute, String icon) { + this.label = label; + this.preValue = preValue; + this.icon = icon; + this.attribute = attribute; + } + + public DisplayAttribute(String label, Attribute attribute, String icon) { + this.label = label; + this.preValue = ""; + this.icon = icon; + this.attribute = attribute; + } + + public DisplayAttribute(Attribute attribute, AttributeDisplayProperty property) { + this.label = property.label(); + this.preValue = ""; + this.icon = property.icon(); + this.attribute = attribute; + } + + public String getLabel() { + return label; + } + + public String getIcon() { + return icon; + } + + public Attribute getAttribute() { + return attribute; + } + + public String getDisplayValue() { + return this.preValue + this.attribute.getValue(); + } + +} diff --git a/yoti-sdk-spring-boot-example/src/main/resources/application.properties b/yoti-sdk-spring-boot-example/src/main/resources/application.properties new file mode 100644 index 00000000..73092c52 --- /dev/null +++ b/yoti-sdk-spring-boot-example/src/main/resources/application.properties @@ -0,0 +1 @@ +logging.level.org.springframework.boot.autoconfigure=ERROR diff --git a/yoti-sdk-spring-boot-example/src/main/resources/static/digital-identity-share.css b/yoti-sdk-spring-boot-example/src/main/resources/static/digital-identity-share.css new file mode 100644 index 00000000..7d06fc28 --- /dev/null +++ b/yoti-sdk-spring-boot-example/src/main/resources/static/digital-identity-share.css @@ -0,0 +1,79 @@ +.yoti-body { + margin: 0; + font-family: Roboto, sans-serif; +} + +.yoti-top-section { + display: flex; + flex-direction: column; + + padding: 38px 0; + + background-color: #f7f8f9; + + align-items: center; +} + +.yoti-logo-section { + margin-bottom: 25px; +} + +.yoti-logo-image { + display: block; +} + +.yoti-top-header { + font-family: Roboto, sans-serif; + font-size: 40px; + font-weight: 700; + line-height: 1.2; + margin-top: 0; + margin-bottom: 80px; + text-align: center; + + color: #000; +} + +.yoti-sponsor-app-section { + display: flex; + flex-direction: column; + + padding: 50px 0; + + align-items: center; +} + +.yoti-sponsor-app-header { + font-family: Roboto, sans-serif; + font-size: 20px; + font-weight: 700; + line-height: 1.2; + + margin: 0; + + text-align: center; + + color: #000; +} + +.yoti-store-buttons-section { + margin-top: 40px; + display: grid; + grid-gap: 10px; + grid-template-columns: 1fr 1fr; +} + +@media (min-width: 600px) { + .yoti-top-header { + line-height: 1.4; + } +} + +.yoti-sdk-integration-section { + margin: 30px 0; +} + +.box { + inline-size: 600px; + overflow-wrap: break-word; +} diff --git a/yoti-sdk-spring-boot-example/src/main/resources/static/error.css b/yoti-sdk-spring-boot-example/src/main/resources/static/error.css new file mode 100644 index 00000000..b79b3516 --- /dev/null +++ b/yoti-sdk-spring-boot-example/src/main/resources/static/error.css @@ -0,0 +1,28 @@ +.yoti-body { + margin: 0; + font-family: Roboto, sans-serif; +} + +.yoti-top-section { + display: flex; + flex-direction: column; + + padding: 38px 0; + + background-color: #f7f8f9; + + align-items: center; +} + +.yoti-logo-section { + margin-bottom: 25px; +} + +.yoti-logo-image { + display: block; +} + +.box { + inline-size: 600px; + overflow-wrap: break-word; +} diff --git a/yoti-sdk-spring-boot-example/src/main/resources/templates/dbs-check.html b/yoti-sdk-spring-boot-example/src/main/resources/templates/dbs-check.html index 1e58fa1a..cbd4952a 100644 --- a/yoti-sdk-spring-boot-example/src/main/resources/templates/dbs-check.html +++ b/yoti-sdk-spring-boot-example/src/main/resources/templates/dbs-check.html @@ -1,76 +1,77 @@ - + - + + + + DBS Standard Example + + + + - - - - DBS Standard Example - - - + +
+
+
+ Yoti +
+ +

We now accept Yoti

+ +
+
+
+ + + + +
+ +
+

The Yoti app is free to download and use:

+ +
+ + Download on the App Store + + + + get it on Google Play + +
+
+
- -
-
-
- Yoti -
- -

We now accept Yoti

- -
-
-
- - - - -
- -
-

The Yoti app is free to download and use:

- -
- - Download on the App Store - - - - get it on Google Play - -
-
-
- - - - + + + 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 9c8baccf..f1f06d77 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 @@ -1,56 +1,81 @@ - + - - - Digital Identity Example - - + + + Digital Identity Example + + + -> -
-
-
- Yoti -
- -

Digital Identity Share Example page

- -

SdkId:

- -
-

Created Session

-

ID:

-

Status:

-

Expiry:

-
-
-

Created Session QR Code

-

ID:

-

URI:

-
- -
-

Fetched Session QR Code

-

Expiry:

-

Extensions:

-

Redirect URI:

-

Session Status:

-

Session Expiry:

-
- -
-

Fetched Session

-

Created:

-

Updated:

-

Expiry:

-

Status:

-

QR Code ID:

-

Receipt ID:

-
-
+ +
+
+
+ Yoti +
+ +

Digital Identity Share Example page

+ +

SdkId:

+ +
+
+
+ +
+ +
+

The Yoti app is free to download and use:

+ +
+ + Download on the App Store + + + + get it on Google Play + +
+
+ + + diff --git a/yoti-sdk-spring-boot-example/src/main/resources/templates/dynamic-share.html b/yoti-sdk-spring-boot-example/src/main/resources/templates/dynamic-share.html index 21e1fce6..97c3e5af 100644 --- a/yoti-sdk-spring-boot-example/src/main/resources/templates/dynamic-share.html +++ b/yoti-sdk-spring-boot-example/src/main/resources/templates/dynamic-share.html @@ -1,76 +1,77 @@ - + - + + + + Dynamic Share Example + + + + - - - - Dynamic Share Example - - - + +
+
+
+ Yoti +
+ +

We now accept Yoti

+ +
+
+
+ + + + +
+ +
+

The Yoti app is free to download and use:

+ +
+ + Download on the App Store + + + + get it on Google Play + +
+
+
- -
-
-
- Yoti -
- -

We now accept Yoti

- -
-
-
- - - - -
- -
-

The Yoti app is free to download and use:

- -
- - Download on the App Store - - - - get it on Google Play - -
-
-
- - - - + + + diff --git a/yoti-sdk-spring-boot-example/src/main/resources/templates/error.html b/yoti-sdk-spring-boot-example/src/main/resources/templates/error.html index 962f37a4..eed82bc9 100644 --- a/yoti-sdk-spring-boot-example/src/main/resources/templates/error.html +++ b/yoti-sdk-spring-boot-example/src/main/resources/templates/error.html @@ -1,14 +1,27 @@ - + - - - Welcome - - - + + + + Error + + + + + + +
+
+
+ Yoti +
+
+

Home

+

Oops, something went wrong.

+

Error:

+
+
+
+ - -

Home

-

Could not login user for the following reason:

- diff --git a/yoti-sdk-spring-boot-example/src/main/resources/templates/index.html b/yoti-sdk-spring-boot-example/src/main/resources/templates/index.html index fb30dc01..bebbd0ba 100644 --- a/yoti-sdk-spring-boot-example/src/main/resources/templates/index.html +++ b/yoti-sdk-spring-boot-example/src/main/resources/templates/index.html @@ -1,76 +1,77 @@ - + - + + + + Yoti client example + + + + - - - - Yoti client example - - - + +
+
+
+ Yoti +
+ +

We now accept Yoti

+ +
+
+
+ + + + +
+ +
+

The Yoti app is free to download and use:

+ +
+ + Download on the App Store + + + + get it on Google Play + +
+
+
- -
-
-
- Yoti -
- -

We now accept Yoti

- -
-
-
- - - - -
- -
-

The Yoti app is free to download and use:

- -
- - Download on the App Store - - - - get it on Google Play - -
-
-
- - - - + + + diff --git a/yoti-sdk-spring-boot-example/src/main/resources/templates/profile.html b/yoti-sdk-spring-boot-example/src/main/resources/templates/profile.html index ad90998f..7ab06d34 100644 --- a/yoti-sdk-spring-boot-example/src/main/resources/templates/profile.html +++ b/yoti-sdk-spring-boot-example/src/main/resources/templates/profile.html @@ -1,110 +1,110 @@ - - - - - - - - Yoti client example - - - - - -
-
-
- Powered by - Yoti -
- -
- -
- Yoti - -
- -
- -
- -
-
- -
-
- -
- -
- -
- -
-
Attribute
-
Value
-
Anchors
-
- -
-
-
S / V
-
Value
-
Sub type
-
-
- -
-
- -
-
- - -
-
- -
-
-
- - - - - -
-
-
- document_image -
-
- document_image -
-
-
-
- -
-
S / V
-
Value
-
Sub type
- -
-
Source
-
-
-
- -
-
Verifier
-
-
-
-
-
-
-
-
- + + + + + + Yoti client example + + + + + + +
+
+
+ Powered by + Yoti +
+ +
+ +
+ Yoti + +
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+
Attribute
+
Value
+
Anchors
+
+ +
+
+
S / V
+
Value
+
Sub type
+
+
+ +
+
+ +
+
+ + +
+
+ +
+
+
+ + + + + +
+
+
+ document_image +
+
+ document_image +
+
+
+
+ +
+
S / V
+
Value
+
Sub type
+ +
+
Source
+
+
+
+ +
+
Verifier
+
+
+
+
+
+
+
+
+ From 3fb892b295236b3441bf8c1816181ee516a18f30 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 5 Oct 2023 17:40:44 +0100 Subject: [PATCH 34/41] =?UTF-8?q?Bump=20spring-boot-dependencies=20version?= =?UTF-8?q?=20(2.7.10=20=E2=86=92=202.7.16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 4a296a86..bcc801c3 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -106,7 +106,7 @@ - 2.7.10 + 2.7.16 4.0.1 From 037241bfc6577bf72c4030fc5e288974751c531d Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 5 Oct 2023 17:41:15 +0100 Subject: [PATCH 35/41] =?UTF-8?q?Bump=20spring-boot-starter-parent=20versi?= =?UTF-8?q?on=20(2.7.10=20=E2=86=92=202.7.16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/doc-scan/pom.xml | 2 +- yoti-sdk-spring-boot-example/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/doc-scan/pom.xml b/examples/doc-scan/pom.xml index d15f1fda..911f6bc7 100644 --- a/examples/doc-scan/pom.xml +++ b/examples/doc-scan/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.10 + 2.7.16 diff --git a/yoti-sdk-spring-boot-example/pom.xml b/yoti-sdk-spring-boot-example/pom.xml index 941cd07b..00a1c98b 100644 --- a/yoti-sdk-spring-boot-example/pom.xml +++ b/yoti-sdk-spring-boot-example/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.10 + 2.7.16 From bf8b10cc9b25b17203454736c727bb2c11ced3c3 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 5 Oct 2023 17:41:51 +0100 Subject: [PATCH 36/41] =?UTF-8?q?Bump=20slf4j=20version=20(2.0.7=20?= =?UTF-8?q?=E2=86=92=202.0.9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index bcc801c3..42935111 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -98,7 +98,7 @@ 8 - 2.0.7 + 2.0.9 1.70 2.15.2 3.22.4 From 0cbf0f25c64c6d70493ae674ce3b4102ad8a648a Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 5 Oct 2023 17:42:14 +0100 Subject: [PATCH 37/41] =?UTF-8?q?Bump=20protobuf-java=20version=20(3.22.4?= =?UTF-8?q?=20=E2=86=92=203.24.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 42935111..aa315e32 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -101,7 +101,7 @@ 2.0.9 1.70 2.15.2 - 3.22.4 + 3.24.4 4.5.14 From febc54bb2afd193c57b18eb8f9ecaf287b214eef Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 5 Oct 2023 17:42:41 +0100 Subject: [PATCH 38/41] =?UTF-8?q?Bump=20commons.lang3=20version=20(3.12.0?= =?UTF-8?q?=20=E2=86=92=203.13.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index aa315e32..557d8dd1 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -113,7 +113,7 @@ 4.13.2 4.11.0 2.2 - 3.12.0 + 3.13.0 3.11.0 From ca3c53371410f76a23c5b500ff754e69ef0e7328 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 5 Oct 2023 17:43:20 +0100 Subject: [PATCH 39/41] =?UTF-8?q?Bump=20dependency-check-maven=20version?= =?UTF-8?q?=20(8.3.1=20=E2=86=92=208.4.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 557d8dd1..5a3f9508 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -122,7 +122,7 @@ 4.7.3.4 1.12.0 - 8.3.1 + 8.4.0 12 1.23 From 84977a4c828a1fb149aca0b1b514cbdcc6dcd3e8 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 5 Oct 2023 17:43:43 +0100 Subject: [PATCH 40/41] =?UTF-8?q?Bump=20extra-enforcer-rules=20version=20(?= =?UTF-8?q?1.6.2=20=E2=86=92=201.7.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yoti-sdk-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 5a3f9508..60165eef 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -130,7 +130,7 @@ 1.0 3.3.0 - 1.6.2 + 1.7.0 1.6.13 From 7416cb91d4d991fd9f589ea672f88cd2d7b2e915 Mon Sep 17 00:00:00 2001 From: irotech Date: Wed, 11 Oct 2023 11:23:04 +0100 Subject: [PATCH 41/41] Release-3.8.0: Set version for release --- README.md | 4 ++-- examples/doc-scan/pom.xml | 2 +- pom.xml | 2 +- yoti-sdk-api/pom.xml | 2 +- .../com/yoti/api/client/spi/remote/call/YotiConstants.java | 2 +- yoti-sdk-parent/pom.xml | 2 +- yoti-sdk-sandbox/pom.xml | 2 +- yoti-sdk-spring-boot-auto-config/README.md | 4 ++-- yoti-sdk-spring-boot-auto-config/pom.xml | 2 +- yoti-sdk-spring-boot-example/README.md | 2 +- yoti-sdk-spring-boot-example/pom.xml | 2 +- yoti-sdk-spring-security/README.md | 4 ++-- yoti-sdk-spring-security/pom.xml | 2 +- 13 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ed0f2db1..2c4847ea 100644 --- a/README.md +++ b/README.md @@ -101,13 +101,13 @@ If you are using Maven, you need to add the following dependency: com.yoti yoti-sdk-api - 3.8.0-SNAPSHOT + 3.8.0 ``` If you are using Gradle, here is the dependency to add: -`compile group: 'com.yoti', name: 'yoti-sdk-api', version: '3.8.0-SNAPSHOT'` +`compile group: 'com.yoti', name: 'yoti-sdk-api', version: '3.8.0'` You will find all classes packaged under `com.yoti.api` diff --git a/examples/doc-scan/pom.xml b/examples/doc-scan/pom.xml index 911f6bc7..0847b23c 100644 --- a/examples/doc-scan/pom.xml +++ b/examples/doc-scan/pom.xml @@ -53,7 +53,7 @@ com.yoti yoti-sdk-api - 3.8.0-SNAPSHOT + 3.8.0 diff --git a/pom.xml b/pom.xml index 65a31096..9f9daab8 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.yoti yoti-sdk pom - 3.8.0-SNAPSHOT + 3.8.0 Yoti SDK Java SDK for simple integration with the Yoti platform https://github.com/getyoti/yoti-java-sdk diff --git a/yoti-sdk-api/pom.xml b/yoti-sdk-api/pom.xml index d3af7f84..72109564 100644 --- a/yoti-sdk-api/pom.xml +++ b/yoti-sdk-api/pom.xml @@ -11,7 +11,7 @@ com.yoti yoti-sdk-parent - 3.8.0-SNAPSHOT + 3.8.0 ../yoti-sdk-parent diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java index c0af35a4..2de0ee21 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java @@ -29,7 +29,7 @@ private YotiConstants() {} public static final String CONTENT_TYPE_JPEG = "image/jpeg"; public static final String JAVA = "Java"; - public static final String SDK_VERSION = JAVA + "-3.8.0-SNAPSHOT"; + public static final String SDK_VERSION = JAVA + "-3.8.0"; public static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; public static final String ASYMMETRIC_CIPHER = "RSA/NONE/PKCS1Padding"; public static final String SYMMETRIC_CIPHER = "AES/CBC/PKCS7Padding"; diff --git a/yoti-sdk-parent/pom.xml b/yoti-sdk-parent/pom.xml index 60165eef..ac6bf9cc 100644 --- a/yoti-sdk-parent/pom.xml +++ b/yoti-sdk-parent/pom.xml @@ -5,7 +5,7 @@ com.yoti yoti-sdk-parent pom - 3.8.0-SNAPSHOT + 3.8.0 Yoti SDK Parent Pom Parent pom for the Java SDK projects https://github.com/getyoti/yoti-java-sdk diff --git a/yoti-sdk-sandbox/pom.xml b/yoti-sdk-sandbox/pom.xml index 6c7d5883..ac841ebb 100644 --- a/yoti-sdk-sandbox/pom.xml +++ b/yoti-sdk-sandbox/pom.xml @@ -11,7 +11,7 @@ com.yoti yoti-sdk-parent - 3.8.0-SNAPSHOT + 3.8.0 ../yoti-sdk-parent diff --git a/yoti-sdk-spring-boot-auto-config/README.md b/yoti-sdk-spring-boot-auto-config/README.md index 06230787..9e8e2e16 100644 --- a/yoti-sdk-spring-boot-auto-config/README.md +++ b/yoti-sdk-spring-boot-auto-config/README.md @@ -18,7 +18,7 @@ If you are using Maven, you need to add the following dependencies: com.yoti yoti-sdk-spring-boot-auto-config - 3.8.0-SNAPSHOT + 3.8.0 ``` @@ -26,7 +26,7 @@ If you are using Maven, you need to add the following dependencies: If you are using Gradle, here is the dependency to add: ``` -compile group: 'com.yoti', name: 'yoti-sdk-spring-boot-auto-config', version: '3.8.0-SNAPSHOT' +compile group: 'com.yoti', name: 'yoti-sdk-spring-boot-auto-config', version: '3.8.0' ``` diff --git a/yoti-sdk-spring-boot-auto-config/pom.xml b/yoti-sdk-spring-boot-auto-config/pom.xml index 6b8cf9dc..49b8800c 100644 --- a/yoti-sdk-spring-boot-auto-config/pom.xml +++ b/yoti-sdk-spring-boot-auto-config/pom.xml @@ -12,7 +12,7 @@ com.yoti yoti-sdk-parent - 3.8.0-SNAPSHOT + 3.8.0 ../yoti-sdk-parent diff --git a/yoti-sdk-spring-boot-example/README.md b/yoti-sdk-spring-boot-example/README.md index 494171be..90b78ceb 100644 --- a/yoti-sdk-spring-boot-example/README.md +++ b/yoti-sdk-spring-boot-example/README.md @@ -17,7 +17,7 @@ Note that: com.yoti yoti-sdk-api - 3.8.0-SNAPSHOT + 3.8.0 ``` diff --git a/yoti-sdk-spring-boot-example/pom.xml b/yoti-sdk-spring-boot-example/pom.xml index 00a1c98b..ad19a98f 100644 --- a/yoti-sdk-spring-boot-example/pom.xml +++ b/yoti-sdk-spring-boot-example/pom.xml @@ -6,7 +6,7 @@ com.yoti yoti-sdk-spring-boot-example Yoti Spring Boot Example - 3.8.0-SNAPSHOT + 3.8.0 org.springframework.boot diff --git a/yoti-sdk-spring-security/README.md b/yoti-sdk-spring-security/README.md index 2fc11090..01dc188f 100644 --- a/yoti-sdk-spring-security/README.md +++ b/yoti-sdk-spring-security/README.md @@ -25,14 +25,14 @@ If you are using Maven, you need to add the following dependencies: com.yoti yoti-sdk-spring-security - 3.8.0-SNAPSHOT + 3.8.0 ``` If you are using Gradle, here is the dependency to add: ``` -compile group: 'com.yoti', name: 'yoti-sdk-spring-security', version: '3.8.0-SNAPSHOT' +compile group: 'com.yoti', name: 'yoti-sdk-spring-security', version: '3.8.0' ``` ### Provide a `YotiClient` instance diff --git a/yoti-sdk-spring-security/pom.xml b/yoti-sdk-spring-security/pom.xml index 9ce80ac9..df577686 100644 --- a/yoti-sdk-spring-security/pom.xml +++ b/yoti-sdk-spring-security/pom.xml @@ -12,7 +12,7 @@ com.yoti yoti-sdk-parent - 3.8.0-SNAPSHOT + 3.8.0 ../yoti-sdk-parent