diff --git a/examples/doc-scan/pom.xml b/examples/doc-scan/pom.xml index d15f1fda6..911f6bc79 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/pom.xml b/pom.xml index 4d97749d2..65a31096d 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ 3.1.1 1.1.0 + 4.7.3.4 @@ -56,6 +57,11 @@ + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs-maven-plugin.version} + diff --git a/yoti-sdk-api/spotbugs/exclude-filter.xml b/yoti-sdk-api/spotbugs/exclude-filter.xml index 8b1061983..c1a507e96 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-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 000000000..2aa6f5c5c --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/DigitalIdentityClient.java @@ -0,0 +1,96 @@ +package com.yoti.api.client; + +import java.io.IOException; +import java.security.KeyPair; +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; +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; + +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) { + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); + + this.sdkId = sdkId; + this.keyPair = loadKeyPair(keyPair); + this.identityService = identityService; + } + + public ShareSession createShareSession(ShareSessionRequest request) throws DigitalIdentityException { + return identityService.createShareSession(sdkId, keyPair, request); + } + + public ShareSession fetchShareSession(String sessionId) throws DigitalIdentityException { + return identityService.fetchShareSession(sdkId, keyPair, sessionId); + } + + public ShareSessionQrCode createShareQrCode(String sessionId) throws DigitalIdentityException { + return identityService.createShareQrCode(sdkId, keyPair, sessionId); + } + + public ShareSessionQrCode fetchShareQrCode(String qrCodeId) throws DigitalIdentityException { + return identityService.fetchShareQrCode(sdkId, keyPair, qrCodeId); + } + + public Receipt fetchShareReceipt(String receiptId) throws DigitalIdentityException { + return identityService.fetchShareReceipt(sdkId, keyPair, receiptId); + } + + private KeyPair loadKeyPair(KeyPairSource keyPairSource) throws InitialisationException { + try { + return keyPairSource.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) { + Validation.notNullOrEmpty(sdkId, "SDK ID"); + + this.sdkId = sdkId; + return this; + } + + public Builder withKeyPairSource(KeyPairSource keyPairSource) { + Validation.notNull(keyPairSource, "Key Pair Source"); + + this.keyPairSource = keyPairSource; + return this; + } + + public DigitalIdentityClient build() { + 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 b535832a6..2112b96af 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 eaf15a4c2..8880c24b9 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 5a6c4e9f5..c4dfce3ae 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/identity/ShareSession.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSession.java new file mode 100644 index 000000000..8273fa403 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSession.java @@ -0,0 +1,92 @@ +package com.yoti.api.client.identity; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ShareSession { + + private String id; + private String status; + private String created; + private String updated; + private String expiry; + private String qrCodeId; + private String receiptId; + + public String getId() { + return id; + } + + 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/identity/ShareSessionNotification.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java new file mode 100644 index 000000000..3c57702c5 --- /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 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 { + + @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() { + Validation.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/ShareSessionQrCode.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionQrCode.java new file mode 100644 index 000000000..30c44a276 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionQrCode.java @@ -0,0 +1,73 @@ +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 { + + @JsonProperty(Property.ID) + private String id; + + @JsonProperty(Property.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 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/identity/ShareSessionRequest.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java new file mode 100644 index 000000000..7f8db0eb9 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java @@ -0,0 +1,125 @@ +package com.yoti.api.client.identity; + +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.yoti.validation.Validation; + +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() { + Validation.notNull(policy, Property.POLICY); + Validation.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 000000000..c86d866f8 --- /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 000000000..eaf043b95 --- /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 000000000..69279fe81 --- /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 000000000..20e38dd3b --- /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 000000000..83e8e8f5c --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java @@ -0,0 +1,35 @@ +package com.yoti.api.client.identity.extension; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Extension { + + @JsonProperty(Property.TYPE) + private final String type; + + @JsonProperty(Property.CONTENT) + private final T content; + + @JsonCreator + Extension(@JsonProperty(Property.TYPE) String type, @JsonProperty(Property.CONTENT) 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 000000000..7c38e8482 --- /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 000000000..eff7b4874 --- /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 000000000..09839f889 --- /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 000000000..81248840f --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java @@ -0,0 +1,55 @@ +package com.yoti.api.client.identity.extension; + +import com.yoti.validation.Validation; + +public class LocationConstraintExtensionBuilder implements ExtensionBuilder { + + 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<>("LOCATION_CONSTRAINT", 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 000000000..05eb6c7ac --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java @@ -0,0 +1,38 @@ +package com.yoti.api.client.identity.extension; + +import java.time.OffsetDateTime; +import java.util.List; + +import com.yoti.api.client.AttributeDefinition; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ThirdPartyAttributeContent { + + private final OffsetDateTime expiry; + + @JsonProperty(Property.DEFINITIONS) + private final List definitions; + + ThirdPartyAttributeContent(OffsetDateTime expiry, List definitions) { + this.expiry = expiry; + this.definitions = definitions; + } + + @JsonProperty(Property.EXPIRY_DATE) + public OffsetDateTime getExpiryDate() { + return expiry; + } + + 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 000000000..fadba8550 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java @@ -0,0 +1,54 @@ +package com.yoti.api.client.identity.extension; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; + +import com.yoti.api.client.AttributeDefinition; +import com.yoti.validation.Validation; + +public class ThirdPartyAttributeExtensionBuilder implements ExtensionBuilder { + + private OffsetDateTime expiryDate; + private List definitions; + + public ThirdPartyAttributeExtensionBuilder() { + this.definitions = new ArrayList<>(); + } + + public ThirdPartyAttributeExtensionBuilder withExpiryDate(OffsetDateTime expiryDate) { + Validation.notNull(expiryDate, Property.EXPIRY_DATE); + + this.expiryDate = expiryDate; + return this; + } + + public ThirdPartyAttributeExtensionBuilder withDefinition(String definition) { + Validation.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<>("THIRD_PARTY_ATTRIBUTE", thirdPartyAttributeContent); + } + + private static final class Property { + + 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 new file mode 100644 index 000000000..913630ca2 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java @@ -0,0 +1,27 @@ +package com.yoti.api.client.identity.extension; + +import com.yoti.validation.Validation; + +public class TransactionalFlowExtensionBuilder implements ExtensionBuilder { + + 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<>("TRANSACTIONAL_FLOW", 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 000000000..06d8bad28 --- /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 000000000..86bf6845d --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java @@ -0,0 +1,82 @@ +package com.yoti.api.client.identity.policy; + +import java.util.Objects; + +import com.yoti.validation.Validation; + +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; + } + + @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(); + } + + 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() { + Validation.notNullOrEmpty(value, Property.NAME); + Validation.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 000000000..010b0dc00 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java @@ -0,0 +1,125 @@ +package com.yoti.api.client.identity.policy; + +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; + +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 boolean hasConstraints() { + return !constraints.isEmpty(); + } + + 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() { + Validation.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/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 778048d8c..5960f0cea 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 9bc1a069a..0c8e7137d 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 f073fa218..64da85845 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/YotiConstants.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/YotiConstants.java index e12607f1f..c0af35a4b 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 0a98e3156..a8a57f589 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,80 +4,121 @@ 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"; + // 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_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"; + 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 createIdentitySessionRetrievalPath(String sessionId) { + return format(IDENTITY_SESSION_RETRIEVAL, base64ToBase64url(sessionId)); + } + + public String createIdentitySessionQrCodePath(String sessionId) { + return format(IDENTITY_SESSION_QR_CODE_CREATION, base64ToBase64url(sessionId)); + } + + public String createIdentitySessionQrCodeRetrievalPath(String 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) { + 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); + return format(DOCS_FETCH_IBV_INSTRUCTIONS, sessionId, appId); } - public String createFetchInstructionsContactProfilePath(String appId, String sessionId) { - return format(DOCS_FETCH_INSTRUCTION_CONTACT_PROFILE_PATH_TEMPLATE, sessionId, appId); + public String createFetchIbvInstructionsPdfPath(String sdkId, String sessionId) { + return format(DOCS_FETCH_IBV_INSTRUCTIONS_PDF, sessionId, sdkId); } public String createGetSupportedDocumentsPath(boolean includeNonLatin) { - return format(DOCS_SUPPORTED_DOCUMENTS_PATH, includeNonLatin); + return format(DOCS_SUPPORTED_DOCUMENTS, includeNonLatin); } - public String createFetchIbvInstructionsPdfPath(String sdkId, String sessionId) { - return format(DOCS_FETCH_IBV_INSTRUCTIONS_PDF_PATH_TEMPLATE, sessionId, sdkId); + public String createFetchInstructionsContactProfilePath(String appId, String sessionId) { + return format(DOCS_FETCH_INSTRUCTION_CONTACT_PROFILE, sessionId, appId); } - public String createNewFaceCaptureResourcePath(String sdkId, String sessionId) { - return format(DOCS_NEW_FACE_CAPTURE_RESOURCE_PATH_TEMPLATE, sessionId, sdkId); + public String createFetchSessionConfigurationPath(String sdkId, String sessionId) { + return format(DOCS_FETCH_SESSION_CONFIGURATION, sessionId, sdkId); } - public String createUploadFaceCaptureImagePath(String sdkId, String sessionId, String resourceId) { - return format(DOCS_UPLOAD_FACE_CAPTURE_IMAGE_PATH_TEMPLATE, sessionId, resourceId, sdkId); + public String createNewFaceCaptureResourcePath(String sdkId, String sessionId) { + return format(DOCS_NEW_FACE_CAPTURE_RESOURCE, sessionId, sdkId); } - public String createFetchSessionConfigurationPath(String sdkId, String sessionId) { - return format(DOCS_FETCH_SESSION_CONFIGURATION_PATH_TEMPLATE, sessionId, sdkId); + public String createUploadFaceCaptureImagePath(String sdkId, String sessionId, String resourceId) { + return format(DOCS_UPLOAD_FACE_CAPTURE_IMAGE, sessionId, resourceId, sdkId); } public String createTriggerIbvEmailNotificationPath(String sdkId, String sessionId) { - return format(DOCS_TRIGGER_IBV_NOTIFICATION_PATH_TEMPLATE, sessionId, sdkId); + return format(DOCS_TRIGGER_IBV_NOTIFICATION, sessionId, sdkId); } } 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 000000000..c0c191bac --- /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 000000000..f2d9d6421 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityService.java @@ -0,0 +1,213 @@ +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; +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 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 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; +import com.yoti.validation.Validation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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; + private final ReceiptParser receiptParser; + + private final String apiUrl; + + 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(), + ReceiptParser.newInstance() + ); + } + + public ShareSession createShareSession(String sdkId, KeyPair keyPair, ShareSessionRequest shareSessionRequest) + throws DigitalIdentityException { + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); + Validation.notNull(shareSessionRequest, "Share Session request"); + + String path = pathFactory.createIdentitySessionPath(); + + LOG.debug("Requesting share session creation for SDK ID '{}' at '{}'", sdkId, path); + + try { + byte[] payload = ResourceMapper.writeValueAsString(shareSessionRequest); + 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) { + throw new DigitalIdentityException("Error while building the share session creation request ", ex); + } catch (GeneralSecurityException ex) { + throw new DigitalIdentityException("Error while signing the share session creation request ", ex); + } catch (ResourceException ex) { + throw new DigitalIdentityException("Error while executing the share session creation request ", ex); + } + } + + public ShareSession fetchShareSession(String sdkId, KeyPair keyPair, String sessionId) + throws DigitalIdentityException { + 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 '{}' at '{}'", sessionId, path); + + try { + return createSignedRequest(sdkId, keyPair, path).execute(ShareSession.class); + } catch (Exception ex) { + throw new DigitalIdentityException( + String.format("Error while fetching the share session '{%s}' ", sessionId), + ex + ); + } + } + + public ShareSessionQrCode createShareQrCode(String sdkId, KeyPair keyPair, String sessionId) + throws DigitalIdentityException { + 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 { + 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) { + throw new DigitalIdentityException("Error while executing the share QR code creation request ", ex); + } + } + + public ShareSessionQrCode fetchShareQrCode(String sdkId, KeyPair keyPair, String qrCodeId) + throws DigitalIdentityException { + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); + Validation.notNullOrEmpty(qrCodeId, "QR Code ID"); + + String path = pathFactory.createIdentitySessionQrCodeRetrievalPath(qrCodeId); + + LOG.debug("Requesting share session QR code '{} at '{}'", qrCodeId, path); + + try { + 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); + + 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 '{%s}' ", receiptId), + ex + ); + } + } + + 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) + throws GeneralSecurityException, UnsupportedEncodingException, URISyntaxException { + 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 request = requestBuilderFactory.create() + .withKeyPair(keyPair) + .withBaseUrl(apiUrl) + .withEndpoint(path) + .withHeader(AUTH_ID_HEADER, sdkId) + .withHttpMethod(method); + + 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 000000000..13709fc6e --- /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 000000000..4f6241932 --- /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 000000000..9b3a1e99b --- /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 000000000..32d8d94eb --- /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/api/client/spi/remote/util/Validation.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/util/Validation.java index 7cc078b04..b3b72e9bc 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/crypto/Algorithm.java b/yoti-sdk-api/src/main/java/com/yoti/crypto/Algorithm.java new file mode 100644 index 000000000..28827d2ea --- /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 000000000..21ce85a08 --- /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 000000000..acc1399ae --- /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 000000000..1e99a37a9 --- /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 new file mode 100644 index 000000000..975485821 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java @@ -0,0 +1,54 @@ +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; +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; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +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)) + .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) + .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/main/java/com/yoti/validation/Validation.java b/yoti-sdk-api/src/main/java/com/yoti/validation/Validation.java new file mode 100644 index 000000000..0eedb2ac3 --- /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 000000000..eaeb6abe1 --- /dev/null +++ b/yoti-sdk-api/src/test/java/com/yoti/api/client/DigitalIdentityClientTest.java @@ -0,0 +1,243 @@ +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.Answers.RETURNS_DEEP_STUBS; +import static org.mockito.ArgumentMatchers.any; +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.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; + +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_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; + + @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_SDK_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_NoKeyPairInFile_InitialisationException() { + KeyPairSource invalidKeyPairSource = new KeyPairSourceTest("no-key-pair-in-file"); + + DigitalIdentityClient.Builder builder = DigitalIdentityClient.builder() + .withClientSdkId(AN_SDK_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_SDK_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() throws IOException { + when(keyPairSource.getFromStream(any(KeyStreamVisitor.class))).thenReturn(keyPair); + + DigitalIdentityClient identityClient = new DigitalIdentityClient( + AN_SDK_ID, + keyPairSource, + identityService + ); + + String exMessage = "Create Share Session Error"; + when(identityService.createShareSession(AN_SDK_ID, keyPair, shareSessionRequest)) + .thenThrow(new DigitalIdentityException(exMessage)); + + DigitalIdentityException ex = assertThrows( + DigitalIdentityException.class, + () -> identityClient.createShareSession(shareSessionRequest) + ); + + assertThat(ex.getMessage(), equalTo(exMessage)); + } + + @Test + public void client_FetchShareSessionException_DigitalIdentityException() throws IOException { + when(keyPairSource.getFromStream(any(KeyStreamVisitor.class))).thenReturn(keyPair); + + DigitalIdentityClient identityClient = new DigitalIdentityClient( + AN_SDK_ID, + keyPairSource, + identityService + ); + + String exMessage = "Fetch Share Session Error"; + when(identityService.fetchShareSession(AN_SDK_ID, keyPair, 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() throws IOException { + when(keyPairSource.getFromStream(any(KeyStreamVisitor.class))).thenReturn(keyPair); + + DigitalIdentityClient identityClient = new DigitalIdentityClient( + AN_SDK_ID, + keyPairSource, + identityService + ); + + String exMessage = "Create Share QR Error"; + when(identityService.createShareQrCode(AN_SDK_ID, keyPair, 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() throws IOException { + when(keyPairSource.getFromStream(any(KeyStreamVisitor.class))).thenReturn(keyPair); + + DigitalIdentityClient identityClient = new DigitalIdentityClient( + AN_SDK_ID, + keyPairSource, + identityService + ); + + String exMessage = "Fetch Share QR Error"; + when(identityService.fetchShareQrCode(AN_SDK_ID, keyPair, 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() throws IOException { + when(keyPairSource.getFromStream(any(KeyStreamVisitor.class))).thenReturn(keyPair); + + DigitalIdentityClient identityClient = new DigitalIdentityClient( + AN_SDK_ID, + keyPairSource, + identityService + ); + + String exMessage = "Fetch Share Receipt Error"; + when(identityService.fetchShareReceipt(AN_SDK_ID, keyPair, 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-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 000000000..035128b46 --- /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); + } + + } + +} 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 000000000..dd458dfbd --- /dev/null +++ b/yoti-sdk-api/src/test/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityServiceTest.java @@ -0,0 +1,280 @@ +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.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.ShareSessionRequest; +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 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_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; + + @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 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, POST, 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, POST, 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, POST, 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, POST, 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 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")); + } + + @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-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 000000000..23e19e6ec --- /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.assertTrue; + +import java.net.URI; +import java.time.OffsetDateTime; +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(OffsetDateTime.now()) + .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-parent/pom.xml b/yoti-sdk-parent/pom.xml index b8f5f89e5..60165eef6 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,27 +93,27 @@ ${project.version} + 1.8 - ${supported.java.version} - ${supported.java.version} - + 8 + - 2.0.7 + 2.0.9 1.70 2.15.2 - 3.22.4 + 3.24.4 4.5.14 - 2.7.10 + 2.7.16 4.0.1 4.13.2 4.11.0 2.2 - 3.12.0 + 3.13.0 3.11.0 @@ -103,7 +122,7 @@ 4.7.3.4 1.12.0 - 8.3.1 + 8.4.0 12 1.23 @@ -111,7 +130,7 @@ 1.0 3.3.0 - 1.6.2 + 1.7.0 1.6.13 @@ -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-auto-config/pom.xml b/yoti-sdk-spring-boot-auto-config/pom.xml index a632ca5e2..6b8cf9dce 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/spotbugs/exclude-filter.xml b/yoti-sdk-spring-boot-auto-config/spotbugs/exclude-filter.xml index b5a528812..551c7d03c 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-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 5eb10a9a4..e0ae440de 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 000000000..56ac48443 --- /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 000000000..2a72e4de6 --- /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 37258e8bd..a7bf2fe5c 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 9dcdbef30..9d40c624a 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 180b64391..ef4b2965f 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 98f14c094..d3e2e9f0b 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 ad2f472e7..382fac42c 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 000000000..2de55c130 --- /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 465e09c2b..8fb3736ba 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/README.md b/yoti-sdk-spring-boot-example/README.md index 4556d125f..494171be0 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 0936a7640..00a1c98b9 100644 --- a/yoti-sdk-spring-boot-example/pom.xml +++ b/yoti-sdk-spring-boot-example/pom.xml @@ -11,15 +11,40 @@ org.springframework.boot spring-boot-starter-parent - 2.7.10 + 2.7.16 + + + 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 + 4.7.3.4 @@ -68,11 +93,22 @@ + ${project.artifactId} + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + org.springframework.boot spring-boot-maven-plugin + org.apache.maven.plugins maven-deploy-plugin @@ -80,6 +116,12 @@ true + + + 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/DisplayAttribute.java b/yoti-sdk-spring-boot-example/src/main/java/com/yoti/api/examples/springboot/DisplayAttribute.java deleted file mode 100644 index 85f174e5c..000000000 --- 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 000000000..68438b53a --- /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 000000000..e037e4840 --- /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 d5382b0f3..124bef78f 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.spring.YotiClientProperties; +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; @@ -38,20 +37,19 @@ 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.thymeleaf.util.StringUtils; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @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 +57,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; } @@ -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 000000000..80790f5b1 --- /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 000000000..e9bfc3d0a --- /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 000000000..cfeb457d8 --- /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 000000000..73092c52c --- /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 000000000..7d06fc281 --- /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 000000000..b79b35166 --- /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 1e58fa1ae..cbd4952a6 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 new file mode 100644 index 000000000..f1f06d772 --- /dev/null +++ b/yoti-sdk-spring-boot-example/src/main/resources/templates/digital-identity-share.html @@ -0,0 +1,81 @@ + + + + + + Digital Identity Example + + + + + + +
+
+
+ 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 21e1fce69..97c3e5af0 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 962f37a45..eed82bc96 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 fb30dc012..bebbd0ba6 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 ad90998f9..7ab06d341 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
+
+
+
+
+
+
+
+
+