From 3b678a926aede5f560003ee69e769b24bed63ff5 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 29 Sep 2023 13:00:52 +0100 Subject: [PATCH 01/47] SDK-2238:added net-create-session work --- src/Yoti.Auth/Constants/Api.cs | 3 + .../DigitalIdentity/DigitalIdentityService.cs | 53 +++ .../Extensions/BaseExtension.cs | 27 ++ .../Extensions/DeviceLocation.cs | 63 ++++ .../DigitalIdentity/Extensions/Extension.cs | 33 ++ .../Extensions/ExtensionBuilder.cs | 25 ++ .../Extensions/LocationConstraintContent.cs | 24 ++ .../LocationConstraintExtensionBuilder.cs | 66 ++++ .../Extensions/ThirdPartyAttributeContent.cs | 31 ++ .../ThirdPartyAttributeExtensionBuilder.cs | 67 ++++ .../TransactionalFlowExtensionBuilder.cs | 25 ++ .../DigitalIdentity/Policy/Constraint.cs | 16 + .../DigitalIdentity/Policy/DynamicPolicy.cs | 95 +++++ .../Policy/DynamicPolicyBuilder.cs | 166 +++++++++ .../Policy/PreferredSources.cs | 20 ++ .../Policy/SourceConstraint.cs | 18 + .../Policy/SourceConstraintBuilder.cs | 72 ++++ .../DigitalIdentity/Policy/WantedAnchor.cs | 19 + .../Policy/WantedAnchorBuilder.cs | 37 ++ .../DigitalIdentity/Policy/WantedAttribute.cs | 34 ++ .../Policy/WantedAttributeBuilder.cs | 62 ++++ src/Yoti.Auth/DigitalIdentity/ShareSession.cs | 67 ++++ .../ShareSessionRequestBuilder.cs | 64 ++++ .../DigitalIdentity/ShareSessionResult.cs | 49 +++ src/Yoti.Auth/DigitalIdentityClient.cs | 112 ++++++ src/Yoti.Auth/DigitalIdentityClientEngine.cs | 42 +++ src/Yoti.Auth/Yoti.Auth.csproj | 6 + .../DigitalIdentityServiceTests.cs | 88 +++++ .../Extensions/ExtensionBuilderTests.cs | 25 ++ ...LocationConstraintExtensionBuilderTests.cs | 100 ++++++ ...hirdPartyAttributeExtensionBuilderTests.cs | 132 +++++++ .../TransactionalFlowExtensionBuilderTests.cs | 44 +++ .../Policy/DynamicPolicyBuilderTests.cs | 329 ++++++++++++++++++ .../Policy/WantedAttributeBuilderTests.cs | 164 +++++++++ .../Policy/WantedAttributeMatcher.cs | 41 +++ .../ShareSessionRequestBuilderTests.cs | 70 ++++ .../DigitalIdentityClientEngineTests.cs | 93 +++++ .../DigitalIdentityClientTests.cs | 148 ++++++++ .../Yoti.Auth.Tests/TestTools/ShareSession.cs | 27 ++ test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj | 6 + 40 files changed, 2563 insertions(+) create mode 100644 src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/BaseExtension.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/DeviceLocation.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/Extension.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/ExtensionBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/LocationConstraintContent.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/LocationConstraintExtensionBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/ThirdPartyAttributeContent.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/ThirdPartyAttributeExtensionBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/TransactionalFlowExtensionBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/Constraint.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicy.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicyBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/PreferredSources.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/SourceConstraint.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/SourceConstraintBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/WantedAnchor.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/WantedAnchorBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/WantedAttributeBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/ShareSession.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs create mode 100644 src/Yoti.Auth/DigitalIdentityClient.cs create mode 100644 src/Yoti.Auth/DigitalIdentityClientEngine.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Extensions/ExtensionBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Extensions/LocationConstraintExtensionBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Extensions/ThirdPartyAttributeExtensionBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Extensions/TransactionalFlowExtensionBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeMatcher.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/ShareSessionRequestBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs create mode 100644 test/Yoti.Auth.Tests/TestTools/ShareSession.cs diff --git a/src/Yoti.Auth/Constants/Api.cs b/src/Yoti.Auth/Constants/Api.cs index 4cc0cfaf1..5f2dec733 100644 --- a/src/Yoti.Auth/Constants/Api.cs +++ b/src/Yoti.Auth/Constants/Api.cs @@ -7,8 +7,11 @@ public static class Api public const string DefaultYotiHost = @"https://api.yoti.com"; public const string YotiApiPathPrefix = "api/v1"; + public const string YotiApiSharePathPrefix = "/share"; public readonly static string DefaultYotiApiUrl = string.Join("/", DefaultYotiHost, YotiApiPathPrefix); + public readonly static string DefaultYotiShareApiUrl = string.Join("/", DefaultYotiHost, YotiApiSharePathPrefix); + public const string YotiDocsPathPrefix = "idverify/v1/"; public readonly static Uri DefaultYotiDocsUrl = new Uri(string.Join("/", DefaultYotiHost, YotiDocsPathPrefix)); diff --git a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs new file mode 100644 index 000000000..c5ef042a6 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs @@ -0,0 +1,53 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Org.BouncyCastle.Crypto; +using Yoti.Auth.Exceptions; +using Yoti.Auth.Web; + +namespace Yoti.Auth.DigitalIdentity +{ + public static class DigitalIdentityService + { + internal static async Task CreateShareSession(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, ShareSessionRequest shareSessionRequest) + { + Validation.NotNull(httpClient, nameof(httpClient)); + Validation.NotNull(apiUrl, nameof(apiUrl)); + Validation.NotNull(sdkId, nameof(sdkId)); + Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(shareSessionRequest, nameof(shareSessionRequest)); + + string serializedScenario = JsonConvert.SerializeObject( + shareSessionRequest, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + byte[] body = Encoding.UTF8.GetBytes(serializedScenario); + + Request shareUrlRequest = new RequestBuilder() + .WithKeyPair(keyPair) + .WithBaseUri(apiUrl) + .WithEndpoint($"/sessions/v2") + .WithHttpMethod(HttpMethod.Post) + .WithContent(body) + .Build(); + + using (HttpResponseMessage response = await shareUrlRequest.Execute(httpClient).ConfigureAwait(false)) + { + if (!response.IsSuccessStatusCode) + { + Response.CreateYotiExceptionFromStatusCode(response); + } + + var responseObject = await response.Content.ReadAsStringAsync(); + var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); + + return deserialized; + + } + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Extensions/BaseExtension.cs b/src/Yoti.Auth/DigitalIdentity/Extensions/BaseExtension.cs new file mode 100644 index 000000000..71f96b96e --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Extensions/BaseExtension.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Extensions +{ + public abstract class BaseExtension + { + [JsonProperty(PropertyName = "type")] + private readonly string _type; + + private protected BaseExtension(string type) + { + _type = type; + } + + /// + /// Get the feature's type + /// + [JsonIgnore] + public string ExtensionType + { + get + { + return _type; + } + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Extensions/DeviceLocation.cs b/src/Yoti.Auth/DigitalIdentity/Extensions/DeviceLocation.cs new file mode 100644 index 000000000..fac070574 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Extensions/DeviceLocation.cs @@ -0,0 +1,63 @@ +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Extensions +{ + public class DeviceLocation + { + [JsonProperty(PropertyName = "latitude")] + private readonly double _latitude; + + [JsonProperty(PropertyName = "longitude")] + private readonly double _longitude; + + [JsonProperty(PropertyName = "radius")] + private readonly double _radius; + + [JsonProperty(PropertyName = "max_uncertainty_radius")] + private readonly double _maxUncertainty; + + public DeviceLocation(double latitude, double longitude, double radius, double maxUncertainty) + { + _latitude = latitude; + _longitude = longitude; + _radius = radius; + _maxUncertainty = maxUncertainty; + } + + [JsonIgnore] + public double Latitude + { + get + { + return _latitude; + } + } + + [JsonIgnore] + public double Longitude + { + get + { + return _longitude; + } + } + + [JsonIgnore] + public double Radius + { + get + { + return _radius; + } + } + + [JsonIgnore] + public double MaxUncertainty + { + get + { + return _maxUncertainty; + } + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Extensions/Extension.cs b/src/Yoti.Auth/DigitalIdentity/Extensions/Extension.cs new file mode 100644 index 000000000..da0243e8f --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Extensions/Extension.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Extensions +{ + /// + /// Type and content of a feature for an application. Implemented , + /// and adds generic content on top + /// + /// Type of the extension's content + public class Extension : BaseExtension + { + [JsonProperty(PropertyName = "content")] + private readonly T _content; + + public Extension(string type, T content) : base(type) + { + _content = content; + } + + /// + /// Get the feature's details + /// + /// The payload of the operation + [JsonIgnore] + public T Content + { + get + { + return _content; + } + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Extensions/ExtensionBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Extensions/ExtensionBuilder.cs new file mode 100644 index 000000000..c5aea619d --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Extensions/ExtensionBuilder.cs @@ -0,0 +1,25 @@ +namespace Yoti.Auth.DigitalIdentity.Extensions +{ + public class ExtensionBuilder + { + private string _type; + private T _content; + + public ExtensionBuilder WithType(string type) + { + _type = type; + return this; + } + + public ExtensionBuilder WithContent(T content) + { + _content = content; + return this; + } + + public Extension Build() + { + return new Extension(_type, _content); + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Extensions/LocationConstraintContent.cs b/src/Yoti.Auth/DigitalIdentity/Extensions/LocationConstraintContent.cs new file mode 100644 index 000000000..15e0cb32f --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Extensions/LocationConstraintContent.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Extensions +{ + public class LocationConstraintContent + { + [JsonProperty(PropertyName = "expected_device_location")] + private readonly DeviceLocation _expectedDeviceLocation; + + public LocationConstraintContent(double latitude, double longitude, double radius, double maxUncertainty) + { + _expectedDeviceLocation = new DeviceLocation(latitude, longitude, radius, maxUncertainty); + } + + [JsonIgnore] + public DeviceLocation ExpectedDeviceLocation + { + get + { + return _expectedDeviceLocation; + } + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Extensions/LocationConstraintExtensionBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Extensions/LocationConstraintExtensionBuilder.cs new file mode 100644 index 000000000..0a633c913 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Extensions/LocationConstraintExtensionBuilder.cs @@ -0,0 +1,66 @@ +namespace Yoti.Auth.DigitalIdentity.Extensions +{ + public class LocationConstraintExtensionBuilder + { + private double _latitude; + private double _longitude; + private double _radius = 150d; + private double _maxUncertainty = 150d; + + /// + /// Allows you to specify the Latitude of the user's expected location. + /// + /// + /// This LocationConstraintExtensionBuilder + public LocationConstraintExtensionBuilder WithLatitude(double latitude) + { + Validation.WithinRange(latitude, -90d, 90d, nameof(latitude)); + _latitude = latitude; + return this; + } + + /// + /// Allows you to specify the Longitude of the user's expected location. + /// + /// + /// This LocationConstraintExtensionBuilder + public LocationConstraintExtensionBuilder WithLongitude(double longitude) + { + Validation.WithinRange(longitude, -180d, 180d, nameof(longitude)); + _longitude = longitude; + return this; + } + + /// + /// Radius of the circle, centred on the specified location coordinates, where the device is + /// allowed to perform the share. If not provided, a default value of 150m will be used. + /// + /// The allowable distance, in metres, from the given lat/long location + /// This LocationConstraintExtensionBuilder + public LocationConstraintExtensionBuilder WithRadius(double radius) + { + Validation.NotLessThan(radius, 0d, nameof(radius)); + _radius = radius; + return this; + } + + /// + /// Maximum acceptable distance, in metres, of the area of uncertainty associated with the + /// device location coordinates. If not provided, a default value of 150m will be used. + /// + /// Maximum allowed measurement uncertainty, in metres + /// This LocationConstraintExtensionBuilder + public LocationConstraintExtensionBuilder WithMaxUncertainty(double maxUncertainty) + { + Validation.NotLessThan(maxUncertainty, 0d, nameof(maxUncertainty)); + _maxUncertainty = maxUncertainty; + return this; + } + + public Extension Build() + { + LocationConstraintContent content = new LocationConstraintContent(_latitude, _longitude, _radius, _maxUncertainty); + return new Extension(Constants.Extension.LocationConstraint, content); + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Extensions/ThirdPartyAttributeContent.cs b/src/Yoti.Auth/DigitalIdentity/Extensions/ThirdPartyAttributeContent.cs new file mode 100644 index 000000000..7aa92fb81 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Extensions/ThirdPartyAttributeContent.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json; +using Yoti.Auth.Share.ThirdParty; + +namespace Yoti.Auth.DigitalIdentity.Extensions +{ + public class ThirdPartyAttributeContent + { + private readonly DateTime _expiryDate; + + public ThirdPartyAttributeContent(DateTime expiryDate, List definitions) + { + _expiryDate = expiryDate; + Definitions = definitions; + } + + [JsonProperty(PropertyName = "definitions")] + public List Definitions { get; private set; } + + [JsonProperty(PropertyName = "expiry_date")] + public string ExpiryDate + { + get + { + return _expiryDate.ToString(Constants.Format.RFC3339PatternMilli, DateTimeFormatInfo.InvariantInfo); + } + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Extensions/ThirdPartyAttributeExtensionBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Extensions/ThirdPartyAttributeExtensionBuilder.cs new file mode 100644 index 000000000..92df28e6f --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Extensions/ThirdPartyAttributeExtensionBuilder.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using Yoti.Auth.Share.ThirdParty; + +namespace Yoti.Auth.DigitalIdentity.Extensions +{ + public class ThirdPartyAttributeExtensionBuilder : ExtensionBuilder + { + private DateTime _expiryDate; + private List _definitions; + + public ThirdPartyAttributeExtensionBuilder() + { + _definitions = new List(); + } + + /// + /// Allows you to specify the expiry date of the third party attribute + /// + /// + public ThirdPartyAttributeExtensionBuilder WithExpiryDate(DateTime expiryDate) + { + _expiryDate = expiryDate; + return this; + } + + /// + /// Add a definition to the list of specified third party attribute definitions + /// + /// + public ThirdPartyAttributeExtensionBuilder WithDefinition(string definition) + { + Validation.NotNullOrEmpty(definition, nameof(definition)); + + _definitions.Add(new AttributeDefinition(definition)); + return this; + } + + /// + /// Set the list of third party attribute definitions (will override any previously set definitions) + /// + /// + public ThirdPartyAttributeExtensionBuilder WithDefinitions(List definitions) + { + Validation.NotNull(definitions, nameof(definitions)); + + var attributeDefinitions = new List(); + + foreach (string definition in definitions) + { + attributeDefinitions.Add(new AttributeDefinition(definition)); + } + + _definitions = attributeDefinitions; + return this; + } + + public new Extension Build() + { + var thirdPartyAttributeContent = new ThirdPartyAttributeContent(_expiryDate, _definitions); + + return new Extension( + Constants.Extension.ThirdPartyAttribute, + thirdPartyAttributeContent); + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Extensions/TransactionalFlowExtensionBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Extensions/TransactionalFlowExtensionBuilder.cs new file mode 100644 index 000000000..05ac8fc88 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Extensions/TransactionalFlowExtensionBuilder.cs @@ -0,0 +1,25 @@ +namespace Yoti.Auth.DigitalIdentity.Extensions +{ + /// + /// Allows you to provide a non-null object representing the content to be submitted in the + /// TRANSACTIONAL_FLOW extension. + /// + /// The type of the content + public class TransactionalFlowExtensionBuilder + { + private T _content; + + public TransactionalFlowExtensionBuilder WithContent(T content) + { + Validation.NotNull(content, nameof(content)); + + _content = content; + return this; + } + + public Extension Build() + { + return new Extension(Constants.Extension.TransactionalFlow, _content); + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/Constraint.cs b/src/Yoti.Auth/DigitalIdentity/Policy/Constraint.cs new file mode 100644 index 000000000..df5fc9e4d --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/Constraint.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class Constraint + { + [JsonRequired] + [JsonProperty(PropertyName = "type")] + public string ConstraintType { get; private set; } + + public Constraint(string constraintType) + { + ConstraintType = constraintType; + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicy.cs b/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicy.cs new file mode 100644 index 000000000..3f479a1b9 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicy.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + /// + /// Set of data required to request a sharing transaction + /// + public class DynamicPolicy + { + internal const int SelfieAuthType = 1; + internal const int PinAuthType = 2; + + [JsonProperty(PropertyName = "wanted")] + private readonly ICollection _wantedAttributes; + + [JsonProperty(PropertyName = "wanted_auth_types")] + private readonly HashSet _wantedAuthTypes; + + [JsonProperty(PropertyName = "wanted_remember_me")] + private readonly bool _wantedRememberMeId; + +#pragma warning disable 0414 //"Value never used" warning: the JsonProperty is used when creating the DynamicPolicy JSON + + [JsonProperty(PropertyName = "wanted_remember_me_optional")] + private readonly bool _isWantedRememberMeIdOptional; + +#pragma warning restore 0414 + + [JsonProperty(PropertyName = "identity_profile_requirements")] + private readonly object _identityProfileRequirements; + + public DynamicPolicy( + ICollection wantedAttributes, + HashSet wantedAuthTypes, + bool wantedRememberMeId, + object identityProfileRequirements = null + ) + { + _wantedAttributes = wantedAttributes; + _wantedAuthTypes = wantedAuthTypes; + _wantedRememberMeId = wantedRememberMeId; + _isWantedRememberMeIdOptional = false; + _identityProfileRequirements = identityProfileRequirements; + } + + /// + /// Set of required + /// + [JsonIgnore] + public ICollection WantedAttributes + { + get + { + return _wantedAttributes; + } + } + + /// + /// Type of authentications + /// + [JsonIgnore] + public HashSet WantedAuthTypes + { + get + { + return _wantedAuthTypes; + } + } + + /// + /// Is RememberMeId wanted in the policy + /// + [JsonIgnore] + public bool WantedRememberMeId + { + get + { + return _wantedRememberMeId; + } + } + + /// + /// IdentityProfileRequirements requested in the policy + /// + [JsonIgnore] + public object IdentityProfileRequirements + { + get + { + return _identityProfileRequirements; + } + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicyBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicyBuilder.cs new file mode 100644 index 000000000..a8d3bcd17 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicyBuilder.cs @@ -0,0 +1,166 @@ +using System.Collections.Generic; +using Yoti.Auth.DocScan.Session.Create; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class DynamicPolicyBuilder + { + private readonly Dictionary _wantedAttributes = new Dictionary(); + private readonly HashSet _wantedAuthTypes = new HashSet(); + private bool _wantedRememberMeId; + private object _identityProfileRequirements; + + public DynamicPolicyBuilder WithWantedAttribute(WantedAttribute wantedAttribute) + { + Validation.NotNull(wantedAttribute, nameof(wantedAttribute)); + + string key = wantedAttribute.Derivation ?? wantedAttribute.Name; + + if (wantedAttribute.Constraints?.Count > 0) + { + key += "-" + wantedAttribute.Constraints.GetHashCode(); + } + + _wantedAttributes[key] = wantedAttribute; + return this; + } + + public DynamicPolicyBuilder WithWantedAttribute(string name, List constraints = null) + { + WantedAttribute wantedAttribute = new WantedAttributeBuilder() + .WithName(name) + .WithConstraints(constraints) + .Build(); + return WithWantedAttribute(wantedAttribute); + } + + public DynamicPolicyBuilder WithFamilyName(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.FamilyNameAttribute, constraints); + } + + public DynamicPolicyBuilder WithGivenNames(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.GivenNamesAttribute, constraints); + } + + public DynamicPolicyBuilder WithFullName(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.FullNameAttribute, constraints); + } + + public DynamicPolicyBuilder WithDateOfBirth(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.DateOfBirthAttribute, constraints); + } + + public DynamicPolicyBuilder WithAgeOver(int age, List constraints = null) + { + return WithAgeDerivedAttribute($"{Constants.UserProfile.AgeOverAttribute}:{age}", constraints); + } + + public DynamicPolicyBuilder WithAgeUnder(int age, List constraints = null) + { + return WithAgeDerivedAttribute($"{Constants.UserProfile.AgeUnderAttribute}:{age}", constraints); + } + + private DynamicPolicyBuilder WithAgeDerivedAttribute(string derivation, List constraints) + { + WantedAttribute wantedAttribute = new WantedAttributeBuilder() + .WithName(Constants.UserProfile.DateOfBirthAttribute) + .WithDerivation(derivation) + .WithConstraints(constraints) + .Build(); + return WithWantedAttribute(wantedAttribute); + } + + public DynamicPolicyBuilder WithGender(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.GenderAttribute, constraints); + } + + public DynamicPolicyBuilder WithPostalAddress(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.PostalAddressAttribute, constraints); + } + + public DynamicPolicyBuilder WithStructuredPostalAddress(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.StructuredPostalAddressAttribute, constraints); + } + + public DynamicPolicyBuilder WithNationality(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.NationalityAttribute, constraints); + } + + public DynamicPolicyBuilder WithPhoneNumber(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.PhoneNumberAttribute, constraints); + } + + public DynamicPolicyBuilder WithSelfie(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.SelfieAttribute, constraints); + } + + public DynamicPolicyBuilder WithEmail(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.EmailAddressAttribute, constraints); + } + + public DynamicPolicyBuilder WithDocumentDetails(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.DocumentDetailsAttribute, constraints); + } + + public DynamicPolicyBuilder WithDocumentImages(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.DocumentImagesAttribute, constraints); + } + + public DynamicPolicyBuilder WithSelfieAuthentication(bool enabled) + { + return WithAuthType(DynamicPolicy.SelfieAuthType, enabled); + } + + public DynamicPolicyBuilder WithPinAuthentication(bool enabled) + { + return WithAuthType(DynamicPolicy.PinAuthType, enabled); + } + + public DynamicPolicyBuilder WithAuthType(int authType, bool enabled) + { + if (enabled) + { + _wantedAuthTypes.Add(authType); + return this; + } + + _wantedAuthTypes.Remove(authType); + return this; + } + + public DynamicPolicyBuilder WithRememberMeId(bool required) + { + _wantedRememberMeId = required; + return this; + } + + /// + /// Use an Identity Profile Requirement object for the share + /// + /// object describing the identity profile requirements to use + /// with the identity profile requirements + public DynamicPolicyBuilder WithIdentityProfileRequirements(object identityProfileRequirements) + { + _identityProfileRequirements = identityProfileRequirements; + return this; + } + + public DynamicPolicy Build() + { + return new DynamicPolicy(_wantedAttributes.Values, _wantedAuthTypes, _wantedRememberMeId, _identityProfileRequirements); + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/PreferredSources.cs b/src/Yoti.Auth/DigitalIdentity/Policy/PreferredSources.cs new file mode 100644 index 000000000..b315166ba --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/PreferredSources.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class PreferredSources + { + [JsonProperty(PropertyName = "anchors")] + public List WantedAnchors { get; private set; } + + [JsonProperty(PropertyName = "soft_preference")] + public bool SoftPreference { get; private set; } + + public PreferredSources(List wantedAnchors, bool softPreference = false) + { + WantedAnchors = wantedAnchors; + SoftPreference = softPreference; + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/SourceConstraint.cs b/src/Yoti.Auth/DigitalIdentity/Policy/SourceConstraint.cs new file mode 100644 index 000000000..1430c5b62 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/SourceConstraint.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class SourceConstraint : Constraint + { + private const string _constraintTypeSource = "SOURCE"; + + [JsonProperty(PropertyName = "preferred_sources")] + public PreferredSources PreferredSources { get; private set; } + + public SourceConstraint(List wantedAnchors, bool softPreference) : base(constraintType: _constraintTypeSource) + { + PreferredSources = new PreferredSources(wantedAnchors, softPreference); + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/SourceConstraintBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Policy/SourceConstraintBuilder.cs new file mode 100644 index 000000000..f11dbf7fd --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/SourceConstraintBuilder.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class SourceConstraintBuilder + { + private readonly List _wantedAnchors = new List(); + private bool _softPreference; + + /// + /// Add an anchor to the source constraints list. + /// This is ordered, from the most preferred one (first in the list) + /// to the least preferred one (last in the list). + /// + /// + public SourceConstraintBuilder WithAnchor(WantedAnchor anchor) + { + _wantedAnchors.Add(anchor); + return this; + } + + /// + /// If set to false, it means that only anchors in the list are + /// accepted, in order of preference. + /// If set to true, it instead means that if none of the anchors + /// in the list can be satisfied, then any other anchor that is + /// not in the list is accepted. + /// + /// + public SourceConstraintBuilder WithSoftPreference(bool softPreference) + { + _softPreference = softPreference; + return this; + } + + public SourceConstraintBuilder WithAnchorByValue(string value, string subType) + { + _wantedAnchors.Add( + new WantedAnchorBuilder() + .WithValue(value) + .WithSubType(subType) + .Build()); + + return this; + } + + public SourceConstraintBuilder WithPassport(string subType = "") + { + return WithAnchorByValue(Constants.DocumentDetails.DocumentTypePassport, subType); + } + + public SourceConstraintBuilder WithDrivingLicense(string subType = "") + { + return WithAnchorByValue(Constants.DocumentDetails.DocumentTypeDrivingLicense, subType); + } + + public SourceConstraintBuilder WithNationalId(string subType = "") + { + return WithAnchorByValue(Constants.DocumentDetails.DocumentTypeNationalId, subType); + } + + public SourceConstraintBuilder WithPasscard(string subType = "") + { + return WithAnchorByValue(Constants.DocumentDetails.DocumentTypePassCard, subType); + } + + public SourceConstraint Build() + { + return new SourceConstraint(_wantedAnchors, _softPreference); + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAnchor.cs b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAnchor.cs new file mode 100644 index 000000000..7cf4e9da0 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAnchor.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class WantedAnchor + { + [JsonProperty(PropertyName = "name")] + public string Name { get; private set; } + + [JsonProperty(PropertyName = "sub_type")] + public string SubType { get; private set; } + + public WantedAnchor(string name, string subType) + { + Name = name; + SubType = subType; + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAnchorBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAnchorBuilder.cs new file mode 100644 index 000000000..de66c6b7a --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAnchorBuilder.cs @@ -0,0 +1,37 @@ +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class WantedAnchorBuilder + { + private string _name; + private string _subType; + + /// + /// WithValue sets the anchor's name + /// + /// Anchor name + public WantedAnchorBuilder WithValue(string name) + { + _name = name; + return this; + } + + /// + /// WithSubType sets the anchor's sub-type + /// + /// Anchor sub-type + public WantedAnchorBuilder WithSubType(string subType) + { + _subType = subType; + return this; + } + + /// + /// Builds the WantedAnchor + /// + /// + public WantedAnchor Build() + { + return new WantedAnchor(_name, _subType); + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs new file mode 100644 index 000000000..fa3529bdb --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class WantedAttribute + { + [JsonRequired] + [JsonProperty(PropertyName = "name")] + public string Name { get; private set; } + + [JsonProperty(PropertyName = "derivation")] + public string Derivation { get; private set; } + + [JsonRequired] + [JsonProperty(PropertyName = "optional")] + public bool Optional { get; private set; } + + [JsonProperty(PropertyName = "accept_self_asserted")] + public bool? AcceptSelfAsserted { get; private set; } + + [JsonProperty(PropertyName = "constraints")] + public List Constraints { get; private set; } + + public WantedAttribute(string name, string derivation, List constraints, bool? acceptSelfAsserted = null) + { + Name = name; + Derivation = derivation; + Optional = false; + AcceptSelfAsserted = acceptSelfAsserted; + Constraints = constraints; + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttributeBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttributeBuilder.cs new file mode 100644 index 000000000..266379fd0 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttributeBuilder.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class WantedAttributeBuilder + { + private string _name; + private string _derivation; + private List _constraints = new List(); + private bool? _acceptSelfAsserted; + + public WantedAttributeBuilder WithName(string name) + { + _name = name; + return this; + } + + public WantedAttributeBuilder WithDerivation(string derivation) + { + _derivation = derivation; + return this; + } + + /// + /// Adds a constraint to the wanted attribute. + /// + /// + public WantedAttributeBuilder WithConstraint(Constraint constraint) + { + _constraints.Add(constraint); + return this; + } + + /// + /// Add constraints to the wanted attribute. + /// Calling this will override any previously set constraints for this attribute. + /// + /// Constraints + public WantedAttributeBuilder WithConstraints(List constraints) + { + _constraints = constraints; + return this; + } + + /// + /// Allow or deny the acceptance of self asserted attributes + /// + /// + public WantedAttributeBuilder WithAcceptSelfAsserted(bool acceptSelfAsserted) + { + _acceptSelfAsserted = acceptSelfAsserted; + return this; + } + + public WantedAttribute Build() + { + Validation.NotNullOrEmpty(_name, nameof(_name)); + + return new WantedAttribute(_name, _derivation, _constraints, _acceptSelfAsserted); + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/ShareSession.cs b/src/Yoti.Auth/DigitalIdentity/ShareSession.cs new file mode 100644 index 000000000..6f796235b --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/ShareSession.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Yoti.Auth.DigitalIdentity.Extensions; +using Yoti.Auth.DigitalIdentity.Policy; + +namespace Yoti.Auth.DigitalIdentity +{ + public class ShareSessionRequest + { + [JsonProperty(PropertyName = "callback_endpoint")] + private readonly string _callbackEndpoint; + + [JsonProperty(PropertyName = "policy")] + private readonly DynamicPolicy _dynamicPolicy; + + [JsonProperty(PropertyName = "extensions")] + private readonly List _extensions; + + [JsonProperty(PropertyName = "subject")] + private readonly object _subject; + + [JsonIgnore] + public string CallbackEndpoint + { + get + { + return _callbackEndpoint; + } + } + + [JsonIgnore] + public DynamicPolicy DynamicPolicy + { + get + { + return _dynamicPolicy; + } + } + + [JsonIgnore] + public List Extensions + { + get + { + return _extensions; + } + } + + + [JsonIgnore] + public object Subject + { + get + { + return _subject; + } + } + + public ShareSessionRequest(string callbackEndpoint, DynamicPolicy dynamicPolicy, List extensions = null, object subject = null) + { + _callbackEndpoint = callbackEndpoint; + _dynamicPolicy = dynamicPolicy; + _extensions = extensions ?? new List(); + _subject = subject; + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs new file mode 100644 index 000000000..644086456 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using Yoti.Auth.DigitalIdentity.Extensions; +using Yoti.Auth.DigitalIdentity.Policy; + +namespace Yoti.Auth.DigitalIdentity +{ + public class ShareSessionRequestBuilder + { + private string _callbackEndpoint; + private DynamicPolicy _dynamicPolicy; + private readonly List _extensions = new List(); + private object _subject; + + /// + /// The device's callback endpoint. Must be a URL relative to the Application Domain + /// specified in Yoti Hub + /// + /// + /// with a Callback Endpoint added + public ShareSessionRequestBuilder WithCallbackEndpoint(string callbackEndpoint) + { + _callbackEndpoint = callbackEndpoint; + return this; + } + + /// + /// The customisable to use in the share + /// + /// + /// with a Dynamic Policy added + public ShareSessionRequestBuilder WithPolicy(DynamicPolicy dynamicPolicy) + { + _dynamicPolicy = dynamicPolicy; + return this; + } + + /// + /// to be activated for the application + /// + /// to add + /// with an extension added + public ShareSessionRequestBuilder WithExtension(BaseExtension extension) + { + _extensions.Add(extension); + return this; + } + + /// + /// The subject object + /// + /// The object describing the subject + /// with the subject details provided + public ShareSessionRequestBuilder WithSubject(object subject) + { + _subject = subject; + return this; + } + + public ShareSessionRequest Build() + { + return new ShareSessionRequest(_callbackEndpoint, _dynamicPolicy, _extensions, _subject); + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs new file mode 100644 index 000000000..d9c41ab9e --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs @@ -0,0 +1,49 @@ +using System; +using System.Reflection.Emit; +using Newtonsoft.Json; +using Yoti.Auth.DataObjects; + +namespace Yoti.Auth.DigitalIdentity +{ + public class ShareSessionResult + { +#pragma warning disable 0649 + + // These fields are assigned to by JSON deserialization + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("expiry")] + public string Expiry { get; set; } + + [JsonProperty("created")] + public string Created { get; set; } + + [JsonProperty("updated")] + public string Updated { get; set; } + + [JsonProperty("qrCode")] + public qrCode QrCode { get; set; } + + [JsonProperty("receipt")] + public receipt Receipt { get; set; } + +#pragma warning restore 0649 + + } + + public class qrCode + { + [JsonProperty("id")] + public string Id { get; set; } + } + + public class receipt + { + [JsonProperty("id")] + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentityClient.cs b/src/Yoti.Auth/DigitalIdentityClient.cs new file mode 100644 index 000000000..7601ada78 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentityClient.cs @@ -0,0 +1,112 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Org.BouncyCastle.Crypto; +using Yoti.Auth.Aml; +using Yoti.Auth.DigitalIdentity; + +namespace Yoti.Auth +{ + public class DigitalIdentityClient + { + private readonly string _sdkId; + private readonly AsymmetricCipherKeyPair _keyPair; + private readonly DigitalIdentityClientEngine _yotiDigitalClientEngine; + internal Uri ApiUri { get; private set; } + + /// + /// Create a + /// + /// The client SDK ID provided on the Yoti Hub. + /// + /// The private key file provided on the Yoti Hub as a . + /// + public DigitalIdentityClient(string sdkId, StreamReader privateKeyStream) + : this(new HttpClient(), sdkId, CryptoEngine.LoadRsaKey(privateKeyStream)) + { + } + + /// + /// Create a with a specified + /// + /// Allows the specification of a HttpClient + /// The client SDK ID provided on the Yoti Hub. + /// + /// The private key file provided on the Yoti Hub as a . + /// + public DigitalIdentityClient(HttpClient httpClient, string sdkId, StreamReader privateKeyStream) + : this(httpClient, sdkId, CryptoEngine.LoadRsaKey(privateKeyStream)) + { + } + + /// + /// Create a with a specified + /// + /// Allows the specification of a HttpClient + /// The client SDK ID provided on the Yoti Hub. + /// The key pair from the Yoti Hub. + public DigitalIdentityClient(HttpClient httpClient, string sdkId, AsymmetricCipherKeyPair keyPair) + { + Validation.NotNullOrEmpty(sdkId, nameof(sdkId)); + Validation.NotNull(keyPair, nameof(keyPair)); + + _sdkId = sdkId; + _keyPair = keyPair; + + SetYotiApiUri(); + + _yotiDigitalClientEngine = new DigitalIdentityClientEngine(httpClient); + } + + + + + /// + /// Initiate a sharing process based on a . + /// + /// + /// Details of the device's callback endpoint, and extensions for the application + /// + /// containing a Sharing URL and Reference ID + public ShareSessionResult CreateShareSession(ShareSessionRequest shareSessionRequest) + { + Task task = Task.Run(async () => await CreateShareSessionAsync(shareSessionRequest).ConfigureAwait(false)); + + return task.Result; + } + + /// + /// Asynchronously initiate a sharing process based on a . + /// + /// + /// Details of the device's callback endpoint, and extensions for the application + /// + /// containing a Sharing URL and Reference ID + public async Task CreateShareSessionAsync(ShareSessionRequest shareSessionRequest) + { + return await _yotiDigitalClientEngine.CreateShareSessionAsync(_sdkId, _keyPair, ApiUri, shareSessionRequest).ConfigureAwait(false); + } + + internal void SetYotiApiUri() + { + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("YOTI_API_URL"))) + { + ApiUri = new Uri(Environment.GetEnvironmentVariable("YOTI_API_URL")); + } + else + { + ApiUri = new Uri(Constants.Api.DefaultYotiApiUrl); + } + } + + public DigitalIdentityClient OverrideApiUri(Uri apiUri) + { + ApiUri = apiUri; + + return this; + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentityClientEngine.cs b/src/Yoti.Auth/DigitalIdentityClientEngine.cs new file mode 100644 index 000000000..85637c433 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentityClientEngine.cs @@ -0,0 +1,42 @@ +using System; + +#pragma warning disable S1128 + +using System.Net; + +#pragma warning restore S1128 + +using System.Net.Http; +using System.Threading.Tasks; +using Org.BouncyCastle.Crypto; +using Yoti.Auth.Aml; +using Yoti.Auth.Exceptions; +using Yoti.Auth.DigitalIdentity; +using Yoti.Auth.Web; + +namespace Yoti.Auth +{ + internal class DigitalIdentityClientEngine + { + private readonly HttpClient _httpClient; + + public DigitalIdentityClientEngine(HttpClient httpClient) + { + _httpClient = httpClient; + +#if NET452 || NET462 || NET472 || NET48 + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; +#endif + } + + + public async Task CreateShareSessionAsync(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, ShareSessionRequest shareSessionRequest) + { + ShareSessionResult result = await Task.Run(async () => await DigitalIdentityService.CreateShareSession( + _httpClient, apiUrl, sdkId, keyPair, shareSessionRequest).ConfigureAwait(false)) + .ConfigureAwait(false); + + return result; + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/Yoti.Auth.csproj b/src/Yoti.Auth/Yoti.Auth.csproj index 167046de8..cda182dfd 100644 --- a/src/Yoti.Auth/Yoti.Auth.csproj +++ b/src/Yoti.Auth/Yoti.Auth.csproj @@ -58,6 +58,9 @@ + + + @@ -68,4 +71,7 @@ Resources.Designer.cs + + + diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs new file mode 100644 index 000000000..9d91c54df --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Org.BouncyCastle.Crypto; +using Yoti.Auth.DigitalIdentity; +using Yoti.Auth.Tests.Common; + +namespace Yoti.Auth.Tests.DigitalIdentity +{ + [TestClass] + public class DigitalIdentityServiceTests + { + private const string _sdkID = "sdkID"; + private readonly Uri _apiURL = new Uri("https://apiurl.com"); + private readonly Dictionary _someHeaders = new Dictionary(); + private readonly HttpClient _httpClient = new HttpClient(); + private readonly AsymmetricCipherKeyPair _keyPair = KeyPair.Get(); + private ShareSessionRequest _someShareSessionRequest; + + [TestInitialize] + public void Startup() + { + _someHeaders.Add("Key", "Value"); + _someShareSessionRequest = TestTools.ShareSession.CreateStandardShareSessionRequest(); + } + + [TestMethod] + public void ShouldFailWithNullHttpClient() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateShareSession(null, _apiURL, _sdkID, _keyPair, _someShareSessionRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("httpClient")); + } + + [TestMethod] + public void ShouldFailWithNullApiUrl() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateShareSession(_httpClient, null, _sdkID, _keyPair, _someShareSessionRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("apiUrl")); + } + + [TestMethod] + public void ShouldFailWithNullSdkId() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateShareSession(_httpClient, _apiURL, null, _keyPair, _someShareSessionRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("sdkId")); + } + + [TestMethod] + public void ShouldFailWithNullKeyPair() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateShareSession(_httpClient, _apiURL, _sdkID, null, _someShareSessionRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("keyPair")); + } + + [TestMethod] + public void ShouldFailWithNullDynamicScenario() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateShareSession(_httpClient, _apiURL, _sdkID, _keyPair, null).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("shareSessionRequest")); + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/ExtensionBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/ExtensionBuilderTests.cs new file mode 100644 index 000000000..901fb5159 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/ExtensionBuilderTests.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Yoti.Auth.DigitalIdentity.Extensions; + +namespace Yoti.Auth.Tests.DigitalIdentity.Extensions +{ + [TestClass] + public class ExtensionBuilderTests + { + private const string _someType = "Some Type"; + private static readonly Dictionary _someContent = new Dictionary(); + + [TestMethod] + public void ShouldBuildWithTypeAndContent() + { + var extension = new ExtensionBuilder>() + .WithType(_someType) + .WithContent(_someContent) + .Build(); + + Assert.AreEqual(_someType, extension.ExtensionType); + Assert.AreEqual(_someContent, extension.Content); + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/LocationConstraintExtensionBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/LocationConstraintExtensionBuilderTests.cs new file mode 100644 index 000000000..eec03f9d4 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/LocationConstraintExtensionBuilderTests.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Yoti.Auth.ShareUrl.Extensions; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; + +namespace Yoti.Auth.Tests.DigitalIdentity.Extensions +{ + [TestClass] + public class LocationConstraintExtensionBuilderTests + { + private const double _someLatitude = 1d; + private const double _someLongitude = 2d; + private const double _someRadius = 3d; + private const double _someUncertainty = 4d; + + [DataTestMethod] + [DataRow(-91)] + [DataRow(91)] + [TestMethod] + public void ShouldFailForLatitudesOutsideOfRange(double latitude) + { + Assert.ThrowsException(() => + { + new LocationConstraintExtensionBuilder() + .WithLatitude(latitude) + .Build(); + }); + } + + [DataTestMethod] + [DataRow(-181)] + [DataRow(181)] + [TestMethod] + public void ShouldFailForLongitudesOutsideOfRange(double longitude) + { + Assert.ThrowsException(() => + { + new LocationConstraintExtensionBuilder() + .WithLongitude(longitude) + .Build(); + }); + } + + [TestMethod] + public void ShouldFailForRadiusLessThanZero() + { + Assert.ThrowsException(() => + { + new LocationConstraintExtensionBuilder() + .WithRadius(-1) + .Build(); + }); + } + + [TestMethod] + public void ShouldFailForUncertaintyLessThanZero() + { + Assert.ThrowsException(() => + { + new LocationConstraintExtensionBuilder() + .WithMaxUncertainty(-1) + .Build(); + }); + } + + [TestMethod] + public void ShouldBuildLocationConstraintWithGivenValues() + { + Extension extension = new LocationConstraintExtensionBuilder() + .WithLatitude(_someLatitude) + .WithLongitude(_someLongitude) + .WithRadius(_someRadius) + .WithMaxUncertainty(_someUncertainty) + .Build(); + + Assert.AreEqual(Constants.Extension.LocationConstraint, extension.ExtensionType); + DeviceLocation deviceLocation = extension.Content.ExpectedDeviceLocation; + Assert.AreEqual(_someLatitude, deviceLocation.Latitude); + Assert.AreEqual(_someLongitude, deviceLocation.Longitude); + Assert.AreEqual(_someRadius, deviceLocation.Radius); + Assert.AreEqual(_someUncertainty, deviceLocation.MaxUncertainty); + } + + [TestMethod] + public void ShouldBuildLocationConstraintWithDefaultValues() + { + Extension extension = new LocationConstraintExtensionBuilder() + .WithLatitude(_someLatitude) + .WithLongitude(_someLongitude) + .Build(); + + Assert.AreEqual(Constants.Extension.LocationConstraint, extension.ExtensionType); + DeviceLocation deviceLocation = extension.Content.ExpectedDeviceLocation; + Assert.AreEqual(_someLatitude, deviceLocation.Latitude); + Assert.AreEqual(_someLongitude, deviceLocation.Longitude); + Assert.AreEqual(150d, deviceLocation.Radius); + Assert.AreEqual(150d, deviceLocation.MaxUncertainty); + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/ThirdPartyAttributeExtensionBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/ThirdPartyAttributeExtensionBuilderTests.cs new file mode 100644 index 000000000..c610e9b18 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/ThirdPartyAttributeExtensionBuilderTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Yoti.Auth.Share.ThirdParty; +using Yoti.Auth.DigitalIdentity.Extensions; + +namespace Yoti.Auth.Tests.DigitalIdentity.Extensions +{ + [TestClass] + public class ThirdPartyAttributeExtensionBuilderTests + { + private readonly DateTime _someDate = DateTime.Today.AddDays(1); + private const string _someDefinition = "com.thirdparty.id"; + + [TestMethod] + public void ShouldFailForNullDefinition() + { + var exception = Assert.ThrowsException(() => + { + new ThirdPartyAttributeExtensionBuilder() + .WithDefinition(null) + .Build(); + }); + + Assert.IsTrue(exception.Message.Contains("definition")); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + public void ShouldFailForInvalidDefinitions(string definition) + { + var exception = Assert.ThrowsException(() => + { + new ThirdPartyAttributeExtensionBuilder() + .WithDefinition(definition) + .Build(); + }); + + Assert.IsTrue(exception.Message.Contains("definition")); + } + + [TestMethod] + public void ShouldBuildThirdPartyAttributeExtensionWithGivenValues() + { + Extension extension = + new ThirdPartyAttributeExtensionBuilder() + .WithDefinition(_someDefinition) + .WithExpiryDate(_someDate) + .Build(); + + Assert.AreEqual(Constants.Extension.ThirdPartyAttribute, extension.ExtensionType); + + string expectedDate = _someDate.ToString(Constants.Format.RFC3339PatternMilli, CultureInfo.InvariantCulture); + Assert.AreEqual(expectedDate, extension.Content.ExpiryDate); + + List definitions = extension.Content.Definitions; + Assert.AreEqual(1, definitions.Count); + Assert.AreEqual(_someDefinition, definitions[0].Name); + } + + [DataTestMethod] + [DataRow("2006-01-02T22:04:05Z", "2006-01-02T22:04:05.000Z")] + [DataRow("2006-01-02T22:04:05.1Z", "2006-01-02T22:04:05.100Z")] + [DataRow("2006-01-02T22:04:05.12Z", "2006-01-02T22:04:05.120Z")] + [DataRow("2006-01-02T22:04:05.123Z", "2006-01-02T22:04:05.123Z")] + [DataRow("2006-01-02T22:04:05.1234Z", "2006-01-02T22:04:05.123Z")] + [DataRow("2006-01-02T22:04:05.999999Z", "2006-01-02T22:04:05.999Z")] + [DataRow("2006-01-02T22:04:05.123456Z", "2006-01-02T22:04:05.123Z")] + [DataRow("2002-10-02T10:00:00.1-05:00", "2002-10-02T15:00:00.100Z")] + [DataRow("2002-10-02T10:00:00.12345+11:00", "2002-10-01T23:00:00.123Z")] + [TestMethod] + public void ShouldBuildThirdPartyAttributeExtensionWithExpiryDates(string expiryDateInputString, string expectedExpiryDate) + { + bool parseSuccess = DateTime.TryParse( + expiryDateInputString, + CultureInfo.InvariantCulture, + DateTimeStyles.AdjustToUniversal, + out DateTime expiryDate); + + Assert.IsTrue(parseSuccess); + + Extension extension = + new ThirdPartyAttributeExtensionBuilder() + .WithDefinition(_someDefinition) + .WithExpiryDate(expiryDate) + .Build(); + + Assert.AreEqual(expectedExpiryDate, extension.Content.ExpiryDate); + } + + [TestMethod] + public void ShouldBuildThirdPartyAttributeExtensionWithMultipleDefinitions() + { + var definitions = new List { "firstDefinition", "secondDefinition" }; + + Extension extension = + new ThirdPartyAttributeExtensionBuilder() + .WithDefinitions(definitions) + .WithExpiryDate(_someDate) + .Build(); + + Assert.AreEqual(Constants.Extension.ThirdPartyAttribute, extension.ExtensionType); + + List result = extension.Content.Definitions; + Assert.AreEqual(2, result.Count); + Assert.AreEqual("firstDefinition", result[0].Name); + Assert.AreEqual("secondDefinition", result[1].Name); + } + + [TestMethod] + public void ShouldOverwriteSingularlyAddedDefinition() + { + var definitions = new List { "firstDefinition", "secondDefinition" }; + + Extension extension = + new ThirdPartyAttributeExtensionBuilder() + .WithExpiryDate(_someDate) + .WithDefinition(_someDefinition) + .WithDefinitions(definitions) + .Build(); + + Assert.AreEqual(Constants.Extension.ThirdPartyAttribute, extension.ExtensionType); + + List result = extension.Content.Definitions; + Assert.AreEqual(2, result.Count); + Assert.AreEqual("firstDefinition", result[0].Name); + Assert.AreEqual("secondDefinition", result[1].Name); + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/TransactionalFlowExtensionBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/TransactionalFlowExtensionBuilderTests.cs new file mode 100644 index 000000000..878a818af --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/TransactionalFlowExtensionBuilderTests.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Yoti.Auth.ShareUrl.Extensions; + +namespace Yoti.Auth.Tests.DigitalIdentity.Extensions +{ + [TestClass] + public class TransactionalFlowExtensionBuilderTests + { + private readonly object _objectContent = new object(); + private readonly DateTime _dateTimeContent = new DateTime(1980, 1, 1); + + [TestMethod] + public void ShouldFailForNullContent() + { + Assert.ThrowsException(() => + { + new TransactionalFlowExtensionBuilder() + .WithContent(null) + .Build(); + }); + } + + [TestMethod] + public void ShouldBuildWithObjectContent() + { + Extension extension = new TransactionalFlowExtensionBuilder() + .WithContent(_objectContent) + .Build(); + + Assert.AreEqual(_objectContent, extension.Content); + } + + [TestMethod] + public void ShouldBuildWithDateTimeContent() + { + Extension extension = new TransactionalFlowExtensionBuilder() + .WithContent(_dateTimeContent) + .Build(); + + Assert.AreEqual(_dateTimeContent, extension.Content); + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs new file mode 100644 index 000000000..d5b42411a --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs @@ -0,0 +1,329 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Yoti.Auth.Constants; +using Yoti.Auth.DigitalIdentity.Policy; +using Yoti.Auth.Tests.TestData; + +namespace Yoti.Auth.Tests.DigitalIdentity.Policy +{ + [TestClass] + public class DynamicPolicyBuilderTests + { + private readonly int _expectedSelfieAuthValue = 1; + private readonly int _expectedPinAuthValue = 2; + + [TestMethod] + public void AttributeShouldOnlyExistOnce() + { + WantedAttribute wantedAttribute = new WantedAttributeBuilder() + .WithName("SomeAttributeName") + .Build(); + + DynamicPolicy result = new DynamicPolicyBuilder() + .WithWantedAttribute(wantedAttribute) + .WithWantedAttribute(wantedAttribute) + .Build(); + + Assert.AreEqual(1, result.WantedAttributes.Count); + Assert.IsTrue(result.WantedAttributes.Contains(wantedAttribute)); + } + + [TestMethod] + public void ShouldContainAllAddedAttributes() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithFamilyName() + .WithGivenNames() + .WithFullName() + .WithDateOfBirth() + .WithGender() + .WithPostalAddress() + .WithStructuredPostalAddress() + .WithNationality() + .WithPhoneNumber() + .WithSelfie() + .WithEmail() + .WithDocumentDetails() + .WithDocumentImages() + .WithAgeOver(55) + .WithAgeUnder(18) + .Build(); + + ICollection result = dynamicPolicy.WantedAttributes; + var attributeMatcher = new WantedAttributeMatcher(result); + + Assert.AreEqual(15, result.Count); + + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.FamilyNameAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.GivenNamesAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.FullNameAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.GenderAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.PostalAddressAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.StructuredPostalAddressAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.NationalityAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.PhoneNumberAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.SelfieAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.EmailAddressAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.DocumentImagesAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.DocumentDetailsAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.DateOfBirthAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.DateOfBirthAttribute, derivation: $"{Constants.UserProfile.AgeOverAttribute}:55")); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.DateOfBirthAttribute, derivation: $"{Constants.UserProfile.AgeUnderAttribute}:18")); + } + + [TestMethod] + public void ShouldBuildWithMultipleAgeDerivedAttributes() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithDateOfBirth() + .WithAgeOver(18) + .WithAgeUnder(30) + .WithAgeUnder(40) + .Build(); + + ICollection result = dynamicPolicy.WantedAttributes; + var attributeMatcher = new WantedAttributeMatcher(result); + + Assert.AreEqual(4, result.Count); + + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.DateOfBirthAttribute)); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.DateOfBirthAttribute, derivation: $"{UserProfile.AgeOverAttribute}:{18}")); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.DateOfBirthAttribute, derivation: $"{UserProfile.AgeUnderAttribute}:{30}")); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.DateOfBirthAttribute, derivation: $"{UserProfile.AgeUnderAttribute}:{40}")); + } + + [TestMethod] + public void ShouldOverwriteIdenticalAgeVerificationToEnsureItOnlyExistsOnce() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithAgeUnder(30) + .WithAgeUnder(30) + .Build(); + + ICollection result = dynamicPolicy.WantedAttributes; + var attributeMatcher = new WantedAttributeMatcher(result); + + Assert.AreEqual(1, result.Count); + + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.DateOfBirthAttribute, derivation: $"{UserProfile.AgeUnderAttribute}:{30}")); + } + + [TestMethod] + public void ShouldAddMultipleAttributesWithSameNameAndDifferentConstraints() + { + var passportConstraint = new SourceConstraintBuilder() + .WithPassport() + .Build(); + + var docImage1 = new WantedAttributeBuilder() + .WithName(Yoti.Auth.Constants.UserProfile.DocumentImagesAttribute) + .WithConstraint(passportConstraint) + .Build(); + + var drivingLicenseConstraint = new SourceConstraintBuilder() + .WithDrivingLicense() + .Build(); + + var docImage2 = new WantedAttributeBuilder() + .WithName(Yoti.Auth.Constants.UserProfile.DocumentImagesAttribute) + .WithConstraints(new List { drivingLicenseConstraint }) + .Build(); + + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithWantedAttribute(docImage1) + .WithWantedAttribute(docImage2) + .Build(); + + ICollection result = dynamicPolicy.WantedAttributes; + var attributeMatcher = new WantedAttributeMatcher(result); + + Assert.AreEqual(2, result.Count); + + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.DocumentImagesAttribute, null, new List { passportConstraint })); + Assert.IsTrue(attributeMatcher.ContainsAttribute(UserProfile.DocumentImagesAttribute, null, new List { drivingLicenseConstraint })); + } + + [TestMethod] + public void ShouldBuildWithAuthTypesTrue() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithSelfieAuthentication(enabled: true) + .WithPinAuthentication(enabled: true) + .WithAuthType(authType: 99, enabled: true) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(3, result.Count); + Assert.IsTrue(result.SetEquals(new HashSet { _expectedSelfieAuthValue, _expectedPinAuthValue, 99 })); + } + + [TestMethod] + public void ShouoldBuildWithAuthTypesFalse() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithSelfieAuthentication(enabled: false) + .WithPinAuthentication(enabled: false) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void ShouldBuildWithAuthTypeEnabledThenDisabled() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithAuthType(24, enabled: true) + .WithAuthType(24, enabled: false) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void ShouldBuildWithAuthTypeDisabledThenEnabled() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithAuthType(23, enabled: false) + .WithAuthType(23, enabled: true) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(1, result.Count); + Assert.IsTrue(result.SetEquals(new HashSet { 23 })); + } + + [TestMethod] + public void ShouldBuildWithSelfieAuthenticationEnabledThenDisabled() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithSelfieAuthentication(enabled: true) + .WithSelfieAuthentication(enabled: false) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void ShouldBuildWithSelfieAuthenticationDisabledThenEnabled() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithSelfieAuthentication(enabled: false) + .WithSelfieAuthentication(enabled: true) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(1, result.Count); + Assert.IsTrue(result.SetEquals(new HashSet { _expectedSelfieAuthValue })); + } + + [TestMethod] + public void ShouldBuildWithSelfieAuthenticationDisabled() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithSelfieAuthentication(enabled: false) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void ShouldFilterSelfieAuthenticationDuplicates() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithSelfieAuthentication(enabled: true) + .WithAuthType(DynamicPolicy.SelfieAuthType, enabled: true) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(1, result.Count); + Assert.IsTrue(result.SetEquals(new HashSet { _expectedSelfieAuthValue })); + } + + [TestMethod] + public void ShouldFilterPinAuthenticationDuplicates() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithPinAuthentication(enabled: true) + .WithAuthType(DynamicPolicy.PinAuthType, enabled: true) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(1, result.Count); + Assert.IsTrue(result.SetEquals(new HashSet { _expectedPinAuthValue })); + } + + [TestMethod] + public void ShouldBuildWithPinAuthenticationEnabledThenDisabled() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithPinAuthentication(enabled: true) + .WithPinAuthentication(enabled: false) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void ShouldBuildWithPinAuthenticationDisabledThenEnabled() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithPinAuthentication(enabled: false) + .WithPinAuthentication(enabled: true) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(1, result.Count); + Assert.IsTrue(result.SetEquals(new HashSet { _expectedPinAuthValue })); + } + + [TestMethod] + public void ShouldBuildWithPinAuthenticationDisabled() + { + DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + .WithPinAuthentication(enabled: false) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void ShouldBuildWithRememberMeFlag() + { + DynamicPolicy result = new DynamicPolicyBuilder() + .WithRememberMeId(true) + .Build(); + + Assert.IsTrue(result.WantedRememberMeId); + } + + [TestMethod] + public void ShouldBuildWithIdentityProfileRequirements() + { + object identityProfileRequirements = IdentityProfiles.CreateStandardIdentityProfileRequirements(); + + DynamicPolicy result = new DynamicPolicyBuilder() + .WithIdentityProfileRequirements(identityProfileRequirements) + .Build(); + + Assert.AreEqual(identityProfileRequirements, result.IdentityProfileRequirements); + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeBuilderTests.cs new file mode 100644 index 000000000..a302eee43 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeBuilderTests.cs @@ -0,0 +1,164 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Yoti.Auth.ShareUrl.Policy; + +namespace Yoti.Auth.Tests.DigitalIdentity.Policy +{ + [TestClass] + public class WantedAttributeBuilderTests + { + private const string _someName = "some name"; + private const string _someDerivation = "some derivation"; + + [TestMethod] + public void BuildsAnAttribute() + { + SourceConstraint sourceConstraint = new SourceConstraintBuilder() + .WithDrivingLicense() + .Build(); + + WantedAttribute result = new WantedAttributeBuilder() + .WithName(_someName) + .WithDerivation(_someDerivation) + .WithConstraint(sourceConstraint) + .Build(); + + Assert.AreEqual(1, result.Constraints.Count); + Assert.AreEqual(_someName, result.Name); + Assert.AreEqual(_someDerivation, result.Derivation); + } + + [TestMethod] + public void ShouldSetAcceptSelfAssertedToNullByDefault() + { + WantedAttribute result = new WantedAttributeBuilder() + .WithName("name") + .Build(); + + Assert.AreEqual(null, result.AcceptSelfAsserted); + } + + [TestMethod] + public void ShouldRetainLatestAcceptSelfAsserted() + { + WantedAttribute result = new WantedAttributeBuilder() + .WithName("name") + .WithAcceptSelfAsserted(false) + .WithAcceptSelfAsserted(true) + .Build(); + + Assert.AreEqual(true, result.AcceptSelfAsserted); + } + + [TestMethod] + public void ShouldGenerateWithAnchor() + { + string wantedAnchorName = "name"; + string wantedAnchorSubType = "subType"; + Constraint sourceConstraint = new SourceConstraintBuilder() + .WithAnchor(new WantedAnchor(wantedAnchorName, wantedAnchorSubType)) + .Build(); + + WantedAttribute wantedAttribute = new WantedAttributeBuilder() + .WithName("attribute_name") + .WithConstraint(sourceConstraint) + .Build(); + + var result = (SourceConstraint)wantedAttribute.Constraints.Single(); + + Assert.AreEqual(wantedAnchorName, result.PreferredSources.WantedAnchors.Single().Name); + Assert.AreEqual(wantedAnchorSubType, result.PreferredSources.WantedAnchors.Single().SubType); + } + + [TestMethod] + public void ShouldGenerateWithPasscard() + { + Constraint sourceConstraint = new SourceConstraintBuilder() + .WithPasscard() + .Build(); + + WantedAttribute wantedAttribute = new WantedAttributeBuilder() + .WithName("attribute_name") + .WithConstraint(sourceConstraint) + .Build(); + + var result = (SourceConstraint)wantedAttribute.Constraints.Single(); + Assert.AreEqual("PASS_CARD", result.PreferredSources.WantedAnchors[0].Name); + Assert.AreEqual("", result.PreferredSources.WantedAnchors[0].SubType); + } + + [TestMethod] + public void ShouldGenerateTwoSourceConstraints() + { + Constraint sourceConstraint = new SourceConstraintBuilder() + .WithPassport() + .WithNationalId("AADHAR") + .WithSoftPreference(true) + .Build(); + + WantedAttribute wantedAttribute = new WantedAttributeBuilder() + .WithName("attribute_name") + .WithConstraint(sourceConstraint) + .Build(); + + var result = (SourceConstraint)wantedAttribute.Constraints.Single(); + Assert.IsTrue(result.PreferredSources.SoftPreference); + Assert.AreEqual("SOURCE", result.ConstraintType); + + Assert.AreEqual("PASSPORT", result.PreferredSources.WantedAnchors[0].Name); + Assert.AreEqual("", result.PreferredSources.WantedAnchors[0].SubType); + + Assert.AreEqual("NATIONAL_ID", result.PreferredSources.WantedAnchors[1].Name); + Assert.AreEqual("AADHAR", result.PreferredSources.WantedAnchors[1].SubType); + } + + [TestMethod] + public void WithConstraintShouldAddToCurrentConstraints() + { + Constraint drivingLicenseConstraint = new SourceConstraintBuilder() + .WithDrivingLicense() + .Build(); + + Constraint passcardConstraint = new SourceConstraintBuilder() + .WithPasscard() + .Build(); + + WantedAttribute wantedAttribute = new WantedAttributeBuilder() + .WithName("attribute_name") + .WithConstraints(new List { drivingLicenseConstraint }) + .WithConstraint(passcardConstraint) + .Build(); + + Assert.AreEqual(2, wantedAttribute.Constraints.Count); + + var sourceConstraint1 = (SourceConstraint)wantedAttribute.Constraints.First(); + Assert.AreEqual("DRIVING_LICENCE", sourceConstraint1.PreferredSources.WantedAnchors[0].Name); + + var sourceConstraint2 = (SourceConstraint)wantedAttribute.Constraints.Last(); + Assert.AreEqual("PASS_CARD", sourceConstraint2.PreferredSources.WantedAnchors[0].Name); + } + + [TestMethod] + public void WithConstraintsShouldOverrideCurrentConstraint() + { + Constraint drivingLicenseConstraint = new SourceConstraintBuilder() + .WithDrivingLicense() + .Build(); + + Constraint passcardConstraint = new SourceConstraintBuilder() + .WithPasscard() + .Build(); + + WantedAttribute wantedAttribute = new WantedAttributeBuilder() + .WithName("attribute_name") + .WithConstraint(passcardConstraint) + .WithConstraints(new List { drivingLicenseConstraint }) + .Build(); + + var result = (SourceConstraint)wantedAttribute.Constraints.Single(); + Assert.AreEqual(1, result.PreferredSources.WantedAnchors.Count); + Assert.AreEqual("DRIVING_LICENCE", result.PreferredSources.WantedAnchors[0].Name); + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeMatcher.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeMatcher.cs new file mode 100644 index 000000000..e3497323c --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeMatcher.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using Yoti.Auth.DigitalIdentity.Policy; + +namespace Yoti.Auth.Tests.DigitalIdentity.Policy +{ + internal class WantedAttributeMatcher + { + private readonly ICollection _attributes; + + public WantedAttributeMatcher(ICollection attributes) + { + _attributes = attributes; + } + + public bool ContainsAttribute(string name, string derivation = null, List constraints = null) + { + var expectedAttribute = new WantedAttribute(name, derivation, constraints); + + foreach (var attribute in _attributes) + { + if (attribute.Name == expectedAttribute.Name + && attribute.Derivation == expectedAttribute.Derivation + && ConstraintsMatch(expectedAttribute.Constraints, attribute.Constraints)) + { + return true; + } + } + + return false; + } + + private static bool ConstraintsMatch(List expectedConstraints, List attributeConstraint) + { + if (expectedConstraints == null && attributeConstraint == null) + return true; + + return Enumerable.SequenceEqual(expectedConstraints, attributeConstraint); + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/ShareSessionRequestBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/ShareSessionRequestBuilderTests.cs new file mode 100644 index 000000000..60f935b77 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/ShareSessionRequestBuilderTests.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Yoti.Auth.DigitalIdentity; +using Yoti.Auth.DigitalIdentity.Extensions; +using Yoti.Auth.DigitalIdentity.Policy; +using Yoti.Auth.Tests.TestData; + +namespace Yoti.Auth.Tests.DigitalIdentity +{ + [TestClass] + public class ShareSessionRequestBuilderTests + { + private const string _someEndpoint = "someEndpoint"; + + private readonly BaseExtension extension1 = new ExtensionBuilder() + .WithContent("content") + .WithType("string type") + .Build(); + + private readonly BaseExtension extension2 = new LocationConstraintExtensionBuilder() + .WithLatitude(51.5044772) + .WithLongitude(-0.082161) + .WithMaxUncertainty(300) + .WithRadius(1500) + .Build(); + + [TestMethod] + public void ShouldBuildADynamicScenario() + { + DynamicPolicy somePolicy = TestTools.ShareSession.CreateStandardPolicy(); + object someSubject = IdentityProfiles.CreateStandardSubject(); + + ShareSessionRequest result = new ShareSessionRequestBuilder() + .WithCallbackEndpoint(_someEndpoint) + .WithPolicy(somePolicy) + .WithExtension(extension1) + .WithExtension(extension2) + .WithSubject(someSubject) + .Build(); + + var expectedExtensions = new List { extension1, extension2 }; + + Assert.AreEqual(_someEndpoint, result.CallbackEndpoint); + Assert.AreEqual(somePolicy, result.DynamicPolicy); + CollectionAssert.AreEqual(expectedExtensions, result.Extensions); + Assert.AreEqual(someSubject, result.Subject); + + string serializedScenario = JsonConvert.SerializeObject( + result, + + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + + object deserializedObject; + using (StreamReader r = File.OpenText("TestData/DynamicPolicy.json")) + { + string json = r.ReadToEnd(); + deserializedObject = JsonConvert.DeserializeObject(json); + } + + string expectedJson = JsonConvert.SerializeObject(deserializedObject); + + Assert.AreEqual(expectedJson, serializedScenario); + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs new file mode 100644 index 000000000..67397f379 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs @@ -0,0 +1,93 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Moq.Protected; +using Org.BouncyCastle.Crypto; +using Yoti.Auth.DigitalIdentity; +using Yoti.Auth.Exceptions; +using Yoti.Auth.Tests.Common; + +namespace Yoti.Auth.Tests +{ + [TestClass] + public class DigitalIdentityClientEngineTests + { + private const string EncryptedToken = "b6H19bUCJhwh6WqQX/sEHWX9RP+A/ANr1fkApwA4Dp2nJQFAjrF9e6YCXhNBpAIhfHnN0iXubyXxXZMNwNMSQ5VOxkqiytrvPykfKQWHC6ypSbfy0ex8ihndaAXG5FUF+qcU8QaFPMy6iF3x0cxnY0Ij0kZj0Ng2t6oiNafb7AhT+VGXxbFbtZu1QF744PpWMuH0LVyBsAa5N5GJw2AyBrnOh67fWMFDKTJRziP5qCW2k4h5vJfiYr/EOiWKCB1d/zINmUm94ZffGXxcDAkq+KxhN1ZuNhGlJ2fKcFh7KxV0BqlUWPsIEiwS0r9CJ2o1VLbEs2U/hCEXaqseEV7L29EnNIinEPVbL4WR7vkF6zQCbK/cehlk2Qwda+VIATqupRO5grKZN78R9lBitvgilDaoE7JB/VFcPoljGQ48kX0wje1mviX4oJHhuO8GdFITS5LTbojGVQWT7LUNgAUe0W0j+FLHYYck3v84OhWTqads5/jmnnLkp9bdJSRuJF0e8pNdePnn2lgF+GIcyW/0kyGVqeXZrIoxnObLpF+YeUteRBKTkSGFcy7a/V/DLiJMPmH8UXDLOyv8TVt3ppzqpyUrLN2JVMbL5wZ4oriL2INEQKvw/boDJjZDGeRlu5m1y7vGDNBRDo64+uQM9fRUULPw+YkABNwC0DeShswzT00="; + private readonly AsymmetricCipherKeyPair _keyPair = KeyPair.Get(); + private static HttpRequestMessage _httpRequestMessage; + private const string SdkId = "fake-sdk-id"; + + [TestMethod] + public async Task CreateSessionAsyncShouldReturnCorrectValues() + { + //string shareUrl = @"https://yoti.com/shareurl"; + string refId = "NpdmVVGC-28356678-c236-4518-9de4-7a93009ccaf0-c5f92f2a-5539-453e-babc-9b06e1d6b7de"; + + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{\"id\":\"" + refId + "\",\"status\":\"SOME_STATUS\",\"expiry\":\"SOME_EXPIRY\",\"created\":\"SOME_CREATED\",\"updated\":\"SOME_UPDATED\",\"qrCode\":{\"id\":\"SOME_QRCODE_ID\"},\"receipt\":{\"id\":\"SOME_RECEIPT_ID\"}}"); + + + + + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + ShareSessionRequest shareSessionRequest = TestTools.ShareSession.CreateStandardShareSessionRequest(); + + ShareSessionResult shareSessionResult = await engine.CreateShareSessionAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), shareSessionRequest); + + Assert.IsNotNull(shareSessionResult); + Assert.AreEqual(refId, shareSessionResult.Id); + } + + [DataTestMethod] + [DataRow(HttpStatusCode.BadRequest)] + [DataRow(HttpStatusCode.Unauthorized)] + [DataRow(HttpStatusCode.InternalServerError)] + [DataRow(HttpStatusCode.RequestTimeout)] + [DataRow(HttpStatusCode.NotFound)] + [DataRow(HttpStatusCode.Forbidden)] + public void CreateShareSessionNonSuccessStatusCodesShouldThrowException(HttpStatusCode httpStatusCode) + { + Mock handlerMock = SetupMockMessageHandler( + httpStatusCode, + "{\"status\":\"bad\""); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + + ShareSessionRequest shareSessionRequest = TestTools.ShareSession.CreateStandardShareSessionRequest(); + + var aggregateException = Assert.ThrowsException(() => + { + engine.CreateShareSessionAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiApiUrl), shareSessionRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + } + + private static Mock SetupMockMessageHandler(HttpStatusCode httpStatusCode, string responseContent) + { + var handlerMock = new Mock(MockBehavior.Strict); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage() + { + StatusCode = httpStatusCode, + Content = new StringContent(responseContent) + }) + .Callback((http, token) => _httpRequestMessage = http) + .Verifiable(); + return handlerMock; + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs new file mode 100644 index 000000000..25886aa89 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs @@ -0,0 +1,148 @@ +using System; +using System.IO; +using System.Net.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Org.BouncyCastle.Crypto; +using Yoti.Auth.Aml; +using Yoti.Auth.Tests.Common; + +namespace Yoti.Auth.Tests +{ + [TestClass] + public class DigitalIdentityClientTests + { + private const string _someSdkId = "some-sdk-id"; + private readonly Uri _expectedDefaultUri = new Uri(Constants.Api.DefaultYotiApiUrl); + + [TestInitialize] + public void BeforeTests() + { + Environment.SetEnvironmentVariable("YOTI_API_URL", null); + } + + [TestMethod] + public void NullSdkIdShouldThrowException() + { + StreamReader keystream = KeyPair.GetValidKeyStream(); + string sdkId = null; + Assert.ThrowsException(() => + { + new DigitalIdentityClient(sdkId, keystream); + }); + } + + [TestMethod] + public void EmptySdkIdShouldThrowException() + { + StreamReader keystream = KeyPair.GetValidKeyStream(); + string sdkId = string.Empty; + Assert.ThrowsException(() => + { + new DigitalIdentityClient(sdkId, keystream); + }); + } + + [TestMethod] + public void NoKeyStreamShouldThrowException() + { + StreamReader keystream = null; + Assert.ThrowsException(() => + { + new DigitalIdentityClient(_someSdkId, keystream); + }); + } + + [TestMethod] + public void InvalidKeyStreamShouldThrowException() + { + StreamReader keystream = KeyPair.GetInvalidFormatKeyStream(); + Assert.ThrowsException(() => + { + new DigitalIdentityClient(_someSdkId, keystream); + }); + } + + [TestMethod] + public void NullDynamicScenarioShouldThrowException() + { + DigitalIdentityClient client = CreateDigitalIdentityClient(); + + var aggregateException = Assert.ThrowsException(() => + { + client.CreateShareSession(null); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + } + + [DataTestMethod] + [DataRow("")] + [DataRow(null)] + public void ApiUriDefaultIsUsedForNullOrEmpty(string envVar) + { + Environment.SetEnvironmentVariable("YOTI_API_URL", envVar); + DigitalIdentityClient client = CreateDigitalIdentityClient(); + + Assert.AreEqual(_expectedDefaultUri, client.ApiUri); + } + + [TestMethod] + public void ApiUriOverriddenOverEnvVariable() + { + Uri overriddenApiUri = new Uri("https://overridden.com"); + Environment.SetEnvironmentVariable("YOTI_API_URL", "https://envapiuri.com"); + DigitalIdentityClient client = CreateDigitalIdentityClient(); + client.OverrideApiUri(overriddenApiUri); + + Assert.AreEqual(overriddenApiUri, client.ApiUri); + } + + [TestMethod] + public void ApiUriEnvVariableIsUsed() + { + Environment.SetEnvironmentVariable("YOTI_API_URL", "https://envapiuri.com"); + DigitalIdentityClient client = CreateDigitalIdentityClient(); + + Uri expectedApiUri = new Uri("https://envapiuri.com"); + Assert.AreEqual(expectedApiUri, client.ApiUri); + } + + private static DigitalIdentityClient CreateDigitalIdentityClient() + { + StreamReader privateStreamKey = KeyPair.GetValidKeyStream(); + + return new DigitalIdentityClient(_someSdkId, privateStreamKey); + } + + [TestMethod] + public void ApiUriSetForPrivateKeyInitialisationHttpClient() + { + AsymmetricCipherKeyPair keyPair = KeyPair.Get(); + + DigitalIdentityClient yotiClient = new DigitalIdentityClient(new HttpClient(), _someSdkId, keyPair); + + Assert.AreEqual(_expectedDefaultUri, yotiClient.ApiUri); + } + + [TestMethod] + public void ApiUriSetForStreamInitialisation() + { + StreamReader privateStreamKey = KeyPair.GetValidKeyStream(); + + DigitalIdentityClient yotiClient = new DigitalIdentityClient(_someSdkId, privateStreamKey); + + Assert.AreEqual(_expectedDefaultUri, yotiClient.ApiUri); + } + + [TestMethod] + public void ApiUriSetForStreamInitialisationHttpClient() + { + StreamReader privateStreamKey = KeyPair.GetValidKeyStream(); + + DigitalIdentityClient yotiClient = new DigitalIdentityClient(new HttpClient(), _someSdkId, privateStreamKey); + + Assert.AreEqual(_expectedDefaultUri, yotiClient.ApiUri); + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/TestTools/ShareSession.cs b/test/Yoti.Auth.Tests/TestTools/ShareSession.cs new file mode 100644 index 000000000..88e51bd50 --- /dev/null +++ b/test/Yoti.Auth.Tests/TestTools/ShareSession.cs @@ -0,0 +1,27 @@ +using Yoti.Auth.ShareUrl; +using Yoti.Auth.DigitalIdentity.Policy; +using Yoti.Auth.Tests.TestData; +using Yoti.Auth.DigitalIdentity; + +namespace Yoti.Auth.Tests.TestTools +{ + internal static class ShareSession + { + public static ShareSessionRequest CreateStandardShareSessionRequest() + { + return new ShareSessionRequest("callback", CreateStandardPolicy()); + } + + public static DynamicPolicy CreateStandardPolicy() + { + return new DynamicPolicyBuilder() + .WithDateOfBirth() + .WithAgeOver(18) + .WithAgeUnder(30) + .WithAgeUnder(40) + .WithPinAuthentication(true) + .WithIdentityProfileRequirements(IdentityProfiles.CreateStandardIdentityProfileRequirements()) + .Build(); + } + } +} diff --git a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj index cc28ada40..2a3c5d764 100644 --- a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj +++ b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj @@ -39,6 +39,9 @@ + + + PreserveNewest @@ -68,4 +71,7 @@ PreserveNewest + + + \ No newline at end of file From c2735eaf45d3bbd09d4e50e4a20c785f2970df9a Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Thu, 5 Oct 2023 14:02:20 +0100 Subject: [PATCH 02/47] SDK:2238 refatoring tests --- src/Yoti.Auth/Constants/Api.cs | 2 +- .../DigitalIdentity/DigitalIdentityService.cs | 18 +++--- .../DigitalIdentity/Policy/DynamicPolicy.cs | 4 +- .../Policy/DynamicPolicyBuilder.cs | 58 +++++++++--------- .../DigitalIdentity/Policy/Notification.cs | 14 +++++ .../DigitalIdentity/Policy/WantedAttribute.cs | 4 +- src/Yoti.Auth/DigitalIdentity/ShareSession.cs | 44 ++++++++----- .../ShareSessionRequestBuilder.cs | 35 +++++++---- test/.DS_Store | Bin 6148 -> 6148 bytes test/Yoti.Auth.Tests/.DS_Store | Bin 8196 -> 10244 bytes .../Policy/DynamicPolicyBuilderTests.cs | 45 +++++++------- .../ShareSessionRequestBuilderTests.cs | 10 +-- .../TestData/DigitalIdentity.json | 56 +++++++++++++++++ .../Yoti.Auth.Tests/TestTools/ShareSession.cs | 8 +-- test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj | 4 ++ 15 files changed, 206 insertions(+), 96 deletions(-) create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs create mode 100644 test/Yoti.Auth.Tests/TestData/DigitalIdentity.json diff --git a/src/Yoti.Auth/Constants/Api.cs b/src/Yoti.Auth/Constants/Api.cs index 5f2dec733..08bc4b75e 100644 --- a/src/Yoti.Auth/Constants/Api.cs +++ b/src/Yoti.Auth/Constants/Api.cs @@ -7,7 +7,7 @@ public static class Api public const string DefaultYotiHost = @"https://api.yoti.com"; public const string YotiApiPathPrefix = "api/v1"; - public const string YotiApiSharePathPrefix = "/share"; + public const string YotiApiSharePathPrefix = "share"; public readonly static string DefaultYotiApiUrl = string.Join("/", DefaultYotiHost, YotiApiPathPrefix); public readonly static string DefaultYotiShareApiUrl = string.Join("/", DefaultYotiHost, YotiApiSharePathPrefix); diff --git a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs index c5ef042a6..a77375f7c 100644 --- a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs +++ b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs @@ -27,15 +27,17 @@ internal static async Task CreateShareSession(HttpClient htt }); byte[] body = Encoding.UTF8.GetBytes(serializedScenario); - Request shareUrlRequest = new RequestBuilder() - .WithKeyPair(keyPair) - .WithBaseUri(apiUrl) - .WithEndpoint($"/sessions/v2") - .WithHttpMethod(HttpMethod.Post) - .WithContent(body) - .Build(); + Request shareSessionlRequest = new RequestBuilder() + .WithKeyPair(keyPair) + .WithBaseUri(apiUrl) + .WithHeader("X-Yoti-Auth-Id", sdkId) + .WithEndpoint($"/v2/sessions") + .WithQueryParam("appId", sdkId) + .WithHttpMethod(HttpMethod.Post) + .WithContent(body) + .Build(); - using (HttpResponseMessage response = await shareUrlRequest.Execute(httpClient).ConfigureAwait(false)) + using (HttpResponseMessage response = await shareSessionlRequest.Execute(httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) { diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicy.cs b/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicy.cs index 3f479a1b9..76950a182 100644 --- a/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicy.cs +++ b/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicy.cs @@ -6,7 +6,7 @@ namespace Yoti.Auth.DigitalIdentity.Policy /// /// Set of data required to request a sharing transaction /// - public class DynamicPolicy + public class Policy { internal const int SelfieAuthType = 1; internal const int PinAuthType = 2; @@ -30,7 +30,7 @@ public class DynamicPolicy [JsonProperty(PropertyName = "identity_profile_requirements")] private readonly object _identityProfileRequirements; - public DynamicPolicy( + public Policy( ICollection wantedAttributes, HashSet wantedAuthTypes, bool wantedRememberMeId, diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicyBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicyBuilder.cs index a8d3bcd17..ff4e18a71 100644 --- a/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicyBuilder.cs +++ b/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicyBuilder.cs @@ -3,14 +3,14 @@ namespace Yoti.Auth.DigitalIdentity.Policy { - public class DynamicPolicyBuilder + public class PolicyBuilder { private readonly Dictionary _wantedAttributes = new Dictionary(); private readonly HashSet _wantedAuthTypes = new HashSet(); private bool _wantedRememberMeId; private object _identityProfileRequirements; - public DynamicPolicyBuilder WithWantedAttribute(WantedAttribute wantedAttribute) + public PolicyBuilder WithWantedAttribute(WantedAttribute wantedAttribute) { Validation.NotNull(wantedAttribute, nameof(wantedAttribute)); @@ -25,7 +25,7 @@ public DynamicPolicyBuilder WithWantedAttribute(WantedAttribute wantedAttribute) return this; } - public DynamicPolicyBuilder WithWantedAttribute(string name, List constraints = null) + public PolicyBuilder WithWantedAttribute(string name, List constraints = null) { WantedAttribute wantedAttribute = new WantedAttributeBuilder() .WithName(name) @@ -34,37 +34,37 @@ public DynamicPolicyBuilder WithWantedAttribute(string name, List co return WithWantedAttribute(wantedAttribute); } - public DynamicPolicyBuilder WithFamilyName(List constraints = null) + public PolicyBuilder WithFamilyName(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.FamilyNameAttribute, constraints); } - public DynamicPolicyBuilder WithGivenNames(List constraints = null) + public PolicyBuilder WithGivenNames(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.GivenNamesAttribute, constraints); } - public DynamicPolicyBuilder WithFullName(List constraints = null) + public PolicyBuilder WithFullName(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.FullNameAttribute, constraints); } - public DynamicPolicyBuilder WithDateOfBirth(List constraints = null) + public PolicyBuilder WithDateOfBirth(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.DateOfBirthAttribute, constraints); } - public DynamicPolicyBuilder WithAgeOver(int age, List constraints = null) + public PolicyBuilder WithAgeOver(int age, List constraints = null) { return WithAgeDerivedAttribute($"{Constants.UserProfile.AgeOverAttribute}:{age}", constraints); } - public DynamicPolicyBuilder WithAgeUnder(int age, List constraints = null) + public PolicyBuilder WithAgeUnder(int age, List constraints = null) { return WithAgeDerivedAttribute($"{Constants.UserProfile.AgeUnderAttribute}:{age}", constraints); } - private DynamicPolicyBuilder WithAgeDerivedAttribute(string derivation, List constraints) + private PolicyBuilder WithAgeDerivedAttribute(string derivation, List constraints) { WantedAttribute wantedAttribute = new WantedAttributeBuilder() .WithName(Constants.UserProfile.DateOfBirthAttribute) @@ -74,62 +74,62 @@ private DynamicPolicyBuilder WithAgeDerivedAttribute(string derivation, List constraints = null) + public PolicyBuilder WithGender(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.GenderAttribute, constraints); } - public DynamicPolicyBuilder WithPostalAddress(List constraints = null) + public PolicyBuilder WithPostalAddress(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.PostalAddressAttribute, constraints); } - public DynamicPolicyBuilder WithStructuredPostalAddress(List constraints = null) + public PolicyBuilder WithStructuredPostalAddress(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.StructuredPostalAddressAttribute, constraints); } - public DynamicPolicyBuilder WithNationality(List constraints = null) + public PolicyBuilder WithNationality(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.NationalityAttribute, constraints); } - public DynamicPolicyBuilder WithPhoneNumber(List constraints = null) + public PolicyBuilder WithPhoneNumber(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.PhoneNumberAttribute, constraints); } - public DynamicPolicyBuilder WithSelfie(List constraints = null) + public PolicyBuilder WithSelfie(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.SelfieAttribute, constraints); } - public DynamicPolicyBuilder WithEmail(List constraints = null) + public PolicyBuilder WithEmail(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.EmailAddressAttribute, constraints); } - public DynamicPolicyBuilder WithDocumentDetails(List constraints = null) + public PolicyBuilder WithDocumentDetails(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.DocumentDetailsAttribute, constraints); } - public DynamicPolicyBuilder WithDocumentImages(List constraints = null) + public PolicyBuilder WithDocumentImages(List constraints = null) { return WithWantedAttribute(Constants.UserProfile.DocumentImagesAttribute, constraints); } - public DynamicPolicyBuilder WithSelfieAuthentication(bool enabled) + public PolicyBuilder WithSelfieAuthentication(bool enabled) { - return WithAuthType(DynamicPolicy.SelfieAuthType, enabled); + return WithAuthType(Policy.SelfieAuthType, enabled); } - public DynamicPolicyBuilder WithPinAuthentication(bool enabled) + public PolicyBuilder WithPinAuthentication(bool enabled) { - return WithAuthType(DynamicPolicy.PinAuthType, enabled); + return WithAuthType(Policy.PinAuthType, enabled); } - public DynamicPolicyBuilder WithAuthType(int authType, bool enabled) + public PolicyBuilder WithAuthType(int authType, bool enabled) { if (enabled) { @@ -141,7 +141,7 @@ public DynamicPolicyBuilder WithAuthType(int authType, bool enabled) return this; } - public DynamicPolicyBuilder WithRememberMeId(bool required) + public PolicyBuilder WithRememberMeId(bool required) { _wantedRememberMeId = required; return this; @@ -151,16 +151,16 @@ public DynamicPolicyBuilder WithRememberMeId(bool required) /// Use an Identity Profile Requirement object for the share /// /// object describing the identity profile requirements to use - /// with the identity profile requirements - public DynamicPolicyBuilder WithIdentityProfileRequirements(object identityProfileRequirements) + /// with the identity profile requirements + public PolicyBuilder WithIdentityProfileRequirements(object identityProfileRequirements) { _identityProfileRequirements = identityProfileRequirements; return this; } - public DynamicPolicy Build() + public Policy Build() { - return new DynamicPolicy(_wantedAttributes.Values, _wantedAuthTypes, _wantedRememberMeId, _identityProfileRequirements); + return new Policy(_wantedAttributes.Values, _wantedAuthTypes, _wantedRememberMeId, _identityProfileRequirements); } } } \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs b/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs new file mode 100644 index 000000000..328e5a55c --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class Notification + { + public string Url { get; set; } // Required if 'notification' is defined + public string Method { get; set; } = "POST"; // Optional, defaults to 'POST' + public Dictionary Headers { get; set; } // Optional + public bool VerifyTls { get; set; } = true; // Optional, defaults to 'true' if URL is HTTPS + } +} + diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs index fa3529bdb..988611a51 100644 --- a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs +++ b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs @@ -22,11 +22,11 @@ public class WantedAttribute [JsonProperty(PropertyName = "constraints")] public List Constraints { get; private set; } - public WantedAttribute(string name, string derivation, List constraints, bool? acceptSelfAsserted = null) + public WantedAttribute(string name, string derivation, List constraints, bool? acceptSelfAsserted = null, bool optional=false) { Name = name; Derivation = derivation; - Optional = false; + Optional = optional; AcceptSelfAsserted = acceptSelfAsserted; Constraints = constraints; } diff --git a/src/Yoti.Auth/DigitalIdentity/ShareSession.cs b/src/Yoti.Auth/DigitalIdentity/ShareSession.cs index 6f796235b..c1d24ddce 100644 --- a/src/Yoti.Auth/DigitalIdentity/ShareSession.cs +++ b/src/Yoti.Auth/DigitalIdentity/ShareSession.cs @@ -7,11 +7,9 @@ namespace Yoti.Auth.DigitalIdentity { public class ShareSessionRequest { - [JsonProperty(PropertyName = "callback_endpoint")] - private readonly string _callbackEndpoint; - + [JsonProperty(PropertyName = "policy")] - private readonly DynamicPolicy _dynamicPolicy; + private readonly Policy.Policy _dynamicPolicy; [JsonProperty(PropertyName = "extensions")] private readonly List _extensions; @@ -19,46 +17,64 @@ public class ShareSessionRequest [JsonProperty(PropertyName = "subject")] private readonly object _subject; + [JsonProperty(PropertyName = "redirectUri")] + public string _redirectUri { get; set; } + + [JsonProperty(PropertyName = "notification")] + public Notification _notification { get; set; } + + + [JsonIgnore] - public string CallbackEndpoint + public Policy.Policy DynamicPolicy { get { - return _callbackEndpoint; + return _dynamicPolicy; } } [JsonIgnore] - public DynamicPolicy DynamicPolicy + public List Extensions { get { - return _dynamicPolicy; + return _extensions; } } + [JsonIgnore] - public List Extensions + public object Subject { get { - return _extensions; + return _subject; } } + [JsonIgnore] + public string RedirectUri + { + get + { + return _redirectUri; + } + } [JsonIgnore] - public object Subject + public Notification Notification { get { - return _subject; + return _notification; } } - public ShareSessionRequest(string callbackEndpoint, DynamicPolicy dynamicPolicy, List extensions = null, object subject = null) + public ShareSessionRequest(Policy.Policy dynamicPolicy, string redirectUri, Notification notification = null, List extensions = null, object subject = null) { - _callbackEndpoint = callbackEndpoint; + _redirectUri = redirectUri; + _notification = notification; _dynamicPolicy = dynamicPolicy; _extensions = extensions ?? new List(); _subject = subject; diff --git a/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs index 644086456..8e1de54ad 100644 --- a/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs +++ b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs @@ -6,20 +6,21 @@ namespace Yoti.Auth.DigitalIdentity { public class ShareSessionRequestBuilder { - private string _callbackEndpoint; - private DynamicPolicy _dynamicPolicy; + private string _redirectUri; + private Policy.Policy _dynamicPolicy; + private Notification _notification; private readonly List _extensions = new List(); private object _subject; /// - /// The device's callback endpoint. Must be a URL relative to the Application Domain + /// The device's redirect url. Must be a URL relative to the Application Domain /// specified in Yoti Hub /// - /// - /// with a Callback Endpoint added - public ShareSessionRequestBuilder WithCallbackEndpoint(string callbackEndpoint) + /// + /// with a Redirect Uri added + public ShareSessionRequestBuilder WithRedirectUri(string redirectUri) { - _callbackEndpoint = callbackEndpoint; + _redirectUri = redirectUri; return this; } @@ -28,17 +29,29 @@ public ShareSessionRequestBuilder WithCallbackEndpoint(string callbackEndpoint) /// /// /// with a Dynamic Policy added - public ShareSessionRequestBuilder WithPolicy(DynamicPolicy dynamicPolicy) + public ShareSessionRequestBuilder WithPolicy(Policy.Policy dynamicPolicy) { _dynamicPolicy = dynamicPolicy; return this; } + /// + /// The customisable to use in the ShareSession + /// + /// + /// with a Notification added + public ShareSessionRequestBuilder WithNotification(Notification notification) + { + + _notification = notification; + return this; + } + /// /// to be activated for the application /// /// to add - /// with an extension added + /// with an extension added public ShareSessionRequestBuilder WithExtension(BaseExtension extension) { _extensions.Add(extension); @@ -49,7 +62,7 @@ public ShareSessionRequestBuilder WithExtension(BaseExtension extension) /// The subject object /// /// The object describing the subject - /// with the subject details provided + /// with the subject details provided public ShareSessionRequestBuilder WithSubject(object subject) { _subject = subject; @@ -58,7 +71,7 @@ public ShareSessionRequestBuilder WithSubject(object subject) public ShareSessionRequest Build() { - return new ShareSessionRequest(_callbackEndpoint, _dynamicPolicy, _extensions, _subject); + return new ShareSessionRequest(_dynamicPolicy,_redirectUri, _notification, _extensions, _subject ); } } } \ No newline at end of file diff --git a/test/.DS_Store b/test/.DS_Store index b493ede371dcef262a0b89e8ef8f614a922472bd..326411952be783e6186fbf88e93cc28859ad65f2 100644 GIT binary patch delta 561 zcmZoMXfc=|#>B!ku~2NHo+2aL#(>?7i$5?kF|uvuVG?8HFtIe(Q82bJnykefSD%?& zR*;mFpTxkxcs8jZC$qT3z~H`s03#DK3o9Et2PYRd4=*3TfS{1DpopNTsHm8@grti28cNrv>6;3d>O(RvKguvCNj)rSjn)P;RM57hW8AA8QB>n8I>5d z8BG{%8SNQ87(E%I7?T-O7&95OAOB)qu~2NHo+2ab#(>?7jI5h^Si~4PjEzin6pYPGCTp?AO>Smy*{s0v jn|U)k2R{c;!)8H_@640=MI1R8fPj&Kfn{@q$Qot(laMNIVmSU31l5mi!2cH{Rab}2m^yMe%+JriJ2KNT?J~YL((Km zK$D)h)?@`C7AD4bo7IGlGfmzrBFKa6?#&9K&zUwhn6L>lgB`)3zzw8bK>@O{@H_Kl iewjcKCP*-8fD|w?Fc^U7$ptdKo8^ViF=8`Of)M~trc}-V delta 162 zcmZn(XmOBWU|?W$DortDU;r^WfEYvza8E20o2aMAD7!IWH$S87W*&jNjFZm_d2ALI z*~28t%@D$n%23Qu!VmGYK*S%>n`mZXn?bGInF( Zcjn3bDuEn~5X%@Q$MZ~O#jgJe69Dm)D=`27 diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs index d5b42411a..fd0136aea 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs @@ -6,6 +6,7 @@ namespace Yoti.Auth.Tests.DigitalIdentity.Policy { + [TestClass] public class DynamicPolicyBuilderTests { @@ -19,7 +20,7 @@ public void AttributeShouldOnlyExistOnce() .WithName("SomeAttributeName") .Build(); - DynamicPolicy result = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy result = new PolicyBuilder() .WithWantedAttribute(wantedAttribute) .WithWantedAttribute(wantedAttribute) .Build(); @@ -31,7 +32,7 @@ public void AttributeShouldOnlyExistOnce() [TestMethod] public void ShouldContainAllAddedAttributes() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithFamilyName() .WithGivenNames() .WithFullName() @@ -74,7 +75,7 @@ public void ShouldContainAllAddedAttributes() [TestMethod] public void ShouldBuildWithMultipleAgeDerivedAttributes() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithDateOfBirth() .WithAgeOver(18) .WithAgeUnder(30) @@ -95,7 +96,7 @@ public void ShouldBuildWithMultipleAgeDerivedAttributes() [TestMethod] public void ShouldOverwriteIdenticalAgeVerificationToEnsureItOnlyExistsOnce() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithAgeUnder(30) .WithAgeUnder(30) .Build(); @@ -129,7 +130,7 @@ public void ShouldAddMultipleAttributesWithSameNameAndDifferentConstraints() .WithConstraints(new List { drivingLicenseConstraint }) .Build(); - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithWantedAttribute(docImage1) .WithWantedAttribute(docImage2) .Build(); @@ -146,7 +147,7 @@ public void ShouldAddMultipleAttributesWithSameNameAndDifferentConstraints() [TestMethod] public void ShouldBuildWithAuthTypesTrue() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithSelfieAuthentication(enabled: true) .WithPinAuthentication(enabled: true) .WithAuthType(authType: 99, enabled: true) @@ -161,7 +162,7 @@ public void ShouldBuildWithAuthTypesTrue() [TestMethod] public void ShouoldBuildWithAuthTypesFalse() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithSelfieAuthentication(enabled: false) .WithPinAuthentication(enabled: false) .Build(); @@ -174,7 +175,7 @@ public void ShouoldBuildWithAuthTypesFalse() [TestMethod] public void ShouldBuildWithAuthTypeEnabledThenDisabled() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithAuthType(24, enabled: true) .WithAuthType(24, enabled: false) .Build(); @@ -187,7 +188,7 @@ public void ShouldBuildWithAuthTypeEnabledThenDisabled() [TestMethod] public void ShouldBuildWithAuthTypeDisabledThenEnabled() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithAuthType(23, enabled: false) .WithAuthType(23, enabled: true) .Build(); @@ -201,7 +202,7 @@ public void ShouldBuildWithAuthTypeDisabledThenEnabled() [TestMethod] public void ShouldBuildWithSelfieAuthenticationEnabledThenDisabled() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithSelfieAuthentication(enabled: true) .WithSelfieAuthentication(enabled: false) .Build(); @@ -214,7 +215,7 @@ public void ShouldBuildWithSelfieAuthenticationEnabledThenDisabled() [TestMethod] public void ShouldBuildWithSelfieAuthenticationDisabledThenEnabled() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithSelfieAuthentication(enabled: false) .WithSelfieAuthentication(enabled: true) .Build(); @@ -228,7 +229,7 @@ public void ShouldBuildWithSelfieAuthenticationDisabledThenEnabled() [TestMethod] public void ShouldBuildWithSelfieAuthenticationDisabled() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithSelfieAuthentication(enabled: false) .Build(); @@ -240,9 +241,9 @@ public void ShouldBuildWithSelfieAuthenticationDisabled() [TestMethod] public void ShouldFilterSelfieAuthenticationDuplicates() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithSelfieAuthentication(enabled: true) - .WithAuthType(DynamicPolicy.SelfieAuthType, enabled: true) + .WithAuthType(Auth.DigitalIdentity.Policy.Policy.SelfieAuthType, enabled: true) .Build(); HashSet result = dynamicPolicy.WantedAuthTypes; @@ -254,9 +255,9 @@ public void ShouldFilterSelfieAuthenticationDuplicates() [TestMethod] public void ShouldFilterPinAuthenticationDuplicates() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithPinAuthentication(enabled: true) - .WithAuthType(DynamicPolicy.PinAuthType, enabled: true) + .WithAuthType(Auth.DigitalIdentity.Policy.Policy.PinAuthType, enabled: true) .Build(); HashSet result = dynamicPolicy.WantedAuthTypes; @@ -268,7 +269,7 @@ public void ShouldFilterPinAuthenticationDuplicates() [TestMethod] public void ShouldBuildWithPinAuthenticationEnabledThenDisabled() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithPinAuthentication(enabled: true) .WithPinAuthentication(enabled: false) .Build(); @@ -281,7 +282,7 @@ public void ShouldBuildWithPinAuthenticationEnabledThenDisabled() [TestMethod] public void ShouldBuildWithPinAuthenticationDisabledThenEnabled() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithPinAuthentication(enabled: false) .WithPinAuthentication(enabled: true) .Build(); @@ -295,7 +296,7 @@ public void ShouldBuildWithPinAuthenticationDisabledThenEnabled() [TestMethod] public void ShouldBuildWithPinAuthenticationDisabled() { - DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() .WithPinAuthentication(enabled: false) .Build(); @@ -307,7 +308,7 @@ public void ShouldBuildWithPinAuthenticationDisabled() [TestMethod] public void ShouldBuildWithRememberMeFlag() { - DynamicPolicy result = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy result = new PolicyBuilder() .WithRememberMeId(true) .Build(); @@ -319,11 +320,13 @@ public void ShouldBuildWithIdentityProfileRequirements() { object identityProfileRequirements = IdentityProfiles.CreateStandardIdentityProfileRequirements(); - DynamicPolicy result = new DynamicPolicyBuilder() + Auth.DigitalIdentity.Policy.Policy result = new PolicyBuilder() .WithIdentityProfileRequirements(identityProfileRequirements) .Build(); Assert.AreEqual(identityProfileRequirements, result.IdentityProfileRequirements); } } + + } \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/ShareSessionRequestBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/ShareSessionRequestBuilderTests.cs index 60f935b77..bc52fe01f 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentity/ShareSessionRequestBuilderTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentity/ShareSessionRequestBuilderTests.cs @@ -26,14 +26,15 @@ public class ShareSessionRequestBuilderTests .WithRadius(1500) .Build(); + [TestMethod] public void ShouldBuildADynamicScenario() { - DynamicPolicy somePolicy = TestTools.ShareSession.CreateStandardPolicy(); + Auth.DigitalIdentity.Policy.Policy somePolicy = TestTools.ShareSession.CreateStandardPolicy(); object someSubject = IdentityProfiles.CreateStandardSubject(); ShareSessionRequest result = new ShareSessionRequestBuilder() - .WithCallbackEndpoint(_someEndpoint) + .WithRedirectUri(_someEndpoint) .WithPolicy(somePolicy) .WithExtension(extension1) .WithExtension(extension2) @@ -42,7 +43,7 @@ public void ShouldBuildADynamicScenario() var expectedExtensions = new List { extension1, extension2 }; - Assert.AreEqual(_someEndpoint, result.CallbackEndpoint); + Assert.AreEqual(_someEndpoint, result.RedirectUri); Assert.AreEqual(somePolicy, result.DynamicPolicy); CollectionAssert.AreEqual(expectedExtensions, result.Extensions); Assert.AreEqual(someSubject, result.Subject); @@ -56,7 +57,7 @@ public void ShouldBuildADynamicScenario() }); object deserializedObject; - using (StreamReader r = File.OpenText("TestData/DynamicPolicy.json")) + using (StreamReader r = File.OpenText("TestData/DigitalIdentity.json")) { string json = r.ReadToEnd(); deserializedObject = JsonConvert.DeserializeObject(json); @@ -66,5 +67,6 @@ public void ShouldBuildADynamicScenario() Assert.AreEqual(expectedJson, serializedScenario); } + } } \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/TestData/DigitalIdentity.json b/test/Yoti.Auth.Tests/TestData/DigitalIdentity.json new file mode 100644 index 000000000..96c60a5dc --- /dev/null +++ b/test/Yoti.Auth.Tests/TestData/DigitalIdentity.json @@ -0,0 +1,56 @@ +{ + "policy": { + "wanted": [ + { + "name": "date_of_birth", + "optional": false + }, + { + "name": "date_of_birth", + "derivation": "age_over:18", + "optional": false + }, + { + "name": "date_of_birth", + "derivation": "age_under:30", + "optional": false + }, + { + "name": "date_of_birth", + "derivation": "age_under:40", + "optional": false + } + ], + "wanted_auth_types": [ 2 ], + "wanted_remember_me": false, + "wanted_remember_me_optional": false, + "identity_profile_requirements": { + "trust_framework": "UK_TFIDA", + "scheme": { + "type": "DBS", + "objective": "STANDARD" + } + } + }, + "extensions": [ + { + "content": "content", + "type": "string type" + }, + { + "content": { + "expected_device_location": { + "latitude": 51.5044772, + "longitude": -0.082161, + "radius": 1500.0, + "max_uncertainty_radius": 300.0 + } + }, + "type": "LOCATION_CONSTRAINT" + } + ], + "subject": { + "subject_id": "some_subject_id_string" + }, + "redirectUri": "someEndpoint" +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/TestTools/ShareSession.cs b/test/Yoti.Auth.Tests/TestTools/ShareSession.cs index 88e51bd50..7be4792a6 100644 --- a/test/Yoti.Auth.Tests/TestTools/ShareSession.cs +++ b/test/Yoti.Auth.Tests/TestTools/ShareSession.cs @@ -9,12 +9,12 @@ internal static class ShareSession { public static ShareSessionRequest CreateStandardShareSessionRequest() { - return new ShareSessionRequest("callback", CreateStandardPolicy()); + return new ShareSessionRequest(CreateStandardPolicy(), "redirecturi"); } - - public static DynamicPolicy CreateStandardPolicy() + + public static Policy CreateStandardPolicy() { - return new DynamicPolicyBuilder() + return new PolicyBuilder() .WithDateOfBirth() .WithAgeOver(18) .WithAgeUnder(30) diff --git a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj index 2a3c5d764..7e57b5808 100644 --- a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj +++ b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj @@ -41,6 +41,7 @@ + @@ -70,6 +71,9 @@ PreserveNewest + + PreserveNewest + From 57a6a7bae6c5eaa3238c851fcc641193622ea8e5 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Thu, 5 Oct 2023 14:04:55 +0100 Subject: [PATCH 03/47] SDK:2238 refatoring tests --- .../DigitalIdentity/Policy/{DynamicPolicy.cs => Policy.cs} | 0 .../Policy/{DynamicPolicyBuilder.cs => PolicyBuilder.cs} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/Yoti.Auth/DigitalIdentity/Policy/{DynamicPolicy.cs => Policy.cs} (100%) rename src/Yoti.Auth/DigitalIdentity/Policy/{DynamicPolicyBuilder.cs => PolicyBuilder.cs} (100%) diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicy.cs b/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs similarity index 100% rename from src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicy.cs rename to src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicyBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs similarity index 100% rename from src/Yoti.Auth/DigitalIdentity/Policy/DynamicPolicyBuilder.cs rename to src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs From 31db97b03ac4f2fd370cc0d2347b6421e1079469 Mon Sep 17 00:00:00 2001 From: saurabh-yoti <108520161+saurabh-yoti@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:04:29 +0100 Subject: [PATCH 04/47] Update WantedAttribute.cs --- src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs index 988611a51..a4b3cc208 100644 --- a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs +++ b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs @@ -22,7 +22,7 @@ public class WantedAttribute [JsonProperty(PropertyName = "constraints")] public List Constraints { get; private set; } - public WantedAttribute(string name, string derivation, List constraints, bool? acceptSelfAsserted = null, bool optional=false) + public WantedAttribute(string name, string derivation, List constraints, bool? acceptSelfAsserted = null, bool optional = false) { Name = name; Derivation = derivation; @@ -31,4 +31,4 @@ public WantedAttribute(string name, string derivation, List constrai Constraints = constraints; } } -} \ No newline at end of file +} From a4507ee19d893458f7db1c3c4080b16ef8cafc1e Mon Sep 17 00:00:00 2001 From: saurabh-yoti <108520161+saurabh-yoti@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:12:09 +0100 Subject: [PATCH 05/47] Update ShareSessionRequestBuilder.cs --- src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs index 8e1de54ad..1b9a83534 100644 --- a/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs +++ b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs @@ -18,7 +18,7 @@ public class ShareSessionRequestBuilder /// /// /// with a Redirect Uri added - public ShareSessionRequestBuilder WithRedirectUri(string redirectUri) + public ShareSessionRequestBuilder WithRedirectUri(string redirectUri) { _redirectUri = redirectUri; return this; @@ -71,7 +71,7 @@ public ShareSessionRequestBuilder WithSubject(object subject) public ShareSessionRequest Build() { - return new ShareSessionRequest(_dynamicPolicy,_redirectUri, _notification, _extensions, _subject ); + return new ShareSessionRequest(_dynamicPolicy, _redirectUri, _notification, _extensions, _subject); } } -} \ No newline at end of file +} From 99d192ce9e3df866a2c77f4f7563e53d76575725 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Thu, 5 Oct 2023 20:46:05 +0100 Subject: [PATCH 06/47] updated test franework version --- src/Yoti.Auth/Conversion.cs | 2 +- src/Yoti.Auth/Yoti.Auth.csproj | 2 +- test/.DS_Store | Bin 6148 -> 6148 bytes test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Yoti.Auth/Conversion.cs b/src/Yoti.Auth/Conversion.cs index 717727dbd..44928f576 100644 --- a/src/Yoti.Auth/Conversion.cs +++ b/src/Yoti.Auth/Conversion.cs @@ -31,7 +31,7 @@ public static byte[] Base64ToBytes(string base64) /// public static byte[] UrlSafeBase64ToBytes(string urlSafeBase64) { -#if NETCOREAPP2_2 || NETCOREAPP3_1 || NETSTANDARD2_1 +#if NETCOREAPP2_2 || NETCOREAPP3_1 || NETSTANDARD2_1 || NET5_0 string base64 = urlSafeBase64.Replace("-", "+", StringComparison.Ordinal).Replace("_", "/", StringComparison.Ordinal); #else string base64 = urlSafeBase64.Replace("-", "+").Replace("_", "/"); diff --git a/src/Yoti.Auth/Yoti.Auth.csproj b/src/Yoti.Auth/Yoti.Auth.csproj index cda182dfd..2757c82af 100644 --- a/src/Yoti.Auth/Yoti.Auth.csproj +++ b/src/Yoti.Auth/Yoti.Auth.csproj @@ -1,7 +1,7 @@  - netstandard1.6;netstandard2.1;netcoreapp1.1;netcoreapp2.2;netcoreapp3.1;net452;net462;net472;net48; + netstandard1.6;netstandard2.1;netcoreapp1.1;netcoreapp2.2;netcoreapp3.1;net6.0;net452;net462;net472;net48; Yoti.Auth Yoti $(PackageTargetFallback);dnxcore50 diff --git a/test/.DS_Store b/test/.DS_Store index 326411952be783e6186fbf88e93cc28859ad65f2..cde0a0dae082b2a894391347ddd4657ffcf9946b 100644 GIT binary patch delta 206 zcmZoMXffC@nU(R-}EK@aF^jd!(T>rMgc}8Mr}qDMq5UEMh`|$#wf;U i#stPph;}AM2+be^rJ)pK*5pL?WfL2?H?wp6x+;K<<15XO+rP{lBjVJ^c;hTRM&816E>XZXv=&M3*K#Hh_^!f4BA&*;JE z$r#0$%$UNM$(RMv&%_8dM;1y$DaK+TgP$RiA)ldyA#-vMi`e7}_Lj};9Dn%%__8({ diff --git a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj index 7e57b5808..e592c9941 100644 --- a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj +++ b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 Yoti.Auth.Tests Yoti.Auth.Tests true From f1f68c0f4c4887a03f3a5fd32384f01bd8039a6f Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Thu, 5 Oct 2023 21:20:25 +0100 Subject: [PATCH 07/47] updated test franework version --- test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj index e592c9941..ab7a81e09 100644 --- a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj +++ b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj @@ -74,6 +74,9 @@ PreserveNewest + + PreserveNewest + From adc51cf8e5044b25737d4c1739c843eeb2b5afc1 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Thu, 5 Oct 2023 22:34:41 +0100 Subject: [PATCH 08/47] updated test franework version --- src/Yoti.Auth/DocScan/DocScanService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Yoti.Auth/DocScan/DocScanService.cs b/src/Yoti.Auth/DocScan/DocScanService.cs index 878756081..48cd7f792 100644 --- a/src/Yoti.Auth/DocScan/DocScanService.cs +++ b/src/Yoti.Auth/DocScan/DocScanService.cs @@ -156,6 +156,11 @@ public async Task GetMediaContent(string sdkId, AsymmetricCipherKeyP return null; } + if (response.Content.Headers.ContentType == null) + { + return null; + } + string contentType = response.Content.Headers.ContentType.MediaType; From 1288d3a135deac7b502c1171255ba037da4209a7 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Fri, 6 Oct 2023 11:25:13 +0100 Subject: [PATCH 09/47] Update azure-pipelines.yml --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c49c740f9..e85f55c19 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -36,7 +36,7 @@ steps: extraProperties: | sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" sonar.links.scm = https://github.com/getyoti/yoti-dotnet-sdk - sonar.exclusions = src/Yoti.Auth/ProtoBuf/**,src/Examples/**,**/obj/**,**/*.dll + sonar.exclusions = src/Yoti.Auth/ProtoBuf/**,src/Examples/**,**/obj/**,**/*.dll,src/Yoti.Auth/DigitalIdentity/** displayName: SonarCloud Prepare Analysis - task: NuGetToolInstaller@1 From 948a5eb5369f9f7ac8647fc807060fb48f6bcd87 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 6 Oct 2023 14:01:16 +0100 Subject: [PATCH 10/47] SDK-2245: net retrieve session --- .../DigitalIdentity/DigitalIdentityService.cs | 38 +++++++++++++++++++ .../DigitalIdentityServiceTests.cs | 36 ++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs index a77375f7c..11a7aabf1 100644 --- a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs +++ b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs @@ -51,5 +51,43 @@ internal static async Task CreateShareSession(HttpClient htt } } + + internal static async Task GetSession(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId) + { + Validation.NotNull(httpClient, nameof(httpClient)); + Validation.NotNull(apiUrl, nameof(apiUrl)); + Validation.NotNull(sdkId, nameof(sdkId)); + Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(sessionId, nameof(sessionId)); + + + //byte[] body = Encoding.UTF8.GetBytes(serializedScenario); + + Request shareSessionlRequest = new RequestBuilder() + .WithKeyPair(keyPair) + .WithBaseUri(apiUrl) + .WithHeader("X-Yoti-Auth-Id", sdkId) + .WithEndpoint(string.Format($"/v2/sessions/{0}", sessionId)) + .WithQueryParam("appId", sdkId) + .WithHttpMethod(HttpMethod.Get) + .Build(); + + using (HttpResponseMessage response = await shareSessionlRequest.Execute(httpClient).ConfigureAwait(false)) + { + if (!response.IsSuccessStatusCode) + { + Response.CreateYotiExceptionFromStatusCode(response); + } + + var responseObject = await response.Content.ReadAsStringAsync(); + var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); + + return deserialized; + + } + } + } + + } \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs index 9d91c54df..cd780a676 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using Microsoft.VisualStudio.TestTools.UnitTesting; using Org.BouncyCastle.Crypto; using Yoti.Auth.DigitalIdentity; using Yoti.Auth.Tests.Common; +using static System.Net.Mime.MediaTypeNames; namespace Yoti.Auth.Tests.DigitalIdentity { @@ -17,6 +19,7 @@ public class DigitalIdentityServiceTests private readonly HttpClient _httpClient = new HttpClient(); private readonly AsymmetricCipherKeyPair _keyPair = KeyPair.Get(); private ShareSessionRequest _someShareSessionRequest; + private const string _sessionID = "someSessionID"; [TestInitialize] public void Startup() @@ -84,5 +87,38 @@ public void ShouldFailWithNullDynamicScenario() Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); Assert.IsTrue(aggregateException.InnerException.Message.Contains("shareSessionRequest")); } + + [TestMethod] + public void RetrieveSessionShouldThrowExceptionForMissingSdkId() + { + var exception = Assert.ThrowsExceptionAsync(async () => + { + await DigitalIdentityService.GetSession(_httpClient, _apiURL, null, _keyPair, _sessionID); + }); + + Assert.IsTrue(exception.Exception.InnerException.Message.Contains("sdkId")); + } + + [TestMethod] + public void RetrieveSessionShouldThrowExceptionForMissingKeyPair() + { + var exception = Assert.ThrowsExceptionAsync(async () => + { + await DigitalIdentityService.GetSession(_httpClient, _apiURL, _sdkID, null, _sessionID); + }).Result; + + Assert.IsTrue(exception.Message.Contains("keyPair")); + } + + [TestMethod] + public void RetrieveSessionShouldThrowExceptionForMissingSessionId() + { + var exception = Assert.ThrowsExceptionAsync(async () => + { + await DigitalIdentityService.GetSession(_httpClient, _apiURL, _sdkID, _keyPair, null); + }).Result; + + Assert.IsTrue(exception.Message.Contains("sessionId")); + } } } \ No newline at end of file From 1d111afe13c64d9faca9e9c832e76ef4873472e3 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 6 Oct 2023 14:13:21 +0100 Subject: [PATCH 11/47] SDK-2245: net retrieve session --- .../DigitalIdentity/DigitalIdentityService.cs | 4 +- .../DigitalIdentity/GetSessionResult.cs | 49 +++++++++++++++++++ .../DigitalIdentity/ShareSessionResult.cs | 27 +--------- 3 files changed, 53 insertions(+), 27 deletions(-) create mode 100644 src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs diff --git a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs index 11a7aabf1..a88cd2f9a 100644 --- a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs +++ b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs @@ -52,7 +52,7 @@ internal static async Task CreateShareSession(HttpClient htt } } - internal static async Task GetSession(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId) + internal static async Task GetSession(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId) { Validation.NotNull(httpClient, nameof(httpClient)); Validation.NotNull(apiUrl, nameof(apiUrl)); @@ -80,7 +80,7 @@ internal static async Task GetSession(HttpClient httpClient, } var responseObject = await response.Content.ReadAsStringAsync(); - var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); + var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); return deserialized; diff --git a/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs b/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs new file mode 100644 index 000000000..d9c41ab9e --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs @@ -0,0 +1,49 @@ +using System; +using System.Reflection.Emit; +using Newtonsoft.Json; +using Yoti.Auth.DataObjects; + +namespace Yoti.Auth.DigitalIdentity +{ + public class ShareSessionResult + { +#pragma warning disable 0649 + + // These fields are assigned to by JSON deserialization + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("expiry")] + public string Expiry { get; set; } + + [JsonProperty("created")] + public string Created { get; set; } + + [JsonProperty("updated")] + public string Updated { get; set; } + + [JsonProperty("qrCode")] + public qrCode QrCode { get; set; } + + [JsonProperty("receipt")] + public receipt Receipt { get; set; } + +#pragma warning restore 0649 + + } + + public class qrCode + { + [JsonProperty("id")] + public string Id { get; set; } + } + + public class receipt + { + [JsonProperty("id")] + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs index d9c41ab9e..858f0caf9 100644 --- a/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs +++ b/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs @@ -5,10 +5,9 @@ namespace Yoti.Auth.DigitalIdentity { - public class ShareSessionResult + public class GetSessionResult { #pragma warning disable 0649 - // These fields are assigned to by JSON deserialization [JsonProperty("id")] public string Id { get; set; } @@ -19,31 +18,9 @@ public class ShareSessionResult [JsonProperty("expiry")] public string Expiry { get; set; } - [JsonProperty("created")] - public string Created { get; set; } - - [JsonProperty("updated")] - public string Updated { get; set; } - - [JsonProperty("qrCode")] - public qrCode QrCode { get; set; } - - [JsonProperty("receipt")] - public receipt Receipt { get; set; } - #pragma warning restore 0649 } - public class qrCode - { - [JsonProperty("id")] - public string Id { get; set; } - } - - public class receipt - { - [JsonProperty("id")] - public string Id { get; set; } - } + } \ No newline at end of file From 5b2544e5e9e47933f0b54fb42611bff11f230fa1 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 6 Oct 2023 14:16:26 +0100 Subject: [PATCH 12/47] SDK-2245: net retrieve session --- .../DigitalIdentity/GetSessionResult.cs | 33 +++---------------- .../DigitalIdentity/ShareSessionResult.cs | 33 +++++++++++++++---- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs b/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs index d9c41ab9e..ea519b2d6 100644 --- a/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs +++ b/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs @@ -1,14 +1,11 @@ -using System; -using System.Reflection.Emit; -using Newtonsoft.Json; -using Yoti.Auth.DataObjects; +using Newtonsoft.Json; + namespace Yoti.Auth.DigitalIdentity { - public class ShareSessionResult + public class GetSessionResult { #pragma warning disable 0649 - // These fields are assigned to by JSON deserialization [JsonProperty("id")] public string Id { get; set; } @@ -19,31 +16,9 @@ public class ShareSessionResult [JsonProperty("expiry")] public string Expiry { get; set; } - [JsonProperty("created")] - public string Created { get; set; } - - [JsonProperty("updated")] - public string Updated { get; set; } - - [JsonProperty("qrCode")] - public qrCode QrCode { get; set; } - - [JsonProperty("receipt")] - public receipt Receipt { get; set; } - #pragma warning restore 0649 } - public class qrCode - { - [JsonProperty("id")] - public string Id { get; set; } - } - public class receipt - { - [JsonProperty("id")] - public string Id { get; set; } - } -} \ No newline at end of file +} diff --git a/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs index 858f0caf9..2642fa8ac 100644 --- a/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs +++ b/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs @@ -1,13 +1,12 @@ -using System; -using System.Reflection.Emit; -using Newtonsoft.Json; -using Yoti.Auth.DataObjects; +using Newtonsoft.Json; + namespace Yoti.Auth.DigitalIdentity { - public class GetSessionResult + public class ShareSessionResult { #pragma warning disable 0649 + // These fields are assigned to by JSON deserialization [JsonProperty("id")] public string Id { get; set; } @@ -18,9 +17,31 @@ public class GetSessionResult [JsonProperty("expiry")] public string Expiry { get; set; } + [JsonProperty("created")] + public string Created { get; set; } + + [JsonProperty("updated")] + public string Updated { get; set; } + + [JsonProperty("qrCode")] + public qrCode QrCode { get; set; } + + [JsonProperty("receipt")] + public receipt Receipt { get; set; } + #pragma warning restore 0649 } - + public class qrCode + { + [JsonProperty("id")] + public string Id { get; set; } + } + + public class receipt + { + [JsonProperty("id")] + public string Id { get; set; } + } } \ No newline at end of file From 1788b9473f73d5385485949973638b381ea334de Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Tue, 10 Oct 2023 12:19:27 +0100 Subject: [PATCH 13/47] SDK-2245: update session response --- .../DigitalIdentity/GetSessionResult.cs | 26 +++++++++++++++++++ .../DigitalIdentity/ShareSessionResult.cs | 25 +----------------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs b/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs index ea519b2d6..e81fc5ef3 100644 --- a/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs +++ b/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs @@ -16,9 +16,35 @@ public class GetSessionResult [JsonProperty("expiry")] public string Expiry { get; set; } + [JsonProperty("created")] + public string Created { get; set; } + + [JsonProperty("updated")] + public string Updated { get; set; } + + [JsonProperty("qrCode")] + public qrCode QrCode { get; set; } + + [JsonProperty("receipt")] + public receipt Receipt { get; set; } + #pragma warning restore 0649 } + public class qrCode + { + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("uri")] + public string Uri { get; set; } + } + + public class receipt + { + [JsonProperty("id")] + public string Id { get; set; } + } + } diff --git a/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs index 2642fa8ac..eb04f88da 100644 --- a/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs +++ b/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs @@ -6,7 +6,6 @@ namespace Yoti.Auth.DigitalIdentity public class ShareSessionResult { #pragma warning disable 0649 - // These fields are assigned to by JSON deserialization [JsonProperty("id")] public string Id { get; set; } @@ -17,31 +16,9 @@ public class ShareSessionResult [JsonProperty("expiry")] public string Expiry { get; set; } - [JsonProperty("created")] - public string Created { get; set; } - - [JsonProperty("updated")] - public string Updated { get; set; } - - [JsonProperty("qrCode")] - public qrCode QrCode { get; set; } - - [JsonProperty("receipt")] - public receipt Receipt { get; set; } - #pragma warning restore 0649 } - public class qrCode - { - [JsonProperty("id")] - public string Id { get; set; } - } - - public class receipt - { - [JsonProperty("id")] - public string Id { get; set; } - } + } \ No newline at end of file From 9562647a0e00c6e89036250313c8ea545aba1643 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Tue, 10 Oct 2023 12:22:22 +0100 Subject: [PATCH 14/47] SDK-2245: update session response --- src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs index a88cd2f9a..0835bed39 100644 --- a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs +++ b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs @@ -88,6 +88,4 @@ internal static async Task GetSession(HttpClient httpClient, U } } - - } \ No newline at end of file From a27a3dbabb2be269899bee4fd098cd13b3c52450 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Mon, 16 Oct 2023 10:47:04 +0100 Subject: [PATCH 15/47] SDK-2245: update session response --- .../DigitalIdentity/GetSessionResult.cs | 2 - src/Yoti.Auth/DigitalIdentity/QrRequest.cs | 72 +++++++++++++++++++ ...ShareSession.cs => ShareSessionRequest.cs} | 0 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/Yoti.Auth/DigitalIdentity/QrRequest.cs rename src/Yoti.Auth/DigitalIdentity/{ShareSession.cs => ShareSessionRequest.cs} (100%) diff --git a/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs b/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs index e81fc5ef3..f5084f6b6 100644 --- a/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs +++ b/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs @@ -36,8 +36,6 @@ public class qrCode { [JsonProperty("id")] public string Id { get; set; } - [JsonProperty("uri")] - public string Uri { get; set; } } public class receipt diff --git a/src/Yoti.Auth/DigitalIdentity/QrRequest.cs b/src/Yoti.Auth/DigitalIdentity/QrRequest.cs new file mode 100644 index 000000000..4b44d9d7d --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/QrRequest.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Yoti.Auth.DigitalIdentity.Extensions; +using Yoti.Auth.DigitalIdentity.Policy; + +namespace Yoti.Auth.DigitalIdentity +{ + public class ShareSessionRequest + { + + [JsonProperty(PropertyName = "transport")] + private readonly string _transport; + + [JsonProperty(PropertyName = "displayMode")] + private readonly string _displayMode; + + [JsonIgnore] + public string Transport + { + get + { + return _transport; + } + } + + [JsonIgnore] + public string DisplayMode + { + get + { + return _displayMode; + } + } + + + [JsonIgnore] + public object Subject + { + get + { + return _subject; + } + } + + [JsonIgnore] + public string RedirectUri + { + get + { + return _redirectUri; + } + } + + [JsonIgnore] + public Notification Notification + { + get + { + return _notification; + } + } + + public ShareSessionRequest(Policy.Policy dynamicPolicy, string redirectUri, Notification notification = null, List extensions = null, object subject = null) + { + _redirectUri = redirectUri; + _notification = notification; + _dynamicPolicy = dynamicPolicy; + _extensions = extensions ?? new List(); + _subject = subject; + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/ShareSession.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequest.cs similarity index 100% rename from src/Yoti.Auth/DigitalIdentity/ShareSession.cs rename to src/Yoti.Auth/DigitalIdentity/ShareSessionRequest.cs From 6da1b85da7cb0d015bb01ae56d8a91174e205656 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Mon, 16 Oct 2023 16:10:51 +0100 Subject: [PATCH 16/47] SDK-2245: update session response --- src/Yoti.Auth/DigitalIdentity/QrRequest.cs | 72 ---------------------- 1 file changed, 72 deletions(-) delete mode 100644 src/Yoti.Auth/DigitalIdentity/QrRequest.cs diff --git a/src/Yoti.Auth/DigitalIdentity/QrRequest.cs b/src/Yoti.Auth/DigitalIdentity/QrRequest.cs deleted file mode 100644 index 4b44d9d7d..000000000 --- a/src/Yoti.Auth/DigitalIdentity/QrRequest.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using Yoti.Auth.DigitalIdentity.Extensions; -using Yoti.Auth.DigitalIdentity.Policy; - -namespace Yoti.Auth.DigitalIdentity -{ - public class ShareSessionRequest - { - - [JsonProperty(PropertyName = "transport")] - private readonly string _transport; - - [JsonProperty(PropertyName = "displayMode")] - private readonly string _displayMode; - - [JsonIgnore] - public string Transport - { - get - { - return _transport; - } - } - - [JsonIgnore] - public string DisplayMode - { - get - { - return _displayMode; - } - } - - - [JsonIgnore] - public object Subject - { - get - { - return _subject; - } - } - - [JsonIgnore] - public string RedirectUri - { - get - { - return _redirectUri; - } - } - - [JsonIgnore] - public Notification Notification - { - get - { - return _notification; - } - } - - public ShareSessionRequest(Policy.Policy dynamicPolicy, string redirectUri, Notification notification = null, List extensions = null, object subject = null) - { - _redirectUri = redirectUri; - _notification = notification; - _dynamicPolicy = dynamicPolicy; - _extensions = extensions ?? new List(); - _subject = subject; - } - } -} \ No newline at end of file From 2ea167a382968d043f03bd2de2b879f4d21302c0 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:05:38 +0300 Subject: [PATCH 17/47] Sdk 2252 net create qr code (#441) * SDK-2252:added net create qr code * SDK-2252:Updated validation rule * SDK-2252:removed net 2.1 support for security issues --- src/Examples/Aml/AmlExample/AmlExample.csproj | 2 +- .../DigitalIdentity/CreateQrResult.cs | 18 +++++++ .../DigitalIdentity/DigitalIdentityService.cs | 45 ++++++++++++++-- src/Yoti.Auth/DigitalIdentity/QrRequest.cs | 40 ++++++++++++++ .../DigitalIdentity/QrRequestBuilder.cs | 39 ++++++++++++++ .../Yoti.Auth.Tests.Common.csproj | 2 +- .../DigitalIdentityServiceTests.cs | 52 +++++++++++++++++++ .../DigitalIdentity/QrRequestBuilderTests.cs | 30 +++++++++++ test/Yoti.Auth.Tests/TestTools/CreateQr.cs | 15 ++++++ 9 files changed, 237 insertions(+), 6 deletions(-) create mode 100644 src/Yoti.Auth/DigitalIdentity/CreateQrResult.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/QrRequest.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/QrRequestBuilder.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/QrRequestBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/TestTools/CreateQr.cs diff --git a/src/Examples/Aml/AmlExample/AmlExample.csproj b/src/Examples/Aml/AmlExample/AmlExample.csproj index 15bf388e9..f96d7c368 100644 --- a/src/Examples/Aml/AmlExample/AmlExample.csproj +++ b/src/Examples/Aml/AmlExample/AmlExample.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + netcoreapp3.1 diff --git a/src/Yoti.Auth/DigitalIdentity/CreateQrResult.cs b/src/Yoti.Auth/DigitalIdentity/CreateQrResult.cs new file mode 100644 index 000000000..d06d380dd --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/CreateQrResult.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + + +namespace Yoti.Auth.DigitalIdentity +{ + public class CreateQrResult + { +#pragma warning disable 0649 + // These fields are assigned to by JSON deserialization + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("uri")] + public string Uri { get; set; } +#pragma warning restore 0649 + + } +} diff --git a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs index 0835bed39..3eacfd55f 100644 --- a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs +++ b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs @@ -58,10 +58,7 @@ internal static async Task GetSession(HttpClient httpClient, U Validation.NotNull(apiUrl, nameof(apiUrl)); Validation.NotNull(sdkId, nameof(sdkId)); Validation.NotNull(keyPair, nameof(keyPair)); - Validation.NotNull(sessionId, nameof(sessionId)); - - - //byte[] body = Encoding.UTF8.GetBytes(serializedScenario); + Validation.NotNull(sessionId, nameof(sessionId)); Request shareSessionlRequest = new RequestBuilder() .WithKeyPair(keyPair) @@ -87,5 +84,45 @@ internal static async Task GetSession(HttpClient httpClient, U } } + internal static async Task CreateQrCode(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId,QrRequest qrRequestPayload) + { + Validation.NotNull(httpClient, nameof(httpClient)); + Validation.NotNull(apiUrl, nameof(apiUrl)); + Validation.NotNull(sdkId, nameof(sdkId)); + Validation.NotNull(keyPair, nameof(keyPair)); + + string serializedQrCode = JsonConvert.SerializeObject( + qrRequestPayload, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + byte[] body = Encoding.UTF8.GetBytes(serializedQrCode); + + Request qrRequest = new RequestBuilder() + .WithKeyPair(keyPair) + .WithBaseUri(apiUrl) + .WithHeader("X-Yoti-Auth-Id", sdkId) + .WithEndpoint(string.Format($"/v2/sessions/{0}/qr-codes", sessionId)) + .WithQueryParam("appId", sdkId) + .WithHttpMethod(HttpMethod.Post) + .WithContent(body) + .Build(); + + using (HttpResponseMessage response = await qrRequest.Execute(httpClient).ConfigureAwait(false)) + { + if (!response.IsSuccessStatusCode) + { + Response.CreateYotiExceptionFromStatusCode(response); + } + + var responseObject = await response.Content.ReadAsStringAsync(); + var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); + + return deserialized; + + } + } + } } \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/QrRequest.cs b/src/Yoti.Auth/DigitalIdentity/QrRequest.cs new file mode 100644 index 000000000..70ccd7c08 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/QrRequest.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Yoti.Auth.DigitalIdentity.Extensions; +using Yoti.Auth.DigitalIdentity.Policy; + +namespace Yoti.Auth.DigitalIdentity +{ + public class QrRequest + { + [JsonProperty(PropertyName = "transport")] + private readonly string _transport; + + [JsonProperty(PropertyName = "displayMode")] + private readonly string _displayMode; + + [JsonIgnore] + public string DisplayMode + { + get + { + return _displayMode; + } + } + + [JsonIgnore] + public string Transport + { + get + { + return _transport; + } + } + + public QrRequest(string transport = null, string displayMode = null) + { + _transport = transport; + _displayMode = displayMode; + } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/QrRequestBuilder.cs b/src/Yoti.Auth/DigitalIdentity/QrRequestBuilder.cs new file mode 100644 index 000000000..2e066d3dd --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/QrRequestBuilder.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Yoti.Auth.DigitalIdentity.Extensions; +using Yoti.Auth.DigitalIdentity.Policy; + +namespace Yoti.Auth.DigitalIdentity +{ + public class QrRequestBuilder + { + private string _transport = ""; + private string _displayMode = ""; + + /// + /// Transport property. Optional - default is 'INLINE' + /// + /// + /// with a Transport added + public QrRequestBuilder WithTransport(string transport) + { + _transport = transport; + return this; + } + + /// + /// DisplayMode property. Optional - default is 'QR_CODE' + /// + /// + /// with a Display Mode added + public QrRequestBuilder WithDisplayMode(string displayMode) + { + _displayMode = displayMode ; + return this; + } + + public QrRequest Build() + { + return new QrRequest(_transport,_displayMode); + } + } +} diff --git a/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj b/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj index 292d43db8..8832cfb2a 100644 --- a/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj +++ b/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp3.1 diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs index cd780a676..40ebac086 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs @@ -20,11 +20,13 @@ public class DigitalIdentityServiceTests private readonly AsymmetricCipherKeyPair _keyPair = KeyPair.Get(); private ShareSessionRequest _someShareSessionRequest; private const string _sessionID = "someSessionID"; + private QrRequest _someCreateQrRequest; [TestInitialize] public void Startup() { _someHeaders.Add("Key", "Value"); + _someCreateQrRequest = TestTools.CreateQr.CreateQrStandard(); _someShareSessionRequest = TestTools.ShareSession.CreateStandardShareSessionRequest(); } @@ -120,5 +122,55 @@ public void RetrieveSessionShouldThrowExceptionForMissingSessionId() Assert.IsTrue(exception.Message.Contains("sessionId")); } + + [TestMethod] + public void CreateQrCodeShouldFailWithNullHttpClient() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateQrCode(null, _apiURL, _sdkID, _keyPair, _sessionID, _someCreateQrRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("httpClient")); + } + + [TestMethod] + public void CreateQrCodeShouldFailWithNullApiUrl() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateQrCode(_httpClient, null, _sdkID, _keyPair, _sessionID, _someCreateQrRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("apiUrl")); + } + + [TestMethod] + public void CreateQrCodeShouldFailWithNullSdkId() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateQrCode(_httpClient, _apiURL, null, _keyPair, _sessionID, _someCreateQrRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("sdkId")); + } + + [TestMethod] + public void CreateQrCodeShouldFailWithNullKeyPair() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateQrCode(_httpClient, _apiURL, _sdkID, null, _sessionID, _someCreateQrRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("keyPair")); + } + + } } \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/QrRequestBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/QrRequestBuilderTests.cs new file mode 100644 index 000000000..422957153 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/QrRequestBuilderTests.cs @@ -0,0 +1,30 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Yoti.Auth.DigitalIdentity; +using Yoti.Auth.Tests.TestData; + +namespace Yoti.Auth.Tests.DigitalIdentity +{ + [TestClass] + public class QrRequestBuilderTests + { + + private const string _someTransportString = "someTransport"; + private const string _someDisplayMode = "someDisplay"; + + + + [TestMethod] + public void ShouldBuildADynamicScenario() + { + QrRequest result = new QrRequestBuilder() + .WithDisplayMode(_someDisplayMode) + .WithTransport(_someTransportString) + .Build(); + + + Assert.AreEqual(_someDisplayMode, result.DisplayMode); + Assert.AreEqual(_someTransportString, result.Transport); + } + + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/TestTools/CreateQr.cs b/test/Yoti.Auth.Tests/TestTools/CreateQr.cs new file mode 100644 index 000000000..0474d4303 --- /dev/null +++ b/test/Yoti.Auth.Tests/TestTools/CreateQr.cs @@ -0,0 +1,15 @@ +using Yoti.Auth.ShareUrl; +using Yoti.Auth.DigitalIdentity.Policy; +using Yoti.Auth.Tests.TestData; +using Yoti.Auth.DigitalIdentity; + +namespace Yoti.Auth.Tests.TestTools +{ + internal static class CreateQr + { + public static QrRequest CreateQrStandard() + { + return new QrRequest(); + } + } +} From cce170c538b09e1ff9db45e8adcbf49071bab94e Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Wed, 10 Jan 2024 21:09:35 +0300 Subject: [PATCH 18/47] Sdk 2257 net retrieve qr code (#442) * SDK-2257:added net retrieve qr code * SDK-2257:Updated GetQrCode Result * SDK-2257:updated exception type * SDK-2257 updated request types vars --- .../DigitalIdentity/DigitalIdentityService.cs | 61 +++++++++++++++---- .../DigitalIdentity/GetQrCodeResult.cs | 32 ++++++++++ .../Exceptions/DigitalIdentityException.cs | 22 +++++++ .../DigitalIdentityServiceTests.cs | 32 ++++++++++ .../DigitalIdentityClientEngineTests.cs | 2 +- 5 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 src/Yoti.Auth/DigitalIdentity/GetQrCodeResult.cs create mode 100644 src/Yoti.Auth/Exceptions/DigitalIdentityException.cs diff --git a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs index 3eacfd55f..532cdab75 100644 --- a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs +++ b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs @@ -11,23 +11,23 @@ namespace Yoti.Auth.DigitalIdentity { public static class DigitalIdentityService { - internal static async Task CreateShareSession(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, ShareSessionRequest shareSessionRequest) + internal static async Task CreateShareSession(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, ShareSessionRequest shareSessionRequestPayload) { Validation.NotNull(httpClient, nameof(httpClient)); Validation.NotNull(apiUrl, nameof(apiUrl)); Validation.NotNull(sdkId, nameof(sdkId)); Validation.NotNull(keyPair, nameof(keyPair)); - Validation.NotNull(shareSessionRequest, nameof(shareSessionRequest)); + Validation.NotNull(shareSessionRequestPayload, nameof(shareSessionRequestPayload)); string serializedScenario = JsonConvert.SerializeObject( - shareSessionRequest, + shareSessionRequestPayload, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); byte[] body = Encoding.UTF8.GetBytes(serializedScenario); - Request shareSessionlRequest = new RequestBuilder() + Request shareSessionRequest = new RequestBuilder() .WithKeyPair(keyPair) .WithBaseUri(apiUrl) .WithHeader("X-Yoti-Auth-Id", sdkId) @@ -37,11 +37,11 @@ internal static async Task CreateShareSession(HttpClient htt .WithContent(body) .Build(); - using (HttpResponseMessage response = await shareSessionlRequest.Execute(httpClient).ConfigureAwait(false)) + using (HttpResponseMessage response = await shareSessionRequest.Execute(httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) { - Response.CreateYotiExceptionFromStatusCode(response); + Response.CreateYotiExceptionFromStatusCode(response); } var responseObject = await response.Content.ReadAsStringAsync(); @@ -60,7 +60,8 @@ internal static async Task GetSession(HttpClient httpClient, U Validation.NotNull(keyPair, nameof(keyPair)); Validation.NotNull(sessionId, nameof(sessionId)); - Request shareSessionlRequest = new RequestBuilder() + + Request getSessionRequest = new RequestBuilder() .WithKeyPair(keyPair) .WithBaseUri(apiUrl) .WithHeader("X-Yoti-Auth-Id", sdkId) @@ -69,11 +70,11 @@ internal static async Task GetSession(HttpClient httpClient, U .WithHttpMethod(HttpMethod.Get) .Build(); - using (HttpResponseMessage response = await shareSessionlRequest.Execute(httpClient).ConfigureAwait(false)) + using (HttpResponseMessage response = await getSessionRequest.Execute(httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) { - Response.CreateYotiExceptionFromStatusCode(response); + Response.CreateYotiExceptionFromStatusCode(response); } var responseObject = await response.Content.ReadAsStringAsync(); @@ -90,7 +91,7 @@ internal static async Task CreateQrCode(HttpClient httpClient, U Validation.NotNull(apiUrl, nameof(apiUrl)); Validation.NotNull(sdkId, nameof(sdkId)); Validation.NotNull(keyPair, nameof(keyPair)); - + string serializedQrCode = JsonConvert.SerializeObject( qrRequestPayload, new JsonSerializerSettings @@ -99,7 +100,8 @@ internal static async Task CreateQrCode(HttpClient httpClient, U }); byte[] body = Encoding.UTF8.GetBytes(serializedQrCode); - Request qrRequest = new RequestBuilder() + + Request createQrRequest = new RequestBuilder() .WithKeyPair(keyPair) .WithBaseUri(apiUrl) .WithHeader("X-Yoti-Auth-Id", sdkId) @@ -109,11 +111,12 @@ internal static async Task CreateQrCode(HttpClient httpClient, U .WithContent(body) .Build(); - using (HttpResponseMessage response = await qrRequest.Execute(httpClient).ConfigureAwait(false)) + + using (HttpResponseMessage response = await createQrRequest.Execute(httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) { - Response.CreateYotiExceptionFromStatusCode(response); + Response.CreateYotiExceptionFromStatusCode(response); } var responseObject = await response.Content.ReadAsStringAsync(); @@ -124,5 +127,37 @@ internal static async Task CreateQrCode(HttpClient httpClient, U } } + + internal static async Task GetQrCode(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, string qrCodeId) + { + Validation.NotNull(httpClient, nameof(httpClient)); + Validation.NotNull(apiUrl, nameof(apiUrl)); + Validation.NotNull(sdkId, nameof(sdkId)); + Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(qrCodeId, nameof(qrCodeId)); + + Request QrCodeRequest = new RequestBuilder() + .WithKeyPair(keyPair) + .WithBaseUri(apiUrl) + .WithHeader("X-Yoti-Auth-Id", sdkId) + .WithEndpoint(string.Format($"/v2/qr-codes/{0}", qrCodeId)) + .WithQueryParam("appId", sdkId) + .WithHttpMethod(HttpMethod.Get) + .Build(); + + using (HttpResponseMessage response = await QrCodeRequest.Execute(httpClient).ConfigureAwait(false)) + { + if (!response.IsSuccessStatusCode) + { + Response.CreateYotiExceptionFromStatusCode(response); + } + + var responseObject = await response.Content.ReadAsStringAsync(); + var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); + + return deserialized; + + } + } } } \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/GetQrCodeResult.cs b/src/Yoti.Auth/DigitalIdentity/GetQrCodeResult.cs new file mode 100644 index 000000000..63ca5e45c --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/GetQrCodeResult.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Yoti.Auth.DigitalIdentity.Extensions; + +namespace Yoti.Auth.DigitalIdentity +{ + public class GetQrCodeResult + { +#pragma warning disable 0649 + // These fields are assigned to by JSON deserialization + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("expiry")] + public string Expiry { get; set; } + + [JsonProperty("policy")] + public string Policy { get; set; } + + [JsonProperty("extensions")] + private List Extensions { get; set; } + + [JsonProperty("session")] + public ShareSessionResult Session { get; set; } + + [JsonProperty("redirectUri")] + public string RedirectUri { get; set; } + +#pragma warning restore 0649 + + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/Exceptions/DigitalIdentityException.cs b/src/Yoti.Auth/Exceptions/DigitalIdentityException.cs new file mode 100644 index 000000000..cfbe594f6 --- /dev/null +++ b/src/Yoti.Auth/Exceptions/DigitalIdentityException.cs @@ -0,0 +1,22 @@ +using System; + +namespace Yoti.Auth.Exceptions +{ + public class DigitalIdentityException : YotiException + { + public DigitalIdentityException() + : base() + { + } + + public DigitalIdentityException(string message) + : base(message) + { + } + + public DigitalIdentityException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs index 40ebac086..0e43838f4 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs @@ -171,6 +171,38 @@ public void CreateQrCodeShouldFailWithNullKeyPair() Assert.IsTrue(aggregateException.InnerException.Message.Contains("keyPair")); } + [TestMethod] + public void RetrieveQrShouldThrowExceptionForMissingSdkId() + { + var exception = Assert.ThrowsExceptionAsync(async () => + { + await DigitalIdentityService.GetQrCode(_httpClient, _apiURL, null, _keyPair, _sessionID); + }); + + Assert.IsTrue(exception.Exception.InnerException.Message.Contains("sdkId")); + } + + [TestMethod] + public void RetrieveQrCodeShouldThrowExceptionForMissingKeyPair() + { + var exception = Assert.ThrowsExceptionAsync(async () => + { + await DigitalIdentityService.GetQrCode(_httpClient, _apiURL, _sdkID, null, _sessionID); + }).Result; + + Assert.IsTrue(exception.Message.Contains("keyPair")); + } + + [TestMethod] + public void RetrieveQrCodeShouldThrowExceptionForMissingSessionId() + { + var exception = Assert.ThrowsExceptionAsync(async () => + { + await DigitalIdentityService.GetQrCode(_httpClient, _apiURL, _sdkID, _keyPair, null); + }).Result; + + Assert.IsTrue(exception.Message.Contains("qrCodeId")); + } } } \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs index 67397f379..35b3aeb3c 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs @@ -67,7 +67,7 @@ public void CreateShareSessionNonSuccessStatusCodesShouldThrowException(HttpStat engine.CreateShareSessionAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiApiUrl), shareSessionRequest).Wait(); }); - Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); } private static Mock SetupMockMessageHandler(HttpStatusCode httpStatusCode, string responseContent) From e72a8c561313f8412fe31ad3f9ade3c6048bf44f Mon Sep 17 00:00:00 2001 From: nikhilPank <49190426+nikhilPank@users.noreply.github.com> Date: Wed, 10 Jan 2024 18:31:55 +0000 Subject: [PATCH 19/47] IN-5590: Update support link (#437) * update versions * Update README.md * change support contact * Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 2 +- .gitignore | 3 --- README.md | 2 +- azure-pipelines.yml | 2 +- .../DocScan/DocScanExample/DocScanExample.sln | 25 ------------------ src/Yoti.Auth/Yoti.Auth.csproj | 2 +- test/.DS_Store | Bin 6148 -> 0 bytes test/Yoti.Auth.Tests/.DS_Store | Bin 10244 -> 0 bytes test/Yoti.Auth.Tests/DocScan/.DS_Store | Bin 6148 -> 0 bytes .../Yoti.Auth.Tests/DocScan/Session/.DS_Store | Bin 6148 -> 0 bytes .../DocScan/Session/Create/.DS_Store | Bin 6148 -> 0 bytes 11 files changed, 4 insertions(+), 32 deletions(-) delete mode 100644 src/Examples/DocScan/DocScanExample/DocScanExample.sln delete mode 100644 test/.DS_Store delete mode 100644 test/Yoti.Auth.Tests/.DS_Store delete mode 100644 test/Yoti.Auth.Tests/DocScan/.DS_Store delete mode 100644 test/Yoti.Auth.Tests/DocScan/Session/.DS_Store delete mode 100644 test/Yoti.Auth.Tests/DocScan/Session/Create/.DS_Store diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index cce3710df..98f0c89dd 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -12,6 +12,6 @@ assignees: '' # # There's a better way to get help! # -# Send your questions or issues to sdksupport@yoti.com +# Send your questions or issues to https://support.yoti.com # # diff --git a/.gitignore b/.gitignore index 82b0fcb20..6e27c66c1 100644 --- a/.gitignore +++ b/.gitignore @@ -34,9 +34,6 @@ Backup*/ # except build/, which is used as an MSBuild target. !**/packages/build/ -.DS_STORE -.DS_Store - # Coverage OpenCover/ diff --git a/README.md b/README.md index 64354c12d..0a1650864 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ The Yoti SDK can be used for the following products, follow the links for more i ## Support -For any questions or support please email [clientsupport@yoti.com](mailto:clientsupport@yoti.com). +For any questions or support please contact us here: https://support.yoti.com Please provide the following to get you up and working as quickly as possible: * Computer type diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e85f55c19..7c3f24ea7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -32,7 +32,7 @@ steps: scannerMode: 'MSBuild' projectKey: 'getyoti:dotnet' projectName: '.NET SDK' - projectVersion: '3.11.0' + projectVersion: '3.13.0' extraProperties: | sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" sonar.links.scm = https://github.com/getyoti/yoti-dotnet-sdk diff --git a/src/Examples/DocScan/DocScanExample/DocScanExample.sln b/src/Examples/DocScan/DocScanExample/DocScanExample.sln deleted file mode 100644 index 52210ccfd..000000000 --- a/src/Examples/DocScan/DocScanExample/DocScanExample.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 25.0.1703.1 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocScanExample", "DocScanExample.csproj", "{33DF7B65-3CBB-40B0-A08A-17A05AB7D071}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {33DF7B65-3CBB-40B0-A08A-17A05AB7D071}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {33DF7B65-3CBB-40B0-A08A-17A05AB7D071}.Debug|Any CPU.Build.0 = Debug|Any CPU - {33DF7B65-3CBB-40B0-A08A-17A05AB7D071}.Release|Any CPU.ActiveCfg = Release|Any CPU - {33DF7B65-3CBB-40B0-A08A-17A05AB7D071}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {D250BACD-0361-4529-98E0-EEE3F48D5863} - EndGlobalSection -EndGlobal diff --git a/src/Yoti.Auth/Yoti.Auth.csproj b/src/Yoti.Auth/Yoti.Auth.csproj index 2757c82af..f5dbfcda6 100644 --- a/src/Yoti.Auth/Yoti.Auth.csproj +++ b/src/Yoti.Auth/Yoti.Auth.csproj @@ -19,7 +19,7 @@ False latest true - 3.12.0 + 3.14.0 diff --git a/test/.DS_Store b/test/.DS_Store deleted file mode 100644 index cde0a0dae082b2a894391347ddd4657ffcf9946b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK-A)rh6g~r0S}L+Yi`8hdu{S0VL?9|LhEg!v7z3dMTLH^%JCud(OtZTMfspiC zuY3TXfm;*ryz&XWGd_a8fYCEQ4Sy=(#u&1vnfcDloNuP{ZO={z07$k`xdUJTKopC> zNGDdmF)=R632W0m7ZHh^!vYtEU>$tmC!)iSP=Qc^|4jjryK$&u-g2o(|O)pIKOx>a^qwGhx(LEhP+C8PIP=p3!QJm{L=u47E% zphHgg*rSxm`DRvEeHs)>Mz>$~xa-bx-w7PfiibvvOFpWf3urCJRynS?iOSE^MVP-p zJs}8dRyyxUq8AIS;zJ%&wDfPF_*I{Mv7Vb|DHjZO;ea*HT-ZmmW|{F z@(*VtHqUH&C(-zsnIR!mAXMPID~9lDpYhJ`tm{a%0%B#h+G}#H)S}GjKZ)E6$lkLszAGH z8shwa^ymBkQBv3n6$lkLR|=5GbbdOETheFiiOq?#*2J=cMVN?dDwGtg^l>a5aTKSp cNW(ExIEWdIO@-Kl7XA^CGK5vAz&};s7skQ>dH?_b diff --git a/test/Yoti.Auth.Tests/.DS_Store b/test/Yoti.Auth.Tests/.DS_Store deleted file mode 100644 index 2c4194866b04e5c94d9b930ba4873de92196ac40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10244 zcmeHM-A)rh6h1>y77-;NhM16SBr#qPM8F^7P5Ci~J7ZB|)L=_1tS&oEw}lu2>9xLt zui&jOqL1OdSNfeZYuTOFA{xcSnP%tg&YttlnQzXV%}j|%EjG&6iF_jRaIqXZi>gE6 z_k6XLo}}e6q=G%s4h2-F*R)KPHf`777;p?Y1{?#90ms0tPYt^ zWYb7vPRf{*N_tkw4n;}tu=7QoRNhEeI|dvB?F?K-(>N{A2KMbiX8&HNB3eR<=|1ji zkQLOcsAK5HGzFV@CDa?GP?vzh3vU6})K@9E<2D<)yZw91ei-)K@Zmms*7QRBsdV>2 zjawLfz=+`M77#FstjFrDrF{w7Rs2HqwSjRnD;oC4@!(4b%sP;2z$#G#cNJO^)E(iJ zpO0X+XVb#W0V$v;JDQ%4owZ84aW$Ex52b1t#P#XvPpT)^d+Kzb*XIp*&k9?5w@@$E z8}VYX_Jp5SwGK<)?~A2JL1lM+=)#hY>ct?c)WjetSD^BAEr`l`cTqQ@axAt5^TO-* z`qziX_V(uHZ%z8Mw`cYz{k{2_=}CWX_U8V6zc)H@?aqVsSK)S~-@tPem85qXG|#P? z=P4QUPN5csI$}mx`vSPE;moNDUNzmtTx~Kl;Iwe#p-#SS_NXukss0um4OjS z1mH|7%QQ!sI+wDZ=xz-kIid{SPy5L8sSZ-Tj31+yBS)`3r;z)AtP$}AiFzxph}UJB zD@*&N<7;;22IA}H+;7I0H7YC4t|SaKkde$h@+s$GmR!Jnc8&jZ-jtk`=696tAzn1r zI7eauIfHXj4gJOVnT*si14}+jBjp6;r4_xLq0(AslKfUh4>gH+^pv%c43xGx1;4iB z%TTd2ISZvVy+ciU2>dXskBpmLOI~Z`E=vY3TKb5_%-UAEhN${n=+qi zyy1>`-F?Cu$&7BKg>j6Skoin|f~Db#W56-s7;p?Y22PZLoax=?^Z&cptpDKu{}c7m zb?z8&4E$LJRPSjravW`iJOX$jhE1B24_Q@h8x+2pcym=t)1DdyZ^E+yN3t!ww%4%@3iGX_i#RM2HVe`zkWaXmVa0J z2MdM|2Vah{#|{^8fnY_0^JuJct-hjV!CGW{^a^+dyaKmTz^x&{)@?{W*DK%^cn}Kk z`XJ$qp~K3eSvpXdBLJ|DYGa7`$B;SFVd${3h!&WzRG_5_cf=5uj(X|xLWh+_ODEwD zAHt0++zCaP(b2y&=_EpnzVr%s1(p@qu$L{~|KI-m{J%`{S6%_Hz`asH)caAthevXI z>)ONdUMt~W;B1^%S^PsmV76l9@>aY9H->u20Wfq}Sws!Y{|FcveBl-NuL}GIeI0w< diff --git a/test/Yoti.Auth.Tests/DocScan/Session/.DS_Store b/test/Yoti.Auth.Tests/DocScan/Session/.DS_Store deleted file mode 100644 index 3f7e57b3c430fb4156a09735ecd4b714d2f46a8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!Ab)$5S`R&Q$*-Np~ryNqSdM(Ue;OX-OC zI+H|ksa~bX3`|}!nb}QVLM8zK(H{0T0BQgrQ3*>`EPfE`C!La#vy>5q`9=oOQKGNI zM2Az}Y?ws`Xzdo@0QwL?0LAsAO|g%<_HM)szu+5=1`lqnU&Dta9%Rktn<&kd=NBrn zA{XVQf2&7+7G%RgJLsQN>qP4${HzDzX&jBZwdDhyWLV z4NRyS4gw+}hl3sMgNbq@l*Uo6UyWYVAxWRk^adzIWWcOYT$sWJV1F_otC1 zhoA6_mBO4J{eGJ0^akw|BjFgC$&jBI@v*SKiJuqL@BCnsg&AN5n1PvRz@3S5app(L z17rr6fxl*e&Ig4`=sCgbhWsp~5{egbhc# za(;n6ut-;8n|Ev H{*-|ayS`-b diff --git a/test/Yoti.Auth.Tests/DocScan/Session/Create/.DS_Store b/test/Yoti.Auth.Tests/DocScan/Session/Create/.DS_Store deleted file mode 100644 index 415710a702370d3ca8796f326f0bd960e332f261..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyG{c^3>-s>NNG}1?l15Ms}T5s{6G*yG$^2mq`r#p;?pvI20a|icjG!J4AG8> j(T=(Cc6<{>S=W5c^IkY42A%n!6ZJFTy2zx!Un_71kIogP From 2f774adbb285ea74bb7f630a92022e5048524b7d Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Thu, 25 Apr 2024 18:51:51 +0300 Subject: [PATCH 20/47] Sdk 2363 net check for and add optional attribute configuration to sdk (#458) * SDK-2363 added check for optional attribute, updated tests * SDK-2363 updated test scenarios to match new changes --- src/Yoti.Auth/ShareUrl/Policy/DynamicPolicyBuilder.cs | 3 ++- src/Yoti.Auth/ShareUrl/Policy/WantedAttribute.cs | 8 ++++---- .../ShareUrl/Policy/WantedAttributeBuilder.cs | 11 +++++++++-- .../ShareUrl/Policy/WantedAttributeBuilderTests.cs | 5 ++++- test/Yoti.Auth.Tests/TestData/DynamicPolicy.json | 11 ++++------- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicyBuilder.cs b/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicyBuilder.cs index 8d4ba4e45..ba32c6e5d 100644 --- a/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicyBuilder.cs +++ b/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicyBuilder.cs @@ -29,6 +29,7 @@ public DynamicPolicyBuilder WithWantedAttribute(string name, List co { WantedAttribute wantedAttribute = new WantedAttributeBuilder() .WithName(name) + .WithOptional(false) .WithConstraints(constraints) .Build(); return WithWantedAttribute(wantedAttribute); @@ -163,4 +164,4 @@ public DynamicPolicy Build() return new DynamicPolicy(_wantedAttributes.Values, _wantedAuthTypes, _wantedRememberMeId, _identityProfileRequirements); } } -} \ No newline at end of file +} diff --git a/src/Yoti.Auth/ShareUrl/Policy/WantedAttribute.cs b/src/Yoti.Auth/ShareUrl/Policy/WantedAttribute.cs index 99e7b6637..fdacbcf50 100644 --- a/src/Yoti.Auth/ShareUrl/Policy/WantedAttribute.cs +++ b/src/Yoti.Auth/ShareUrl/Policy/WantedAttribute.cs @@ -14,7 +14,7 @@ public class WantedAttribute [JsonRequired] [JsonProperty(PropertyName = "optional")] - public bool Optional { get; private set; } + public bool? Optional { get; private set; } [JsonProperty(PropertyName = "accept_self_asserted")] public bool? AcceptSelfAsserted { get; private set; } @@ -22,13 +22,13 @@ public class WantedAttribute [JsonProperty(PropertyName = "constraints")] public List Constraints { get; private set; } - public WantedAttribute(string name, string derivation, List constraints, bool? acceptSelfAsserted = null) + public WantedAttribute(string name, string derivation, List constraints, bool? acceptSelfAsserted = null, bool? optional = false) { Name = name; Derivation = derivation; - Optional = false; + Optional = optional; AcceptSelfAsserted = acceptSelfAsserted; Constraints = constraints; } } -} \ No newline at end of file +} diff --git a/src/Yoti.Auth/ShareUrl/Policy/WantedAttributeBuilder.cs b/src/Yoti.Auth/ShareUrl/Policy/WantedAttributeBuilder.cs index 4bf1b2737..f4f35cb46 100644 --- a/src/Yoti.Auth/ShareUrl/Policy/WantedAttributeBuilder.cs +++ b/src/Yoti.Auth/ShareUrl/Policy/WantedAttributeBuilder.cs @@ -8,6 +8,7 @@ public class WantedAttributeBuilder private string _derivation; private List _constraints = new List(); private bool? _acceptSelfAsserted; + private bool? _optional; public WantedAttributeBuilder WithName(string name) { @@ -15,6 +16,12 @@ public WantedAttributeBuilder WithName(string name) return this; } + public WantedAttributeBuilder WithOptional(bool optional) + { + _optional = optional; + return this; + } + public WantedAttributeBuilder WithDerivation(string derivation) { _derivation = derivation; @@ -56,7 +63,7 @@ public WantedAttribute Build() { Validation.NotNullOrEmpty(_name, nameof(_name)); - return new WantedAttribute(_name, _derivation, _constraints, _acceptSelfAsserted); + return new WantedAttribute(_name, _derivation, _constraints, _acceptSelfAsserted, _optional); } } -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/ShareUrl/Policy/WantedAttributeBuilderTests.cs b/test/Yoti.Auth.Tests/ShareUrl/Policy/WantedAttributeBuilderTests.cs index b39fead0b..23b931598 100644 --- a/test/Yoti.Auth.Tests/ShareUrl/Policy/WantedAttributeBuilderTests.cs +++ b/test/Yoti.Auth.Tests/ShareUrl/Policy/WantedAttributeBuilderTests.cs @@ -10,6 +10,7 @@ public class WantedAttributeBuilderTests { private const string _someName = "some name"; private const string _someDerivation = "some derivation"; + private const bool _someOptional = true; [TestMethod] public void BuildsAnAttribute() @@ -22,11 +23,13 @@ public void BuildsAnAttribute() .WithName(_someName) .WithDerivation(_someDerivation) .WithConstraint(sourceConstraint) + .WithOptional(_someOptional) .Build(); Assert.AreEqual(1, result.Constraints.Count); Assert.AreEqual(_someName, result.Name); Assert.AreEqual(_someDerivation, result.Derivation); + Assert.AreEqual(_someOptional, result.Optional); } [TestMethod] @@ -161,4 +164,4 @@ public void WithConstraintsShouldOverrideCurrentConstraint() Assert.AreEqual("DRIVING_LICENCE", result.PreferredSources.WantedAnchors[0].Name); } } -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/TestData/DynamicPolicy.json b/test/Yoti.Auth.Tests/TestData/DynamicPolicy.json index 86b8ba46f..e65922887 100644 --- a/test/Yoti.Auth.Tests/TestData/DynamicPolicy.json +++ b/test/Yoti.Auth.Tests/TestData/DynamicPolicy.json @@ -8,18 +8,15 @@ }, { "name": "date_of_birth", - "derivation": "age_over:18", - "optional": false + "derivation": "age_over:18" }, { "name": "date_of_birth", - "derivation": "age_under:30", - "optional": false + "derivation": "age_under:30" }, { "name": "date_of_birth", - "derivation": "age_under:40", - "optional": false + "derivation": "age_under:40" } ], "wanted_auth_types": [ 2 ], @@ -53,4 +50,4 @@ "subject": { "subject_id": "some_subject_id_string" } -} \ No newline at end of file +} From b85fa2732449534c4a7515d742129df7c695d25c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 17:37:04 +0100 Subject: [PATCH 21/47] Bump Google.Protobuf from 3.21.3 to 3.22.0 (#425) Bumps [Google.Protobuf](https://github.com/protocolbuffers/protobuf) from 3.21.3 to 3.22.0. - [Release notes](https://github.com/protocolbuffers/protobuf/releases) - [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/generate_changelog.py) - [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.21.3...v3.22.0) --- updated-dependencies: - dependency-name: Google.Protobuf dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/Yoti.Auth/Yoti.Auth.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Yoti.Auth/Yoti.Auth.csproj b/src/Yoti.Auth/Yoti.Auth.csproj index f5dbfcda6..6d2b1d696 100644 --- a/src/Yoti.Auth/Yoti.Auth.csproj +++ b/src/Yoti.Auth/Yoti.Auth.csproj @@ -37,7 +37,7 @@ - + all From 9ce5386b753aca1958abfd83f0dd863294ca3020 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 17:51:33 +0100 Subject: [PATCH 22/47] Bump Microsoft.CodeAnalysis.CSharp.Workspaces, Microsoft.CodeAnalysis.Common and Microsoft.CodeAnalysis.CSharp (#460) Bumps [Microsoft.CodeAnalysis.CSharp.Workspaces](https://github.com/dotnet/roslyn), [Microsoft.CodeAnalysis.Common](https://github.com/dotnet/roslyn) and [Microsoft.CodeAnalysis.CSharp](https://github.com/dotnet/roslyn). These dependencies needed to be updated together. Updates `Microsoft.CodeAnalysis.CSharp.Workspaces` from 4.2.0 to 4.9.2 - [Release notes](https://github.com/dotnet/roslyn/releases) - [Changelog](https://github.com/dotnet/roslyn/blob/main/docs/Breaking%20API%20Changes.md) - [Commits](https://github.com/dotnet/roslyn/commits) Updates `Microsoft.CodeAnalysis.Common` from 4.2.0 to 4.9.2 - [Release notes](https://github.com/dotnet/roslyn/releases) - [Changelog](https://github.com/dotnet/roslyn/blob/main/docs/Breaking%20API%20Changes.md) - [Commits](https://github.com/dotnet/roslyn/commits) Updates `Microsoft.CodeAnalysis.CSharp` from 4.2.0 to 4.9.2 - [Release notes](https://github.com/dotnet/roslyn/releases) - [Changelog](https://github.com/dotnet/roslyn/blob/main/docs/Breaking%20API%20Changes.md) - [Commits](https://github.com/dotnet/roslyn/commits) --- updated-dependencies: - dependency-name: Microsoft.CodeAnalysis.CSharp.Workspaces dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: Microsoft.CodeAnalysis.Common dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: Microsoft.CodeAnalysis.CSharp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/Examples/Profile/CoreExample/CoreExample.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Examples/Profile/CoreExample/CoreExample.csproj b/src/Examples/Profile/CoreExample/CoreExample.csproj index 769193e74..230361a71 100644 --- a/src/Examples/Profile/CoreExample/CoreExample.csproj +++ b/src/Examples/Profile/CoreExample/CoreExample.csproj @@ -17,9 +17,9 @@ - - - + + + From 8633048aa31c8f4287dfe469540f014d1b014c18 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 17:52:06 +0100 Subject: [PATCH 23/47] Bump Microsoft.VisualStudio.Azure.Containers.Tools.Targets (#459) Bumps Microsoft.VisualStudio.Azure.Containers.Tools.Targets from 1.16.1 to 1.20.1. --- updated-dependencies: - dependency-name: Microsoft.VisualStudio.Azure.Containers.Tools.Targets dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/Examples/Profile/CoreExample/CoreExample.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Examples/Profile/CoreExample/CoreExample.csproj b/src/Examples/Profile/CoreExample/CoreExample.csproj index 230361a71..e65661230 100644 --- a/src/Examples/Profile/CoreExample/CoreExample.csproj +++ b/src/Examples/Profile/CoreExample/CoreExample.csproj @@ -21,7 +21,7 @@ - + From 7b73f5ad07a9560323e938564bfbbd6498edecc3 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Thu, 25 Apr 2024 20:33:04 +0300 Subject: [PATCH 24/47] SDK-2264 share v2 net-retrieve-receipt (#457) * SDK-2264:Added GetShareReceipt and CryptoEngine methods,added models,unit tests, example digitalidentity project, controllers * SDK-2416 added support for advanced identity profile to share v2 (#461) * SDK-2264 updated Newtonsoft.Json 13.0.3 --- src/Examples/Aml/AmlExample/AmlExample.csproj | 2 +- .../DigitalIdentity/.dockerignore | 25 ++ .../DigitalIdentity/.env.example | 2 + .../AdvancedIdentityShareController.cs | 108 +++++ .../Controllers/HomeController.cs | 85 ++++ .../Controllers/SuccessController.cs | 163 +++++++ .../DigitalIdentityExample.csproj | 55 +++ .../DigitalIdentity/Dockerfile | 20 + .../DigitalIdentity/GlobalSuppressions.cs | 9 + .../Models/DisplayAttribute.cs | 62 +++ .../Models/DisplayAttributes.cs | 28 ++ .../DigitalIdentity/Program.cs | 17 + .../Properties/launchSettings.json | 26 ++ .../DigitalIdentity/DigitalIdentity/README.md | 21 + .../DigitalIdentity/Startup.cs | 87 ++++ .../AdvancedIdentityShare.cshtml | 92 ++++ .../Views/Home/DigitalIdentity.cshtml | 92 ++++ .../Views/Success/Error.cshtml | 19 + .../Views/Success/SuccessResult.cshtml | 171 +++++++ .../DigitalIdentity/Views/Web.config | 43 ++ .../appsettings.Development.json | 9 + .../DigitalIdentity/appsettings.json | 8 + .../DigitalIdentity/docker-compose.dcproj | 18 + .../docker-compose.override.yml | 18 + .../DigitalIdentity/docker-compose.yml | 8 + .../https/DigitalIdentityExample.pfx | Bin 0 -> 2700 bytes .../wwwroot/static/assets/app-store-badge.png | Bin 0 -> 4077 bytes .../static/assets/app-store-badge@2x.png | Bin 0 -> 8819 bytes .../wwwroot/static/assets/company-logo.jpg | Bin 0 -> 4682 bytes .../static/assets/google-play-badge.png | Bin 0 -> 4957 bytes .../static/assets/google-play-badge@2x.png | Bin 0 -> 11267 bytes .../wwwroot/static/assets/icons/address.svg | 3 + .../wwwroot/static/assets/icons/calendar.svg | 5 + .../static/assets/icons/chevron-down-grey.svg | 7 + .../wwwroot/static/assets/icons/document.svg | 3 + .../wwwroot/static/assets/icons/email.svg | 14 + .../wwwroot/static/assets/icons/gender.svg | 5 + .../static/assets/icons/nationality.svg | 3 + .../wwwroot/static/assets/icons/phone.svg | 3 + .../wwwroot/static/assets/icons/profile.svg | 3 + .../wwwroot/static/assets/icons/verified.svg | 6 + .../wwwroot/static/assets/logo.png | Bin 0 -> 2988 bytes .../wwwroot/static/assets/logo@2x.png | Bin 0 -> 5609 bytes .../DigitalIdentity/wwwroot/static/index.css | 152 +++++++ .../wwwroot/static/profile.css | 420 ++++++++++++++++++ src/Yoti.Auth.sln | 7 + src/Yoti.Auth/CryptoEngine.cs | 66 ++- .../DigitalIdentity/DigitalIdentityService.cs | 238 +++++++++- src/Yoti.Auth/DigitalIdentity/GetReceipt.cs | 46 ++ .../DigitalIdentity/Policy/Notification.cs | 6 +- .../DigitalIdentity/Policy/Policy.cs | 22 +- .../DigitalIdentity/Policy/PolicyBuilder.cs | 16 +- .../DigitalIdentity/ReceiptItemKeyResponse.cs | 16 + .../DigitalIdentity/SharedReceiptResponse.cs | 29 ++ src/Yoti.Auth/DigitalIdentityClient.cs | 30 +- src/Yoti.Auth/DigitalIdentityClientEngine.cs | 23 +- src/Yoti.Auth/Yoti.Auth.csproj | 40 +- .../Yoti.Auth.Tests.Common.csproj | 2 +- test/Yoti.Auth.Tests/CryptoEngineTests.cs | 63 ++- .../Policy/DynamicPolicyBuilderTests.cs | 13 +- .../DigitalIdentityClientEngineTests.cs | 29 +- .../DigitalIdentityClientTests.cs | 22 +- .../DigitalIdentityExceptionTests.cs | 36 ++ .../TestData/IdentityProfiles.cs | 40 +- 64 files changed, 2481 insertions(+), 75 deletions(-) create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/.dockerignore create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/.env.example create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Controllers/AdvancedIdentityShareController.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Controllers/SuccessController.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/DigitalIdentityExample.csproj create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Dockerfile create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/GlobalSuppressions.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Models/DisplayAttribute.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Models/DisplayAttributes.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Program.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Properties/launchSettings.json create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/README.md create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Startup.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Views/AdvancedIdentityShare/AdvancedIdentityShare.cshtml create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/Error.cshtml create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/SuccessResult.cshtml create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Views/Web.config create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/appsettings.Development.json create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/appsettings.json create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.dcproj create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.override.yml create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.yml create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/https/DigitalIdentityExample.pfx create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge.png create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge@2x.png create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/company-logo.jpg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/google-play-badge.png create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/google-play-badge@2x.png create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/address.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/calendar.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/chevron-down-grey.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/document.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/email.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/gender.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/nationality.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/phone.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/profile.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/verified.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/logo.png create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/logo@2x.png create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/index.css create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/profile.css create mode 100644 src/Yoti.Auth/DigitalIdentity/GetReceipt.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/ReceiptItemKeyResponse.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentityExceptionTests.cs diff --git a/src/Examples/Aml/AmlExample/AmlExample.csproj b/src/Examples/Aml/AmlExample/AmlExample.csproj index f96d7c368..ab68935fb 100644 --- a/src/Examples/Aml/AmlExample/AmlExample.csproj +++ b/src/Examples/Aml/AmlExample/AmlExample.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/.dockerignore b/src/Examples/DigitalIdentity/DigitalIdentity/.dockerignore new file mode 100644 index 000000000..e7b690f11 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/.env.example b/src/Examples/DigitalIdentity/DigitalIdentity/.env.example new file mode 100644 index 000000000..e8d11ad0c --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/.env.example @@ -0,0 +1,2 @@ +YOTI_CLIENT_SDK_ID=yourClientSdkId +YOTI_KEY_FILE_PATH=yourKeyFilePath diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/AdvancedIdentityShareController.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/AdvancedIdentityShareController.cs new file mode 100644 index 000000000..7fe32c974 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/AdvancedIdentityShareController.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Yoti.Auth; +using Yoti.Auth.DigitalIdentity; +using Yoti.Auth.DigitalIdentity.Policy; + +namespace DigitalIdentityExample.Controllers +{ + public class AdvancedIdentityShareController : Controller + { + private readonly string _clientSdkId; + private readonly ILogger _logger; + public AdvancedIdentityShareController(ILogger logger) + { + _logger = logger; + + _clientSdkId = Environment.GetEnvironmentVariable("YOTI_CLIENT_SDK_ID"); + _logger.LogInformation(string.Format("Yoti Client SDK ID='{0}'", _clientSdkId)); + } + + // GET: /advanced-identity-share + [Route("advanced-identity-share")] + public IActionResult DigitalIdentity() + { + try + { + string yotiKeyFilePath = Environment.GetEnvironmentVariable("YOTI_KEY_FILE_PATH"); + _logger.LogInformation( + string.Format( + "yotiKeyFilePath='{0}'", + yotiKeyFilePath)); + + StreamReader privateKeyStream = System.IO.File.OpenText(yotiKeyFilePath); + + var yotiClient = new DigitalIdentityClient(_clientSdkId, privateKeyStream); + + string advancedIdentityProfileJson = @" + { + ""profiles"": [ + { + ""trust_framework"": ""UK_TFIDA"", + ""schemes"": [ + { + ""label"": ""dbs-standard"", + ""objective"": ""STANDARD"", + ""type"": ""DBS"", + }, + { + ""label"": ""rtw"", + ""type"": ""RTW"", + ""objective"": """" + } + ] + }, + { + ""trust_framework"": ""YOTI_GLOBAL"", + ""schemes"": [ + { + ""label"": ""identity-AL-L1"", + ""type"": ""IDENTITY"", + ""objective"": ""AL_L1"", + + }, + { + ""label"": ""identity-AL-M1"", + ""type"": ""IDENTITY"", + ""objective"": ""AL_M1"", + + } + ] + } + ] + + }"; + + string sessionSpecJson = JsonConvert.SerializeObject(advancedIdentityProfileJson); + var policy = new PolicyBuilder() + .WithAdvancedIdentityProfileRequirements(sessionSpecJson) + .Build(); + + var sessionReq = new ShareSessionRequestBuilder().WithPolicy(policy) + .WithRedirectUri("https:/www.yoti.com") + .Build(); + + var SessionResult = yotiClient.CreateShareSession(sessionReq); + + var sharedReceiptResponse = new SharedReceiptResponse(); + ViewBag.YotiClientSdkId = _clientSdkId; + ViewBag.sessionID = SessionResult.Id; + + return View("AdvancedIdentityShare", sharedReceiptResponse); + } + catch (Exception e) + { + _logger.LogError( + exception: e, + message: e.Message); + + TempData["Error"] = e.Message; + TempData["InnerException"] = e.InnerException?.Message; + return RedirectToAction("Error", "Success"); + } + } + } +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs new file mode 100644 index 000000000..7b0ad261c --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs @@ -0,0 +1,85 @@ +using System; +using System.IO; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Yoti.Auth; +using Yoti.Auth.DigitalIdentity; +using Yoti.Auth.DigitalIdentity.Policy; + +namespace DigitalIdentityExample.Controllers +{ + public class HomeController : Controller + { + private readonly string _clientSdkId; + private readonly ILogger _logger; + public HomeController(ILogger logger) + { + _logger = logger; + + _clientSdkId = Environment.GetEnvironmentVariable("YOTI_CLIENT_SDK_ID"); + _logger.LogInformation(string.Format("Yoti Client SDK ID='{0}'", _clientSdkId)); + } + + // GET: /generate-share + [Route("generate-share")] + public IActionResult DigitalIdentity() + { + try + { + string yotiKeyFilePath = Environment.GetEnvironmentVariable("YOTI_KEY_FILE_PATH"); + _logger.LogInformation( + string.Format( + "yotiKeyFilePath='{0}'", + yotiKeyFilePath)); + + StreamReader privateKeyStream = System.IO.File.OpenText(yotiKeyFilePath); + + var yotiClient = new DigitalIdentityClient(_clientSdkId, privateKeyStream); + + var policy = new PolicyBuilder() + .WithFullName() + .WithEmail() + .WithPhoneNumber() + .WithSelfie() + .WithAgeOver(18) + .WithNationality() + .WithGender() + .WithDocumentDetails() + .WithDocumentImages() + .Build(); + + var sessionReq = new ShareSessionRequestBuilder().WithPolicy(policy) + .WithNotification(new Notification + { + Headers = { }, + Url = "https://example.com/webhook", + Method = "POST", + VerifyTls = true + + }) + .WithRedirectUri("https:/www.yoti.com").WithSubject(new + { + subject_id = "some_subject_id_string" + }).Build(); + + var SessionResult = yotiClient.CreateShareSession(sessionReq); + + var sharedReceiptResponse = new SharedReceiptResponse(); + ViewBag.YotiClientSdkId = _clientSdkId; + ViewBag.sessionID = SessionResult.Id; + + return View("DigitalIdentity", sharedReceiptResponse); + } + catch (Exception e) + { + _logger.LogError( + exception: e, + message: e.Message); + + TempData["Error"] = e.Message; + TempData["InnerException"] = e.InnerException?.Message; + return RedirectToAction("Error", "Success"); + } + } + } +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/SuccessController.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/SuccessController.cs new file mode 100644 index 000000000..aeac61a24 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/SuccessController.cs @@ -0,0 +1,163 @@ +using System; +using System.IO; +using DigitalIdentity.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Yoti.Auth; +using Yoti.Auth.Attribute; +using Yoti.Auth.Document; +using Yoti.Auth.Images; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Newtonsoft.Json.Linq; + +namespace DigitalIdentityExample.Controllers +{ + public class SuccessController : Controller + { + private readonly string _clientSdkId; + private readonly ILogger _logger; + public SuccessController(ILogger logger) + { + _logger = logger; + + _clientSdkId = Environment.GetEnvironmentVariable("YOTI_CLIENT_SDK_ID"); + _logger.LogInformation(string.Format("Yoti Client SDK ID='{0}'", _clientSdkId)); + } + public ActionResult Error() + { + return View(); + } + [Route("receipt-info")] + // GET: receipt-info?ReceiptID + public IActionResult ReceiptInfo(string ReceiptID) + { + try + { + string yotiKeyFilePath = Environment.GetEnvironmentVariable("YOTI_KEY_FILE_PATH"); + _logger.LogInformation( + string.Format( + "yotiKeyFilePath='{0}'", + yotiKeyFilePath)); + + StreamReader privateKeyStream = System.IO.File.OpenText(yotiKeyFilePath); + + var yotiClient = new DigitalIdentityClient(_clientSdkId, privateKeyStream); + + var ReceiptResult = yotiClient.GetShareReceipt(ReceiptID); + + DisplayAttributes displayAttributes = CreateDisplayAttributes(ReceiptResult.UserContent.UserProfile.AttributeCollection); + if (ReceiptResult.UserContent.UserProfile.FullName != null) + { + displayAttributes.FullName = ReceiptResult.UserContent.UserProfile.FullName.GetValue(); + } + + YotiAttribute selfie = ReceiptResult.UserContent.UserProfile.Selfie; + if (ReceiptResult.UserContent.UserProfile.Selfie != null) + { + displayAttributes.Base64Selfie = selfie.GetValue().GetBase64URI(); + } + ViewBag.YotiClientSdkId = _clientSdkId; + + return View("SuccessResult", displayAttributes); + } + catch (Exception e) + { + _logger.LogError( + exception: e, + message: e.Message); + + TempData["Error"] = e.Message; + TempData["InnerException"] = e.InnerException?.Message; + return RedirectToAction("Error", "Success"); + } + } + + private static DisplayAttributes CreateDisplayAttributes(ReadOnlyCollection attributes) + { + var displayAttributes = new DisplayAttributes(); + + foreach (var yotiAttribute in attributes) + { + switch (yotiAttribute.GetName()) + { + case Yoti.Auth.Constants.UserProfile.FullNameAttribute: + // Do nothing - we are displaying this already + break; + + case Yoti.Auth.Constants.UserProfile.GivenNamesAttribute: + AddDisplayAttribute("Given name", "yoti-icon-profile", yotiAttribute, displayAttributes); + break; + + case Yoti.Auth.Constants.UserProfile.FamilyNameAttribute: + AddDisplayAttribute("Family name", "yoti-icon-profile", yotiAttribute, displayAttributes); + break; + + case Yoti.Auth.Constants.UserProfile.NationalityAttribute: + AddDisplayAttribute("Nationality", "yoti-icon-nationality", yotiAttribute, displayAttributes); + break; + + case Yoti.Auth.Constants.UserProfile.PostalAddressAttribute: + AddDisplayAttribute("Postal Address", "yoti-icon-address", yotiAttribute, displayAttributes); + break; + + case Yoti.Auth.Constants.UserProfile.StructuredPostalAddressAttribute: + AddDisplayAttribute>("Structured Postal Address", "yoti-icon-address", yotiAttribute, displayAttributes); + break; + + case Yoti.Auth.Constants.UserProfile.PhoneNumberAttribute: + AddDisplayAttribute("Mobile number", "yoti-icon-phone", yotiAttribute, displayAttributes); + break; + + case Yoti.Auth.Constants.UserProfile.EmailAddressAttribute: + AddDisplayAttribute("Email address", "yoti-icon-email", yotiAttribute, displayAttributes); + break; + + case Yoti.Auth.Constants.UserProfile.DateOfBirthAttribute: + AddDisplayAttribute("Date of birth", "yoti-icon-calendar", yotiAttribute, displayAttributes); + break; + + case Yoti.Auth.Constants.UserProfile.SelfieAttribute: + // Do nothing - we already display the selfie + break; + + case Yoti.Auth.Constants.UserProfile.GenderAttribute: + AddDisplayAttribute("Gender", "yoti-icon-gender", yotiAttribute, displayAttributes); + break; + + case Yoti.Auth.Constants.UserProfile.DocumentDetailsAttribute: + AddDisplayAttribute("Document Details", "yoti-icon-profile", yotiAttribute, displayAttributes); + break; + + case Yoti.Auth.Constants.UserProfile.DocumentImagesAttribute: + AddDisplayAttribute>("Document Images", "yoti-icon-profile", yotiAttribute, displayAttributes); + break; + + case Yoti.Auth.Constants.UserProfile.IdentityProfileReportAttribute: + AddDisplayAttribute>("Identity Profile Report", "yoti-icon-profile", yotiAttribute, displayAttributes); + break; + + default: + if (yotiAttribute is YotiAttribute stringAttribute) + { + if (stringAttribute.GetName().Contains(":")) + { + displayAttributes.Add(new DisplayAttribute("Age Verification/", "Age verified", "yoti-icon-verified", stringAttribute.GetAnchors(), stringAttribute.GetValue())); + break; + } + + AddDisplayAttribute(stringAttribute.GetName(), "yoti-icon-profile", yotiAttribute, displayAttributes); + } + break; + } + } + + return displayAttributes; + } + private static void AddDisplayAttribute(string name, string icon, BaseAttribute baseAttribute, DisplayAttributes displayAttributes) + { + if (baseAttribute is YotiAttribute yotiAttribute) + displayAttributes.Add(name, icon, yotiAttribute.GetAnchors(), yotiAttribute.GetValue()); + } + } +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/DigitalIdentityExample.csproj b/src/Examples/DigitalIdentity/DigitalIdentity/DigitalIdentityExample.csproj new file mode 100644 index 000000000..b24d78cf1 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/DigitalIdentityExample.csproj @@ -0,0 +1,55 @@ + + + + net6.0 + Linux + 9c82fa55-c27e-4405-8983-72662528e16f + ..\..\docker-compose.dcproj + ..\..\.. + DigitalIdentityExample + DigitalIdentityExample + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + + + <_ContentIncludedByDefault Remove="Pages\Success\SuccessResult.cshtml" /> + + \ No newline at end of file diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Dockerfile b/src/Examples/DigitalIdentity/DigitalIdentity/Dockerfile new file mode 100644 index 000000000..bfd9c6630 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base + +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY Examples/DigitalIdentity/DigitalIdentity/DigitalIdentityExample.csproj Examples/DigitalIdentity/DigitalIdentity/ +COPY Yoti.Auth/Yoti.Auth.csproj Yoti.Auth/ +COPY . . +WORKDIR /src/Examples/DigitalIdentity/DigitalIdentity + +FROM build AS publish +RUN dotnet publish DigitalIdentityExample.csproj -c Release -r linux-x64 -o /app -p:TargetFrameworks=netcoreapp6.0 -f netcoreapp6.0 + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "./DigitalIdentityExample.dll"] diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/GlobalSuppressions.cs b/src/Examples/DigitalIdentity/DigitalIdentity/GlobalSuppressions.cs new file mode 100644 index 000000000..87fc554ae --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/GlobalSuppressions.cs @@ -0,0 +1,9 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Auto-generated", Scope = "member", Target = "~M:DigitalIdentityExample.Startup.ConfigureServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Auto-generated", Scope = "member", Target = "~M:DigitalIdentityExample.Startup.Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder,Microsoft.AspNetCore.Hosting.IHostingEnvironment)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "We want to show all errors here to aid debugging", Scope = "member", Target = "~M:DigitalIdentityExample.Controllers.AccountController.Connect(System.String)~Microsoft.AspNetCore.Mvc.ActionResult")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "We want to show all errors here to aid debugging", Scope = "member", Target = "~M:DigitalIdentityExample.Controllers.HomeController.DynamicScenario~Microsoft.AspNetCore.Mvc.IActionResult")] diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Models/DisplayAttribute.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Models/DisplayAttribute.cs new file mode 100644 index 000000000..6625d295d --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Models/DisplayAttribute.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Yoti.Auth.Anchors; + +namespace DigitalIdentity.Models +{ + public class DisplayAttribute + { + private readonly string _displayName; + private readonly string _preValue; + private readonly string _icon; + private readonly List _anchors; + private readonly object _value; + + public DisplayAttribute(string displayName, string icon, List anchors, object value) + { + _displayName = displayName; + _preValue = ""; + _icon = icon; + _anchors = anchors; + _value = value; + } + + public DisplayAttribute(string preValue, string displayName, string icon, List anchors, object value) + { + _displayName = displayName; + _preValue = preValue; + _icon = icon; + _anchors = anchors; + _value = value; + } + + public string GetDisplayName() + { + return _displayName; + } + + public string GetPreValue() + { + return _preValue; + } + + public string GetIcon() + { + return _icon; + } + + public List GetAnchors() + { + return _anchors; + } + + public string GetDisplayValue() + { + return _preValue + _value.ToString(); + } + + public object GetValue() + { + return _value; + } + } +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Models/DisplayAttributes.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Models/DisplayAttributes.cs new file mode 100644 index 000000000..417d3e7ef --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Models/DisplayAttributes.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Yoti.Auth.Anchors; + +namespace DigitalIdentity.Models +{ + public class DisplayAttributes + { + public List AttributeList { get; internal set; } + public string Base64Selfie { get; internal set; } + public string FullName { get; internal set; } + + internal DisplayAttributes() + { + AttributeList = new List(); + } + + internal void Add(DisplayAttribute displayAttribute) + { + AttributeList.Add(displayAttribute); + } + + internal void Add(string displayName, string icon, List anchors, object value) + { + DisplayAttribute displayAttribute = new DisplayAttribute(displayName, icon, anchors, value); + AttributeList.Add(displayAttribute); + } + } +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Program.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Program.cs new file mode 100644 index 000000000..2e32c9abe --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Program.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace CoreExample +{ + public static class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Properties/launchSettings.json b/src/Examples/DigitalIdentity/DigitalIdentity/Properties/launchSettings.json new file mode 100644 index 000000000..cb95eb61a --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55000", + "sslPort": 44380 + } + }, + "profiles": { + "CoreExample": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:44344;http://localhost:44343", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": {} + } + } +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/README.md b/src/Examples/DigitalIdentity/DigitalIdentity/README.md new file mode 100644 index 000000000..64b310ce0 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/README.md @@ -0,0 +1,21 @@ +# .NET Core Example project + +## 1) Setup +1) Clone this repo +1) Navigate to this folder +1) Rename the [.env.example](.env.example) file to `.env` +1) Fill in the environment variables in this file with the ones specific to your application, generated in the [Yoti Hub](https://hub.yoti.com) when you create (and then publish) your application + +## 2a) Running With Docker +1) From the Yoti Hub, set the application domain to `localhost:44380` +1) `docker-compose build --no-cache` +1) `docker-compose up` +1) Navigate to + +>If you encounter a "permission denied" error when trying to access the mounted .pem file, try disabling and re-enabling your shared drive in Docker settings. + +## 2b) Running With .NET Core installed locally +1) From the Yoti Hub, set the application domain to `localhost:44344` +1) Download the .NET SDK for your operating system from step no.1 on ([Windows](https://www.microsoft.com/net/learn/get-started/windows) | [Linux](https://www.microsoft.com/net/learn/get-started/linux/rhel) | [MacOS](https://www.microsoft.com/net/learn/get-started/macos)) +1) Enter `dotnet run -p DigitalIdentityExample.csproj` into the terminal +1) Navigate to the page specified in the terminal window, which should be diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Startup.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Startup.cs new file mode 100644 index 000000000..04dd39196 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Startup.cs @@ -0,0 +1,87 @@ +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace CoreExample +{ + public class Startup + { + public Startup(IConfiguration configuration, ILogger logger) + { + Configuration = configuration; + if (File.Exists(".env")) + { + logger.LogInformation("using environment variables from .env file"); + DotNetEnv.Env.Load(); + } + if (string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("YOTI_CLIENT_SDK_ID"))) + logger.LogCritical("'YOTI_CLIENT_SDK_ID' environment variable not found. " + + "Either pass these in the .env file, or as a standard environment variable."); + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.Configure(options => + { + // This lambda determines whether user consent for non-essential cookies is needed + // for a given request. + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = SameSiteMode.None; + }); + + services.AddMvc() + .AddSessionStateTempDataProvider(); + services.AddMemoryCache(); + services.AddDistributedMemoryCache(); + services.AddSession(options => + { + options.Cookie.IsEssential = true; + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + app.UseHsts(); + } + + app.UseRouting(); + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseSession(); + app.UseCookiePolicy(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + app.Use(async (context, next) => + { + if (context.Request.Path == "/") + { + context.Response.Redirect("/generate-share"); + return; + } + + await next(); + }); + } + } +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Views/AdvancedIdentityShare/AdvancedIdentityShare.cshtml b/src/Examples/DigitalIdentity/DigitalIdentity/Views/AdvancedIdentityShare/AdvancedIdentityShare.cshtml new file mode 100644 index 000000000..fe627f052 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Views/AdvancedIdentityShare/AdvancedIdentityShare.cshtml @@ -0,0 +1,92 @@ +@{ + ViewData["Title"] = "Advanced Identity Share"; +} +@model Yoti.Auth.DigitalIdentity.SharedReceiptResponse + + + + + + + + Yoti Digital Identity Client Example + + + + + + +
+
+
+ Yoti +
+ +

Advanced Identity Share Example

+ +
+
+
+ +
+ +
+

The Yoti app is free to download and use:

+ +
+ + Download on the App Store + + + + get it on Google Play + +
+
+
+ + + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml new file mode 100644 index 000000000..841d32ac8 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml @@ -0,0 +1,92 @@ +@{ + ViewData["Title"] = "Digital Identity"; +} +@model Yoti.Auth.DigitalIdentity.SharedReceiptResponse + + + + + + + + Yoti Digital Identity Client Example + + + + + + +
+
+
+ Yoti +
+ +

Digital Identity Share Example

+ +
+
+
+ +
+ +
+

The Yoti app is free to download and use:

+ +
+ + Download on the App Store + + + + get it on Google Play + +
+
+
+ + + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/Error.cshtml b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/Error.cshtml new file mode 100644 index 000000000..9cd2d4599 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/Error.cshtml @@ -0,0 +1,19 @@ +@{ + ViewData["Title"] = "Error"; +} + + + + Welcome + + +

Home

+

+ Could not login user for the following reason: @TempData["Error"] +

+

Inner exception:

+

+ @TempData["InnerException"] +

+ + \ No newline at end of file diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/SuccessResult.cshtml b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/SuccessResult.cshtml new file mode 100644 index 000000000..cba7d31b1 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/SuccessResult.cshtml @@ -0,0 +1,171 @@ +@{ + ViewData["Title"] = "Connect"; +} +@using System.Globalization; +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq; +@using Yoti.Auth.Document; +@using Yoti.Auth.Images; +@model DigitalIdentity.Models.DisplayAttributes + + + + + + Yoti client example + + + + +
+
+ +
+ Powered by + Yoti +
+ +
+ @if (!string.IsNullOrEmpty(Model.Base64Selfie)) + { +
+ Yoti + + +
+ } + +
+ @Model.FullName +
+
+
+ +
+ + +
+
Attribute
+
Value
+
Anchors
+
+ +
+
+
S / V
+
Value
+
Sub type
+
+
+ +
+ @foreach (DigitalIdentity.Models.DisplayAttribute a in Model.AttributeList) + { +
+
+
+ + @a.GetDisplayName() +
+
+ +
+
+ @switch (a.GetDisplayName()) + { + case "Structured Postal Address": + + @foreach (var item in (Dictionary)a.GetValue()) + { + + + + + } +
@item.Key@item.Value
+ break; + + case "Identity Profile Report": + + @foreach (var item in (Dictionary)a.GetValue()) + { + + + + + + } +
@item.Key +
+                                                            @Html.Raw(@item.Value.ToString(Formatting.Indented))
+                                                        
+
+ break; + + case "Document Details": + { + DocumentDetails documentDetailsValue = (DocumentDetails)a.GetValue(); + + + + + + + + + + + + + + + + + + + + + +
Type@documentDetailsValue.DocumentType
Issuing Country@documentDetailsValue.IssuingCountry
Issuing Authority@documentDetailsValue.IssuingAuthority
Document Number@documentDetailsValue.DocumentNumber
Expiration Date@documentDetailsValue.ExpirationDate.ToString()
+ } + break; + + case "Document Images": + foreach (var image in (List)a.GetValue()) + { + + } + break; + + default: + @a.GetDisplayValue() + break; + } +
+
+ +
+
S / V
+
Value
+
Sub type
+ + @foreach (var source in a.GetAnchors().Where(s => s.GetAnchorType() == Yoti.Auth.Anchors.AnchorType.SOURCE)) + { +
Source
+
@string.Join(", ", source.GetValue())
+
@source.GetSubType()
+ } + @foreach (var verifier in a.GetAnchors().Where(v => v.GetAnchorType() == Yoti.Auth.Anchors.AnchorType.VERIFIER)) + { +
Verifier
+
@string.Join(", ", verifier.GetValue())
+
@verifier.GetSubType()
+ } +
+
+ } +
+
+
+ + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Views/Web.config b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Web.config new file mode 100644 index 000000000..9bc7eab38 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Web.config @@ -0,0 +1,43 @@ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/appsettings.Development.json b/src/Examples/DigitalIdentity/DigitalIdentity/appsettings.Development.json new file mode 100644 index 000000000..e203e9407 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/appsettings.json b/src/Examples/DigitalIdentity/DigitalIdentity/appsettings.json new file mode 100644 index 000000000..def9159a7 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.dcproj b/src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.dcproj new file mode 100644 index 000000000..86377fe65 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.dcproj @@ -0,0 +1,18 @@ + + + + 2.1 + Linux + 85f77b23-0b47-448a-a498-fc0aa1a7b46a + LaunchBrowser + {Scheme}://localhost:{ServicePort} + digitalidentity + + + + docker-compose.yml + + + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.override.yml b/src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.override.yml new file mode 100644 index 000000000..701f4c3f2 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.override.yml @@ -0,0 +1,18 @@ +version: '3.4' + +services: + digitalidentityexample: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=https://+:443;http://+:80 + - ASPNETCORE_HTTPS_PORT=44380 + - YOTI_SCENARIO_ID=${YOTI_SCENARIO_ID} + - YOTI_CLIENT_SDK_ID=${YOTI_CLIENT_SDK_ID} + - YOTI_KEY_FILE_PATH=/YotiKey.pem + - ASPNETCORE_Kestrel__Certificates__Default__Password=b0a3e118-0420-4e3c-920c-c2623296ffbf + - ASPNETCORE_Kestrel__Certificates__Default__Path=https/DigitalIdentityExample.pfx + ports: + - "55000:80" + - "44380:443" + volumes: + - ${YOTI_KEY_FILE_PATH}:/YotiKey.pem diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.yml b/src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.yml new file mode 100644 index 000000000..11d8ff637 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + digitalidentityexample: + image: ${DOCKER_REGISTRY-}digitalidentityexample + build: + context: ../../../ + dockerfile: Examples/DigitalIdentity/DigitalIdentity/Dockerfile diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/https/DigitalIdentityExample.pfx b/src/Examples/DigitalIdentity/DigitalIdentity/https/DigitalIdentityExample.pfx new file mode 100644 index 0000000000000000000000000000000000000000..f1b4fc482e7af7424ba205fa425b226ccbb04705 GIT binary patch literal 2700 zcmZXUcU05a7KMKyAwU932}&^(rAVCw1f+UdP79L4yz=-5-`szZ@?=H}MdXo3zihQ6>SWx*y48Ogb+0jpB%t zOF#J(7!DM_9KuK9xaA0xjnATd!kfH)qLhCroP@!1tiR8&h&v-ftSDQq`qQ= za(9l?(_YMgD`T`Q^;IItKIn_uCNDfLYzI-X$7pZ(Pi*QO$TX{&aWgJXzL@?Yr z&ACJ{&h~DNu@!0$z9l;pFO)rP<-*XZ=bj}x@o=0xj}%~6e?2MUe_UDdG+lP_lEw%A z%JKf-B|~gv{C>2ReEWdu<4Et>ncy+=*6_vg zYJrq{m2!cKm^U$Zf0k-VR>$~^UUN5|Cnp>G5A)xsZeM$HrC)IW@b!l>3efqEGh2kE zgJ;UAyxrCqWwKtc=Xjelk3?;T6Y_$tFQb0amOaczu+Ugv_||49-xu&6|2I9oYW&pU zzzqjG6+WvsaNy;tIk$(ofbYPXGL!Kbv9UNRV|y^yQTXIYg2eP&+%~xoaVFG4J${Gl ztZ9e0x{YvNHYY<|^($FkB1CDwjx9g@{Ul%Hhn45%4!v%QP0te4jBjB1MbKd`zHPhT*)Dj zxt?!ZMBDcJ$;noTIrFY_O#Ys5P?5#(07>G&FSBW9%q6efNTa7EE5+7R+bgaw(-!Ei zioiAKbTVTcOntL4QYm7e%^Kl-h{W;dk7QPw8)4NL49Eke^IaGz(O7D#pT;O3%sc3BhIhW{6~s7kTv+XYaWf0=f%a` z!ilu7`LmHa1?#$cv3#mKx^Wu~HE<+K zEecd%2lBM0Z)H|QJF%61iecengBQRoa6V@RmXn*S5s4$8+w-W zTt9gxUu?eP8z|VAKr1Bb9lA5d!_&FFpR>hfY@GffbVA)F<~5l#n`RJFjDlg4DNqT8HPnl2kkm|x`rBB zH5`thh1yUGq~Z*g_>n~3BdYq6$ZGn91j*b&)Oq?IoDZU5Q}^L;+DTULs>K2S@$4Tf@}Qqdk!co)K9XI z6dMR&9mszj(ErDLZhWm?*I1vBFV2aJb{#5N74P@fZiaVPZ0;{iXztz> z(Vg3ITH4BvW<)OulfHEWy}b9WibrrX&R4Zj?7G*=Ua`gONb1dK%*%1W+W6bHQ(qdY z23Rod+N+)N<39C)efKi(NcA5V=iS;`c}9yFgX1K%WtG7;DCVo39OXT7dQQ(g)dXkK z;<>vgq+T3zCkn6=*zeLcBhpWHlKlH;dY&H0s>+lSx1=jtz>ONpS}SdHwEDyb(w-80 z%jdT58PskuUCLE?)8uBWk<5Fae4>J*E3~LX+QF(`#9yq0R&-&AX+4k^8iC&ztK2>e zvv_#*@v=J+o{L(JpY#j5Z1QBY+FM3wM-Dfe*#;aV4^!0YPn}MWd|Fc=K0jQgsWsJV zV5oL{qd-Nv-Yvo-IMrYW`yROUo|`$J#!1QYKcm{^ zC9>7>XRaIV>}zR;zC_aNZ!}p%Fz@Yh2mgofwTfE6yx3Th;ZIr5Mwh!=Str^iW@6t} zQP9wK9kNiRn&~KlUUl^OkOmH?Qmv^Uen=?3E5pXXS!~bMf}t^XG;!%<)Q?zo!>FSR zH?BqfP;z1OxAlsZW?%DPucN1>Hc)t*984PcA60YuBpr>IUrFvJ0$p{{VO&q6ttAbq z{Mb6Lt+m<0MX#TA0OhS1H9JE%xi}tW++UGtC zUFEEoOh>2VE!?eZlx@i&xWVce9TO25pFeMG#v6WBu_C7{jb{vg;tJgE!>B#nhv@A%H$TnxE z`ZXKdQsad+ogg|Mj{`$m``9-E!01n+v?00btMH!pOxeso%IMJ+#Tze&rY*aCf_dmST#7c3kM=yPIu*xyjvC~*`A zlwIKvm`wzN_~A@VYMT{&eDs-Uf!nTP>8*?>1PDUB;8c1G|Cae<0{c@{v1i%49Xr8) H1;&2>#y8Wj literal 0 HcmV?d00001 diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge.png b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge.png new file mode 100644 index 0000000000000000000000000000000000000000..3ec996cc6288d68279c1d735c9d627c64d8a48c6 GIT binary patch literal 4077 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 literal 0 HcmV?d00001 diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge@2x.png b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..84b34068fc22aa74f388f6db1a583ac6592f5420 GIT binary patch literal 8819 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 MlZs4*lPCscyWU2PMP1t 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 + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/calendar.svg b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/calendar.svg new file mode 100644 index 000000000..4f6b9bb77 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/calendar.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/chevron-down-grey.svg b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/chevron-down-grey.svg new file mode 100644 index 000000000..6753becbf --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/chevron-down-grey.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/document.svg b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/document.svg new file mode 100644 index 000000000..4c41271e7 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/document.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/email.svg b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/email.svg new file mode 100644 index 000000000..c4582d6e4 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/email.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/gender.svg b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/gender.svg new file mode 100644 index 000000000..af5c5772d --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/gender.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/nationality.svg b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/nationality.svg new file mode 100644 index 000000000..e57d75227 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/nationality.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/phone.svg b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/phone.svg new file mode 100644 index 000000000..b19cce046 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/phone.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/profile.svg b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/profile.svg new file mode 100644 index 000000000..5c514fc1d --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/profile.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/verified.svg b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/verified.svg new file mode 100644 index 000000000..7ca4dbb3b --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/verified.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/logo.png b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c60227fabf339e9540e5daac4d2d25e121137752 GIT binary patch literal 2988 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 CreateShareSession(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, ShareSessionRequest shareSessionRequestPayload) { Validation.NotNull(httpClient, nameof(httpClient)); @@ -30,9 +43,9 @@ internal static async Task CreateShareSession(HttpClient htt Request shareSessionRequest = new RequestBuilder() .WithKeyPair(keyPair) .WithBaseUri(apiUrl) - .WithHeader("X-Yoti-Auth-Id", sdkId) - .WithEndpoint($"/v2/sessions") - .WithQueryParam("appId", sdkId) + .WithHeader(yotiAuthId, sdkId) + .WithEndpoint(sessionCreation) + .WithQueryParam("sdkID", sdkId) .WithHttpMethod(HttpMethod.Post) .WithContent(body) .Build(); @@ -48,7 +61,6 @@ internal static async Task CreateShareSession(HttpClient htt var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); return deserialized; - } } @@ -64,8 +76,8 @@ internal static async Task GetSession(HttpClient httpClient, U Request getSessionRequest = new RequestBuilder() .WithKeyPair(keyPair) .WithBaseUri(apiUrl) - .WithHeader("X-Yoti-Auth-Id", sdkId) - .WithEndpoint(string.Format($"/v2/sessions/{0}", sessionId)) + .WithHeader(yotiAuthId, sdkId) + .WithEndpoint(string.Format("{0}/{1}", sessionCreation, sessionId)) .WithQueryParam("appId", sdkId) .WithHttpMethod(HttpMethod.Get) .Build(); @@ -81,7 +93,6 @@ internal static async Task GetSession(HttpClient httpClient, U var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); return deserialized; - } } @@ -104,14 +115,13 @@ internal static async Task CreateQrCode(HttpClient httpClient, U Request createQrRequest = new RequestBuilder() .WithKeyPair(keyPair) .WithBaseUri(apiUrl) - .WithHeader("X-Yoti-Auth-Id", sdkId) + .WithHeader(yotiAuthId, sdkId) .WithEndpoint(string.Format($"/v2/sessions/{0}/qr-codes", sessionId)) .WithQueryParam("appId", sdkId) .WithHttpMethod(HttpMethod.Post) .WithContent(body) .Build(); - - + using (HttpResponseMessage response = await createQrRequest.Execute(httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) @@ -123,11 +133,9 @@ internal static async Task CreateQrCode(HttpClient httpClient, U var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); return deserialized; - } } - - + internal static async Task GetQrCode(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, string qrCodeId) { Validation.NotNull(httpClient, nameof(httpClient)); @@ -139,7 +147,7 @@ internal static async Task GetQrCode(HttpClient httpClient, Uri Request QrCodeRequest = new RequestBuilder() .WithKeyPair(keyPair) .WithBaseUri(apiUrl) - .WithHeader("X-Yoti-Auth-Id", sdkId) + .WithHeader(yotiAuthId, sdkId) .WithEndpoint(string.Format($"/v2/qr-codes/{0}", qrCodeId)) .WithQueryParam("appId", sdkId) .WithHttpMethod(HttpMethod.Get) @@ -156,8 +164,208 @@ internal static async Task GetQrCode(HttpClient httpClient, Uri var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); return deserialized; + } + } + + private static async Task GetReceipt(HttpClient httpClient, string receiptId, string sdkId,Uri apiUrl, AsymmetricCipherKeyPair keyPair) + { + Validation.NotNull(httpClient, nameof(httpClient)); + Validation.NotNull(apiUrl, nameof(apiUrl)); + Validation.NotNull(sdkId, nameof(sdkId)); + Validation.NotNull(keyPair, nameof(keyPair)); + + string receiptUrl = Base64ToBase64URL(receiptId); + string endpoint = string.Format(receiptRetrieval, receiptUrl); + + Request ReceiptRequest = new RequestBuilder() + .WithKeyPair(keyPair) + .WithBaseUri(apiUrl) + .WithHeader(yotiAuthId, sdkId) + .WithEndpoint(endpoint) + .WithQueryParam("sdkID", sdkId) + .WithHttpMethod(HttpMethod.Get) + .Build(); + + using (HttpResponseMessage response = await ReceiptRequest.Execute(httpClient).ConfigureAwait(false)) + { + if (!response.IsSuccessStatusCode) + { + Response.CreateYotiExceptionFromStatusCode(response); + } + + var responseObject = await response.Content.ReadAsStringAsync(); + var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); + + return deserialized; + } + } + + + public static string Base64ToBase64URL(string base64Str) + { + try + { + byte[] decodedBytes = Convert.FromBase64String(base64Str); + string base64URL = Convert.ToBase64String(decodedBytes) + .Replace('+', '-') + .Replace('/', '_') + .TrimEnd('='); + return base64URL; + } + catch (FormatException) + { + return ""; + } + } + + public static async Task GetShareReceipt(HttpClient httpClient, string clientSdkId, Uri apiUrl, AsymmetricCipherKeyPair key, string receiptId) + { + Validation.NotNullOrEmpty(receiptId, nameof(receiptId)); + try + { + var receiptResponse = await GetReceipt(httpClient, receiptId, clientSdkId, apiUrl, key); + var itemKeyId = receiptResponse.WrappedItemKeyId; + + var encryptedItemKeyResponse = await GetReceiptItemKey(httpClient, itemKeyId, clientSdkId, apiUrl, key); + + var receiptContentKey = CryptoEngine.UnwrapReceiptKey(receiptResponse.WrappedKey, encryptedItemKeyResponse.Value, encryptedItemKeyResponse.Iv, key); + + var (attrData, aextra, decryptAttrDataError) = DecryptReceiptContent(receiptResponse.Content, receiptContentKey); + if (decryptAttrDataError != null) + { + throw new Exception($"An unexpected error occurred: {decryptAttrDataError.Message}"); + } + var parsedAttributesApp = AttributeConverter.ConvertToBaseAttributes(attrData); + var appProfile = new ApplicationProfile(parsedAttributesApp + ); + + var (attrOtherData, aOtherExtra, decryptOtherAttrDataError) = DecryptReceiptContent(receiptResponse.OtherPartyContent, receiptContentKey); + if (decryptAttrDataError != null) + { + throw new Exception($"An unexpected error occurred: {decryptAttrDataError.Message}"); + } + + var userProfile = new YotiProfile(); + if (attrOtherData != null) + { + var parsedAttributesUser = AttributeConverter.ConvertToBaseAttributes(attrOtherData); + userProfile = new YotiProfile(parsedAttributesUser); + } + + + ExtraData userExtraData = new ExtraData(); + if (aOtherExtra != null) + { + userExtraData = ExtraDataConverter.ParseExtraDataProto(aOtherExtra); + } + ExtraData appExtraData = new ExtraData(); + if (aextra != null) + { + + appExtraData = ExtraDataConverter.ParseExtraDataProto(aextra); + } + + var sharedReceiptResponse = new SharedReceiptResponse + { + ID = receiptResponse.ID, + SessionID = receiptResponse.SessionID, + RememberMeID = receiptResponse.RememberMeID, + ParentRememberMeID = receiptResponse.ParentRememberMeID, + Timestamp = receiptResponse.Timestamp, + UserContent = new UserContent + { + UserProfile = userProfile, + ExtraData = userExtraData + }, + ApplicationContent = new ApplicationContent + { + ApplicationProfile = appProfile, + ExtraData = appExtraData + }, + Error = receiptResponse.Error + }; + + return sharedReceiptResponse; + } + catch (Exception ex) + { + throw new Exception($"An unexpected error occurred: {ex.Message}"); + + } + } + + private static async Task GetReceiptItemKey(HttpClient httpClient, string receiptItemKeyId, string sdkId, Uri apiUrl, AsymmetricCipherKeyPair keyPair) + { + Validation.NotNull(httpClient, nameof(httpClient)); + Validation.NotNull(apiUrl, nameof(apiUrl)); + Validation.NotNull(sdkId, nameof(sdkId)); + Validation.NotNull(keyPair, nameof(keyPair)); + string endpoint = string.Format(receiptKeyRetrieval, receiptItemKeyId); + + Request ReceiptItemKeyRequest = new RequestBuilder() + .WithKeyPair(keyPair) + .WithBaseUri(apiUrl) + .WithHeader(yotiAuthId, sdkId) + .WithEndpoint(endpoint) + .WithQueryParam("appId", sdkId) + .WithHttpMethod(HttpMethod.Get) + .Build(); + + using (HttpResponseMessage response = await ReceiptItemKeyRequest.Execute(httpClient).ConfigureAwait(false)) + { + if (!response.IsSuccessStatusCode) + { + Response.CreateYotiExceptionFromStatusCode(response); + } + + var responseObject = await response.Content.ReadAsStringAsync(); + var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); + + return deserialized; + } + } + + public static (AttributeList attrData, byte[] aextra, Exception error) DecryptReceiptContent(Content content, byte[] key) + { + AttributeList attrData = null; + byte[] aextra = null; + Exception error = null; + + if (content != null) + { + if (content.Profile != null && content.Profile.Length > 0) + { + try + { + byte[] aattr = CryptoEngine.DecryptReceiptContent(content.Profile, key); + attrData = new AttributeList(); + attrData.MergeFrom(aattr); + } + catch (Exception ex) + { + error = new Exception($"failed to decrypt content profile: {ex.Message}", ex); + return (null, null, error); + } + } + + if (content.ExtraData != null && content.ExtraData.Length > 0) + { + try + { + aextra = CryptoEngine.DecryptReceiptContent(content.ExtraData, key); + } + catch (Exception ex) + { + error = new Exception($"failed to decrypt receipt content extra data: {ex.Message}", ex); + return (null, null, error); + } + } } + + return (attrData, aextra, null); } } -} \ No newline at end of file + + +} diff --git a/src/Yoti.Auth/DigitalIdentity/GetReceipt.cs b/src/Yoti.Auth/DigitalIdentity/GetReceipt.cs new file mode 100644 index 000000000..d1821a8f9 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/GetReceipt.cs @@ -0,0 +1,46 @@ +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity +{ + public class Content + { + [JsonProperty("profile")] + public byte[] Profile { get; set; } + + [JsonProperty("extraData")] + public byte[] ExtraData { get; set; } + } + + public class ReceiptResponse + { + [JsonProperty("id")] + public string ID { get; set; } + + [JsonProperty("sessionId")] + public string SessionID { get; set; } + + [JsonProperty("timestamp")] + public string Timestamp { get; set; } + + [JsonProperty("rememberMeId")] + public string RememberMeID { get; set; } + + [JsonProperty("parentRememberMeId")] + public string ParentRememberMeID { get; set; } + + [JsonProperty("content")] + public Content Content { get; set; } + + [JsonProperty("otherPartyContent")] + public Content OtherPartyContent { get; set; } + + [JsonProperty("wrappedItemKeyId")] + public string WrappedItemKeyId { get; set; } + + [JsonProperty("wrappedKey")] + public byte[] WrappedKey { get; set; } + + [JsonProperty("error")] + public string Error { get; set; } + } +} diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs b/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs index 328e5a55c..a0e60b5cf 100644 --- a/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs +++ b/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs @@ -1,14 +1,18 @@ using System; using System.Collections.Generic; +using Newtonsoft.Json; namespace Yoti.Auth.DigitalIdentity.Policy { public class Notification { + [JsonProperty(PropertyName = "url")] public string Url { get; set; } // Required if 'notification' is defined + [JsonProperty(PropertyName = "method")] public string Method { get; set; } = "POST"; // Optional, defaults to 'POST' + [JsonProperty(PropertyName = "headers")] public Dictionary Headers { get; set; } // Optional + [JsonProperty(PropertyName = "verifyTls")] public bool VerifyTls { get; set; } = true; // Optional, defaults to 'true' if URL is HTTPS } } - diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs b/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs index 76950a182..3f943bb3e 100644 --- a/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs +++ b/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs @@ -30,11 +30,15 @@ public class Policy [JsonProperty(PropertyName = "identity_profile_requirements")] private readonly object _identityProfileRequirements; + [JsonProperty(PropertyName = "advanded_identity_profile_requirements")] + private readonly object _advancedIdentityProfileRequirements; + public Policy( ICollection wantedAttributes, HashSet wantedAuthTypes, bool wantedRememberMeId, - object identityProfileRequirements = null + object identityProfileRequirements = null, + object advancedIdentityProfileRequirements = null ) { _wantedAttributes = wantedAttributes; @@ -42,6 +46,8 @@ public Policy( _wantedRememberMeId = wantedRememberMeId; _isWantedRememberMeIdOptional = false; _identityProfileRequirements = identityProfileRequirements; + _advancedIdentityProfileRequirements = advancedIdentityProfileRequirements; + } /// @@ -91,5 +97,17 @@ public object IdentityProfileRequirements return _identityProfileRequirements; } } + + /// + /// AdvancedIdentityProfileRequirements requested in the policy + /// + [JsonIgnore] + public object AdvancedIdentityProfileRequirements + { + get + { + return _advancedIdentityProfileRequirements; + } + } } -} \ No newline at end of file +} diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs index ff4e18a71..820a22951 100644 --- a/src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs +++ b/src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs @@ -9,6 +9,7 @@ public class PolicyBuilder private readonly HashSet _wantedAuthTypes = new HashSet(); private bool _wantedRememberMeId; private object _identityProfileRequirements; + private object _advandedIdentityProfileRequirements; public PolicyBuilder WithWantedAttribute(WantedAttribute wantedAttribute) { @@ -157,10 +158,21 @@ public PolicyBuilder WithIdentityProfileRequirements(object identityProfileRequi _identityProfileRequirements = identityProfileRequirements; return this; } + + /// + /// Use an Advanced Identity Profile Requirement object for the share + /// + /// object describing the advanced identity profile requirements to use + /// with the advanced identity profile requirements + public PolicyBuilder WithAdvancedIdentityProfileRequirements(object advancedIdentityProfileRequirements) + { + _advandedIdentityProfileRequirements = advancedIdentityProfileRequirements; + return this; + } public Policy Build() { - return new Policy(_wantedAttributes.Values, _wantedAuthTypes, _wantedRememberMeId, _identityProfileRequirements); + return new Policy(_wantedAttributes.Values, _wantedAuthTypes, _wantedRememberMeId, _identityProfileRequirements, _advandedIdentityProfileRequirements); } } -} \ No newline at end of file +} diff --git a/src/Yoti.Auth/DigitalIdentity/ReceiptItemKeyResponse.cs b/src/Yoti.Auth/DigitalIdentity/ReceiptItemKeyResponse.cs new file mode 100644 index 000000000..da03a5ba1 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/ReceiptItemKeyResponse.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity +{ + public class ReceiptItemKeyResponse + { + [JsonProperty("id")] + public string ID { get; set; } + + [JsonProperty("iv")] + public byte[] Iv { get; set; } + + [JsonProperty("value")] + public byte[] Value { get; set; } + } +} diff --git a/src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs b/src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs new file mode 100644 index 000000000..bc2e649a8 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs @@ -0,0 +1,29 @@ +using Yoti.Auth.Profile; +using Yoti.Auth.Share; + +namespace Yoti.Auth.DigitalIdentity +{ + public class SharedReceiptResponse + { + public string ID { get; set; } + public string SessionID { get; set; } + public string RememberMeID { get; set; } + public string ParentRememberMeID { get; set; } + public string Timestamp { get; set; } + public string Error { get; set; } + public UserContent UserContent { get; set; } + public ApplicationContent ApplicationContent { get; set; } + } + + public class ApplicationContent + { + public ApplicationProfile ApplicationProfile { get; set; } + public ExtraData ExtraData { get; set; } + } + + public class UserContent + { + public YotiProfile UserProfile { get; set; } + public ExtraData ExtraData { get; set; } + } +} diff --git a/src/Yoti.Auth/DigitalIdentityClient.cs b/src/Yoti.Auth/DigitalIdentityClient.cs index 7601ada78..e17070ca6 100644 --- a/src/Yoti.Auth/DigitalIdentityClient.cs +++ b/src/Yoti.Auth/DigitalIdentityClient.cs @@ -3,7 +3,6 @@ using System.Net.Http; using System.Threading.Tasks; using Org.BouncyCastle.Crypto; -using Yoti.Auth.Aml; using Yoti.Auth.DigitalIdentity; namespace Yoti.Auth @@ -58,18 +57,15 @@ public DigitalIdentityClient(HttpClient httpClient, string sdkId, AsymmetricCiph _yotiDigitalClientEngine = new DigitalIdentityClientEngine(httpClient); } - - - /// - /// Initiate a sharing process based on a . + /// Initiate a sharing process based on a . /// - /// + /// /// Details of the device's callback endpoint, and extensions for the application + /// cref="Yoti.Auth.DigitalIdentity.Policy"/> and extensions for the application /// - /// containing a Sharing URL and Reference ID + /// public ShareSessionResult CreateShareSession(ShareSessionRequest shareSessionRequest) { Task task = Task.Run(async () => await CreateShareSessionAsync(shareSessionRequest).ConfigureAwait(false)); @@ -78,18 +74,24 @@ public ShareSessionResult CreateShareSession(ShareSessionRequest shareSessionReq } /// - /// Asynchronously initiate a sharing process based on a . + /// Asynchronously initiate a sharing process based on a . /// - /// + /// /// Details of the device's callback endpoint, and extensions for the application + /// cref="Yoti.Auth.DigitalIdentity.Policy"/> and extensions for the application /// - /// containing a Sharing URL and Reference ID + /// public async Task CreateShareSessionAsync(ShareSessionRequest shareSessionRequest) { return await _yotiDigitalClientEngine.CreateShareSessionAsync(_sdkId, _keyPair, ApiUri, shareSessionRequest).ConfigureAwait(false); } + public SharedReceiptResponse GetShareReceipt(string receiptId) + { + Task task = Task.Run(async () => await _yotiDigitalClientEngine.GetShareReceipt(_sdkId, _keyPair, ApiUri, receiptId).ConfigureAwait(false)); + return task.Result; + } + internal void SetYotiApiUri() { if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("YOTI_API_URL"))) @@ -98,7 +100,7 @@ internal void SetYotiApiUri() } else { - ApiUri = new Uri(Constants.Api.DefaultYotiApiUrl); + ApiUri = new Uri(Constants.Api.DefaultYotiShareApiUrl); } } @@ -109,4 +111,4 @@ public DigitalIdentityClient OverrideApiUri(Uri apiUri) return this; } } -} \ No newline at end of file +} diff --git a/src/Yoti.Auth/DigitalIdentityClientEngine.cs b/src/Yoti.Auth/DigitalIdentityClientEngine.cs index 85637c433..2492e8c5e 100644 --- a/src/Yoti.Auth/DigitalIdentityClientEngine.cs +++ b/src/Yoti.Auth/DigitalIdentityClientEngine.cs @@ -1,18 +1,11 @@ using System; - #pragma warning disable S1128 - using System.Net; - #pragma warning restore S1128 - using System.Net.Http; using System.Threading.Tasks; using Org.BouncyCastle.Crypto; -using Yoti.Auth.Aml; -using Yoti.Auth.Exceptions; using Yoti.Auth.DigitalIdentity; -using Yoti.Auth.Web; namespace Yoti.Auth { @@ -24,11 +17,10 @@ public DigitalIdentityClientEngine(HttpClient httpClient) { _httpClient = httpClient; -#if NET452 || NET462 || NET472 || NET48 + #if NET452 || NET462 || NET472 || NET48 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; -#endif + #endif } - public async Task CreateShareSessionAsync(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, ShareSessionRequest shareSessionRequest) { @@ -38,5 +30,14 @@ public async Task CreateShareSessionAsync(string sdkId, Asym return result; } + + public async Task GetShareReceipt(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, string receiptId) + { + SharedReceiptResponse result = await Task.Run(async () => await DigitalIdentityService.GetShareReceipt( + _httpClient, sdkId, apiUrl, keyPair, receiptId).ConfigureAwait(false)) + .ConfigureAwait(false); + + return result; + } } -} \ No newline at end of file +} diff --git a/src/Yoti.Auth/Yoti.Auth.csproj b/src/Yoti.Auth/Yoti.Auth.csproj index 6d2b1d696..e53744550 100644 --- a/src/Yoti.Auth/Yoti.Auth.csproj +++ b/src/Yoti.Auth/Yoti.Auth.csproj @@ -1,7 +1,7 @@  - netstandard1.6;netstandard2.1;netcoreapp1.1;netcoreapp2.2;netcoreapp3.1;net6.0;net452;net462;net472;net48; + netstandard1.6;netstandard2.1;netcoreapp3.1;net6.0;net452;net462;net472;net48; Yoti.Auth Yoti $(PackageTargetFallback);dnxcore50 @@ -19,7 +19,7 @@ False latest true - 3.14.0 + 3.16.0 @@ -37,17 +37,18 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + @@ -71,7 +72,32 @@ Resources.Designer.cs - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj b/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj index 8832cfb2a..b0d0f0467 100644 --- a/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj +++ b/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 diff --git a/test/Yoti.Auth.Tests/CryptoEngineTests.cs b/test/Yoti.Auth.Tests/CryptoEngineTests.cs index 2f2cf09b2..134cdc957 100644 --- a/test/Yoti.Auth.Tests/CryptoEngineTests.cs +++ b/test/Yoti.Auth.Tests/CryptoEngineTests.cs @@ -1,5 +1,6 @@ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Org.BouncyCastle.Crypto; using Yoti.Auth.Tests.Common; namespace Yoti.Auth.Tests @@ -28,5 +29,65 @@ public void EmptyOneTimeUseTokenThrowsError() Assert.IsTrue(exception.Message.Contains("one time use token")); } + + [TestMethod] + public void DecryptAesGcm_EmptySecretsThrowsError() + { + byte[] iv = new byte[12]; + byte[] secret = new byte[16]; + byte[] cipherText = new byte[32]; + + var exception = Assert.ThrowsException(() => + { + CryptoEngine.DecryptAesGcm(cipherText, iv, secret); + }); + + Assert.IsTrue(exception.Message.Contains("Failed to decrypt receipt key")); + } + + [TestMethod] + public void UnwrapReceiptKey_EmptySecretsThrowsError() + { + byte[] wrappedReceiptKey = new byte[32]; + byte[] encryptedItemKey = new byte[32]; + byte[] itemKeyIv = new byte[12]; + AsymmetricCipherKeyPair key = null; + + var exception = Assert.ThrowsException(() => + { + byte[] unwrappedKey = CryptoEngine.UnwrapReceiptKey(wrappedReceiptKey, encryptedItemKey, itemKeyIv, key); + }); + + Assert.IsTrue(exception.Message.Contains("Failed to unwrap receipt key")); + } + + [TestMethod] + public void DecryptContent_EmptySecretsThrowsError() + { + byte[] content = new byte[] { 0x01, 0x02, 0x03 }; // Example content + byte[] receiptContentKey = new byte[16]; // Example receipt content key + + var exception = Assert.ThrowsException(() => + { + byte[] decryptedContent = CryptoEngine.DecryptReceiptContent(content, receiptContentKey); + + }); + + Assert.IsTrue(exception.Message.Contains("Failed to decrypt receipt content")); + } + + [TestMethod] + public void DecryptReceiptContent_NullContentThrowsError() + { + byte[] content = null; // Example content + byte[] receiptContentKey = new byte[] { 0x01, 0x02, 0x03 }; // Example receipt content key + + var exception = Assert.ThrowsException(() => + { + byte[] unwrappedKey = CryptoEngine.DecryptReceiptContent(content, receiptContentKey); + }); + + Assert.IsTrue(exception.Message.Contains("Failed to decrypt receipt content: Failed to decrypt receipt content: content is null ")); + } } -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs index fd0136aea..65fa437ab 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs @@ -326,7 +326,18 @@ public void ShouldBuildWithIdentityProfileRequirements() Assert.AreEqual(identityProfileRequirements, result.IdentityProfileRequirements); } + + [TestMethod] + public void ShouldBuildWithAdvancedIdentityProfileRequirements() + { + object advancedIdentityProfileRequirements = IdentityProfiles.CreateAdvancedIdentityProfileRequirements(); + Auth.DigitalIdentity.Policy.Policy result = new PolicyBuilder() + .WithAdvancedIdentityProfileRequirements(advancedIdentityProfileRequirements) + .Build(); + + Assert.AreEqual(advancedIdentityProfileRequirements, result.AdvancedIdentityProfileRequirements); + } } -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs index 35b3aeb3c..16b732304 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs @@ -1,7 +1,6 @@ using System; using System.Net; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -25,17 +24,12 @@ public class DigitalIdentityClientEngineTests [TestMethod] public async Task CreateSessionAsyncShouldReturnCorrectValues() { - //string shareUrl = @"https://yoti.com/shareurl"; string refId = "NpdmVVGC-28356678-c236-4518-9de4-7a93009ccaf0-c5f92f2a-5539-453e-babc-9b06e1d6b7de"; Mock handlerMock = SetupMockMessageHandler( HttpStatusCode.OK, "{\"id\":\"" + refId + "\",\"status\":\"SOME_STATUS\",\"expiry\":\"SOME_EXPIRY\",\"created\":\"SOME_CREATED\",\"updated\":\"SOME_UPDATED\",\"qrCode\":{\"id\":\"SOME_QRCODE_ID\"},\"receipt\":{\"id\":\"SOME_RECEIPT_ID\"}}"); - - - - var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); ShareSessionRequest shareSessionRequest = TestTools.ShareSession.CreateStandardShareSessionRequest(); @@ -45,7 +39,26 @@ public async Task CreateSessionAsyncShouldReturnCorrectValues() Assert.AreEqual(refId, shareSessionResult.Id); } - [DataTestMethod] + [TestMethod] + public void TestGetShareReceipt() + { + Uri apiUrl = new Uri("https://example.com/api"); + string receiptId = "some_receiptid"; + string refId = "NpdmVVGC-28356678-c236-4518-9de4-7a93009ccaf0-c5f92f2a-5539-453e-babc-9b06e1d6b7de"; + + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{\"id\":\"" + refId + "\",\"status\":\"SOME_STATUS\",\"expiry\":\"SOME_EXPIRY\",\"created\":\"SOME_CREATED\",\"updated\":\"SOME_UPDATED\",\"qrCode\":{\"id\":\"SOME_QRCODE_ID\"},\"receipt\":{\"id\":\"SOME_RECEIPT_ID\"}}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + + Assert.ThrowsException(() => + { + SharedReceiptResponse response = engine.GetShareReceipt(SdkId, _keyPair, apiUrl, receiptId).Result; + }); + } + + [DataTestMethod] [DataRow(HttpStatusCode.BadRequest)] [DataRow(HttpStatusCode.Unauthorized)] [DataRow(HttpStatusCode.InternalServerError)] @@ -90,4 +103,4 @@ private static Mock SetupMockMessageHandler(HttpStatusCode h return handlerMock; } } -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs index 25886aa89..805165dee 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs @@ -2,9 +2,7 @@ using System.IO; using System.Net.Http; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; using Org.BouncyCastle.Crypto; -using Yoti.Auth.Aml; using Yoti.Auth.Tests.Common; namespace Yoti.Auth.Tests @@ -13,7 +11,7 @@ namespace Yoti.Auth.Tests public class DigitalIdentityClientTests { private const string _someSdkId = "some-sdk-id"; - private readonly Uri _expectedDefaultUri = new Uri(Constants.Api.DefaultYotiApiUrl); + private readonly Uri _expectedDefaultUri = new Uri(Constants.Api.DefaultYotiShareApiUrl); [TestInitialize] public void BeforeTests() @@ -76,6 +74,19 @@ public void NullDynamicScenarioShouldThrowException() Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); } + [TestMethod] + public void EmptyReceiptShouldThrowException() + { + DigitalIdentityClient client = CreateDigitalIdentityClient(); + var aggregateException = Assert.ThrowsException(() => + { + client.GetShareReceipt(""); + }); + var status = + TestTools.Exceptions.IsExceptionInAggregateException(aggregateException); + Assert.IsTrue(!status); + } + [DataTestMethod] [DataRow("")] [DataRow(null)] @@ -107,7 +118,6 @@ public void ApiUriEnvVariableIsUsed() Uri expectedApiUri = new Uri("https://envapiuri.com"); Assert.AreEqual(expectedApiUri, client.ApiUri); } - private static DigitalIdentityClient CreateDigitalIdentityClient() { StreamReader privateStreamKey = KeyPair.GetValidKeyStream(); @@ -133,7 +143,7 @@ public void ApiUriSetForStreamInitialisation() DigitalIdentityClient yotiClient = new DigitalIdentityClient(_someSdkId, privateStreamKey); Assert.AreEqual(_expectedDefaultUri, yotiClient.ApiUri); - } + } [TestMethod] public void ApiUriSetForStreamInitialisationHttpClient() @@ -145,4 +155,4 @@ public void ApiUriSetForStreamInitialisationHttpClient() Assert.AreEqual(_expectedDefaultUri, yotiClient.ApiUri); } } -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/DigitalIdentityExceptionTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityExceptionTests.cs new file mode 100644 index 000000000..c3bdf6608 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentityExceptionTests.cs @@ -0,0 +1,36 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Yoti.Auth.Exceptions; + +namespace Yoti.Auth.Tests +{ + [TestClass] + public class DigitalIdentityExceptionTests + { + [TestMethod] + public void DigitalIdentityException_NoParameters_ErrorMessageIsNull() + { + + var exception = new DigitalIdentityException(); + Assert.IsNotNull(exception.Message); + } + + [TestMethod] + public void DigitalIdentityException_WithMessage_MessageIsSet() + { + var message = "Test message"; + var exception = new DigitalIdentityException(message); + Assert.AreEqual(message, exception.Message); + } + + [TestMethod] + public void DigitalIdentityException_WithMessageAndInnerException_MessageAndInnerExceptionAreSet() + { + var message = "Test message"; + var innerException = new Exception("Inner exception message"); + var exception = new DigitalIdentityException(message, innerException); + Assert.AreEqual(message, exception.Message); + Assert.AreEqual(innerException, exception.InnerException); + } + } +} diff --git a/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs b/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs index 5d81d7a7b..d3b7d42d0 100644 --- a/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs +++ b/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs @@ -1,4 +1,6 @@ -namespace Yoti.Auth.Tests.TestData +using Newtonsoft.Json; + +namespace Yoti.Auth.Tests.TestData { internal static class IdentityProfiles { @@ -14,6 +16,42 @@ public static object CreateStandardIdentityProfileRequirements() } }; } + + public static object CreateAdvancedIdentityProfileRequirements() + { + string advancedIdentityProfileJson = @" + { + ""profiles"": [ + { + ""trust_framework"": ""UK_TFIDA"", + ""schemes"": [ + { + ""label"": ""LB912"", + ""type"": ""RTW"" + }, + { + ""label"": ""LB777"", + ""type"": ""DBS"", + ""objective"": ""BASIC"" + } + ] + }, + { + ""trust_framework"": ""YOTI_GLOBAL"", + ""schemes"": [ + { + ""label"": ""LB321"", + ""type"": ""IDENTITY"", + ""objective"": ""AL_L1"", + ""config"": {} + } + ] + } + ] + }"; + string sessionSpecJson = JsonConvert.SerializeObject(advancedIdentityProfileJson); + return sessionSpecJson; + } public static object CreateStandardSubject() { From 642a0100a88bafc5fd1a255e62af6e780e4bfc10 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Mon, 13 May 2024 17:38:44 +0300 Subject: [PATCH 25/47] Sdk 2416 update for json generation method (#463) * SDK-2416 updated json generation method * SDK-2416 updated tests for generation method * SDK-2416: added examples for wanted attribute optional parameter --- .../AdvancedIdentityShareController.cs | 20 ++----------- .../Controllers/HomeController.cs | 6 ++++ .../Views/Success/SuccessResult.cshtml | 3 +- .../CoreExample/Controllers/HomeController.cs | 3 +- .../Policy/AdvancedIdentityProfile.cs | 30 +++++++++++++++++++ .../DigitalIdentity/Policy/Policy.cs | 2 +- .../DigitalIdentity/Policy/PolicyBuilder.cs | 8 ++--- .../DigitalIdentity/Policy/WantedAttribute.cs | 4 +-- .../Policy/WantedAttributeBuilder.cs | 11 +++++-- .../Policy/DynamicPolicyBuilderTests.cs | 4 ++- .../Policy/WantedAttributeBuilderTests.cs | 4 ++- .../ShareUrl/DynamicScenarioBuilderTests.cs | 3 +- .../Policy/WantedAttributeBuilderTests.cs | 2 ++ .../TestData/DigitalIdentity.json | 14 ++++----- .../TestData/IdentityProfiles.cs | 7 +++-- 15 files changed, 78 insertions(+), 43 deletions(-) create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/AdvancedIdentityProfile.cs diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/AdvancedIdentityShareController.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/AdvancedIdentityShareController.cs index 7fe32c974..b6cf7f5b1 100644 --- a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/AdvancedIdentityShareController.cs +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/AdvancedIdentityShareController.cs @@ -40,21 +40,6 @@ public IActionResult DigitalIdentity() string advancedIdentityProfileJson = @" { ""profiles"": [ - { - ""trust_framework"": ""UK_TFIDA"", - ""schemes"": [ - { - ""label"": ""dbs-standard"", - ""objective"": ""STANDARD"", - ""type"": ""DBS"", - }, - { - ""label"": ""rtw"", - ""type"": ""RTW"", - ""objective"": """" - } - ] - }, { ""trust_framework"": ""YOTI_GLOBAL"", ""schemes"": [ @@ -76,9 +61,10 @@ public IActionResult DigitalIdentity() }"; - string sessionSpecJson = JsonConvert.SerializeObject(advancedIdentityProfileJson); + var advancedIdentityProfile = JsonConvert.DeserializeObject(advancedIdentityProfileJson); + var policy = new PolicyBuilder() - .WithAdvancedIdentityProfileRequirements(sessionSpecJson) + .WithAdvancedIdentityProfileRequirements(advancedIdentityProfile) .Build(); var sessionReq = new ShareSessionRequestBuilder().WithPolicy(policy) diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs index 7b0ad261c..9099ec630 100644 --- a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs @@ -36,7 +36,13 @@ public IActionResult DigitalIdentity() var yotiClient = new DigitalIdentityClient(_clientSdkId, privateKeyStream); + var givenNamesWantedAttribute = new WantedAttributeBuilder() + .WithName("given_names") + .WithOptional(false) + .Build(); + var policy = new PolicyBuilder() + .WithWantedAttribute(givenNamesWantedAttribute) .WithFullName() .WithEmail() .WithPhoneNumber() diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/SuccessResult.cshtml b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/SuccessResult.cshtml index cba7d31b1..a27d3c400 100644 --- a/src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/SuccessResult.cshtml +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/SuccessResult.cshtml @@ -91,8 +91,9 @@ { - @item.Key + + @item.Key
                                                             @Html.Raw(@item.Value.ToString(Formatting.Indented))
                                                         
diff --git a/src/Examples/Profile/CoreExample/Controllers/HomeController.cs b/src/Examples/Profile/CoreExample/Controllers/HomeController.cs index d51cca3ab..b72234321 100644 --- a/src/Examples/Profile/CoreExample/Controllers/HomeController.cs +++ b/src/Examples/Profile/CoreExample/Controllers/HomeController.cs @@ -50,6 +50,7 @@ public IActionResult DynamicScenario() var givenNamesWantedAttribute = new WantedAttributeBuilder() .WithName("given_names") + .WithOptional(false) .Build(); DynamicPolicy dynamicPolicy = new DynamicPolicyBuilder() @@ -137,4 +138,4 @@ public IActionResult DBSStandard() } } } -} \ No newline at end of file +} diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/AdvancedIdentityProfile.cs b/src/Yoti.Auth/DigitalIdentity/Policy/AdvancedIdentityProfile.cs new file mode 100644 index 000000000..40896fdd3 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/AdvancedIdentityProfile.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class AdvancedIdentityProfile + { + [JsonProperty(PropertyName = "profiles")] + public List Profiles { get; set; } + } + + public class Profile + { + [JsonProperty(PropertyName = "trust_framework")] + public string TrustFramework { get; set; } + [JsonProperty(PropertyName = "schemes")] + public List Schemes { get; set; } + } + + public class Scheme + { + [JsonProperty(PropertyName = "label")] + public string Label { get; set; } + [JsonProperty(PropertyName = "objective")] + public string Objective { get; set; } + [JsonProperty(PropertyName = "type")] + public string Type { get; set; } + } + +} diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs b/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs index 3f943bb3e..9bd799ecd 100644 --- a/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs +++ b/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs @@ -30,7 +30,7 @@ public class Policy [JsonProperty(PropertyName = "identity_profile_requirements")] private readonly object _identityProfileRequirements; - [JsonProperty(PropertyName = "advanded_identity_profile_requirements")] + [JsonProperty(PropertyName = "advanced_identity_profile_requirements")] private readonly object _advancedIdentityProfileRequirements; public Policy( diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs index 820a22951..d85c96947 100644 --- a/src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs +++ b/src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs @@ -9,7 +9,7 @@ public class PolicyBuilder private readonly HashSet _wantedAuthTypes = new HashSet(); private bool _wantedRememberMeId; private object _identityProfileRequirements; - private object _advandedIdentityProfileRequirements; + private AdvancedIdentityProfile _advancedIdentityProfileRequirements; public PolicyBuilder WithWantedAttribute(WantedAttribute wantedAttribute) { @@ -164,15 +164,15 @@ public PolicyBuilder WithIdentityProfileRequirements(object identityProfileRequi ///
/// object describing the advanced identity profile requirements to use /// with the advanced identity profile requirements - public PolicyBuilder WithAdvancedIdentityProfileRequirements(object advancedIdentityProfileRequirements) + public PolicyBuilder WithAdvancedIdentityProfileRequirements(AdvancedIdentityProfile advancedIdentityProfileRequirements) { - _advandedIdentityProfileRequirements = advancedIdentityProfileRequirements; + _advancedIdentityProfileRequirements = advancedIdentityProfileRequirements; return this; } public Policy Build() { - return new Policy(_wantedAttributes.Values, _wantedAuthTypes, _wantedRememberMeId, _identityProfileRequirements, _advandedIdentityProfileRequirements); + return new Policy(_wantedAttributes.Values, _wantedAuthTypes, _wantedRememberMeId, _identityProfileRequirements, _advancedIdentityProfileRequirements); } } } diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs index a4b3cc208..a53199389 100644 --- a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs +++ b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs @@ -14,7 +14,7 @@ public class WantedAttribute [JsonRequired] [JsonProperty(PropertyName = "optional")] - public bool Optional { get; private set; } + public bool? Optional { get; private set; } [JsonProperty(PropertyName = "accept_self_asserted")] public bool? AcceptSelfAsserted { get; private set; } @@ -22,7 +22,7 @@ public class WantedAttribute [JsonProperty(PropertyName = "constraints")] public List Constraints { get; private set; } - public WantedAttribute(string name, string derivation, List constraints, bool? acceptSelfAsserted = null, bool optional = false) + public WantedAttribute(string name, string derivation, List constraints, bool? acceptSelfAsserted = null, bool? optional = false) { Name = name; Derivation = derivation; diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttributeBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttributeBuilder.cs index 266379fd0..34bfc1faa 100644 --- a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttributeBuilder.cs +++ b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttributeBuilder.cs @@ -8,6 +8,7 @@ public class WantedAttributeBuilder private string _derivation; private List _constraints = new List(); private bool? _acceptSelfAsserted; + private bool? _optional; public WantedAttributeBuilder WithName(string name) { @@ -15,6 +16,12 @@ public WantedAttributeBuilder WithName(string name) return this; } + public WantedAttributeBuilder WithOptional(bool optional) + { + _optional = optional; + return this; + } + public WantedAttributeBuilder WithDerivation(string derivation) { _derivation = derivation; @@ -56,7 +63,7 @@ public WantedAttribute Build() { Validation.NotNullOrEmpty(_name, nameof(_name)); - return new WantedAttribute(_name, _derivation, _constraints, _acceptSelfAsserted); + return new WantedAttribute(_name, _derivation, _constraints, _acceptSelfAsserted, _optional); } } -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs index 65fa437ab..b1fec0d31 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; using Yoti.Auth.Constants; using Yoti.Auth.DigitalIdentity.Policy; using Yoti.Auth.Tests.TestData; @@ -330,7 +331,8 @@ public void ShouldBuildWithIdentityProfileRequirements() [TestMethod] public void ShouldBuildWithAdvancedIdentityProfileRequirements() { - object advancedIdentityProfileRequirements = IdentityProfiles.CreateAdvancedIdentityProfileRequirements(); + var advancedIdentityProfileRequirements = IdentityProfiles.CreateAdvancedIdentityProfileRequirements(); + Auth.DigitalIdentity.Policy.Policy result = new PolicyBuilder() .WithAdvancedIdentityProfileRequirements(advancedIdentityProfileRequirements) .Build(); diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeBuilderTests.cs index a302eee43..a856536cb 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeBuilderTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeBuilderTests.cs @@ -34,9 +34,11 @@ public void ShouldSetAcceptSelfAssertedToNullByDefault() { WantedAttribute result = new WantedAttributeBuilder() .WithName("name") + .WithOptional(true) .Build(); Assert.AreEqual(null, result.AcceptSelfAsserted); + Assert.AreEqual(true, result.Optional); } [TestMethod] @@ -161,4 +163,4 @@ public void WithConstraintsShouldOverrideCurrentConstraint() Assert.AreEqual("DRIVING_LICENCE", result.PreferredSources.WantedAnchors[0].Name); } } -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/ShareUrl/DynamicScenarioBuilderTests.cs b/test/Yoti.Auth.Tests/ShareUrl/DynamicScenarioBuilderTests.cs index 22781fb6b..3bdb9fec9 100644 --- a/test/Yoti.Auth.Tests/ShareUrl/DynamicScenarioBuilderTests.cs +++ b/test/Yoti.Auth.Tests/ShareUrl/DynamicScenarioBuilderTests.cs @@ -37,6 +37,7 @@ public void ShouldBuildADynamicScenario() .WithPolicy(somePolicy) .WithExtension(extension1) .WithExtension(extension2) + .WithSubject(someSubject) .Build(); @@ -67,4 +68,4 @@ public void ShouldBuildADynamicScenario() Assert.AreEqual(expectedJson, serializedScenario); } } -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/ShareUrl/Policy/WantedAttributeBuilderTests.cs b/test/Yoti.Auth.Tests/ShareUrl/Policy/WantedAttributeBuilderTests.cs index 23b931598..733871f0f 100644 --- a/test/Yoti.Auth.Tests/ShareUrl/Policy/WantedAttributeBuilderTests.cs +++ b/test/Yoti.Auth.Tests/ShareUrl/Policy/WantedAttributeBuilderTests.cs @@ -37,9 +37,11 @@ public void ShouldSetAcceptSelfAssertedToNullByDefault() { WantedAttribute result = new WantedAttributeBuilder() .WithName("name") + .WithOptional(true) .Build(); Assert.AreEqual(null, result.AcceptSelfAsserted); + Assert.AreEqual(true, result.Optional); } [TestMethod] diff --git a/test/Yoti.Auth.Tests/TestData/DigitalIdentity.json b/test/Yoti.Auth.Tests/TestData/DigitalIdentity.json index 96c60a5dc..1aa053d81 100644 --- a/test/Yoti.Auth.Tests/TestData/DigitalIdentity.json +++ b/test/Yoti.Auth.Tests/TestData/DigitalIdentity.json @@ -2,23 +2,19 @@ "policy": { "wanted": [ { - "name": "date_of_birth", - "optional": false + "name": "date_of_birth" }, { "name": "date_of_birth", - "derivation": "age_over:18", - "optional": false + "derivation": "age_over:18" }, { "name": "date_of_birth", - "derivation": "age_under:30", - "optional": false + "derivation": "age_under:30" }, { "name": "date_of_birth", - "derivation": "age_under:40", - "optional": false + "derivation": "age_under:40" } ], "wanted_auth_types": [ 2 ], @@ -53,4 +49,4 @@ "subject_id": "some_subject_id_string" }, "redirectUri": "someEndpoint" -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs b/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs index d3b7d42d0..4669f14e8 100644 --- a/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs +++ b/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using Yoti.Auth.DigitalIdentity.Policy; namespace Yoti.Auth.Tests.TestData { @@ -17,7 +18,7 @@ public static object CreateStandardIdentityProfileRequirements() }; } - public static object CreateAdvancedIdentityProfileRequirements() + public static AdvancedIdentityProfile CreateAdvancedIdentityProfileRequirements() { string advancedIdentityProfileJson = @" { @@ -49,8 +50,8 @@ public static object CreateAdvancedIdentityProfileRequirements() } ] }"; - string sessionSpecJson = JsonConvert.SerializeObject(advancedIdentityProfileJson); - return sessionSpecJson; + var advancedIdentityProfile = JsonConvert.DeserializeObject(advancedIdentityProfileJson); + return advancedIdentityProfile; } public static object CreateStandardSubject() From b7e9b97ef45e5e0f3c506cbc630479c114d7431f Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:58:37 +0300 Subject: [PATCH 26/47] SDK-2466 added notification builder and test (#468) --- .../Controllers/HomeController.cs | 15 +++-- .../DigitalIdentity/Policy/Notification.cs | 8 +++ .../Policy/NotificationBuilder.cs | 62 +++++++++++++++++++ .../ShareSessionRequestBuilder.cs | 1 - 4 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/NotificationBuilder.cs diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs index 9099ec630..04a06bfaf 100644 --- a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs @@ -41,6 +41,12 @@ public IActionResult DigitalIdentity() .WithOptional(false) .Build(); + var notification = new NotificationBuilder() + .WithUrl("https://example.com/webhook") + .WithMethod("POST") + .WithVerifyTls(true) + .Build(); + var policy = new PolicyBuilder() .WithWantedAttribute(givenNamesWantedAttribute) .WithFullName() @@ -55,14 +61,7 @@ public IActionResult DigitalIdentity() .Build(); var sessionReq = new ShareSessionRequestBuilder().WithPolicy(policy) - .WithNotification(new Notification - { - Headers = { }, - Url = "https://example.com/webhook", - Method = "POST", - VerifyTls = true - - }) + .WithNotification(notification) .WithRedirectUri("https:/www.yoti.com").WithSubject(new { subject_id = "some_subject_id_string" diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs b/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs index a0e60b5cf..a385526df 100644 --- a/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs +++ b/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs @@ -14,5 +14,13 @@ public class Notification public Dictionary Headers { get; set; } // Optional [JsonProperty(PropertyName = "verifyTls")] public bool VerifyTls { get; set; } = true; // Optional, defaults to 'true' if URL is HTTPS + + public Notification(string url, string method, Dictionary headers, bool verifyTls) + { + Url = url; + Method = method; + Headers = headers; + VerifyTls = verifyTls; + } } } diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/NotificationBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Policy/NotificationBuilder.cs new file mode 100644 index 000000000..57ffe60e5 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/NotificationBuilder.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class NotificationBuilder + { + private string _url; + private string _method; + private Dictionary _headers; + private bool _verifyTls; + + /// + /// Set the URL for the notification, required if 'notification' is defined, required + /// + /// + /// + public NotificationBuilder WithUrl(string url) + { + _url = url; + return this; + } + + /// + /// Set the method for the notification, defaults to 'POST', optional + /// + /// + /// + public NotificationBuilder WithMethod(string method) + { + _method = method; + return this; + } + + /// + /// Set the headers for the notification, optional + /// + /// + /// + public NotificationBuilder WithHeaders(Dictionary headers) + { + _headers = headers; + return this; + } + + /// + /// Set to false to disable TLS verification, defaults to 'true' if URL is HTTPS, optional + /// + /// + /// + public NotificationBuilder WithVerifyTls(bool verifyTls) + { + _verifyTls = verifyTls; + return this; + } + + public Notification Build() + { + Validation.NotNullOrEmpty(_url, nameof(_url)); + return new Notification(_url, _method, _headers, _verifyTls); + } + } +} diff --git a/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs index 1b9a83534..fed8b1071 100644 --- a/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs +++ b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs @@ -42,7 +42,6 @@ public ShareSessionRequestBuilder WithPolicy(Policy.Policy dynamicPolicy) /// with a Notification added public ShareSessionRequestBuilder WithNotification(Notification notification) { - _notification = notification; return this; } From 39a45662f38f9205ddc82f32b1b99c89afd8d422 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:59:06 +0300 Subject: [PATCH 27/47] SDK-2354 added error details and related tests (#467) * SDK-2354 added error details and related tests --- .../DigitalIdentity/DigitalIdentityService.cs | 4 +- src/Yoti.Auth/DigitalIdentity/ErrorDetails.cs | 17 ++++++ src/Yoti.Auth/DigitalIdentity/ErrorReason.cs | 14 +++++ src/Yoti.Auth/DigitalIdentity/GetReceipt.cs | 2 + .../RequirementNotMetDetails.cs | 23 ++++++++ .../DigitalIdentity/SharedReceiptResponse.cs | 1 + .../RequirementNotMetDetails.cs | 53 +++++++++++++++++++ test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj | 3 -- 8 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 src/Yoti.Auth/DigitalIdentity/ErrorDetails.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/ErrorReason.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/RequirementNotMetDetails.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/RequirementNotMetDetails.cs diff --git a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs index fa6fc0f40..3043fca82 100644 --- a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs +++ b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs @@ -283,7 +283,9 @@ public static async Task GetShareReceipt(HttpClient httpC ApplicationProfile = appProfile, ExtraData = appExtraData }, - Error = receiptResponse.Error + Error = receiptResponse.Error, + ErrorDetails = receiptResponse.ErrorDetails + }; return sharedReceiptResponse; diff --git a/src/Yoti.Auth/DigitalIdentity/ErrorDetails.cs b/src/Yoti.Auth/DigitalIdentity/ErrorDetails.cs new file mode 100644 index 000000000..b100a87d4 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/ErrorDetails.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using Yoti.Auth.DigitalIdentity; + +namespace Yoti.Auth.DigitalIdentity +{ + public class ErrorDetails + { + public ErrorReason ErrorReason { get; private set; } + + public ErrorReason GetErrorReason() + { + return ErrorReason; + } + } + +} diff --git a/src/Yoti.Auth/DigitalIdentity/ErrorReason.cs b/src/Yoti.Auth/DigitalIdentity/ErrorReason.cs new file mode 100644 index 000000000..f62445135 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/ErrorReason.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using Yoti.Auth.DigitalIdentity; +using Yoti.DigitalIdentity; + +namespace Yoti.Auth.DigitalIdentity +{ + public class ErrorReason + { + public RequirementNotMetDetails RequirementNotMetDetails { get; private set; } + + } + +} diff --git a/src/Yoti.Auth/DigitalIdentity/GetReceipt.cs b/src/Yoti.Auth/DigitalIdentity/GetReceipt.cs index d1821a8f9..e6dd05886 100644 --- a/src/Yoti.Auth/DigitalIdentity/GetReceipt.cs +++ b/src/Yoti.Auth/DigitalIdentity/GetReceipt.cs @@ -42,5 +42,7 @@ public class ReceiptResponse [JsonProperty("error")] public string Error { get; set; } + [JsonProperty("error_details")] + public ErrorDetails ErrorDetails { get; set; } } } diff --git a/src/Yoti.Auth/DigitalIdentity/RequirementNotMetDetails.cs b/src/Yoti.Auth/DigitalIdentity/RequirementNotMetDetails.cs new file mode 100644 index 000000000..345a18678 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/RequirementNotMetDetails.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Yoti.DigitalIdentity +{ + public class RequirementNotMetDetails + { + [JsonProperty(PropertyName = "failure_type")] + public string FailureType { get; private set; } + + [JsonProperty(PropertyName = "details")] + public string Details { get; private set; } + + [JsonProperty(PropertyName = "audit_id")] + public string AuditId { get; private set; } + + [JsonProperty(PropertyName = "document_country_iso_code")] + public string DocumentCountryIsoCode { get; private set; } + + [JsonProperty(PropertyName = "document_type")] + public string DocumentType { get; private set; } + } +} diff --git a/src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs b/src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs index bc2e649a8..01ca14d00 100644 --- a/src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs +++ b/src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs @@ -11,6 +11,7 @@ public class SharedReceiptResponse public string ParentRememberMeID { get; set; } public string Timestamp { get; set; } public string Error { get; set; } + public ErrorDetails ErrorDetails { get; set; } public UserContent UserContent { get; set; } public ApplicationContent ApplicationContent { get; set; } } diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/RequirementNotMetDetails.cs b/test/Yoti.Auth.Tests/DigitalIdentity/RequirementNotMetDetails.cs new file mode 100644 index 000000000..9fb6a1322 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/RequirementNotMetDetails.cs @@ -0,0 +1,53 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; + +namespace Yoti.DigitalIdentity.Tests +{ + [TestClass] + public class RequirementNotMetDetailsTests + { + [TestMethod] + public void DeserializeValidJsonCreatesRequirementNotMetDetails() + { + var json = @" + { + ""failure_type"": ""DOCUMENT_EXPIRED"", + ""details"": ""The document has expired."", + ""audit_id"": ""AUDIT123"", + ""document_country_iso_code"": ""USA"", + ""document_type"": ""PASSPORT"" + }"; + + var details = JsonConvert.DeserializeObject(json); + + Assert.IsNotNull(details); + Assert.AreEqual("DOCUMENT_EXPIRED", details.FailureType); + Assert.AreEqual("The document has expired.", details.Details); + Assert.AreEqual("AUDIT123", details.AuditId); + Assert.AreEqual("USA", details.DocumentCountryIsoCode); + Assert.AreEqual("PASSPORT", details.DocumentType); + } + + + [TestMethod] + public void PropertyGettersReturnCorrectValues() + { + var json = @" + { + ""failure_type"": ""DOCUMENT_EXPIRED"", + ""details"": ""The document has expired."", + ""audit_id"": ""AUDIT123"", + ""document_country_iso_code"": ""USA"", + ""document_type"": ""PASSPORT"" + }"; + + var details = JsonConvert.DeserializeObject(json); + + Assert.AreEqual("DOCUMENT_EXPIRED", details.FailureType); + Assert.AreEqual("The document has expired.", details.Details); + Assert.AreEqual("AUDIT123", details.AuditId); + Assert.AreEqual("USA", details.DocumentCountryIsoCode); + Assert.AreEqual("PASSPORT", details.DocumentType); + } + } +} diff --git a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj index ab7a81e09..7dccb1808 100644 --- a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj +++ b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj @@ -78,7 +78,4 @@ PreserveNewest - - - \ No newline at end of file From ebfe197ec9e72ccaed4ac1fc115b291b7a4377f0 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:59:27 +0300 Subject: [PATCH 28/47] SDK-2374 added failure reason info to idv and added tests (#466) * SDK-2374 added failure reason info to idv and added tests --- .../IdentityProfile/FailureReasonResponse.cs | 5 ++- .../RequirementNotMetDetails.cs | 17 ++++++++ .../FailureReasonResponseTest.cs | 39 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/Yoti.Auth/DocScan/Session/Retrieve/IdentityProfile/RequirementNotMetDetails.cs create mode 100644 test/Yoti.Auth.Tests/DocScan/Session/Retrieve/IdentityProfile/FailureReasonResponseTest.cs diff --git a/src/Yoti.Auth/DocScan/Session/Retrieve/IdentityProfile/FailureReasonResponse.cs b/src/Yoti.Auth/DocScan/Session/Retrieve/IdentityProfile/FailureReasonResponse.cs index 3ee204fe0..682b69db2 100644 --- a/src/Yoti.Auth/DocScan/Session/Retrieve/IdentityProfile/FailureReasonResponse.cs +++ b/src/Yoti.Auth/DocScan/Session/Retrieve/IdentityProfile/FailureReasonResponse.cs @@ -5,6 +5,9 @@ namespace Yoti.Auth.DocScan.Session.Retrieve.IdentityProfile public class FailureReasonResponse { [JsonProperty(PropertyName = "reason_code")] - public string ReasonCode { get; private set; } + public string ReasonCode { get; private set; } + + [JsonProperty(PropertyName = "requirements_not_met_details")] + public RequirementNotMetDetails RequirementNotMetDetails { get; private set; } } } diff --git a/src/Yoti.Auth/DocScan/Session/Retrieve/IdentityProfile/RequirementNotMetDetails.cs b/src/Yoti.Auth/DocScan/Session/Retrieve/IdentityProfile/RequirementNotMetDetails.cs new file mode 100644 index 000000000..1ceee6347 --- /dev/null +++ b/src/Yoti.Auth/DocScan/Session/Retrieve/IdentityProfile/RequirementNotMetDetails.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace Yoti.Auth.DocScan.Session.Retrieve.IdentityProfile; + +public class RequirementNotMetDetails +{ + [JsonProperty(PropertyName = "failure_type")] + public string FailureType { get; private set; } + [JsonProperty(PropertyName = "document_type")] + public string DocumentType { get; private set; } + [JsonProperty(PropertyName = "document_country_iso_code")] + public string DocumentCountryIsoCode { get; private set; } + [JsonProperty(PropertyName = "audit_id")] + public string AuditId { get; private set; } + [JsonProperty(PropertyName = "details")] + public string Details { get; private set; } +} diff --git a/test/Yoti.Auth.Tests/DocScan/Session/Retrieve/IdentityProfile/FailureReasonResponseTest.cs b/test/Yoti.Auth.Tests/DocScan/Session/Retrieve/IdentityProfile/FailureReasonResponseTest.cs new file mode 100644 index 000000000..505e255c1 --- /dev/null +++ b/test/Yoti.Auth.Tests/DocScan/Session/Retrieve/IdentityProfile/FailureReasonResponseTest.cs @@ -0,0 +1,39 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Yoti.Auth.DocScan.Session.Retrieve.IdentityProfile.Tests +{ + [TestClass] + public class FailureReasonResponseTests + { + [TestMethod] + public void Deserialize_ValidJson_CreatesFailureReasonResponse() + { + // Arrange + var json = @" + { + ""reason_code"": ""CODE123"", + ""requirements_not_met_details"": { + ""failure_type"": ""DOCUMENT_EXPIRED"", + ""details"": ""The document has expired."", + ""audit_id"": ""AUDIT123"", + ""document_country_iso_code"": ""USA"", + ""document_type"": ""PASSPORT"" + } + }"; + + var response = JsonConvert.DeserializeObject(json); + + Assert.IsNotNull(response); + Assert.AreEqual("CODE123", response.ReasonCode); + Assert.IsNotNull(response.RequirementNotMetDetails); + Assert.AreEqual("DOCUMENT_EXPIRED", response.RequirementNotMetDetails.FailureType); + Assert.AreEqual("The document has expired.", response.RequirementNotMetDetails.Details); + Assert.AreEqual("AUDIT123", response.RequirementNotMetDetails.AuditId); + Assert.AreEqual("USA", response.RequirementNotMetDetails.DocumentCountryIsoCode); + Assert.AreEqual("PASSPORT", response.RequirementNotMetDetails.DocumentType); + } + } + +} From 76df6b844a8d8e1a073691b4121034c58170542e Mon Sep 17 00:00:00 2001 From: nikhilPank <49190426+nikhilPank@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:54:15 +0100 Subject: [PATCH 29/47] SDK-2382: Update IDV example project defaults (#440) * update example session configurations * IN5631 Updated to remove unsupported dotnet versions * IN5631 Updated tests for dotnet version support --- .../DocScanExample/Controllers/DbsController.cs | 2 +- .../DocScanExample/Controllers/HomeController.cs | 13 ++++++------- src/Yoti.Auth/DocScan/DocScanService.cs | 5 ++--- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Examples/DocScan/DocScanExample/Controllers/DbsController.cs b/src/Examples/DocScan/DocScanExample/Controllers/DbsController.cs index a7d74c9f9..7ce05854b 100644 --- a/src/Examples/DocScan/DocScanExample/Controllers/DbsController.cs +++ b/src/Examples/DocScan/DocScanExample/Controllers/DbsController.cs @@ -38,7 +38,7 @@ public IActionResult Index() //Build Session Spec var sessionSpec = new SessionSpecificationBuilder() .WithClientSessionTokenTtl(600) - .WithResourcesTtl(90000) + .WithResourcesTtl(86400) .WithUserTrackingId("some-user-tracking-id") //Add Sdk Config (with builder) .WithSdkConfig( diff --git a/src/Examples/DocScan/DocScanExample/Controllers/HomeController.cs b/src/Examples/DocScan/DocScanExample/Controllers/HomeController.cs index f15ed5f4d..e614c0bfb 100644 --- a/src/Examples/DocScan/DocScanExample/Controllers/HomeController.cs +++ b/src/Examples/DocScan/DocScanExample/Controllers/HomeController.cs @@ -43,18 +43,17 @@ public IActionResult Index() //Build Session Spec var sessionSpec = new SessionSpecificationBuilder() .WithClientSessionTokenTtl(600) - .WithResourcesTtl(90000) + .WithResourcesTtl(86400) .WithUserTrackingId("some-user-tracking-id") //Add Checks (using builders) .WithRequestedCheck( new RequestedDocumentAuthenticityCheckBuilder() - .WithManualCheckAlways() + .WithManualCheckFallback() .Build() ) .WithRequestedCheck( new RequestedLivenessCheckBuilder() - .ForZoomLiveness() - //.ForStaticLiveness() + .ForStaticLiveness() .Build() ) //.WithRequestedCheck( @@ -64,7 +63,7 @@ public IActionResult Index() // ) .WithRequestedCheck( new RequestedFaceMatchCheckBuilder() - .WithManualCheckAlways() + .WithManualCheckFallback() .Build() ) .WithRequestedCheck( @@ -82,13 +81,13 @@ public IActionResult Index() //Add Tasks (using builders) .WithRequestedTask( new RequestedTextExtractionTaskBuilder() - .WithManualCheckAlways() + .WithManualCheckFallback() .WithChipDataDesired() .Build() ) .WithRequestedTask( new RequestedSupplementaryDocTextExtractionTaskBuilder() - .WithManualCheckAlways() + .WithManualCheckFallback() .Build() ) .WithNotifications(notificationConfig) diff --git a/src/Yoti.Auth/DocScan/DocScanService.cs b/src/Yoti.Auth/DocScan/DocScanService.cs index 48cd7f792..9cf6484c6 100644 --- a/src/Yoti.Auth/DocScan/DocScanService.cs +++ b/src/Yoti.Auth/DocScan/DocScanService.cs @@ -160,10 +160,9 @@ public async Task GetMediaContent(string sdkId, AsymmetricCipherKeyP { return null; } - + string contentType = response.Content.Headers.ContentType.MediaType; - var responseObject = await response.Content.ReadAsByteArrayAsync(); var deserialized = await Task.Factory.StartNew(() => new MediaValue(contentType, responseObject)); @@ -356,4 +355,4 @@ private static string MediaEndpoint(string sessionId, string mediaId) private static JsonSerializerSettings YotiDefaultJsonSettings => new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; } -} \ No newline at end of file +} From 4eac834717b081b5c0885c87d9dac4fa1f49bc94 Mon Sep 17 00:00:00 2001 From: Mehmet Ali Sepici Date: Thu, 21 Nov 2024 15:11:09 +0300 Subject: [PATCH 30/47] Create and Get Qr Methods Added --- src/Yoti.Auth/DigitalIdentityClient.cs | 11 +++++++++++ src/Yoti.Auth/DigitalIdentityClientEngine.cs | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/Yoti.Auth/DigitalIdentityClient.cs b/src/Yoti.Auth/DigitalIdentityClient.cs index e17070ca6..cfe1abc40 100644 --- a/src/Yoti.Auth/DigitalIdentityClient.cs +++ b/src/Yoti.Auth/DigitalIdentityClient.cs @@ -91,6 +91,17 @@ public SharedReceiptResponse GetShareReceipt(string receiptId) Task task = Task.Run(async () => await _yotiDigitalClientEngine.GetShareReceipt(_sdkId, _keyPair, ApiUri, receiptId).ConfigureAwait(false)); return task.Result; } + + + public async Task CreateQrCode(string sessionId, QrRequest qrRequest) + { + return await _yotiDigitalClientEngine.CreateQrCodeAsync(_sdkId, _keyPair, ApiUri, sessionId, qrRequest).ConfigureAwait(false); + } + + public async Task GetQrCode(string qrCodeId) + { + return await _yotiDigitalClientEngine.GetQrCodeAsync(_sdkId, _keyPair, ApiUri, qrCodeId).ConfigureAwait(false); + } internal void SetYotiApiUri() { diff --git a/src/Yoti.Auth/DigitalIdentityClientEngine.cs b/src/Yoti.Auth/DigitalIdentityClientEngine.cs index 2492e8c5e..7b2e01354 100644 --- a/src/Yoti.Auth/DigitalIdentityClientEngine.cs +++ b/src/Yoti.Auth/DigitalIdentityClientEngine.cs @@ -39,5 +39,23 @@ public async Task GetShareReceipt(string sdkId, Asymmetri return result; } + + public async Task CreateQrCodeAsync(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, string sessionid, QrRequest qRRequest) + { + CreateQrResult result = await Task.Run(async () => await DigitalIdentityService.CreateQrCode( + _httpClient, apiUrl, sdkId, keyPair, sessionid, qRRequest).ConfigureAwait(false)) + .ConfigureAwait(false); + + return result; + } + + public async Task GetQrCodeAsync(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, string qrcodeId) + { + GetQrCodeResult result = await Task.Run(async () => await DigitalIdentityService.GetQrCode( + _httpClient, apiUrl, sdkId, keyPair, qrcodeId).ConfigureAwait(false)) + .ConfigureAwait(false); + + return result; + } } } From dd6beb81259eca8346b59b0ce62f6674d31d2374 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 7 Mar 2025 10:58:26 +0000 Subject: [PATCH 31/47] added getSession method --- .../DigitalIdentity/Views/Home/DigitalIdentity.cshtml | 2 +- src/Yoti.Auth/DigitalIdentityClient.cs | 5 +++++ src/Yoti.Auth/DigitalIdentityClientEngine.cs | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml index 841d32ac8..e8ea2686a 100644 --- a/src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml @@ -86,7 +86,7 @@ await Yoti.ready() await onReadyToStart() } - + diff --git a/src/Yoti.Auth/DigitalIdentityClient.cs b/src/Yoti.Auth/DigitalIdentityClient.cs index cfe1abc40..628a1a06c 100644 --- a/src/Yoti.Auth/DigitalIdentityClient.cs +++ b/src/Yoti.Auth/DigitalIdentityClient.cs @@ -102,6 +102,11 @@ public async Task GetQrCode(string qrCodeId) { return await _yotiDigitalClientEngine.GetQrCodeAsync(_sdkId, _keyPair, ApiUri, qrCodeId).ConfigureAwait(false); } + + public async Task GetSession(string sessionId) + { + return await _yotiDigitalClientEngine.GetSession(_sdkId, _keyPair, ApiUri, sessionId).ConfigureAwait(false); + } internal void SetYotiApiUri() { diff --git a/src/Yoti.Auth/DigitalIdentityClientEngine.cs b/src/Yoti.Auth/DigitalIdentityClientEngine.cs index 7b2e01354..0db680235 100644 --- a/src/Yoti.Auth/DigitalIdentityClientEngine.cs +++ b/src/Yoti.Auth/DigitalIdentityClientEngine.cs @@ -57,5 +57,14 @@ public async Task GetQrCodeAsync(string sdkId, AsymmetricCipher return result; } + + public async Task GetSession(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, string sessionId) + { + var result = await Task.Run(async () => await DigitalIdentityService.GetSession( + _httpClient, apiUrl, sdkId, keyPair, sessionId).ConfigureAwait(false)) + .ConfigureAwait(false); + + return result; + } } } From e566e3a8961755527a499879c1d35ead008aaa63 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 27 Jun 2025 12:51:18 +0100 Subject: [PATCH 32/47] Added unit tests --- .../DigitalIdentityClientEngineTests.cs | 302 +++++++++++++++++- 1 file changed, 291 insertions(+), 11 deletions(-) diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs index 16b732304..6b1c0b2c4 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs @@ -40,22 +40,66 @@ public async Task CreateSessionAsyncShouldReturnCorrectValues() } [TestMethod] - public void TestGetShareReceipt() + public async Task CreateQrCodeAsyncShouldReturnCorrectValues() { - Uri apiUrl = new Uri("https://example.com/api"); - string receiptId = "some_receiptid"; - string refId = "NpdmVVGC-28356678-c236-4518-9de4-7a93009ccaf0-c5f92f2a-5539-453e-babc-9b06e1d6b7de"; + string qrCodeId = "test-qr-code-id"; + string qrCodeUri = "https://code.yoti.com/CAEaJDlkOGI4ZGFjLTEyMzQtNTY3OC05MDEyLWFiY2RlZjEyMzQ1Ng=="; Mock handlerMock = SetupMockMessageHandler( HttpStatusCode.OK, - "{\"id\":\"" + refId + "\",\"status\":\"SOME_STATUS\",\"expiry\":\"SOME_EXPIRY\",\"created\":\"SOME_CREATED\",\"updated\":\"SOME_UPDATED\",\"qrCode\":{\"id\":\"SOME_QRCODE_ID\"},\"receipt\":{\"id\":\"SOME_RECEIPT_ID\"}}"); - + "{\"id\":\"" + qrCodeId + "\",\"uri\":\"" + qrCodeUri + "\"}"); + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + QrRequest qrRequest = TestTools.CreateQr.CreateQrStandard(); + string sessionId = "test-session-id"; + + CreateQrResult result = await engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId, qrRequest); - Assert.ThrowsException(() => - { - SharedReceiptResponse response = engine.GetShareReceipt(SdkId, _keyPair, apiUrl, receiptId).Result; - }); + Assert.IsNotNull(result); + Assert.AreEqual(qrCodeId, result.Id); + Assert.AreEqual(qrCodeUri, result.Uri); + } + + [TestMethod] + public async Task GetQrCodeAsyncShouldReturnCorrectValues() + { + string qrCodeId = "test-qr-code-id"; + string expiry = "2025-12-31T23:59:59Z"; + string policy = "test-policy"; + + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{\"id\":\"" + qrCodeId + "\",\"expiry\":\"" + expiry + "\",\"policy\":\"" + policy + "\",\"session\":{\"id\":\"session-123\",\"status\":\"ACTIVE\"},\"redirectUri\":\"https://example.com/redirect\"}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + + GetQrCodeResult result = await engine.GetQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), qrCodeId); + + Assert.IsNotNull(result); + Assert.AreEqual(qrCodeId, result.Id); + Assert.AreEqual(expiry, result.Expiry); + Assert.AreEqual(policy, result.Policy); + } + + [TestMethod] + public async Task GetSessionShouldReturnCorrectValues() + { + string sessionId = "test-session-id"; + string status = "ACTIVE"; + string expiry = "2025-12-31T23:59:59Z"; + + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{\"id\":\"" + sessionId + "\",\"status\":\"" + status + "\",\"expiry\":\"" + expiry + "\",\"created\":\"2025-06-27T10:00:00Z\",\"updated\":\"2025-06-27T11:00:00Z\",\"qrCode\":{\"id\":\"qr-123\"},\"receipt\":{\"id\":\"receipt-123\"}}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + + GetSessionResult result = await engine.GetSession(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); + + Assert.IsNotNull(result); + Assert.AreEqual(sessionId, result.Id); + Assert.AreEqual(status, result.Status); + Assert.AreEqual(expiry, result.Expiry); } [DataTestMethod] @@ -80,12 +124,108 @@ public void CreateShareSessionNonSuccessStatusCodesShouldThrowException(HttpStat engine.CreateShareSessionAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiApiUrl), shareSessionRequest).Wait(); }); + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + } [DataTestMethod] + [DataRow(HttpStatusCode.BadRequest)] + [DataRow(HttpStatusCode.Unauthorized)] + [DataRow(HttpStatusCode.InternalServerError)] + [DataRow(HttpStatusCode.RequestTimeout)] + [DataRow(HttpStatusCode.NotFound)] + [DataRow(HttpStatusCode.Forbidden)] + public void GetShareReceiptNonSuccessStatusCodesShouldThrowException(HttpStatusCode httpStatusCode) + { + Mock handlerMock = SetupMockMessageHandler( + httpStatusCode, + "{\"status\":\"bad\"}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + Uri apiUrl = new Uri("https://example.com/api"); + string receiptId = "some_receiptid"; + + var aggregateException = Assert.ThrowsException(() => + { + engine.GetShareReceipt(SdkId, _keyPair, apiUrl, receiptId).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + } + + [DataTestMethod] + [DataRow(HttpStatusCode.BadRequest)] + [DataRow(HttpStatusCode.Unauthorized)] + [DataRow(HttpStatusCode.InternalServerError)] + [DataRow(HttpStatusCode.RequestTimeout)] + [DataRow(HttpStatusCode.NotFound)] + [DataRow(HttpStatusCode.Forbidden)] + public void CreateQrCodeAsyncNonSuccessStatusCodesShouldThrowException(HttpStatusCode httpStatusCode) + { + Mock handlerMock = SetupMockMessageHandler( + httpStatusCode, + "{\"status\":\"bad\"}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + QrRequest qrRequest = TestTools.CreateQr.CreateQrStandard(); + string sessionId = "test-session-id"; + + var aggregateException = Assert.ThrowsException(() => + { + engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId, qrRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + } + + [DataTestMethod] + [DataRow(HttpStatusCode.BadRequest)] + [DataRow(HttpStatusCode.Unauthorized)] + [DataRow(HttpStatusCode.InternalServerError)] + [DataRow(HttpStatusCode.RequestTimeout)] + [DataRow(HttpStatusCode.NotFound)] + [DataRow(HttpStatusCode.Forbidden)] + public void GetQrCodeAsyncNonSuccessStatusCodesShouldThrowException(HttpStatusCode httpStatusCode) + { + Mock handlerMock = SetupMockMessageHandler( + httpStatusCode, + "{\"status\":\"bad\"}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + string qrCodeId = "test-qr-code-id"; + + var aggregateException = Assert.ThrowsException(() => + { + engine.GetQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), qrCodeId).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + } + + [DataTestMethod] + [DataRow(HttpStatusCode.BadRequest)] + [DataRow(HttpStatusCode.Unauthorized)] + [DataRow(HttpStatusCode.InternalServerError)] + [DataRow(HttpStatusCode.RequestTimeout)] + [DataRow(HttpStatusCode.NotFound)] + [DataRow(HttpStatusCode.Forbidden)] + public void GetSessionNonSuccessStatusCodesShouldThrowException(HttpStatusCode httpStatusCode) + { + Mock handlerMock = SetupMockMessageHandler( + httpStatusCode, + "{\"status\":\"bad\"}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + string sessionId = "test-session-id"; + + var aggregateException = Assert.ThrowsException(() => + { + engine.GetSession(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId).Wait(); + }); + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); } private static Mock SetupMockMessageHandler(HttpStatusCode httpStatusCode, string responseContent) { - var handlerMock = new Mock(MockBehavior.Strict); + var handlerMock = new Mock(MockBehavior.Loose); handlerMock .Protected() .Setup>( @@ -100,7 +240,147 @@ private static Mock SetupMockMessageHandler(HttpStatusCode h }) .Callback((http, token) => _httpRequestMessage = http) .Verifiable(); + return handlerMock; } + + [TestMethod] + public void ConstructorShouldAcceptHttpClient() + { + var httpClient = new HttpClient(); + + var engine = new DigitalIdentityClientEngine(httpClient); + + Assert.IsNotNull(engine); + } + + [TestMethod] + public async Task GetShareReceiptShouldThrowWhenReceiptIdIsEmpty() + { + var httpClient = new HttpClient(); + var engine = new DigitalIdentityClientEngine(httpClient); + Uri apiUrl = new Uri("https://example.com/api"); + + await Assert.ThrowsExceptionAsync(() => + engine.GetShareReceipt(SdkId, _keyPair, apiUrl, "")); + } + + [TestMethod] + public async Task CreateShareSessionAsyncShouldHandleEmptyResponse() + { + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + ShareSessionRequest shareSessionRequest = TestTools.ShareSession.CreateStandardShareSessionRequest(); + + ShareSessionResult shareSessionResult = await engine.CreateShareSessionAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), shareSessionRequest); + + Assert.IsNotNull(shareSessionResult); + } + + [TestMethod] + public async Task CreateQrCodeAsyncShouldHandleEmptyResponse() + { + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + QrRequest qrRequest = TestTools.CreateQr.CreateQrStandard(); + string sessionId = "test-session-id"; + + CreateQrResult result = await engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId, qrRequest); + + Assert.IsNotNull(result); + } + + [TestMethod] + public async Task GetQrCodeAsyncShouldHandleEmptyResponse() + { + string qrCodeId = "test-qr-code-id"; + + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + + GetQrCodeResult result = await engine.GetQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), qrCodeId); + + Assert.IsNotNull(result); + } + + [TestMethod] + public async Task GetSessionShouldHandleEmptyResponse() + { + string sessionId = "test-session-id"; + + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + + GetSessionResult result = await engine.GetSession(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); + + Assert.IsNotNull(result); + } + + [TestMethod] + public async Task CreateShareSessionAsyncShouldHandleSpecialCharactersInId() + { + string refId = "session-with-special-chars-123456"; + + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{\"id\":\"" + refId + "\",\"status\":\"ACTIVE\"}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + ShareSessionRequest shareSessionRequest = TestTools.ShareSession.CreateStandardShareSessionRequest(); + + ShareSessionResult shareSessionResult = await engine.CreateShareSessionAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), shareSessionRequest); + + Assert.IsNotNull(shareSessionResult); + Assert.AreEqual(refId, shareSessionResult.Id); + } + + [TestMethod] + public async Task GetQrCodeAsyncShouldHandleNullSessionInResponse() + { + string qrCodeId = "test-qr-code-id"; + + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{\"id\":\"" + qrCodeId + "\",\"session\":null}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + + GetQrCodeResult result = await engine.GetQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), qrCodeId); + + Assert.IsNotNull(result); + Assert.AreEqual(qrCodeId, result.Id); + Assert.IsNull(result.Session); + } + + [TestMethod] + public async Task GetSessionShouldHandleNullQrCodeAndReceiptInResponse() + { + string sessionId = "test-session-id"; + + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{\"id\":\"" + sessionId + "\",\"qrCode\":null,\"receipt\":null}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + + GetSessionResult result = await engine.GetSession(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); + + Assert.IsNotNull(result); + Assert.AreEqual(sessionId, result.Id); + Assert.IsNull(result.QrCode); + Assert.IsNull(result.Receipt); + } } } From 9e07572d9b4ac18dac004c8e4f3ac0af2ea3bb7e Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Fri, 27 Jun 2025 14:39:00 +0100 Subject: [PATCH 33/47] Update azure-pipelines.yml --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7c3f24ea7..6aba10147 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -31,7 +31,7 @@ steps: organization: 'getyoti' scannerMode: 'MSBuild' projectKey: 'getyoti:dotnet' - projectName: '.NET SDK' + projectName: 'dotnet-sdk' projectVersion: '3.13.0' extraProperties: | sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" From e00ca734682cc54b7a70f7bfb03a7a1c09c601fb Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:00:02 +0100 Subject: [PATCH 34/47] Update azure-pipelines.yml --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6aba10147..6ae17666c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -28,11 +28,11 @@ steps: - task: SonarCloudPrepare@1 inputs: SonarCloud: 'Yoti SonarCloud' - organization: 'getyoti' + organization: 'yoti' scannerMode: 'MSBuild' projectKey: 'getyoti:dotnet' projectName: 'dotnet-sdk' - projectVersion: '3.13.0' + projectVersion: '3.18.0' extraProperties: | sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" sonar.links.scm = https://github.com/getyoti/yoti-dotnet-sdk From 5089a601cbd5103e7298fc9c5182f0c4ff8d622f Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:11:19 +0100 Subject: [PATCH 35/47] Update azure-pipelines.yml --- azure-pipelines.yml | 66 +++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6ae17666c..abbf46e61 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,7 +25,10 @@ variables: buildConfiguration: 'Release' steps: -- task: SonarCloudPrepare@1 +# STEP 1: PREPARE ANALYSIS ON SONARCLOUD +# Updated to the modern and correct @2 version. All inputs are preserved. +- task: SonarCloudPrepare@2 # <-- CHANGED from @1 to @2 + displayName: 'SonarCloud: Prepare Analysis' inputs: SonarCloud: 'Yoti SonarCloud' organization: 'yoti' @@ -34,71 +37,62 @@ steps: projectName: 'dotnet-sdk' projectVersion: '3.18.0' extraProperties: | - sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" - sonar.links.scm = https://github.com/getyoti/yoti-dotnet-sdk - sonar.exclusions = src/Yoti.Auth/ProtoBuf/**,src/Examples/**,**/obj/**,**/*.dll,src/Yoti.Auth/DigitalIdentity/** - displayName: SonarCloud Prepare Analysis + sonar.cs.opencover.reportsPaths="$(Build.SourcesDirectory)/**/*coverage.opencover.xml" + sonar.links.scm=https://github.com/getyoti/yoti-dotnet-sdk + sonar.exclusions=src/Yoti.Auth/ProtoBuf/**,src/Examples/**,**/obj/**,**/*.dll,src/Yoti.Auth/DigitalIdentity/** +# STEP 2: RESTORE, BUILD, AND TEST +# These steps are required for the SonarCloud analysis to work correctly. - task: NuGetToolInstaller@1 - task: NuGetCommand@2 + displayName: 'NuGet: Restore Solution' inputs: restoreSolution: '$(solution)' - displayName: Restore Solution - task: VSBuild@1 + displayName: 'Build: Build Solution' inputs: solution: '$(solution)' msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=false /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - displayName: Build Solution -# Cobertura is used to display tests and code coverage in Azure +# Run tests to generate coverage reports for both Azure DevOps and SonarCloud - task: DotNetCoreCLI@2 + displayName: 'Test: Generate Cobertura & OpenCover reports' inputs: command: test - arguments: '--configuration $(BuildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:Exclude="[*]Yoti.Auth.ProtoBuf.*" /p:ExcludeByAttribute="GeneratedCodeAttribute"' + arguments: '--configuration $(BuildConfiguration) --logger trx /p:CollectCoverage=true /p:CoverletOutputFormat="cobertura,opencover" /p:Exclude="[*]Yoti.Auth.ProtoBuf.*" /p:ExcludeByAttribute="GeneratedCodeAttribute"' projects: '**/*Tests/*.csproj' nobuild: true - displayName: Run Tests With Cobertura - -# OpenCover is used to display code coverage in SonarCloud -- task: DotNetCoreCLI@2 - inputs: - command: test - arguments: '--logger trx --configuration $(BuildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[*]Yoti.Auth.ProtoBuf.*" /p:ExcludeByAttribute="GeneratedCodeAttribute"' - projects: '**/*Tests/*.csproj' - nobuild: true - displayName: Run Tests With OpenCover - -# Generate the report using ReportGenerator (https://github.com/danielpalme/ReportGenerator) + +# STEP 3: PUBLISH COVERAGE TO AZURE DEVOPS (Optional, for UI display) +# This section generates and publishes the Cobertura report for the Azure DevOps UI. - task: DotNetCoreCLI@2 + displayName: 'ReportGenerator: Install Tool' inputs: command: custom custom: tool arguments: install --tool-path . dotnet-reportgenerator-globaltool - displayName: Install ReportGenerator tool - -- script: reportgenerator -reports:$(Build.SourcesDirectory)/test/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:Cobertura - displayName: Cobertura Code Coverage -- script: reportgenerator -reports:$(Build.SourcesDirectory)/test/**/coverage.opencover.xml -targetdir:D:\Reports - displayName: OpenCover Code Coverage +- script: reportgenerator "-reports:$(Build.SourcesDirectory)/**/*coverage.cobertura.xml" "-targetdir:$(Build.SourcesDirectory)/CodeCoverage" -reporttypes:Cobertura + displayName: 'ReportGenerator: Create Cobertura Report' -# Publish the code coverage result (summary and web site) -# The summary allows to view the coverage percentage in the summary tab -# The web site allows to view which lines are covered directly in Azure Pipeline - task: PublishCodeCoverageResults@1 + displayName: 'Azure DevOps: Publish Cobertura Coverage' inputs: codeCoverageTool: Cobertura summaryFileLocation: '$(Build.SourcesDirectory)/CodeCoverage/Cobertura.xml' - displayName: 'Publish Cobertura Code Coverage' - -- task: SonarCloudAnalyze@1 - displayName: Sonar Cloud Analyze -- task: SonarCloudPublish@1 +# STEP 4: RUN SONARCLOUD ANALYSIS +# This task collects the data from the build and test steps. +- task: SonarCloudAnalyze@2 # <-- CHANGED from @1 to @2 + displayName: 'SonarCloud: Run Code Analysis' + +# STEP 5: PUBLISH SONARCLOUD QUALITY GATE +# This task pushes the results to SonarCloud and checks the Quality Gate. +- task: SonarCloudPublish@1 # <-- CHANGED from @1 to @2 + displayName: 'SonarCloud: Publish Quality Gate Result' inputs: pollingTimeoutSec: '300' - displayName: Sonar Cloud Publish From 7cac72d6acb2969852ae7cfd78555162f33b149c Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:25:13 +0100 Subject: [PATCH 36/47] Update azure-pipelines.yml --- azure-pipelines.yml | 68 ++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index abbf46e61..67180dea3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,74 +25,80 @@ variables: buildConfiguration: 'Release' steps: -# STEP 1: PREPARE ANALYSIS ON SONARCLOUD -# Updated to the modern and correct @2 version. All inputs are preserved. -- task: SonarCloudPrepare@2 # <-- CHANGED from @1 to @2 - displayName: 'SonarCloud: Prepare Analysis' +- task: SonarCloudPrepare@1 inputs: - SonarCloud: 'Yoti SonarCloud' + SonarCloud: 'Yoti SonarCloud' organization: 'yoti' scannerMode: 'MSBuild' projectKey: 'getyoti:dotnet' projectName: 'dotnet-sdk' projectVersion: '3.18.0' extraProperties: | - sonar.cs.opencover.reportsPaths="$(Build.SourcesDirectory)/**/*coverage.opencover.xml" - sonar.links.scm=https://github.com/getyoti/yoti-dotnet-sdk - sonar.exclusions=src/Yoti.Auth/ProtoBuf/**,src/Examples/**,**/obj/**,**/*.dll,src/Yoti.Auth/DigitalIdentity/** + sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" + sonar.links.scm = https://github.com/getyoti/yoti-dotnet-sdk + sonar.exclusions = src/Yoti.Auth/ProtoBuf/**,src/Examples/**,**/obj/**,**/*.dll,src/Yoti.Auth/DigitalIdentity/** + displayName: SonarCloud Prepare Analysis -# STEP 2: RESTORE, BUILD, AND TEST -# These steps are required for the SonarCloud analysis to work correctly. - task: NuGetToolInstaller@1 - task: NuGetCommand@2 - displayName: 'NuGet: Restore Solution' inputs: restoreSolution: '$(solution)' + displayName: Restore Solution - task: VSBuild@1 - displayName: 'Build: Build Solution' inputs: solution: '$(solution)' msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=false /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' + displayName: Build Solution -# Run tests to generate coverage reports for both Azure DevOps and SonarCloud +# Cobertura is used to display tests and code coverage in Azure - task: DotNetCoreCLI@2 - displayName: 'Test: Generate Cobertura & OpenCover reports' inputs: command: test - arguments: '--configuration $(BuildConfiguration) --logger trx /p:CollectCoverage=true /p:CoverletOutputFormat="cobertura,opencover" /p:Exclude="[*]Yoti.Auth.ProtoBuf.*" /p:ExcludeByAttribute="GeneratedCodeAttribute"' + arguments: '--configuration $(BuildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:Exclude="[*]Yoti.Auth.ProtoBuf.*" /p:ExcludeByAttribute="GeneratedCodeAttribute"' projects: '**/*Tests/*.csproj' nobuild: true - -# STEP 3: PUBLISH COVERAGE TO AZURE DEVOPS (Optional, for UI display) -# This section generates and publishes the Cobertura report for the Azure DevOps UI. + displayName: Run Tests With Cobertura + +# OpenCover is used to display code coverage in SonarCloud +- task: DotNetCoreCLI@2 + inputs: + command: test + arguments: '--logger trx --configuration $(BuildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[*]Yoti.Auth.ProtoBuf.*" /p:ExcludeByAttribute="GeneratedCodeAttribute"' + projects: '**/*Tests/*.csproj' + nobuild: true + displayName: Run Tests With OpenCover + +# Generate the report using ReportGenerator (https://github.com/danielpalme/ReportGenerator) - task: DotNetCoreCLI@2 - displayName: 'ReportGenerator: Install Tool' inputs: command: custom custom: tool arguments: install --tool-path . dotnet-reportgenerator-globaltool + displayName: Install ReportGenerator tool + +- script: reportgenerator -reports:$(Build.SourcesDirectory)/test/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:Cobertura + displayName: Cobertura Code Coverage -- script: reportgenerator "-reports:$(Build.SourcesDirectory)/**/*coverage.cobertura.xml" "-targetdir:$(Build.SourcesDirectory)/CodeCoverage" -reporttypes:Cobertura - displayName: 'ReportGenerator: Create Cobertura Report' +- script: reportgenerator -reports:$(Build.SourcesDirectory)/test/**/coverage.opencover.xml -targetdir:D:\Reports + displayName: OpenCover Code Coverage +# Publish the code coverage result (summary and web site) +# The summary allows to view the coverage percentage in the summary tab +# The web site allows to view which lines are covered directly in Azure Pipeline - task: PublishCodeCoverageResults@1 - displayName: 'Azure DevOps: Publish Cobertura Coverage' inputs: codeCoverageTool: Cobertura summaryFileLocation: '$(Build.SourcesDirectory)/CodeCoverage/Cobertura.xml' + displayName: 'Publish Cobertura Code Coverage' + +- task: SonarCloudAnalyze@1 + displayName: Sonar Cloud Analyze -# STEP 4: RUN SONARCLOUD ANALYSIS -# This task collects the data from the build and test steps. -- task: SonarCloudAnalyze@2 # <-- CHANGED from @1 to @2 - displayName: 'SonarCloud: Run Code Analysis' - -# STEP 5: PUBLISH SONARCLOUD QUALITY GATE -# This task pushes the results to SonarCloud and checks the Quality Gate. -- task: SonarCloudPublish@1 # <-- CHANGED from @1 to @2 - displayName: 'SonarCloud: Publish Quality Gate Result' +- task: SonarCloudPublish@1 inputs: pollingTimeoutSec: '300' + displayName: Sonar Cloud Publish From 73b33a3ee550b0a348d5b91f3c270538a72de83b Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 27 Jun 2025 15:29:44 +0100 Subject: [PATCH 37/47] updated pipeline --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 67180dea3..f1bec3d83 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,7 +22,7 @@ pool: variables: solution: '**/*.sln' buildPlatform: 'Any CPU' - buildConfiguration: 'Release' + buildConfiguration: 'Release' steps: - task: SonarCloudPrepare@1 From f382a0bc63ec4206aa5fed4f06e9a04d240015b9 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 27 Jun 2025 15:32:36 +0100 Subject: [PATCH 38/47] updated pipeline --- azure-pipelines.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f1bec3d83..7c3f24ea7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,17 +22,17 @@ pool: variables: solution: '**/*.sln' buildPlatform: 'Any CPU' - buildConfiguration: 'Release' + buildConfiguration: 'Release' steps: - task: SonarCloudPrepare@1 inputs: - SonarCloud: 'Yoti SonarCloud' - organization: 'yoti' + SonarCloud: 'Yoti SonarCloud' + organization: 'getyoti' scannerMode: 'MSBuild' projectKey: 'getyoti:dotnet' - projectName: 'dotnet-sdk' - projectVersion: '3.18.0' + projectName: '.NET SDK' + projectVersion: '3.13.0' extraProperties: | sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" sonar.links.scm = https://github.com/getyoti/yoti-dotnet-sdk From 80717bbae4fab89908f17c10f5467f865ca54f68 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 27 Jun 2025 15:33:19 +0100 Subject: [PATCH 39/47] updated pipeline --- azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7c3f24ea7..6ae17666c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -28,11 +28,11 @@ steps: - task: SonarCloudPrepare@1 inputs: SonarCloud: 'Yoti SonarCloud' - organization: 'getyoti' + organization: 'yoti' scannerMode: 'MSBuild' projectKey: 'getyoti:dotnet' - projectName: '.NET SDK' - projectVersion: '3.13.0' + projectName: 'dotnet-sdk' + projectVersion: '3.18.0' extraProperties: | sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" sonar.links.scm = https://github.com/getyoti/yoti-dotnet-sdk From b826690fe63a6db15160ab93bbadde9f78ca8b7b Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 27 Jun 2025 15:53:47 +0100 Subject: [PATCH 40/47] updated pipeline --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6ae17666c..ab08ca7d0 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -28,10 +28,10 @@ steps: - task: SonarCloudPrepare@1 inputs: SonarCloud: 'Yoti SonarCloud' - organization: 'yoti' + organization: 'getyoti' scannerMode: 'MSBuild' projectKey: 'getyoti:dotnet' - projectName: 'dotnet-sdk' + projectName: '.NET SDK' projectVersion: '3.18.0' extraProperties: | sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" From 80727e36078fecf57474a8bcf653836a0ce7c283 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:13:32 +0100 Subject: [PATCH 41/47] Update test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs index 6b1c0b2c4..c5a65216f 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs @@ -147,7 +147,7 @@ public void GetShareReceiptNonSuccessStatusCodesShouldThrowException(HttpStatusC engine.GetShareReceipt(SdkId, _keyPair, apiUrl, receiptId).Wait(); }); - Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); } [DataTestMethod] From 8bf94c00126296986b6fab7c45ac77ad13fc9937 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 27 Jun 2025 16:15:44 +0100 Subject: [PATCH 42/47] updated frame url --- .../DigitalIdentity/Views/Home/DigitalIdentity.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml index e8ea2686a..841d32ac8 100644 --- a/src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml @@ -86,7 +86,7 @@ await Yoti.ready() await onReadyToStart() } - + From 8a899a9888c33ee729b08aa92f951a728d653739 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:16:29 +0100 Subject: [PATCH 43/47] Update test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs index c5a65216f..655a0ad9a 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs @@ -125,7 +125,9 @@ public void CreateShareSessionNonSuccessStatusCodesShouldThrowException(HttpStat }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); - } [DataTestMethod] + } + + [DataTestMethod] [DataRow(HttpStatusCode.BadRequest)] [DataRow(HttpStatusCode.Unauthorized)] [DataRow(HttpStatusCode.InternalServerError)] From e7e4b242a1cbb257abca94c6c14da5c7f3f26302 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 27 Jun 2025 16:32:19 +0100 Subject: [PATCH 44/47] updated tests --- test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs index 655a0ad9a..8dddfe73b 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs @@ -149,7 +149,7 @@ public void GetShareReceiptNonSuccessStatusCodesShouldThrowException(HttpStatusC engine.GetShareReceipt(SdkId, _keyPair, apiUrl, receiptId).Wait(); }); - Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); } [DataTestMethod] From 0b3f25c411ca6dcc89f1799851729a6557678f5d Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 26 Sep 2025 12:44:48 +0100 Subject: [PATCH 45/47] added create-qr sample --- .../Controllers/HomeController.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs index 04a06bfaf..1adb34126 100644 --- a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Yoti.Auth; @@ -86,5 +87,50 @@ public IActionResult DigitalIdentity() return RedirectToAction("Error", "Success"); } } + + // GET: /create-qr/{sessionId} + [Route("create-qr/{sessionId}")] + public async Task CreateQrCode(string sessionId) + { + try + { + string yotiKeyFilePath = Environment.GetEnvironmentVariable("YOTI_KEY_FILE_PATH"); + _logger.LogInformation("Creating QR code for session: {SessionId}", sessionId); + + StreamReader privateKeyStream = System.IO.File.OpenText(yotiKeyFilePath); + var yotiClient = new DigitalIdentityClient(_clientSdkId, privateKeyStream); + + // Create QR request with default settings + var qrRequest = new QrRequestBuilder() + .WithTransport("INLINE") + .WithDisplayMode("QR_CODE") + .Build(); + + var qrResult = await yotiClient.CreateQrCode(sessionId, qrRequest); + + _logger.LogInformation("QR code created with ID: {QrId}", qrResult.Id); + + return Ok(new + { + sessionId = sessionId, + qrId = qrResult.Id, + qrUri = qrResult.Uri, + success = true, + message = "QR code created successfully" + }); + } + catch (Exception e) + { + _logger.LogError(exception: e, "Error creating QR code for session {SessionId}: {Error}", sessionId, e.Message); + + return BadRequest(new + { + sessionId = sessionId, + success = false, + error = e.Message, + innerError = e.InnerException?.Message + }); + } + } } } From 90d5637474cc95da0eaf25ba7a9e6bc9041f4424 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Thu, 2 Oct 2025 11:29:52 +0100 Subject: [PATCH 46/47] updated createqr with sessionid --- .../HomeControllerCreateQrCodeTests.cs | 244 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 3 - test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj | 6 + 3 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/HomeControllerCreateQrCodeTests.cs diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/HomeControllerCreateQrCodeTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/HomeControllerCreateQrCodeTests.cs new file mode 100644 index 000000000..2573dfa49 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/HomeControllerCreateQrCodeTests.cs @@ -0,0 +1,244 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Yoti.Auth.Tests.DigitalIdentity +{ + [TestClass] + public class HomeControllerCreateQrCodeTests + { + private Mock> _mockLogger; + private DigitalIdentityExample.Controllers.HomeController _homeController; + private const string ValidSessionId = "ss.v2.abc123def456ghi789"; + private const string InvalidSessionId = "0"; + private const string ClientSdkId = "test-sdk-id"; + + [TestInitialize] + public void Setup() + { + _mockLogger = new Mock>(); + + // Set up environment variables for the controller + Environment.SetEnvironmentVariable("YOTI_CLIENT_SDK_ID", ClientSdkId); + Environment.SetEnvironmentVariable("YOTI_KEY_FILE_PATH", GetTestKeyFilePath()); + + _homeController = new DigitalIdentityExample.Controllers.HomeController(_mockLogger.Object); + } + + [TestCleanup] + public void Cleanup() + { + // Clean up environment variables + Environment.SetEnvironmentVariable("YOTI_CLIENT_SDK_ID", null); + Environment.SetEnvironmentVariable("YOTI_KEY_FILE_PATH", null); + } + + [TestMethod] + public async Task CreateQrCode_WithValidSessionId_AttemptsApiCall() + { + // Act + var result = await _homeController.CreateQrCode(ValidSessionId); + + // Assert + // Since we're using test credentials, the API call will fail with an authentication error + // But this verifies that validation passed and the method attempted the API call + Assert.IsInstanceOfType(result, typeof(BadRequestObjectResult)); + var badRequestResult = result as BadRequestObjectResult; + Assert.IsNotNull(badRequestResult); + + // Verify the error response structure (should be API error, not validation error) + dynamic response = badRequestResult.Value; + Assert.AreEqual(ValidSessionId, response.GetType().GetProperty("sessionId").GetValue(response)); + Assert.IsFalse((bool)response.GetType().GetProperty("success").GetValue(response)); + + var error = response.GetType().GetProperty("error").GetValue(response)?.ToString(); + // Should NOT be validation errors since session ID format is correct + Assert.IsFalse(error != null && error.Contains("Invalid session ID format")); + Assert.IsFalse(error != null && error.Contains("Session ID is required")); + } + + [TestMethod] + public async Task CreateQrCode_WithNullSessionId_ReturnsBadRequest() + { + // Act + var result = await _homeController.CreateQrCode(null); + + // Assert + Assert.IsInstanceOfType(result, typeof(BadRequestObjectResult)); + var badRequestResult = result as BadRequestObjectResult; + Assert.IsNotNull(badRequestResult); + + // Verify the error response + dynamic response = badRequestResult.Value; + Assert.IsFalse((bool)response.GetType().GetProperty("success").GetValue(response)); + Assert.AreEqual("Session ID is required", response.GetType().GetProperty("error").GetValue(response)); + } + + [TestMethod] + public async Task CreateQrCode_WithEmptySessionId_ReturnsBadRequest() + { + // Act + var result = await _homeController.CreateQrCode(""); + + // Assert + Assert.IsInstanceOfType(result, typeof(BadRequestObjectResult)); + var badRequestResult = result as BadRequestObjectResult; + Assert.IsNotNull(badRequestResult); + + // Verify the error response + dynamic response = badRequestResult.Value; + Assert.IsFalse((bool)response.GetType().GetProperty("success").GetValue(response)); + Assert.AreEqual("Session ID is required", response.GetType().GetProperty("error").GetValue(response)); + } + + [TestMethod] + public async Task CreateQrCode_WithWhitespaceSessionId_ReturnsBadRequest() + { + // Act + var result = await _homeController.CreateQrCode(" "); + + // Assert + Assert.IsInstanceOfType(result, typeof(BadRequestObjectResult)); + var badRequestResult = result as BadRequestObjectResult; + Assert.IsNotNull(badRequestResult); + + // Verify the error response + dynamic response = badRequestResult.Value; + Assert.IsFalse((bool)response.GetType().GetProperty("success").GetValue(response)); + Assert.AreEqual("Session ID is required", response.GetType().GetProperty("error").GetValue(response)); + } + + [TestMethod] + public async Task CreateQrCode_WithInvalidSessionIdFormat_ReturnsBadRequest() + { + // Act + var result = await _homeController.CreateQrCode(InvalidSessionId); + + // Assert + Assert.IsInstanceOfType(result, typeof(BadRequestObjectResult)); + var badRequestResult = result as BadRequestObjectResult; + Assert.IsNotNull(badRequestResult); + + // Verify the error response + dynamic response = badRequestResult.Value; + Assert.AreEqual(InvalidSessionId, response.GetType().GetProperty("sessionId").GetValue(response)); + Assert.IsFalse((bool)response.GetType().GetProperty("success").GetValue(response)); + Assert.AreEqual("Invalid session ID format", response.GetType().GetProperty("error").GetValue(response)); + Assert.IsTrue(response.GetType().GetProperty("message").GetValue(response).ToString().Contains("ss.v2.")); + Assert.AreEqual("ss.v2.xxxxx...", response.GetType().GetProperty("expectedFormat").GetValue(response)); + } + + [TestMethod] + [DataRow("abc123")] + [DataRow("session123")] + [DataRow("ss.v1.abc123")] + [DataRow("invalid-format")] + public async Task CreateQrCode_WithVariousInvalidFormats_ReturnsBadRequest(string invalidSessionId) + { + // Act + var result = await _homeController.CreateQrCode(invalidSessionId); + + // Assert + Assert.IsInstanceOfType(result, typeof(BadRequestObjectResult)); + var badRequestResult = result as BadRequestObjectResult; + Assert.IsNotNull(badRequestResult); + + // Verify the error response + dynamic response = badRequestResult.Value; + Assert.AreEqual(invalidSessionId, response.GetType().GetProperty("sessionId").GetValue(response)); + Assert.IsFalse((bool)response.GetType().GetProperty("success").GetValue(response)); + Assert.AreEqual("Invalid session ID format", response.GetType().GetProperty("error").GetValue(response)); + } + + [TestMethod] + [DataRow("ss.v2.abc123")] + [DataRow("ss.v2.1234567890abcdef")] + [DataRow("ss.v2.very-long-session-id-with-multiple-parts")] + public async Task CreateQrCode_WithValidSessionIdFormats_AttemptsApiCall(string validSessionId) + { + // Note: This test will attempt actual API call and may fail with authentication errors, + // but it verifies that the validation passes and the method proceeds to the API call + + // Act + var result = await _homeController.CreateQrCode(validSessionId); + + // Assert + // The result should be either OkObjectResult (if API call succeeds) + // or BadRequestObjectResult (if API call fails due to auth/network issues) + // But it should NOT be validation error about session ID format + Assert.IsTrue(result is OkObjectResult || result is BadRequestObjectResult); + + if (result is BadRequestObjectResult badRequest) + { + dynamic response = badRequest.Value; + var error = response.GetType().GetProperty("error").GetValue(response)?.ToString(); + + // Should not be a validation error about session ID format + Assert.IsFalse(error != null && error.Contains("Invalid session ID format")); + Assert.IsFalse(error != null && error.Contains("Session ID is required")); + } + } + + [TestMethod] + public async Task CreateQrCode_WithMissingEnvironmentVariables_HandlesGracefully() + { + // Arrange - Remove environment variables + Environment.SetEnvironmentVariable("YOTI_KEY_FILE_PATH", null); + + // Act + var result = await _homeController.CreateQrCode(ValidSessionId); + + // Assert + Assert.IsInstanceOfType(result, typeof(BadRequestObjectResult)); + var badRequestResult = result as BadRequestObjectResult; + Assert.IsNotNull(badRequestResult); + + // Verify it's an error related to missing configuration, not validation + dynamic response = badRequestResult.Value; + Assert.IsFalse((bool)response.GetType().GetProperty("success").GetValue(response)); + Assert.IsNotNull(response.GetType().GetProperty("error").GetValue(response)); + } + + [TestMethod] + public void CreateQrCode_MethodHasCorrectAttributes() + { + // Get the method info + var methodInfo = typeof(DigitalIdentityExample.Controllers.HomeController) + .GetMethod("CreateQrCode"); + + // Verify method exists + Assert.IsNotNull(methodInfo); + + // Verify it's async + Assert.IsTrue(methodInfo.ReturnType == typeof(Task)); + + // Verify it has the Route attribute + var routeAttributes = methodInfo.GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.RouteAttribute), false); + Assert.AreEqual(1, routeAttributes.Length); + var routeAttribute = routeAttributes[0] as Microsoft.AspNetCore.Mvc.RouteAttribute; + Assert.AreEqual("create-qr/{sessionId}", routeAttribute.Template); + + // Verify it has the HttpPost attribute + var httpPostAttributes = methodInfo.GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.HttpPostAttribute), false); + Assert.AreEqual(1, httpPostAttributes.Length); + } + + private static string GetTestKeyFilePath() + { + // Create a temporary test key file path + var testKeyPath = Path.GetTempFileName(); + + // Write a dummy PEM content (this won't work for actual API calls but prevents file not found errors) + // This is a test-only dummy key that is not a real private key + File.WriteAllText(testKeyPath, "-----BEGIN PRIVATE KEY-----\n" + + "DUMMY_TEST_KEY_NOT_A_REAL_PRIVATE_KEY_FOR_TESTING_ONLY\n" + + "-----END PRIVATE KEY-----"); + + return testKeyPath; + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/Properties/AssemblyInfo.cs b/test/Yoti.Auth.Tests/Properties/AssemblyInfo.cs index 3750ee091..6bcafbaa7 100644 --- a/test/Yoti.Auth.Tests/Properties/AssemblyInfo.cs +++ b/test/Yoti.Auth.Tests/Properties/AssemblyInfo.cs @@ -3,9 +3,6 @@ // General Information about an assembly is controlled through the following set of attributes. // Change these attribute values to modify the information associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Yoti.Auth.Tests")] [assembly: AssemblyTrademark("")] // Setting ComVisible to false makes the types in this assembly not visible to COM components. If you diff --git a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj index 7dccb1808..79cbf0f2b 100644 --- a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj +++ b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj @@ -9,6 +9,11 @@ false false false + false + false + false + false + false Full true @@ -25,6 +30,7 @@ + From d62f7e2cb6b6a740845fc7404d0731a871e56a3e Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Fri, 3 Oct 2025 15:15:13 +0100 Subject: [PATCH 47/47] updated create qr core method --- .../Controllers/HomeController.cs | 40 ++++++++--- .../DigitalIdentity/QA_TESTING_GUIDE.md | 72 +++++++++++++++++++ .../DigitalIdentity/DigitalIdentityService.cs | 14 +--- src/Yoti.Auth/DigitalIdentityClient.cs | 4 +- src/Yoti.Auth/DigitalIdentityClientEngine.cs | 4 +- .../DigitalIdentityServiceTests.cs | 8 +-- .../DigitalIdentityClientEngineTests.cs | 9 +-- 7 files changed, 117 insertions(+), 34 deletions(-) create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/QA_TESTING_GUIDE.md diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs index 1adb34126..45e1685d2 100644 --- a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs @@ -74,6 +74,10 @@ public IActionResult DigitalIdentity() ViewBag.YotiClientSdkId = _clientSdkId; ViewBag.sessionID = SessionResult.Id; + // CreateQrCode metodunun başına + Console.WriteLine($"[DEBUG] CreateQrCode called - sessionId: {SessionResult.Id}"); + Console.WriteLine($"[DEBUG] Endpoint: /v2/sessions/{SessionResult.Id}/qr-codes"); + return View("DigitalIdentity", sharedReceiptResponse); } catch (Exception e) @@ -88,25 +92,44 @@ public IActionResult DigitalIdentity() } } - // GET: /create-qr/{sessionId} + // POST: /create-qr/{sessionId} + [Route("create-qr/{sessionId}")] public async Task CreateQrCode(string sessionId) { try { + // Validate session ID format + if (string.IsNullOrWhiteSpace(sessionId)) + { + return BadRequest(new + { + success = false, + error = "Session ID is required", + message = "Please provide a valid session ID. Use /generate-share endpoint first to get a session ID." + }); + } + + if (!sessionId.StartsWith("ss.v2.")) + { + return BadRequest(new + { + sessionId = sessionId, + success = false, + error = "Invalid session ID format", + message = "Session ID must start with 'ss.v2.'. Use /generate-share endpoint first to get a valid session ID.", + expectedFormat = "ss.v2.xxxxx..." + }); + } + string yotiKeyFilePath = Environment.GetEnvironmentVariable("YOTI_KEY_FILE_PATH"); _logger.LogInformation("Creating QR code for session: {SessionId}", sessionId); StreamReader privateKeyStream = System.IO.File.OpenText(yotiKeyFilePath); var yotiClient = new DigitalIdentityClient(_clientSdkId, privateKeyStream); - // Create QR request with default settings - var qrRequest = new QrRequestBuilder() - .WithTransport("INLINE") - .WithDisplayMode("QR_CODE") - .Build(); - var qrResult = await yotiClient.CreateQrCode(sessionId, qrRequest); + var qrResult = await yotiClient.CreateQrCode(sessionId); _logger.LogInformation("QR code created with ID: {QrId}", qrResult.Id); @@ -128,7 +151,8 @@ public async Task CreateQrCode(string sessionId) sessionId = sessionId, success = false, error = e.Message, - innerError = e.InnerException?.Message + innerError = e.InnerException?.Message, + hint = "If you're getting 'UNKNOWN_SESSION' error, make sure to use a valid session ID from /generate-share endpoint" }); } } diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/QA_TESTING_GUIDE.md b/src/Examples/DigitalIdentity/DigitalIdentity/QA_TESTING_GUIDE.md new file mode 100644 index 000000000..5256d085a --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/QA_TESTING_GUIDE.md @@ -0,0 +1,72 @@ +# QA Testing Guide for CreateQrCode Endpoint + +## Overview +The CreateQrCode endpoint has been added to support QA testing of the QR code creation functionality in the Digital Identity API. + +## Proper Testing Workflow + +### Step 1: Create a Digital Identity Session +First, you need to create a valid Digital Identity session to get a session ID: + +**Endpoint:** `GET /generate-share` + +**Response:** You'll get a session ID that starts with `ss.v2.` + +Example response will show the session ID in the UI or logs. + +### Step 2: Create QR Code Using Session ID +Use the session ID from Step 1 to create a QR code: + +**Endpoint:** `POST /create-qr/{sessionId}` +**Method:** POST +**URL Example:** `POST /create-qr/ss.v2.abc123def456...` + +## Expected Session ID Format +✅ **Valid:** `ss.v2.` followed by additional characters (e.g., `ss.v2.abc123def456...`) +❌ **Invalid:** `0`, `123`, `test`, or any string not starting with `ss.v2.` + +## Common Issues and Solutions + +### Issue: "UNKNOWN_SESSION" Error +**Error Message:** `Invalid session ID '0' - 'value must start with 'ss.v2.'` + +**Cause:** Using an invalid session ID (like `'0'`) instead of a real session ID from the Digital Identity API. + +**Solution:** +1. First call `/generate-share` to create a session and get a valid session ID +2. Use that session ID (starting with `ss.v2.`) in the `/create-qr/{sessionId}` call + +### Issue: Session ID Format Validation +The endpoint now includes validation to help identify format issues: + +**If session ID doesn't start with `ss.v2.`:** +```json +{ + "sessionId": "0", + "success": false, + "error": "Invalid session ID format", + "message": "Session ID must start with 'ss.v2.'. Use /generate-share endpoint first to get a valid session ID.", + "expectedFormat": "ss.v2.xxxxx..." +} +``` + +## Success Response +When everything works correctly, you'll get: +```json +{ + "sessionId": "ss.v2.abc123...", + "qrId": "51ce09b10da75c5b7da9cb1773c8f388", + "qrUri": "https://...", + "success": true, + "message": "QR code created successfully" +} +``` + +## Testing Tips +1. **Always start with `/generate-share`** to get a valid session ID +2. **Copy the exact session ID** from the Digital Identity session creation +3. **Use POST method** for the `/create-qr/{sessionId}` endpoint +4. **Check the logs** if you're unsure about the session ID format + +## SDK Manipulation +The Yoti .NET SDK does not manipulate or transform session IDs. The session ID you pass to the CreateQrCode method is sent directly to the Yoti API. If you're getting format errors, it means the session ID being passed doesn't match what the Yoti API expects. \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs index 3043fca82..0fa22bec6 100644 --- a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs +++ b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs @@ -96,30 +96,20 @@ internal static async Task GetSession(HttpClient httpClient, U } } - internal static async Task CreateQrCode(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId,QrRequest qrRequestPayload) + internal static async Task CreateQrCode(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId) { Validation.NotNull(httpClient, nameof(httpClient)); Validation.NotNull(apiUrl, nameof(apiUrl)); Validation.NotNull(sdkId, nameof(sdkId)); Validation.NotNull(keyPair, nameof(keyPair)); - string serializedQrCode = JsonConvert.SerializeObject( - qrRequestPayload, - new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore - }); - byte[] body = Encoding.UTF8.GetBytes(serializedQrCode); - - Request createQrRequest = new RequestBuilder() .WithKeyPair(keyPair) .WithBaseUri(apiUrl) .WithHeader(yotiAuthId, sdkId) - .WithEndpoint(string.Format($"/v2/sessions/{0}/qr-codes", sessionId)) + .WithEndpoint(string.Format("/v2/sessions/{0}/qr-codes", sessionId)) .WithQueryParam("appId", sdkId) .WithHttpMethod(HttpMethod.Post) - .WithContent(body) .Build(); using (HttpResponseMessage response = await createQrRequest.Execute(httpClient).ConfigureAwait(false)) diff --git a/src/Yoti.Auth/DigitalIdentityClient.cs b/src/Yoti.Auth/DigitalIdentityClient.cs index 628a1a06c..9ea129309 100644 --- a/src/Yoti.Auth/DigitalIdentityClient.cs +++ b/src/Yoti.Auth/DigitalIdentityClient.cs @@ -93,9 +93,9 @@ public SharedReceiptResponse GetShareReceipt(string receiptId) } - public async Task CreateQrCode(string sessionId, QrRequest qrRequest) + public async Task CreateQrCode(string sessionId) { - return await _yotiDigitalClientEngine.CreateQrCodeAsync(_sdkId, _keyPair, ApiUri, sessionId, qrRequest).ConfigureAwait(false); + return await _yotiDigitalClientEngine.CreateQrCodeAsync(_sdkId, _keyPair, ApiUri, sessionId).ConfigureAwait(false); } public async Task GetQrCode(string qrCodeId) diff --git a/src/Yoti.Auth/DigitalIdentityClientEngine.cs b/src/Yoti.Auth/DigitalIdentityClientEngine.cs index 0db680235..865858ae1 100644 --- a/src/Yoti.Auth/DigitalIdentityClientEngine.cs +++ b/src/Yoti.Auth/DigitalIdentityClientEngine.cs @@ -40,10 +40,10 @@ public async Task GetShareReceipt(string sdkId, Asymmetri return result; } - public async Task CreateQrCodeAsync(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, string sessionid, QrRequest qRRequest) + public async Task CreateQrCodeAsync(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, string sessionid) { CreateQrResult result = await Task.Run(async () => await DigitalIdentityService.CreateQrCode( - _httpClient, apiUrl, sdkId, keyPair, sessionid, qRRequest).ConfigureAwait(false)) + _httpClient, apiUrl, sdkId, keyPair, sessionid).ConfigureAwait(false)) .ConfigureAwait(false); return result; diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs index 0e43838f4..df58e1e9d 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs @@ -128,7 +128,7 @@ public void CreateQrCodeShouldFailWithNullHttpClient() { var aggregateException = Assert.ThrowsException(() => { - DigitalIdentityService.CreateQrCode(null, _apiURL, _sdkID, _keyPair, _sessionID, _someCreateQrRequest).Wait(); + DigitalIdentityService.CreateQrCode(null, _apiURL, _sdkID, _keyPair, _sessionID).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -140,7 +140,7 @@ public void CreateQrCodeShouldFailWithNullApiUrl() { var aggregateException = Assert.ThrowsException(() => { - DigitalIdentityService.CreateQrCode(_httpClient, null, _sdkID, _keyPair, _sessionID, _someCreateQrRequest).Wait(); + DigitalIdentityService.CreateQrCode(_httpClient, null, _sdkID, _keyPair, _sessionID).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -152,7 +152,7 @@ public void CreateQrCodeShouldFailWithNullSdkId() { var aggregateException = Assert.ThrowsException(() => { - DigitalIdentityService.CreateQrCode(_httpClient, _apiURL, null, _keyPair, _sessionID, _someCreateQrRequest).Wait(); + DigitalIdentityService.CreateQrCode(_httpClient, _apiURL, null, _keyPair, _sessionID).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -164,7 +164,7 @@ public void CreateQrCodeShouldFailWithNullKeyPair() { var aggregateException = Assert.ThrowsException(() => { - DigitalIdentityService.CreateQrCode(_httpClient, _apiURL, _sdkID, null, _sessionID, _someCreateQrRequest).Wait(); + DigitalIdentityService.CreateQrCode(_httpClient, _apiURL, _sdkID, null, _sessionID).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs index 8dddfe73b..e84261b13 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs @@ -50,10 +50,9 @@ public async Task CreateQrCodeAsyncShouldReturnCorrectValues() "{\"id\":\"" + qrCodeId + "\",\"uri\":\"" + qrCodeUri + "\"}"); var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); - QrRequest qrRequest = TestTools.CreateQr.CreateQrStandard(); string sessionId = "test-session-id"; - CreateQrResult result = await engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId, qrRequest); + CreateQrResult result = await engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); Assert.IsNotNull(result); Assert.AreEqual(qrCodeId, result.Id); @@ -166,12 +165,11 @@ public void CreateQrCodeAsyncNonSuccessStatusCodesShouldThrowException(HttpStatu "{\"status\":\"bad\"}"); var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); - QrRequest qrRequest = TestTools.CreateQr.CreateQrStandard(); string sessionId = "test-session-id"; var aggregateException = Assert.ThrowsException(() => { - engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId, qrRequest).Wait(); + engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -290,10 +288,9 @@ public async Task CreateQrCodeAsyncShouldHandleEmptyResponse() "{}"); var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); - QrRequest qrRequest = TestTools.CreateQr.CreateQrStandard(); string sessionId = "test-session-id"; - CreateQrResult result = await engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId, qrRequest); + CreateQrResult result = await engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); Assert.IsNotNull(result); }