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 c49c740f9..ab08ca7d0 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -32,11 +32,11 @@ steps: scannerMode: 'MSBuild' projectKey: 'getyoti:dotnet' projectName: '.NET SDK' - projectVersion: '3.11.0' + 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 + sonar.exclusions = src/Yoti.Auth/ProtoBuf/**,src/Examples/**,**/obj/**,**/*.dll,src/Yoti.Auth/DigitalIdentity/** displayName: SonarCloud Prepare Analysis - task: NuGetToolInstaller@1 diff --git a/src/Examples/Aml/AmlExample/AmlExample.csproj b/src/Examples/Aml/AmlExample/AmlExample.csproj index 15bf388e9..ab68935fb 100644 --- a/src/Examples/Aml/AmlExample/AmlExample.csproj +++ b/src/Examples/Aml/AmlExample/AmlExample.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + netcoreapp3.1 @@ -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..b6cf7f5b1 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/AdvancedIdentityShareController.cs @@ -0,0 +1,94 @@ +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"": ""YOTI_GLOBAL"", + ""schemes"": [ + { + ""label"": ""identity-AL-L1"", + ""type"": ""IDENTITY"", + ""objective"": ""AL_L1"", + + }, + { + ""label"": ""identity-AL-M1"", + ""type"": ""IDENTITY"", + ""objective"": ""AL_M1"", + + } + ] + } + ] + + }"; + + var advancedIdentityProfile = JsonConvert.DeserializeObject(advancedIdentityProfileJson); + + var policy = new PolicyBuilder() + .WithAdvancedIdentityProfileRequirements(advancedIdentityProfile) + .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..45e1685d2 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs @@ -0,0 +1,160 @@ +using System; +using System.IO; +using System.Threading.Tasks; +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 givenNamesWantedAttribute = new WantedAttributeBuilder() + .WithName("given_names") + .WithOptional(false) + .Build(); + + var notification = new NotificationBuilder() + .WithUrl("https://example.com/webhook") + .WithMethod("POST") + .WithVerifyTls(true) + .Build(); + + var policy = new PolicyBuilder() + .WithWantedAttribute(givenNamesWantedAttribute) + .WithFullName() + .WithEmail() + .WithPhoneNumber() + .WithSelfie() + .WithAgeOver(18) + .WithNationality() + .WithGender() + .WithDocumentDetails() + .WithDocumentImages() + .Build(); + + var sessionReq = new ShareSessionRequestBuilder().WithPolicy(policy) + .WithNotification(notification) + .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; + + // 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) + { + _logger.LogError( + exception: e, + message: e.Message); + + TempData["Error"] = e.Message; + TempData["InnerException"] = e.InnerException?.Message; + return RedirectToAction("Error", "Success"); + } + } + + // 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); + + + var qrResult = await yotiClient.CreateQrCode(sessionId); + + _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, + 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/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/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/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..a27d3c400 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/SuccessResult.cshtml @@ -0,0 +1,172 @@ +@{ + 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 000000000..f1b4fc482 Binary files /dev/null and b/src/Examples/DigitalIdentity/DigitalIdentity/https/DigitalIdentityExample.pfx differ 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 000000000..3ec996cc6 Binary files /dev/null and b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge.png differ 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 000000000..84b34068f Binary files /dev/null and b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge@2x.png differ diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/company-logo.jpg b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/company-logo.jpg new file mode 100644 index 000000000..551474bfe Binary files /dev/null and b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/company-logo.jpg differ diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/google-play-badge.png b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/google-play-badge.png new file mode 100644 index 000000000..761f237b1 Binary files /dev/null and b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/google-play-badge.png differ diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/google-play-badge@2x.png b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/google-play-badge@2x.png new file mode 100644 index 000000000..46707cea8 Binary files /dev/null and b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/google-play-badge@2x.png differ diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/address.svg b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/address.svg new file mode 100644 index 000000000..f7d9b2a70 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/address.svg @@ -0,0 +1,3 @@ + + + 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 000000000..c60227fab Binary files /dev/null and b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/logo.png differ diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/logo@2x.png b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/logo@2x.png new file mode 100644 index 000000000..9f29784d1 Binary files /dev/null and b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/logo@2x.png differ diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/index.css b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/index.css new file mode 100644 index 000000000..e3184163d --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/index.css @@ -0,0 +1,152 @@ +.yoti-body { + margin: 0; +} + +.yoti-top-section { + display: flex; + flex-direction: column; + padding: 38px 0; + background-color: #f7f8f9; + align-items: center; +} + +.yoti-logo-section { + margin-bottom: 25px; +} + +.yoti-logo-image { + display: block; +} + +.yoti-top-header { + font-family: Roboto, sans-serif; + font-size: 40px; + font-weight: 700; + line-height: 1.2; + margin-top: 0; + margin-bottom: 80px; + text-align: center; + color: #000; +} + +@media (min-width: 600px) { + .yoti-top-header { + line-height: 1.4; + } +} + +.yoti-sdk-integration-section { + margin: 30px 0; +} + +#yoti-share-button { + width: 250px; + height: 45px; +} + +.yoti-login-or-separator { + text-transform: uppercase; + font-family: Roboto; + font-size: 16px; + font-weight: bold; + line-height: 1.5; + text-align: center; + margin-top: 30px; +} + +.yoti-login-dialog { + display: grid; + box-sizing: border-box; + width: 100%; + padding: 35px 38px; + border-radius: 5px; + background: #fff; + grid-gap: 25px; +} + +@media (min-width: 600px) { + .yoti-login-dialog { + width: 560px; + padding: 35px 88px; + } +} + +.yoti-login-dialog-header { + font-family: Roboto, sans-serif; + font-size: 24px; + font-weight: 700; + line-height: 1.1; + margin: 0; + color: #000; +} + +.yoti-input { + font-family: Roboto, sans-serif; + font-size: 16px; + line-height: 1.5; + box-sizing: border-box; + padding: 12px 15px; + color: #000; + border: solid 2px #000; + border-radius: 4px; + background-color: #fff; +} + +.yoti-login-actions { + display: flex; + justify-content: space-between; + align-items: center; +} + +.yoti-login-forgot-button { + font-family: Roboto, sans-serif; + font-size: 16px; + text-transform: capitalize; +} + +.yoti-login-button { + font-family: Roboto, sans-serif; + font-size: 16px; + box-sizing: border-box; + width: 145px; + height: 50px; + text-transform: uppercase; + color: #fff; + border: 0; + background-color: #000; +} + +.yoti-sponsor-app-section { + display: flex; + flex-direction: column; + padding: 70px 0; + align-items: center; +} + +.yoti-sponsor-app-header { + font-family: Roboto, sans-serif; + font-size: 20px; + font-weight: 700; + line-height: 1.2; + margin: 0; + text-align: center; + color: #000; +} + +.yoti-store-buttons-section { + margin-top: 40px; + display: grid; + grid-gap: 10px; + grid-template-columns: 1fr; +} + +@media (min-width: 600px) { + .yoti-store-buttons-section { + grid-template-columns: 1fr 1fr; + grid-gap: 25px; + } +} + +.yoti-app-button-link { + text-decoration: none; +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/profile.css b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/profile.css new file mode 100644 index 000000000..80871acd0 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/profile.css @@ -0,0 +1,420 @@ +.yoti-html { + height: 100%; +} + +.yoti-body { + margin: 0; + height: 100%; +} + +.yoti-icon-profile, +.yoti-icon-phone, +.yoti-icon-email, +.yoti-icon-calendar, +.yoti-icon-verified, +.yoti-icon-address, +.yoti-icon-gender, +.yoti-icon-nationality { + display: inline-block; + height: 28px; + width: 28px; + flex-shrink: 0; +} + +.yoti-icon-profile { + background: no-repeat url('/static/assets/icons/profile.svg'); +} + +.yoti-icon-phone { + background: no-repeat url('/static/assets/icons/phone.svg'); +} + +.yoti-icon-email { + background: no-repeat url('/static/assets/icons/email.svg'); +} + +.yoti-icon-calendar { + background: no-repeat url('/static/assets/icons/calendar.svg'); +} + +.yoti-icon-verified { + background: no-repeat url('/static/assets/icons/verified.svg'); +} + +.yoti-icon-address { + background: no-repeat url('/static/assets/icons/address.svg'); +} + +.yoti-icon-gender { + background: no-repeat url('/static/assets/icons/gender.svg'); +} + +.yoti-icon-nationality { + background: no-repeat url('/static/assets/icons/nationality.svg'); +} + +.yoti-profile-layout { + display: grid; + grid-template-columns: 1fr; +} + +@media (min-width: 1100px) { + .yoti-profile-layout { + grid-template-columns: 360px 1fr; + height: 100%; + } +} + +.yoti-profile-user-section { + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: column; + padding: 40px 0; + background-color: #f7f8f9; +} + +@media (min-width: 1100px) { + .yoti-profile-user-section { + display: grid; + grid-template-rows: repeat(3, min-content); + align-items: center; + justify-content: center; + position: relative; + } +} + +.yoti-profile-picture-image { + width: 220px; + height: 220px; + border-radius: 50%; + margin-left: auto; + margin-right: auto; + display: block; +} + +.yoti-profile-picture-powered, +.yoti-profile-picture-account-creation { + font-family: Roboto; + font-size: 14px; + color: #b6bfcb; +} + +.yoti-profile-picture-powered-section { + display: flex; + flex-direction: column; + text-align: center; + align-items: center; +} + +@media (min-width: 1100px) { + .yoti-profile-picture-powered-section { + align-self: start; + } +} + +.yoti-profile-picture-powered { + margin-bottom: 20px; +} + +.yoti-profile-picture-section { + display: flex; + flex-direction: column; + align-items: center; +} + +@media (min-width: 1100px) { + .yoti-profile-picture-section { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 100%; + } +} + +.yoti-logo-image { + margin-bottom: 25px; +} + +.yoti-profile-picture-area { + position: relative; + display: inline-block; +} + +.yoti-profile-picture-verified-icon { + display: block; + background: no-repeat url("/static/assets/icons/verified.svg"); + background-size: cover; + height: 40px; + width: 40px; + position: absolute; + top: 10px; + right: 10px; +} + +.yoti-profile-name { + margin-top: 20px; + font-family: Roboto, sans-serif; + font-size: 24px; + text-align: center; + color: #333b40; +} + +.yoti-attributes-section { + display: flex; + flex-direction: column; + justify-content: start; + align-items: center; + width: 100%; + padding: 40px 0; +} + + .yoti-attributes-section.-condensed { + padding: 0; + } + +@media (min-width: 1100px) { + .yoti-attributes-section { + padding: 60px 0; + align-items: start; + overflow-y: scroll; + } + + .yoti-attributes-section.-condensed { + padding: 0; + } +} + +.yoti-company-logo { + margin-bottom: 40px; +} + +@media (min-width: 1100px) { + .yoti-company-logo { + margin-left: 130px; + } +} + +/* extended layout list */ +.yoti-attribute-list-header, +.yoti-attribute-list-subheader { + display: none; +} + +@media (min-width: 1100px) { + .yoti-attribute-list-header, + .yoti-attribute-list-subheader { + width: 100%; + display: grid; + grid-template-columns: 200px 1fr 1fr; + grid-template-rows: 40px; + align-items: center; + text-align: center; + font-family: Roboto; + font-size: 14px; + color: #b6bfcb; + } +} + +.yoti-attribute-list-header-attribute, +.yoti-attribute-list-header-value { + justify-self: start; + padding: 0 20px; +} + +.yoti-attribute-list-subheader { + grid-template-rows: 30px; +} + +.yoti-attribute-list-subhead-layout { + grid-column: 3; + display: grid; + grid-template-columns: 1fr 1fr 1fr; +} + +.yoti-attribute-list { + display: grid; + width: 100%; +} + +.yoti-attribute-list-item:first-child { + border-top: 2px solid #f7f8f9; +} + +.yoti-attribute-list-item { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: minmax(60px, auto); + border-bottom: 2px solid #f7f8f9; + border-right: none; + border-left: none; +} + + .yoti-attribute-list-item.-condensed { + grid-template-columns: 50% 50%; + padding: 5px 35px; + } + +@media (min-width: 1100px) { + .yoti-attribute-list-item { + display: grid; + grid-template-columns: 200px 1fr 1fr; + grid-template-rows: minmax(80px, auto); + } + + .yoti-attribute-list-item.-condensed { + grid-template-columns: 200px 1fr; + padding: 0 75px; + } +} + +.yoti-attribute-cell { + display: flex; + align-items: center; +} + +.yoti-attribute-name { + grid-column: 1 / 2; + display: flex; + align-items: center; + justify-content: center; + border-right: 2px solid #f7f8f9; + padding: 20px; +} + +@media (min-width: 1100px) { + .yoti-attribute-name { + justify-content: start; + } +} + +.yoti-attribute-name.-condensed { + justify-content: start; +} + +.yoti-attribute-name-cell { + display: flex; + align-items: center; +} + +.yoti-attribute-name-cell-text { + font-family: Roboto, sans-serif; + font-size: 16px; + color: #b6bfcb; + margin-left: 12px; +} + +.yoti-attribute-value { + grid-column: 2 / 3; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +@media (min-width: 1100px) { + .yoti-attribute-value { + justify-content: start; + } +} + +.yoti-attribute-value.-condensed { + justify-content: start; +} + +.yoti-attribute-value-text { + font-family: Roboto, sans-serif; + font-size: 18px; + color: #333b40; + word-break: break-word; +} + + .yoti-attribute-value-text table { + font-size: 14px; + border-spacing: 0; + } + + .yoti-attribute-value-text table td:first-child { + font-weight: bold; + } + + .yoti-attribute-value-text table td { + border-bottom: 1px solid #f7f8f9; + padding: 5px; + } + + .yoti-attribute-value-text img { + width: 100%; + } + +.yoti-attribute-anchors-layout { + grid-column: 1 / 3; + grid-row: 2 / 2; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: minmax(40px, auto); + font-family: Roboto, sans-serif; + font-size: 14px; + background-color: #f7f8f9; + border: 5px solid white; +} + +@media (min-width: 1100px) { + .yoti-attribute-anchors-layout { + grid-column: 3 / 4; + grid-row: 1 / 2; + } +} + +.yoti-attribute-anchors-head { + border-bottom: 1px solid #dde2e5; + display: flex; + align-items: center; + justify-content: center; +} + +@media (min-width: 1100px) { + .yoti-attribute-anchors-head { + display: none; + } +} + +.yoti-attribute-anchors { + display: flex; + align-items: center; + justify-content: center; +} + +.yoti-attribute-anchors-head.-s-v { + grid-column-start: span 1 s-v; +} + +.yoti-attribute-anchors-head.-value { + grid-column-start: span 1 value; +} + +.yoti-attribute-anchors-head.-subtype { + grid-column-start: span 1 subtype; +} + +.yoti-attribute-anchors.-s-v { + grid-column-start: span 1 s-v; +} + +.yoti-attribute-anchors.-value { + grid-column-start: span 1 value; +} + +.yoti-attribute-anchors.-subtype { + grid-column-start: span 1 subtype; +} + +.yoti-edit-section { + padding: 50px 20px; +} + +@media (min-width: 1100px) { + .yoti-edit-section { + padding: 75px 110px; + } +} 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/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/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/Examples/Profile/CoreExample/CoreExample.csproj b/src/Examples/Profile/CoreExample/CoreExample.csproj index 769193e74..e65661230 100644 --- a/src/Examples/Profile/CoreExample/CoreExample.csproj +++ b/src/Examples/Profile/CoreExample/CoreExample.csproj @@ -17,11 +17,11 @@ - - - + + + - + diff --git a/src/Yoti.Auth.sln b/src/Yoti.Auth.sln index f2630d1d1..7368b4488 100644 --- a/src/Yoti.Auth.sln +++ b/src/Yoti.Auth.sln @@ -28,6 +28,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocScanExample", "Examples\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DocScan", "DocScan", "{106324DB-4181-443F-85BA-6C3D3BD7E8DF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalIdentityExample", "Examples\DigitalIdentity\DigitalIdentity\DigitalIdentityExample.csproj", "{5FC08A25-9A60-483B-9957-73D61AA09B93}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +60,10 @@ Global {FFA5585A-C7BA-4F34-96F1-8E2312E8758F}.Debug|Any CPU.Build.0 = Debug|Any CPU {FFA5585A-C7BA-4F34-96F1-8E2312E8758F}.Release|Any CPU.ActiveCfg = Release|Any CPU {FFA5585A-C7BA-4F34-96F1-8E2312E8758F}.Release|Any CPU.Build.0 = Release|Any CPU + {5FC08A25-9A60-483B-9957-73D61AA09B93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FC08A25-9A60-483B-9957-73D61AA09B93}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FC08A25-9A60-483B-9957-73D61AA09B93}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FC08A25-9A60-483B-9957-73D61AA09B93}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -69,6 +75,7 @@ Global {34AC42F8-1D13-4D12-9C77-DB7A42494918} = {C3CAC5F0-C455-4634-A989-7443EBDC09E3} {FFA5585A-C7BA-4F34-96F1-8E2312E8758F} = {106324DB-4181-443F-85BA-6C3D3BD7E8DF} {106324DB-4181-443F-85BA-6C3D3BD7E8DF} = {F3EB26B1-6385-4A89-A8F9-8BAFFEC581F9} + {5FC08A25-9A60-483B-9957-73D61AA09B93} = {F3EB26B1-6385-4A89-A8F9-8BAFFEC581F9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6A3AE710-68BB-4034-921C-EBAE5186F5DF} diff --git a/src/Yoti.Auth/Constants/Api.cs b/src/Yoti.Auth/Constants/Api.cs index 4cc0cfaf1..08bc4b75e 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/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/CryptoEngine.cs b/src/Yoti.Auth/CryptoEngine.cs index d275c21cd..6951da935 100644 --- a/src/Yoti.Auth/CryptoEngine.cs +++ b/src/Yoti.Auth/CryptoEngine.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Security.Cryptography; +using Google.Protobuf; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Encodings; using Org.BouncyCastle.Crypto.Engines; @@ -41,10 +43,10 @@ internal static byte[] DecipherAes(byte[] key, byte[] iv, byte[] cipherBytes) var result = new byte[numOutputBytes]; Array.Copy(outputBuffer, result, numOutputBytes); - + return result; } - + internal static byte[] DecryptRsa(byte[] cipherBytes, AsymmetricCipherKeyPair keypair) { // decrypt using rsa with private key and PKCS 1 v1.5 padding @@ -82,7 +84,6 @@ internal static string GenerateNonce() internal static string DecryptToken(string encryptedConnectToken, AsymmetricCipherKeyPair keyPair) { Validation.NotNullOrEmpty(encryptedConnectToken, "one time use token"); - // token was encoded as a URL-safe base64 so it can be transferred in a URL byte[] cipherBytes = Conversion.UrlSafeBase64ToBytes(encryptedConnectToken); @@ -132,5 +133,62 @@ internal static string GetAuthKey(AsymmetricCipherKeyPair keyPair) return Conversion.BytesToBase64(publicKey); } + + public static byte[] DecryptAesGcm(byte[] cipherText, byte[] iv, byte[] secret) + { + try + { + GcmBlockCipher cipher = new GcmBlockCipher(new Org.BouncyCastle.Crypto.Engines.AesEngine()); + ParametersWithIV parameters = new ParametersWithIV(new KeyParameter(secret), iv); + + cipher.Init(false, parameters); + + byte[] plainText = new byte[cipher.GetOutputSize(cipherText.Length)]; + int length = cipher.ProcessBytes(cipherText, 0, cipherText.Length, plainText, 0); + cipher.DoFinal(plainText, length); + + return plainText; + } + catch (Exception ex) + { + throw new Exception($"Failed to decrypt receipt key: {ex.Message}", ex); + } + } + + public static byte[] UnwrapReceiptKey(byte[] wrappedReceiptKey, byte[] encryptedItemKey, byte[] itemKeyIv, AsymmetricCipherKeyPair key) + { + try + { + byte[] decryptedItemKey = DecryptRsa(encryptedItemKey, key); + + byte[] plainText = DecryptAesGcm(wrappedReceiptKey, itemKeyIv, decryptedItemKey); + + return plainText; + } + catch (Exception ex) + { + throw new Exception($"Failed to unwrap receipt key: {ex.Message}", ex); + } + } + + public static byte[] DecryptReceiptContent(byte[] content, byte[] receiptContentKey) + { + try + { + if (content == null) + { + throw new ArgumentNullException("content", "Failed to decrypt receipt content: content is null"); + } + + var decodedData = new EncryptedData(); + decodedData.MergeFrom(content); + + return DecipherAes(receiptContentKey, decodedData.Iv.ToByteArray(), decodedData.CipherText.ToByteArray()); + } + catch(Exception ex) + { + throw new Exception($"Failed to decrypt receipt content: {ex.Message}", ex); + } + } } -} \ No newline at end of file +} 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 new file mode 100644 index 000000000..0fa22bec6 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs @@ -0,0 +1,363 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Google.Protobuf; +using Newtonsoft.Json; +using Org.BouncyCastle.Crypto; +using Yoti.Auth.Attribute; +using Yoti.Auth.Exceptions; +using Yoti.Auth.Profile; +using Yoti.Auth.Web; +using Yoti.Auth.ProtoBuf.Attribute; +using Yoti.Auth.Share; +using ApplicationProfile = Yoti.Auth.Profile.ApplicationProfile; + +namespace Yoti.Auth.DigitalIdentity +{ + public static class DigitalIdentityService + { + private const string receiptRetrieval = "/v2/receipts/{0}"; + private const string receiptKeyRetrieval = "/v2/wrapped-item-keys/{0}"; + private const string sessionCreation = "/v2/sessions"; + private const string yotiAuthId = "X-Yoti-Auth-Id"; + + 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(shareSessionRequestPayload, nameof(shareSessionRequestPayload)); + + string serializedScenario = JsonConvert.SerializeObject( + shareSessionRequestPayload, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + byte[] body = Encoding.UTF8.GetBytes(serializedScenario); + + Request shareSessionRequest = new RequestBuilder() + .WithKeyPair(keyPair) + .WithBaseUri(apiUrl) + .WithHeader(yotiAuthId, sdkId) + .WithEndpoint(sessionCreation) + .WithQueryParam("sdkID", sdkId) + .WithHttpMethod(HttpMethod.Post) + .WithContent(body) + .Build(); + + using (HttpResponseMessage response = await shareSessionRequest.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; + } + } + + 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)); + + + Request getSessionRequest = new RequestBuilder() + .WithKeyPair(keyPair) + .WithBaseUri(apiUrl) + .WithHeader(yotiAuthId, sdkId) + .WithEndpoint(string.Format("{0}/{1}", sessionCreation, sessionId)) + .WithQueryParam("appId", sdkId) + .WithHttpMethod(HttpMethod.Get) + .Build(); + + using (HttpResponseMessage response = await getSessionRequest.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; + } + } + + 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)); + + Request createQrRequest = new RequestBuilder() + .WithKeyPair(keyPair) + .WithBaseUri(apiUrl) + .WithHeader(yotiAuthId, sdkId) + .WithEndpoint(string.Format("/v2/sessions/{0}/qr-codes", sessionId)) + .WithQueryParam("appId", sdkId) + .WithHttpMethod(HttpMethod.Post) + .Build(); + + using (HttpResponseMessage response = await createQrRequest.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; + } + } + + 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(yotiAuthId, 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; + } + } + + 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, + ErrorDetails = receiptResponse.ErrorDetails + + }; + + 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); + } + } + + +} 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/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/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/DigitalIdentity/GetReceipt.cs b/src/Yoti.Auth/DigitalIdentity/GetReceipt.cs new file mode 100644 index 000000000..e6dd05886 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/GetReceipt.cs @@ -0,0 +1,48 @@ +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; } + [JsonProperty("error_details")] + public ErrorDetails ErrorDetails { get; set; } + } +} diff --git a/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs b/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs new file mode 100644 index 000000000..f5084f6b6 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; + + +namespace Yoti.Auth.DigitalIdentity +{ + public class GetSessionResult + { +#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; } + } + + +} 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/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/Notification.cs b/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs new file mode 100644 index 000000000..a385526df --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs @@ -0,0 +1,26 @@ +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 + + 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/Policy/Policy.cs b/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs new file mode 100644 index 000000000..9bd799ecd --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + /// + /// Set of data required to request a sharing transaction + /// + public class Policy + { + 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; + + [JsonProperty(PropertyName = "advanced_identity_profile_requirements")] + private readonly object _advancedIdentityProfileRequirements; + + public Policy( + ICollection wantedAttributes, + HashSet wantedAuthTypes, + bool wantedRememberMeId, + object identityProfileRequirements = null, + object advancedIdentityProfileRequirements = null + ) + { + _wantedAttributes = wantedAttributes; + _wantedAuthTypes = wantedAuthTypes; + _wantedRememberMeId = wantedRememberMeId; + _isWantedRememberMeIdOptional = false; + _identityProfileRequirements = identityProfileRequirements; + _advancedIdentityProfileRequirements = advancedIdentityProfileRequirements; + + } + + /// + /// 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; + } + } + + /// + /// AdvancedIdentityProfileRequirements requested in the policy + /// + [JsonIgnore] + public object AdvancedIdentityProfileRequirements + { + get + { + return _advancedIdentityProfileRequirements; + } + } + } +} diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs new file mode 100644 index 000000000..d85c96947 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; +using Yoti.Auth.DocScan.Session.Create; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class PolicyBuilder + { + private readonly Dictionary _wantedAttributes = new Dictionary(); + private readonly HashSet _wantedAuthTypes = new HashSet(); + private bool _wantedRememberMeId; + private object _identityProfileRequirements; + private AdvancedIdentityProfile _advancedIdentityProfileRequirements; + + public PolicyBuilder 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 PolicyBuilder WithWantedAttribute(string name, List constraints = null) + { + WantedAttribute wantedAttribute = new WantedAttributeBuilder() + .WithName(name) + .WithConstraints(constraints) + .Build(); + return WithWantedAttribute(wantedAttribute); + } + + public PolicyBuilder WithFamilyName(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.FamilyNameAttribute, constraints); + } + + public PolicyBuilder WithGivenNames(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.GivenNamesAttribute, constraints); + } + + public PolicyBuilder WithFullName(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.FullNameAttribute, constraints); + } + + public PolicyBuilder WithDateOfBirth(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.DateOfBirthAttribute, constraints); + } + + public PolicyBuilder WithAgeOver(int age, List constraints = null) + { + return WithAgeDerivedAttribute($"{Constants.UserProfile.AgeOverAttribute}:{age}", constraints); + } + + public PolicyBuilder WithAgeUnder(int age, List constraints = null) + { + return WithAgeDerivedAttribute($"{Constants.UserProfile.AgeUnderAttribute}:{age}", constraints); + } + + private PolicyBuilder WithAgeDerivedAttribute(string derivation, List constraints) + { + WantedAttribute wantedAttribute = new WantedAttributeBuilder() + .WithName(Constants.UserProfile.DateOfBirthAttribute) + .WithDerivation(derivation) + .WithConstraints(constraints) + .Build(); + return WithWantedAttribute(wantedAttribute); + } + + public PolicyBuilder WithGender(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.GenderAttribute, constraints); + } + + public PolicyBuilder WithPostalAddress(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.PostalAddressAttribute, constraints); + } + + public PolicyBuilder WithStructuredPostalAddress(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.StructuredPostalAddressAttribute, constraints); + } + + public PolicyBuilder WithNationality(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.NationalityAttribute, constraints); + } + + public PolicyBuilder WithPhoneNumber(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.PhoneNumberAttribute, constraints); + } + + public PolicyBuilder WithSelfie(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.SelfieAttribute, constraints); + } + + public PolicyBuilder WithEmail(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.EmailAddressAttribute, constraints); + } + + public PolicyBuilder WithDocumentDetails(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.DocumentDetailsAttribute, constraints); + } + + public PolicyBuilder WithDocumentImages(List constraints = null) + { + return WithWantedAttribute(Constants.UserProfile.DocumentImagesAttribute, constraints); + } + + public PolicyBuilder WithSelfieAuthentication(bool enabled) + { + return WithAuthType(Policy.SelfieAuthType, enabled); + } + + public PolicyBuilder WithPinAuthentication(bool enabled) + { + return WithAuthType(Policy.PinAuthType, enabled); + } + + public PolicyBuilder WithAuthType(int authType, bool enabled) + { + if (enabled) + { + _wantedAuthTypes.Add(authType); + return this; + } + + _wantedAuthTypes.Remove(authType); + return this; + } + + public PolicyBuilder 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 PolicyBuilder WithIdentityProfileRequirements(object identityProfileRequirements) + { + _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(AdvancedIdentityProfile advancedIdentityProfileRequirements) + { + _advancedIdentityProfileRequirements = advancedIdentityProfileRequirements; + return this; + } + + public Policy Build() + { + return new Policy(_wantedAttributes.Values, _wantedAuthTypes, _wantedRememberMeId, _identityProfileRequirements, _advancedIdentityProfileRequirements); + } + } +} 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..a53199389 --- /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, bool? optional = false) + { + Name = name; + Derivation = derivation; + Optional = optional; + AcceptSelfAsserted = acceptSelfAsserted; + Constraints = constraints; + } + } +} diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttributeBuilder.cs b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttributeBuilder.cs new file mode 100644 index 000000000..34bfc1faa --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/WantedAttributeBuilder.cs @@ -0,0 +1,69 @@ +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; + private bool? _optional; + + public WantedAttributeBuilder WithName(string name) + { + _name = name; + return this; + } + + public WantedAttributeBuilder WithOptional(bool optional) + { + _optional = optional; + 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, _optional); + } + } +} 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/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/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/ShareSessionRequest.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequest.cs new file mode 100644 index 000000000..c1d24ddce --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequest.cs @@ -0,0 +1,83 @@ +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 = "policy")] + private readonly Policy.Policy _dynamicPolicy; + + [JsonProperty(PropertyName = "extensions")] + private readonly List _extensions; + + [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 Policy.Policy DynamicPolicy + { + get + { + return _dynamicPolicy; + } + } + + [JsonIgnore] + public List Extensions + { + get + { + return _extensions; + } + } + + + [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/ShareSessionRequestBuilder.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs new file mode 100644 index 000000000..fed8b1071 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using Yoti.Auth.DigitalIdentity.Extensions; +using Yoti.Auth.DigitalIdentity.Policy; + +namespace Yoti.Auth.DigitalIdentity +{ + public class ShareSessionRequestBuilder + { + private string _redirectUri; + private Policy.Policy _dynamicPolicy; + private Notification _notification; + private readonly List _extensions = new List(); + private object _subject; + + /// + /// The device's redirect url. Must be a URL relative to the Application Domain + /// specified in Yoti Hub + /// + /// + /// with a Redirect Uri added + public ShareSessionRequestBuilder WithRedirectUri(string redirectUri) + { + _redirectUri = redirectUri; + return this; + } + + /// + /// The customisable to use in the share + /// + /// + /// with a Dynamic Policy added + 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 + 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(_dynamicPolicy, _redirectUri, _notification, _extensions, _subject); + } + } +} diff --git a/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs new file mode 100644 index 000000000..eb04f88da --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + + +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; } + +#pragma warning restore 0649 + + } + + +} \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs b/src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs new file mode 100644 index 000000000..01ca14d00 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs @@ -0,0 +1,30 @@ +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 ErrorDetails ErrorDetails { 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 new file mode 100644 index 000000000..9ea129309 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentityClient.cs @@ -0,0 +1,130 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Org.BouncyCastle.Crypto; +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 + /// + /// + 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 + /// + /// + 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; + } + + + public async Task CreateQrCode(string sessionId) + { + return await _yotiDigitalClientEngine.CreateQrCodeAsync(_sdkId, _keyPair, ApiUri, sessionId).ConfigureAwait(false); + } + + 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() + { + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("YOTI_API_URL"))) + { + ApiUri = new Uri(Environment.GetEnvironmentVariable("YOTI_API_URL")); + } + else + { + ApiUri = new Uri(Constants.Api.DefaultYotiShareApiUrl); + } + } + + public DigitalIdentityClient OverrideApiUri(Uri apiUri) + { + ApiUri = apiUri; + + return this; + } + } +} diff --git a/src/Yoti.Auth/DigitalIdentityClientEngine.cs b/src/Yoti.Auth/DigitalIdentityClientEngine.cs new file mode 100644 index 000000000..865858ae1 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentityClientEngine.cs @@ -0,0 +1,70 @@ +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.DigitalIdentity; + +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; + } + + 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; + } + + 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).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; + } + + 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; + } + } +} diff --git a/src/Yoti.Auth/DocScan/DocScanService.cs b/src/Yoti.Auth/DocScan/DocScanService.cs index 878756081..9cf6484c6 100644 --- a/src/Yoti.Auth/DocScan/DocScanService.cs +++ b/src/Yoti.Auth/DocScan/DocScanService.cs @@ -156,9 +156,13 @@ public async Task GetMediaContent(string sdkId, AsymmetricCipherKeyP return null; } + if (response.Content.Headers.ContentType == null) + { + 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)); @@ -351,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 +} 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/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/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/src/Yoti.Auth/Yoti.Auth.csproj b/src/Yoti.Auth/Yoti.Auth.csproj index 167046de8..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;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.12.0 + 3.16.0 @@ -37,17 +37,18 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + @@ -58,6 +59,9 @@ + + + @@ -68,4 +72,32 @@ Resources.Designer.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/.DS_Store b/test/.DS_Store deleted file mode 100644 index b493ede37..000000000 Binary files a/test/.DS_Store and /dev/null differ 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..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 @@  - netcoreapp2.1 + net6.0 diff --git a/test/Yoti.Auth.Tests/.DS_Store b/test/Yoti.Auth.Tests/.DS_Store deleted file mode 100644 index 077415fc1..000000000 Binary files a/test/Yoti.Auth.Tests/.DS_Store and /dev/null differ 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/DigitalIdentityServiceTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs new file mode 100644 index 000000000..df58e1e9d --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs @@ -0,0 +1,208 @@ +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 +{ + [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; + private const string _sessionID = "someSessionID"; + private QrRequest _someCreateQrRequest; + + [TestInitialize] + public void Startup() + { + _someHeaders.Add("Key", "Value"); + _someCreateQrRequest = TestTools.CreateQr.CreateQrStandard(); + _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")); + } + + [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")); + } + + [TestMethod] + public void CreateQrCodeShouldFailWithNullHttpClient() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateQrCode(null, _apiURL, _sdkID, _keyPair, _sessionID).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).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).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).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + 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/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/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/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs new file mode 100644 index 000000000..b1fec0d31 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs @@ -0,0 +1,345 @@ +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; + +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(); + + Auth.DigitalIdentity.Policy.Policy result = new PolicyBuilder() + .WithWantedAttribute(wantedAttribute) + .WithWantedAttribute(wantedAttribute) + .Build(); + + Assert.AreEqual(1, result.WantedAttributes.Count); + Assert.IsTrue(result.WantedAttributes.Contains(wantedAttribute)); + } + + [TestMethod] + public void ShouldContainAllAddedAttributes() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .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() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .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() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .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(); + + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .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() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .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() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .WithSelfieAuthentication(enabled: false) + .WithPinAuthentication(enabled: false) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void ShouldBuildWithAuthTypeEnabledThenDisabled() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .WithAuthType(24, enabled: true) + .WithAuthType(24, enabled: false) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void ShouldBuildWithAuthTypeDisabledThenEnabled() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .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() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .WithSelfieAuthentication(enabled: true) + .WithSelfieAuthentication(enabled: false) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void ShouldBuildWithSelfieAuthenticationDisabledThenEnabled() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .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() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .WithSelfieAuthentication(enabled: false) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void ShouldFilterSelfieAuthenticationDuplicates() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .WithSelfieAuthentication(enabled: true) + .WithAuthType(Auth.DigitalIdentity.Policy.Policy.SelfieAuthType, enabled: true) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(1, result.Count); + Assert.IsTrue(result.SetEquals(new HashSet { _expectedSelfieAuthValue })); + } + + [TestMethod] + public void ShouldFilterPinAuthenticationDuplicates() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .WithPinAuthentication(enabled: true) + .WithAuthType(Auth.DigitalIdentity.Policy.Policy.PinAuthType, enabled: true) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(1, result.Count); + Assert.IsTrue(result.SetEquals(new HashSet { _expectedPinAuthValue })); + } + + [TestMethod] + public void ShouldBuildWithPinAuthenticationEnabledThenDisabled() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .WithPinAuthentication(enabled: true) + .WithPinAuthentication(enabled: false) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void ShouldBuildWithPinAuthenticationDisabledThenEnabled() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .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() + { + Auth.DigitalIdentity.Policy.Policy dynamicPolicy = new PolicyBuilder() + .WithPinAuthentication(enabled: false) + .Build(); + + HashSet result = dynamicPolicy.WantedAuthTypes; + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void ShouldBuildWithRememberMeFlag() + { + Auth.DigitalIdentity.Policy.Policy result = new PolicyBuilder() + .WithRememberMeId(true) + .Build(); + + Assert.IsTrue(result.WantedRememberMeId); + } + + [TestMethod] + public void ShouldBuildWithIdentityProfileRequirements() + { + object identityProfileRequirements = IdentityProfiles.CreateStandardIdentityProfileRequirements(); + + Auth.DigitalIdentity.Policy.Policy result = new PolicyBuilder() + .WithIdentityProfileRequirements(identityProfileRequirements) + .Build(); + + Assert.AreEqual(identityProfileRequirements, result.IdentityProfileRequirements); + } + + [TestMethod] + public void ShouldBuildWithAdvancedIdentityProfileRequirements() + { + var advancedIdentityProfileRequirements = IdentityProfiles.CreateAdvancedIdentityProfileRequirements(); + + Auth.DigitalIdentity.Policy.Policy result = new PolicyBuilder() + .WithAdvancedIdentityProfileRequirements(advancedIdentityProfileRequirements) + .Build(); + + Assert.AreEqual(advancedIdentityProfileRequirements, result.AdvancedIdentityProfileRequirements); + } + } + + +} 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..a856536cb --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeBuilderTests.cs @@ -0,0 +1,166 @@ +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") + .WithOptional(true) + .Build(); + + Assert.AreEqual(null, result.AcceptSelfAsserted); + Assert.AreEqual(true, result.Optional); + } + + [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); + } + } +} 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/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/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/DigitalIdentity/ShareSessionRequestBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/ShareSessionRequestBuilderTests.cs new file mode 100644 index 000000000..bc52fe01f --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/ShareSessionRequestBuilderTests.cs @@ -0,0 +1,72 @@ +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() + { + Auth.DigitalIdentity.Policy.Policy somePolicy = TestTools.ShareSession.CreateStandardPolicy(); + object someSubject = IdentityProfiles.CreateStandardSubject(); + + ShareSessionRequest result = new ShareSessionRequestBuilder() + .WithRedirectUri(_someEndpoint) + .WithPolicy(somePolicy) + .WithExtension(extension1) + .WithExtension(extension2) + .WithSubject(someSubject) + .Build(); + + var expectedExtensions = new List { extension1, extension2 }; + + Assert.AreEqual(_someEndpoint, result.RedirectUri); + 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/DigitalIdentity.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..e84261b13 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs @@ -0,0 +1,385 @@ +using System; +using System.Net; +using System.Net.Http; +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 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); + } + + [TestMethod] + public async Task CreateQrCodeAsyncShouldReturnCorrectValues() + { + string qrCodeId = "test-qr-code-id"; + string qrCodeUri = "https://code.yoti.com/CAEaJDlkOGI4ZGFjLTEyMzQtNTY3OC05MDEyLWFiY2RlZjEyMzQ1Ng=="; + + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{\"id\":\"" + qrCodeId + "\",\"uri\":\"" + qrCodeUri + "\"}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + string sessionId = "test-session-id"; + + CreateQrResult result = await engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); + + 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] + [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)); + } + + [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)); + string sessionId = "test-session-id"; + + var aggregateException = Assert.ThrowsException(() => + { + engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId).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.Loose); + 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; + } + + [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)); + string sessionId = "test-session-id"; + + CreateQrResult result = await engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); + + 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); + } + } +} diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs new file mode 100644 index 000000000..805165dee --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs @@ -0,0 +1,158 @@ +using System; +using System.IO; +using System.Net.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Org.BouncyCastle.Crypto; +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.DefaultYotiShareApiUrl); + + [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)); + } + + [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)] + 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); + } + } +} 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/DocScan/.DS_Store b/test/Yoti.Auth.Tests/DocScan/.DS_Store deleted file mode 100644 index 35cfc3a98..000000000 Binary files a/test/Yoti.Auth.Tests/DocScan/.DS_Store and /dev/null differ 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 3f7e57b3c..000000000 Binary files a/test/Yoti.Auth.Tests/DocScan/Session/.DS_Store and /dev/null differ 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 415710a70..000000000 Binary files a/test/Yoti.Auth.Tests/DocScan/Session/Create/.DS_Store and /dev/null differ 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); + } + } + +} 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/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 b39fead0b..733871f0f 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] @@ -34,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] @@ -161,4 +166,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/DigitalIdentity.json b/test/Yoti.Auth.Tests/TestData/DigitalIdentity.json new file mode 100644 index 000000000..1aa053d81 --- /dev/null +++ b/test/Yoti.Auth.Tests/TestData/DigitalIdentity.json @@ -0,0 +1,52 @@ +{ + "policy": { + "wanted": [ + { + "name": "date_of_birth" + }, + { + "name": "date_of_birth", + "derivation": "age_over:18" + }, + { + "name": "date_of_birth", + "derivation": "age_under:30" + }, + { + "name": "date_of_birth", + "derivation": "age_under:40" + } + ], + "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" +} 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 +} diff --git a/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs b/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs index 5d81d7a7b..4669f14e8 100644 --- a/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs +++ b/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs @@ -1,4 +1,7 @@ -namespace Yoti.Auth.Tests.TestData +using Newtonsoft.Json; +using Yoti.Auth.DigitalIdentity.Policy; + +namespace Yoti.Auth.Tests.TestData { internal static class IdentityProfiles { @@ -14,6 +17,42 @@ public static object CreateStandardIdentityProfileRequirements() } }; } + + public static AdvancedIdentityProfile 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"": {} + } + ] + } + ] + }"; + var advancedIdentityProfile = JsonConvert.DeserializeObject(advancedIdentityProfileJson); + return advancedIdentityProfile; + } public static object CreateStandardSubject() { 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(); + } + } +} diff --git a/test/Yoti.Auth.Tests/TestTools/ShareSession.cs b/test/Yoti.Auth.Tests/TestTools/ShareSession.cs new file mode 100644 index 000000000..7be4792a6 --- /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(CreateStandardPolicy(), "redirecturi"); + } + + public static Policy CreateStandardPolicy() + { + return new PolicyBuilder() + .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..79cbf0f2b 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 @@ -9,6 +9,11 @@ false false false + false + false + false + false + false Full true @@ -25,6 +30,7 @@ + @@ -39,6 +45,10 @@ + + + + PreserveNewest @@ -67,5 +77,11 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + \ No newline at end of file