From 50e226de8b4cb56e8930adfdb3847bc6030cecd1 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Mon, 9 Jan 2023 14:04:51 +0000 Subject: [PATCH 01/10] Set version -> 3.13.0 --- .DS_Store | Bin 6148 -> 6148 bytes azure-pipelines.yml | 2 +- src/.DS_Store | Bin 6148 -> 8196 bytes src/Yoti.Auth/Yoti.Auth.csproj | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.DS_Store b/.DS_Store index 2cd034b27a1e786fe4cbf3702a79255fb0c4d66e..9120dba210b5e85593dd16fcb8aeb7c27fdc04af 100644 GIT binary patch delta 49 zcmZoMXffCj%*wcHatLc7uT*ulu91O(j)I|?rRC%ctkR4N*z)8?ko_-uA;%k}d4Z?cSYQsh*0o-8cJ;)&BqSxGG^X(f}% zp2=m-rk2mwdhgR`!&*Ljs^``o{?;=@Zu!affk6MNv6)sM@`N)PxaOgKek?-i1SW4ae{nKEW6G z4nN@!CNPVaa1Gb-8s0z+i+C4z@i7|sH0(5oA;N_zP61uLMO$9D>Y1Gk`#V8%Vfx&1}2C_AiEhh$Mei#1^|7%8fX9j diff --git a/src/Yoti.Auth/Yoti.Auth.csproj b/src/Yoti.Auth/Yoti.Auth.csproj index 167046de..4849982c 100644 --- a/src/Yoti.Auth/Yoti.Auth.csproj +++ b/src/Yoti.Auth/Yoti.Auth.csproj @@ -19,7 +19,7 @@ False latest true - 3.12.0 + 3.13.0 From 8b57ee1bda84a76dbbb8e6e3b7ce119a773ce271 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Mon, 9 Jan 2023 14:12:57 +0000 Subject: [PATCH 02/10] Set version -> 3.13.0 --- .DS_Store | Bin 6148 -> 0 bytes src/.DS_Store | Bin 8196 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store delete mode 100644 src/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 9120dba210b5e85593dd16fcb8aeb7c27fdc04af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKTWb?R6h4zgvuP^?R49V5;A1fgzUzkA&_S; zf5E@ttN26wFFxrvmuPp>_*jF?fpfmOoHLhi=dwdYtTpy_iHbyIK?UYEU@j0*FLX&V zx?~H;WE%t8p&|Lu!IHNrFbo(5){OzNcdt{A-qDyw*tLK5Z-9Lqq9md+y`?h>4g=nI z1MVV192fi0@=qy9;rJkrIQ1z?`F%n2k5=^bBNdUwd`HwzwwpP=*vp+nL(RxHcNq9lT&a9# znQZRL)x4FrHm#S=37fiX7wMk+)E`W`#T#uN#yvkAjAVfC4dC+P znIC$5(&FRLi)5^$Dy)K4=oW8Hr#sb3rDX5c8;x1Xo;Iq;?A7YCS;4w_=kEQZ?rCrq z@{brhgyDpia!um`J|UrJ{l*!E0T0D&g?A2Yi|-#N*^poUbK=WuzjJ(iy>vddr_);X zUdgW2_x{4hdo}HqY={-~d)_HN@(Vt&1|vF2u_D&_c~(?M>umLkur}9ukY;R#0mHyQ zVL+S@HdJ6!<6NOUI$-1z0GLCu6r{z^frzm+HZ{%_q6WfLC{Tqm{ls7@9Obt5YigV; zRN=(*THn!HZ{%_q6A`o1SAc{7zQqt FfnTlx?SKFP diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 5bb7ef58565fc5508a261ad45a756d845216d2be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHLO-~a+7=8y(wj#283VtLTdockKib9A7*8(x<0b&W30G4gLl$B+s?r!<8A?aN` z`vd#|ob{wXK#yKM_y@dr#&mKr3(_6u>i^C*y>3Us`pk z70?R&mkRLzgN2!+t*|0d{5r4@Apl|%Hj9LHi~|J6QfMozNEB4;Q$`O&QHcsMh@_+4 z;_isH!iq#m2O{Y})W}3dC`62oJWIF(u_fwKE1(sqS3m~$Q?LyVY}Cf@S)XOmoo}Z{ zm73)Ee7LMNU)-lTNV6pXM0z zv(>tjo%h_WjM+88Xyka*4FtiEIWaPl7_bs! zqq_rEc|0*RVBH%T-QA5D-SN8*o@Lhk4a(k#sRL2}4``Lof6KR6t9!A{R6u=33uxoK z7%TvT2s{`>QA$`_<5wLuQthEvtyIS_YQ*|<=3-qdm+vHoP@v(_@$rAC#nr%cqFV3J zc+9(bxlOeY$k*z-{V>hyQixq@x!iiC&1~=JZ0~CC?(VtT+uhf3{YJ*@%sRRCLcl-q z1oK&t^h<8WYzuLrOy?M#4ctv!AnC~6j#A!hG<6&yJah9e4w~k6is;jI&>%W}9JnrB zECgPDW_2}mqjXJufsd98nD5WB(2G1q7na26k-ITXR~e4TXE0Obrf}X3Lc}^wdcg?- zhZfxA&a&@hU4MoqSroC2w1;oOXD)_J6`h<%3-O1)gvt<$+qY%Sgt)8##O?HFQ8&7( zO$x~zWz*KQNOz2pI|&;P^H z>nE+iX(`YsJMQK2|J2F%|ED#)?pG_I6*vb9kfzDhWD+M*J^qi*iO1R&<`v95aleX0 nLBT?V Date: Mon, 9 Jan 2023 16:19:51 +0000 Subject: [PATCH 03/10] Cleanup unneccessary files --- test/.DS_Store | Bin 6148 -> 0 bytes test/Yoti.Auth.Tests/.DS_Store | Bin 8196 -> 0 bytes test/Yoti.Auth.Tests/DocScan/.DS_Store | Bin 6148 -> 0 bytes test/Yoti.Auth.Tests/DocScan/Session/.DS_Store | Bin 6148 -> 0 bytes .../DocScan/Session/Create/.DS_Store | Bin 6148 -> 0 bytes 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/.DS_Store delete mode 100644 test/Yoti.Auth.Tests/.DS_Store delete mode 100644 test/Yoti.Auth.Tests/DocScan/.DS_Store delete mode 100644 test/Yoti.Auth.Tests/DocScan/Session/.DS_Store delete mode 100644 test/Yoti.Auth.Tests/DocScan/Session/Create/.DS_Store diff --git a/test/.DS_Store b/test/.DS_Store deleted file mode 100644 index b493ede371dcef262a0b89e8ef8f614a922472bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!EVz)5S>j^>L{wn0Z=bUD{+lVXiBAui<_1MS6qP*pkR}zHMrg?cE}-$LYM28zsk(#Br^#z`19C-um7-yi0D4O1E-`E z72lyTqC-UcG(`1FDz355ZpKa~#j$_(d2*bkMcL_mSGA@3^1YQ{C1?g8qeC-^%D5aC zy?FGVd;7*@$weF|Z_?qU-+HiX@-j~I;m8uw!4N4Q-lqA$OnPRV4+_hT+(gg_8vWMV zblTl`-qzi%%~@Mdw>LU%z1iKG%^JbOCr@9z?tjgWa&ux;AcZgA+N#G3I7RRmjlM;r zJTv(LGWjl#{;Vj}-G$;uxC~vZj(QCKlRRwr` z&~V1kW985;9cb(k09ePcHN^b0$Q3`{vH&{35?Vkk$)xb}IW$I79jlk$fT z<)5tl2}Rkbh87QPMkbzNnKb8|g~NfMei11AKR%re)g0y*;$$Q)B;)+tB1lV zKmUQ*zWsvD5-f!j+q3EWR?wP2KP`NZX`)X}>zKzrqtIXPb>MaUoLk31e9_V1qavIX zdL>@99@@2|q?!82*IF|QllI))chy(wAGk5-4SK`gt4c#3Roc~dCt0bsUhvbF)=}-W zUaf71!O`yM#Hx>p+XF5gRpQZkekbV({8PgNs*n|U6nkt7{U-Psf4wk1#oP0i zN@C@$?L~_HH?MUw_5P;#DMti)vN7VHdCUOD)sTH7C}^^e<~6-DO?rk|M7BQSk<_i6 zFM65pu1S1W3;OUzOMf`i-xP*A;0*hhjbe^1)KG@n0!JE{A_6{PA;vL!vqxTWudu9) z(YCB=NOhT=S1dtc^n(o$i^`~ZH4M3q!^TS4*~A{&+cpP{6D+9UbplA E7f*1up8x;= diff --git a/test/Yoti.Auth.Tests/DocScan/.DS_Store b/test/Yoti.Auth.Tests/DocScan/.DS_Store deleted file mode 100644 index 35cfc3a98007ddf8d186a4c146885488999f6b82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKL2uJA6n^eHnrcGofuUU{S>jravW`iJOX$jhE1B24_Q@h8x+2pcym=t)1DdyZ^E+yN3t!ww%4%@3iGX_i#RM2HVe`zkWaXmVa0J z2MdM|2Vah{#|{^8fnY_0^JuJct-hjV!CGW{^a^+dyaKmTz^x&{)@?{W*DK%^cn}Kk z`XJ$qp~K3eSvpXdBLJ|DYGa7`$B;SFVd${3h!&WzRG_5_cf=5uj(X|xLWh+_ODEwD zAHt0++zCaP(b2y&=_EpnzVr%s1(p@qu$L{~|KI-m{J%`{S6%_Hz`asH)caAthevXI z>)ONdUMt~W;B1^%S^PsmV76l9@>aY9H->u20Wfq}Sws!Y{|FcveBl-NuL}GIeI0w< diff --git a/test/Yoti.Auth.Tests/DocScan/Session/.DS_Store b/test/Yoti.Auth.Tests/DocScan/Session/.DS_Store deleted file mode 100644 index 3f7e57b3c430fb4156a09735ecd4b714d2f46a8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!Ab)$5S`R&Q$*-Np~ryNqSdM(Ue;OX-OC zI+H|ksa~bX3`|}!nb}QVLM8zK(H{0T0BQgrQ3*>`EPfE`C!La#vy>5q`9=oOQKGNI zM2Az}Y?ws`Xzdo@0QwL?0LAsAO|g%<_HM)szu+5=1`lqnU&Dta9%Rktn<&kd=NBrn zA{XVQf2&7+7G%RgJLsQN>qP4${HzDzX&jBZwdDhyWLV z4NRyS4gw+}hl3sMgNbq@l*Uo6UyWYVAxWRk^adzIWWcOYT$sWJV1F_otC1 zhoA6_mBO4J{eGJ0^akw|BjFgC$&jBI@v*SKiJuqL@BCnsg&AN5n1PvRz@3S5app(L z17rr6fxl*e&Ig4`=sCgbhWsp~5{egbhc# za(;n6ut-;8n|Ev H{*-|ayS`-b diff --git a/test/Yoti.Auth.Tests/DocScan/Session/Create/.DS_Store b/test/Yoti.Auth.Tests/DocScan/Session/Create/.DS_Store deleted file mode 100644 index 415710a702370d3ca8796f326f0bd960e332f261..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyG{c^3>-s>NNG}1?l15Ms}T5s{6G*yG$^2mq`r#p;?pvI20a|icjG!J4AG8> j(T=(Cc6<{>S=W5c^IkY42A%n!6ZJFTy2zx!Un_71kIogP From c5913652c5389ff7eb6eda7598448c1920a50c79 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Mon, 9 Jan 2023 16:25:33 +0000 Subject: [PATCH 04/10] Cleanup unneccessary files --- .../DocScan/DocScanExample/DocScanExample.sln | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/Examples/DocScan/DocScanExample/DocScanExample.sln diff --git a/src/Examples/DocScan/DocScanExample/DocScanExample.sln b/src/Examples/DocScan/DocScanExample/DocScanExample.sln deleted file mode 100644 index 52210ccf..00000000 --- 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 From 4cadc4107e500d2df1cc50d4de9e927466db7ccd Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Mon, 9 Jan 2023 16:34:13 +0000 Subject: [PATCH 05/10] Update .gitignore --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 82b0fcb2..6e27c66c 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/ From 85d448382da97f23d12e9ca84fa449393dd1ef99 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Thu, 20 Apr 2023 16:35:49 +0100 Subject: [PATCH 06/10] update versions --- src/Yoti.Auth/Yoti.Auth.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Yoti.Auth/Yoti.Auth.csproj b/src/Yoti.Auth/Yoti.Auth.csproj index 167046de..36c5346e 100644 --- a/src/Yoti.Auth/Yoti.Auth.csproj +++ b/src/Yoti.Auth/Yoti.Auth.csproj @@ -19,7 +19,7 @@ False latest true - 3.12.0 + 3.14.0 From d53bf73b7e78892605638e2da6ca648e526fc941 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Tue, 13 Feb 2024 21:37:52 +0300 Subject: [PATCH 07/10] Release/3.15.0 (#450) * SDK-2368: added advanced identity profiles for share1 and idv * SDK-2342: added support for enabling expanded document fields * SDK-2341: added support to retrieve expanded doc fields and media, added succedd page example * SDK-2342: updated test fw version for security and azure pipelines * SDK-2341 Removed CameraAndUpload on dbs example page * Sdk 2368: Generated Advanced Identity Profile Example Page * Create new issue template workflow --- .github/ISSUE_TEMPLATE.md | 17 -- .github/ISSUE_TEMPLATE/config.yml | 5 + src/Examples/Aml/AmlExample/AmlExample.csproj | 2 +- .../Controllers/AdvancedIdentityController.cs | 148 ++++++++++++++++++ .../Controllers/DbsController.cs | 11 +- .../Controllers/HomeController.cs | 3 +- .../AdvancedIdentityProfile/Index.cshtml | 4 + .../Views/IdVerify/Success.cshtml | 60 +++++++ src/Yoti.Auth/DocScan/DocScanService.cs | 2 +- .../Session/Create/AdvancedIdentityProfile.cs | 24 +++ .../Session/Create/SessionSpecification.cs | 6 +- .../Create/SessionSpecificationBuilder.cs | 20 ++- .../RequestedTextExtractionTaskBuilder.cs | 13 +- .../Task/RequestedTextExtractionTaskConfig.cs | 6 +- .../AdvancedIdentityProfilePreviewResponse.cs | 12 ++ .../AdvancedIdentityProfileResponse.cs | 19 +++ .../FailureReasonResponse.cs | 10 ++ .../Retrieve/ExpandedDocumentFieldResponse.cs | 13 ++ .../Session/Retrieve/GetSessionResult.cs | 6 + .../Resource/IdDocumentResourceResponse.cs | 3 + .../ShareUrl/Policy/DynamicPolicy.cs | 19 ++- .../ShareUrl/Policy/DynamicPolicyBuilder.cs | 14 +- .../Yoti.Auth.Tests.Common.csproj | 2 +- .../DocScan/DocScanClientTests.cs | 31 ++++ .../SessionSpecificationBuilderTests.cs | 82 ++++++++++ ...RequestedTextExtractionTaskBuilderTests.cs | 27 +++- .../Session/Retrieve/GetSessionResultTests.cs | 17 ++ .../Policy/DynamicPolicyBuilderTests.cs | 13 ++ ...sionResultWithAdvancedIdentityProfile.json | 42 +++++ .../TestData/IdentityProfiles.cs | 42 ++++- test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj | 5 +- 31 files changed, 638 insertions(+), 40 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 src/Examples/DocScan/DocScanExample/Controllers/AdvancedIdentityController.cs create mode 100644 src/Examples/DocScan/DocScanExample/Views/AdvancedIdentityProfile/Index.cshtml create mode 100644 src/Yoti.Auth/DocScan/Session/Create/AdvancedIdentityProfile.cs create mode 100644 src/Yoti.Auth/DocScan/Session/Retrieve/AdvancedIdentityProfile/AdvancedIdentityProfilePreviewResponse.cs create mode 100644 src/Yoti.Auth/DocScan/Session/Retrieve/AdvancedIdentityProfile/AdvancedIdentityProfileResponse.cs create mode 100644 src/Yoti.Auth/DocScan/Session/Retrieve/AdvancedIdentityProfile/FailureReasonResponse.cs create mode 100644 src/Yoti.Auth/DocScan/Session/Retrieve/ExpandedDocumentFieldResponse.cs create mode 100644 test/Yoti.Auth.Tests/TestData/GetSessionResultWithAdvancedIdentityProfile.json diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index cce3710d..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: Custom issue template -about: " There's a better way to get help!" -title: '' -labels: '' -assignees: '' - ---- - -# -# Wait ✋ -# -# There's a better way to get help! -# -# Send your questions or issues to sdksupport@yoti.com -# -# diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..9486bc24 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Yoti Support + url: https://support.yoti.com/yotisupport/s/contactsupport + about: Please send your questions or issues here. diff --git a/src/Examples/Aml/AmlExample/AmlExample.csproj b/src/Examples/Aml/AmlExample/AmlExample.csproj index 15bf388e..f96d7c36 100644 --- a/src/Examples/Aml/AmlExample/AmlExample.csproj +++ b/src/Examples/Aml/AmlExample/AmlExample.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + netcoreapp3.1 diff --git a/src/Examples/DocScan/DocScanExample/Controllers/AdvancedIdentityController.cs b/src/Examples/DocScan/DocScanExample/Controllers/AdvancedIdentityController.cs new file mode 100644 index 00000000..c114d552 --- /dev/null +++ b/src/Examples/DocScan/DocScanExample/Controllers/AdvancedIdentityController.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net.Http; +using DocScanExample.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Yoti.Auth; +using Yoti.Auth.DocScan; +using Yoti.Auth.DocScan.Session.Create; + +namespace DocScanExample.Controllers +{ + public class AdvancedIdentityProfileController : Controller + { + private readonly DocScanClient _client; + + private readonly string _baseUrl; + private readonly Uri _apiUrl; + + public AdvancedIdentityProfileController(IHttpContextAccessor httpContextAccessor) + { + var request = httpContextAccessor.HttpContext.Request; + + _baseUrl = $"{request.Scheme}://{request.Host}"; ; + _apiUrl = GetApiUrl(); + _client = GetDocScanClient(_apiUrl); + } + + public IActionResult Index() + { + AdvancedIdentityProfile data = new AdvancedIdentityProfile + { + profiles = new List + { + new Profile + { + trust_framework = "UK_TFIDA", + schemes = new List + { + new Scheme + { + label = "LB912", + type = "RTW" + } + } + }, + new Profile + { + trust_framework = "YOTI_GLOBAL", + schemes = new List + { + new Scheme + { + label = "LB321", + type = "IDENTITY", + objective = "AL_L1" + } + } + } + } + }; + //Build Session Spec + var sessionSpec = new SessionSpecificationBuilder() + .WithClientSessionTokenTtl(600) + .WithResourcesTtl(90000) + .WithUserTrackingId("some-user-tracking-id") + //Add Sdk Config (with builder) + .WithSdkConfig( + new SdkConfigBuilder() + .WithAllowsCamera() + .WithPrimaryColour("#2d9fff") + .WithSecondaryColour("#FFFFFF") + .WithFontColour("#FFFFFF") + .WithLocale("en-GB") + .WithPresetIssuingCountry("GBR") + .WithSuccessUrl($"{_baseUrl}/idverify/success") + .WithErrorUrl($"{_baseUrl}/idverify/error") + .WithPrivacyPolicyUrl($"{_baseUrl}/privacy-policy") + .Build() + ) + .WithCreateIdentityProfilePreview(true) + .WithAdvancedIdentityProfileRequirements(data) + .WithSubject(new + { + subject_id = "some_subject_id_string" + }) + .Build(); + + CreateSessionResult createSessionResult = _client.CreateSession(sessionSpec); + string sessionId = createSessionResult.SessionId; + + string path = $"web/index.html?sessionID={sessionId}&sessionToken={createSessionResult.ClientSessionToken}"; + Uri uri = new Uri(_apiUrl, path); + + ViewBag.iframeUrl = uri.ToString(); + + TempData["sessionId"] = sessionId; + return View(); + } + + public IActionResult Media(string mediaId, string sessionId) + { + MediaValue media = _client.GetMediaContent(sessionId, mediaId); + + return File(media.GetContent(), media.GetMIMEType()); + } + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + + internal static DocScanClient GetDocScanClient(Uri apiUrl = null) + { + if (apiUrl == null) + apiUrl = GetApiUrl(); + + StreamReader privateKeyStream = System.IO.File.OpenText(Environment.GetEnvironmentVariable("YOTI_KEY_FILE_PATH")); + var key = CryptoEngine.LoadRsaKey(privateKeyStream); + + string clientSdkId = Environment.GetEnvironmentVariable("YOTI_CLIENT_SDK_ID"); + + return new DocScanClient(clientSdkId, key, new HttpClient(), apiUrl); + } + + internal static Uri GetApiUrl() + { + string apiUrl = Environment.GetEnvironmentVariable("YOTI_DOC_SCAN_API_URL"); + if (string.IsNullOrEmpty(apiUrl)) + { + return Yoti.Auth.Constants.Api.DefaultYotiDocsUrl; + } + + if (!apiUrl.EndsWith("/", StringComparison.Ordinal)) + apiUrl += "/"; + + return new Uri(apiUrl); + } + + public IActionResult PrivacyPolicy() + { + return View(); + } + } +} diff --git a/src/Examples/DocScan/DocScanExample/Controllers/DbsController.cs b/src/Examples/DocScan/DocScanExample/Controllers/DbsController.cs index a7d74c9f..2a1200be 100644 --- a/src/Examples/DocScan/DocScanExample/Controllers/DbsController.cs +++ b/src/Examples/DocScan/DocScanExample/Controllers/DbsController.cs @@ -9,10 +9,6 @@ using Yoti.Auth; using Yoti.Auth.DocScan; using Yoti.Auth.DocScan.Session.Create; -using Yoti.Auth.DocScan.Session.Create.Check; -using Yoti.Auth.DocScan.Session.Create.Filter; -using Yoti.Auth.DocScan.Session.Create.Objectives; -using Yoti.Auth.DocScan.Session.Create.Task; namespace DocScanExample.Controllers { @@ -34,7 +30,6 @@ public DbsController(IHttpContextAccessor httpContextAccessor) public IActionResult Index() { - //Build Session Spec var sessionSpec = new SessionSpecificationBuilder() .WithClientSessionTokenTtl(600) @@ -43,7 +38,7 @@ public IActionResult Index() //Add Sdk Config (with builder) .WithSdkConfig( new SdkConfigBuilder() - .WithAllowsCameraAndUpload() + .WithAllowsCamera() .WithPrimaryColour("#2d9fff") .WithSecondaryColour("#FFFFFF") .WithFontColour("#FFFFFF") @@ -55,7 +50,7 @@ public IActionResult Index() .Build() ) .WithCreateIdentityProfilePreview(true) - .WithIdentityProfileRequirements(new + .WithIdentityProfileRequirements(new { trust_framework = "UK_TFIDA", scheme = new @@ -127,4 +122,4 @@ public IActionResult PrivacyPolicy() return View(); } } -} \ No newline at end of file +} diff --git a/src/Examples/DocScan/DocScanExample/Controllers/HomeController.cs b/src/Examples/DocScan/DocScanExample/Controllers/HomeController.cs index f15ed5f4..1713dc4b 100644 --- a/src/Examples/DocScan/DocScanExample/Controllers/HomeController.cs +++ b/src/Examples/DocScan/DocScanExample/Controllers/HomeController.cs @@ -82,8 +82,9 @@ public IActionResult Index() //Add Tasks (using builders) .WithRequestedTask( new RequestedTextExtractionTaskBuilder() - .WithManualCheckAlways() + .WithManualCheckFallback() .WithChipDataDesired() + .WithCreateExpandedDocumentFields() .Build() ) .WithRequestedTask( diff --git a/src/Examples/DocScan/DocScanExample/Views/AdvancedIdentityProfile/Index.cshtml b/src/Examples/DocScan/DocScanExample/Views/AdvancedIdentityProfile/Index.cshtml new file mode 100644 index 00000000..fa3beadf --- /dev/null +++ b/src/Examples/DocScan/DocScanExample/Views/AdvancedIdentityProfile/Index.cshtml @@ -0,0 +1,4 @@ +@{ + ViewData["Title"] = "Advanced Identity Profile Page"; +} + \ No newline at end of file diff --git a/src/Examples/DocScan/DocScanExample/Views/IdVerify/Success.cshtml b/src/Examples/DocScan/DocScanExample/Views/IdVerify/Success.cshtml index c3c2a1db..284bcaff 100644 --- a/src/Examples/DocScan/DocScanExample/Views/IdVerify/Success.cshtml +++ b/src/Examples/DocScan/DocScanExample/Views/IdVerify/Success.cshtml @@ -289,6 +289,31 @@ } + @if (Model.AdvancedIdentityProfilePreviewResponse != null && Model.AdvancedIdentityProfilePreviewResponse.Media != null) + { +
+
+

Advanced Identity Profile Preview

+
+
+ + } @@ -344,6 +369,41 @@ } + @if (document.ExpandedDocumentFields != null) + { +
+
+

+ +

+
+
+
+ @if (document.ExpandedDocumentFields.Media != null) + { +
Media
+ + + + + + + +
ID + + @document.ExpandedDocumentFields.Media.Id + +
+ } +
+
+
+ } + @if (document.DocumentIdPhoto != null) {
diff --git a/src/Yoti.Auth/DocScan/DocScanService.cs b/src/Yoti.Auth/DocScan/DocScanService.cs index 87875608..404c19e5 100644 --- a/src/Yoti.Auth/DocScan/DocScanService.cs +++ b/src/Yoti.Auth/DocScan/DocScanService.cs @@ -151,7 +151,7 @@ public async Task GetMediaContent(string sdkId, AsymmetricCipherKeyP Response.CreateYotiExceptionFromStatusCode(response); } - if (response.Content == null) + if (response.Content == null || response.Content.Headers.ContentType == null) { return null; } diff --git a/src/Yoti.Auth/DocScan/Session/Create/AdvancedIdentityProfile.cs b/src/Yoti.Auth/DocScan/Session/Create/AdvancedIdentityProfile.cs new file mode 100644 index 00000000..eaa2cbff --- /dev/null +++ b/src/Yoti.Auth/DocScan/Session/Create/AdvancedIdentityProfile.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Yoti.Auth.DocScan.Session.Create +{ + public class Scheme + { + public string label { get; set; } + public string type { get; set; } + public string objective { get; set; } + } + + public class Profile + { + public string trust_framework { get; set; } + public List schemes { get; set; } + } + + public class AdvancedIdentityProfile + { + public List profiles { get; set; } + } +} + diff --git a/src/Yoti.Auth/DocScan/Session/Create/SessionSpecification.cs b/src/Yoti.Auth/DocScan/Session/Create/SessionSpecification.cs index e09a461a..129d800d 100644 --- a/src/Yoti.Auth/DocScan/Session/Create/SessionSpecification.cs +++ b/src/Yoti.Auth/DocScan/Session/Create/SessionSpecification.cs @@ -9,7 +9,7 @@ namespace Yoti.Auth.DocScan.Session.Create { public class SessionSpecification { - internal SessionSpecification(int? clientSessionTokenTtl, int? resourcesTtl, string userTrackingId, NotificationConfig notifications, List requestedChecks, List requestedTasks, SdkConfig sdkConfig, List requiredDocuments, bool? blockBiometricConsent, DateTimeOffset? sessionDeadline, object identityProfileRequirements, object subject, bool createIdentityProfilePreview) + internal SessionSpecification(int? clientSessionTokenTtl, int? resourcesTtl, string userTrackingId, NotificationConfig notifications, List requestedChecks, List requestedTasks, SdkConfig sdkConfig, List requiredDocuments, bool? blockBiometricConsent, DateTimeOffset? sessionDeadline, object identityProfileRequirements, object subject, bool createIdentityProfilePreview, object advancedIdentityProfileRequirements) { ClientSessionTokenTtl = clientSessionTokenTtl; ResourcesTtl = resourcesTtl; @@ -24,6 +24,7 @@ internal SessionSpecification(int? clientSessionTokenTtl, int? resourcesTtl, str IdentityProfileRequirements = identityProfileRequirements; Subject = subject; CreateIdentityProfilePreview = createIdentityProfilePreview; + AdvancedIdentityProfileRequirements = advancedIdentityProfileRequirements; } [JsonProperty(PropertyName = "client_session_token_ttl")] @@ -64,5 +65,8 @@ internal SessionSpecification(int? clientSessionTokenTtl, int? resourcesTtl, str [JsonProperty(PropertyName = "subject")] public object Subject { get; } + + [JsonProperty(PropertyName = "advanced_identity_profile_requirements")] + public object AdvancedIdentityProfileRequirements { get; } } } diff --git a/src/Yoti.Auth/DocScan/Session/Create/SessionSpecificationBuilder.cs b/src/Yoti.Auth/DocScan/Session/Create/SessionSpecificationBuilder.cs index ba413ec2..ee7c9f20 100644 --- a/src/Yoti.Auth/DocScan/Session/Create/SessionSpecificationBuilder.cs +++ b/src/Yoti.Auth/DocScan/Session/Create/SessionSpecificationBuilder.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using Newtonsoft.Json; using Yoti.Auth.DocScan.Session.Create.Check; using Yoti.Auth.DocScan.Session.Create.Filter; using Yoti.Auth.DocScan.Session.Create.Task; namespace Yoti.Auth.DocScan.Session.Create -{ +{ + public class SessionSpecificationBuilder { private readonly List _requestedChecks = new List(); @@ -21,6 +23,7 @@ public class SessionSpecificationBuilder private object _identityProfileRequirements; private object _subject; private bool _createIdentityProfilePreview; + private AdvancedIdentityProfile _advancedIdentityProfileRequirements; /// /// Sets the client session token TTL (time-to-live) @@ -151,6 +154,7 @@ public SessionSpecificationBuilder WithIdentityProfileRequirements(object identi _identityProfileRequirements = identityProfileRequirements; return this; } + /// /// Sets the Subject object for the session @@ -174,6 +178,17 @@ public SessionSpecificationBuilder WithCreateIdentityProfilePreview(bool createI return this; } + /// + /// Sets the Advanced Identity Profile Requirements for the session + /// + /// The Advanced Identity Profile Requirements for the session + /// the builder + public SessionSpecificationBuilder WithAdvancedIdentityProfileRequirements(AdvancedIdentityProfile profile) + { + _advancedIdentityProfileRequirements = profile; + return this; + } + /// /// Builds the based on the values supplied to the builder /// @@ -193,7 +208,8 @@ public SessionSpecification Build() _sessionDeadline, _identityProfileRequirements, _subject, - _createIdentityProfilePreview + _createIdentityProfilePreview, + _advancedIdentityProfileRequirements ); } } diff --git a/src/Yoti.Auth/DocScan/Session/Create/Task/RequestedTextExtractionTaskBuilder.cs b/src/Yoti.Auth/DocScan/Session/Create/Task/RequestedTextExtractionTaskBuilder.cs index 2ae53754..6fba3bda 100644 --- a/src/Yoti.Auth/DocScan/Session/Create/Task/RequestedTextExtractionTaskBuilder.cs +++ b/src/Yoti.Auth/DocScan/Session/Create/Task/RequestedTextExtractionTaskBuilder.cs @@ -9,6 +9,7 @@ public class RequestedTextExtractionTaskBuilder { private string _manualCheck; private string _chipData; + private bool _createExpandedDocumentFields; /// /// Requires that a manual follow-up check is always performed @@ -60,11 +61,21 @@ public RequestedTextExtractionTaskBuilder WithChipDataIgnore() return this; } + /// + /// Sets the value of createExpandedDocumentFields data to "true" + /// + /// The builder + public RequestedTextExtractionTaskBuilder WithCreateExpandedDocumentFields() + { + _createExpandedDocumentFields = true; + return this; + } + public RequestedTextExtractionTask Build() { Validation.NotNullOrEmpty(_manualCheck, nameof(_manualCheck)); - RequestedTextExtractionTaskConfig config = new RequestedTextExtractionTaskConfig(_manualCheck, _chipData); + RequestedTextExtractionTaskConfig config = new RequestedTextExtractionTaskConfig(_manualCheck, _chipData, _createExpandedDocumentFields); return new RequestedTextExtractionTask(config); } diff --git a/src/Yoti.Auth/DocScan/Session/Create/Task/RequestedTextExtractionTaskConfig.cs b/src/Yoti.Auth/DocScan/Session/Create/Task/RequestedTextExtractionTaskConfig.cs index b98ad03f..e98fa7ce 100644 --- a/src/Yoti.Auth/DocScan/Session/Create/Task/RequestedTextExtractionTaskConfig.cs +++ b/src/Yoti.Auth/DocScan/Session/Create/Task/RequestedTextExtractionTaskConfig.cs @@ -4,10 +4,11 @@ namespace Yoti.Auth.DocScan.Session.Create.Task { public class RequestedTextExtractionTaskConfig : RequestedTaskConfig { - public RequestedTextExtractionTaskConfig(string manualCheck, string chipData = null) + public RequestedTextExtractionTaskConfig(string manualCheck, string chipData = null, bool? createExpandedDocumentFields = false) { ManualCheck = manualCheck; ChipData = chipData; + CreateExpandedDocumentFields = createExpandedDocumentFields; } [JsonProperty(PropertyName = "manual_check")] @@ -15,5 +16,8 @@ public RequestedTextExtractionTaskConfig(string manualCheck, string chipData = n [JsonProperty(PropertyName = "chip_data")] public string ChipData { get; } + + [JsonProperty(PropertyName = "create_expanded_document_fields")] + public bool? CreateExpandedDocumentFields { get; } } } \ No newline at end of file diff --git a/src/Yoti.Auth/DocScan/Session/Retrieve/AdvancedIdentityProfile/AdvancedIdentityProfilePreviewResponse.cs b/src/Yoti.Auth/DocScan/Session/Retrieve/AdvancedIdentityProfile/AdvancedIdentityProfilePreviewResponse.cs new file mode 100644 index 00000000..981bcac0 --- /dev/null +++ b/src/Yoti.Auth/DocScan/Session/Retrieve/AdvancedIdentityProfile/AdvancedIdentityProfilePreviewResponse.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace Yoti.Auth.DocScan.Session.Retrieve.AdvancedIdentityProfilePreview +{ + public class AdvancedIdentityProfilePreviewResponse + { + [JsonProperty(PropertyName = "media")] + public MediaResponse Media { get; private set; } + } +} diff --git a/src/Yoti.Auth/DocScan/Session/Retrieve/AdvancedIdentityProfile/AdvancedIdentityProfileResponse.cs b/src/Yoti.Auth/DocScan/Session/Retrieve/AdvancedIdentityProfile/AdvancedIdentityProfileResponse.cs new file mode 100644 index 00000000..a2b843f5 --- /dev/null +++ b/src/Yoti.Auth/DocScan/Session/Retrieve/AdvancedIdentityProfile/AdvancedIdentityProfileResponse.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace Yoti.Auth.DocScan.Session.Retrieve.AdvancedIdentityProfile +{ + public class AdvancedIdentityProfileResponse + { + [JsonProperty(PropertyName = "subject_id")] + public string SubjectId { get; private set; } + [JsonProperty(PropertyName = "result")] + public string Result { get; private set; } + [JsonProperty(PropertyName = "failure_reason")] + public FailureReasonResponse FailureReason { get; private set; } + [JsonProperty(PropertyName = "identity_profile_report")] + public Dictionary Report { get; private set; } + + } +} diff --git a/src/Yoti.Auth/DocScan/Session/Retrieve/AdvancedIdentityProfile/FailureReasonResponse.cs b/src/Yoti.Auth/DocScan/Session/Retrieve/AdvancedIdentityProfile/FailureReasonResponse.cs new file mode 100644 index 00000000..b55c83d8 --- /dev/null +++ b/src/Yoti.Auth/DocScan/Session/Retrieve/AdvancedIdentityProfile/FailureReasonResponse.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Yoti.Auth.DocScan.Session.Retrieve.AdvancedIdentityProfile +{ + public class FailureReasonResponse + { + [JsonProperty(PropertyName = "reason_code")] + public string ReasonCode { get; private set; } + } +} diff --git a/src/Yoti.Auth/DocScan/Session/Retrieve/ExpandedDocumentFieldResponse.cs b/src/Yoti.Auth/DocScan/Session/Retrieve/ExpandedDocumentFieldResponse.cs new file mode 100644 index 00000000..af117608 --- /dev/null +++ b/src/Yoti.Auth/DocScan/Session/Retrieve/ExpandedDocumentFieldResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Yoti.Auth.DocScan.Session.Retrieve +{ + /// + /// ExpandedDocumentFieldsResponse represents the document fields in a document + /// + public class ExpandedDocumentFieldsResponse : IResponseWithMediaProperty + { + [JsonProperty(PropertyName = "media")] + public MediaResponse Media { get; private set; } + } +} \ No newline at end of file diff --git a/src/Yoti.Auth/DocScan/Session/Retrieve/GetSessionResult.cs b/src/Yoti.Auth/DocScan/Session/Retrieve/GetSessionResult.cs index b34905a7..89e67609 100644 --- a/src/Yoti.Auth/DocScan/Session/Retrieve/GetSessionResult.cs +++ b/src/Yoti.Auth/DocScan/Session/Retrieve/GetSessionResult.cs @@ -56,6 +56,12 @@ public class GetSessionResult [JsonProperty(PropertyName = "identity_profile_preview")] public IdentityProfilePreviewResponse IdentityProfilePreviewResponse { get; internal set; } + [JsonProperty(PropertyName = "advanced_identity_profile")] + public IdentityProfileResponse AdvancedIdentityProfile { get; internal set; } + + [JsonProperty(PropertyName = "advanced_identity_profile_preview")] + public IdentityProfilePreviewResponse AdvancedIdentityProfilePreviewResponse { get; internal set; } + public List GetAuthenticityChecks() { if (Checks == null) diff --git a/src/Yoti.Auth/DocScan/Session/Retrieve/Resource/IdDocumentResourceResponse.cs b/src/Yoti.Auth/DocScan/Session/Retrieve/Resource/IdDocumentResourceResponse.cs index 3948c4cd..3136cc1d 100644 --- a/src/Yoti.Auth/DocScan/Session/Retrieve/Resource/IdDocumentResourceResponse.cs +++ b/src/Yoti.Auth/DocScan/Session/Retrieve/Resource/IdDocumentResourceResponse.cs @@ -25,6 +25,9 @@ public class IdDocumentResourceResponse : ResourceResponse [JsonProperty(PropertyName = "document_id_photo")] public DocumentIdPhotoResponse DocumentIdPhoto { get; internal set; } + [JsonProperty(PropertyName = "expanded_document_fields")] + public ExpandedDocumentFieldsResponse ExpandedDocumentFields { get; internal set; } + /// /// Filters the tasks for the text extraction tasks associated with the ID document /// diff --git a/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicy.cs b/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicy.cs index 488cb28d..787ff990 100644 --- a/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicy.cs +++ b/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicy.cs @@ -29,12 +29,16 @@ public class DynamicPolicy [JsonProperty(PropertyName = "identity_profile_requirements")] private readonly object _identityProfileRequirements; + + [JsonProperty(PropertyName = "advanced_identity_profile_requirements")] + private readonly object _advancedIdentityProfileRequirements; public DynamicPolicy( ICollection wantedAttributes, HashSet wantedAuthTypes, bool wantedRememberMeId, - object identityProfileRequirements = null + object identityProfileRequirements = null, + object advancedIdentityProfileRequirements = null ) { _wantedAttributes = wantedAttributes; @@ -42,6 +46,7 @@ public DynamicPolicy( _wantedRememberMeId = wantedRememberMeId; _isWantedRememberMeIdOptional = false; _identityProfileRequirements = identityProfileRequirements; + _advancedIdentityProfileRequirements = advancedIdentityProfileRequirements; } /// @@ -91,5 +96,17 @@ public object IdentityProfileRequirements return _identityProfileRequirements; } } + + /// + /// AdvancedIdentityProfileRequirements requested in the policy + /// + [JsonIgnore] + public object AdvancedIdentityProfileRequirements + { + get + { + return _advancedIdentityProfileRequirements; + } + } } } \ No newline at end of file diff --git a/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicyBuilder.cs b/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicyBuilder.cs index 8d4ba4e4..78ff280e 100644 --- a/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicyBuilder.cs +++ b/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicyBuilder.cs @@ -9,6 +9,7 @@ public class DynamicPolicyBuilder private readonly HashSet _wantedAuthTypes = new HashSet(); private bool _wantedRememberMeId; private object _identityProfileRequirements; + private object _advancedIdentityProfileRequirements; public DynamicPolicyBuilder WithWantedAttribute(WantedAttribute wantedAttribute) { @@ -157,10 +158,21 @@ public DynamicPolicyBuilder WithIdentityProfileRequirements(object identityProfi _identityProfileRequirements = identityProfileRequirements; return this; } + + /// + /// Use an AdvancedIdentity Profile Requirement object for the share + /// + /// object describing the advanced identity profile requirements to use + /// with the identity profile requirements + public DynamicPolicyBuilder WithAdvancedIdentityProfileRequirements(object advancedIdentityProfileRequirements) + { + _advancedIdentityProfileRequirements = advancedIdentityProfileRequirements; + return this; + } public DynamicPolicy Build() { - return new DynamicPolicy(_wantedAttributes.Values, _wantedAuthTypes, _wantedRememberMeId, _identityProfileRequirements); + return new DynamicPolicy(_wantedAttributes.Values, _wantedAuthTypes, _wantedRememberMeId, _identityProfileRequirements, _advancedIdentityProfileRequirements); } } } \ No newline at end of file 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 292d43db..8832cfb2 100644 --- a/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj +++ b/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp3.1 diff --git a/test/Yoti.Auth.Tests/DocScan/DocScanClientTests.cs b/test/Yoti.Auth.Tests/DocScan/DocScanClientTests.cs index 944a6127..243aec81 100644 --- a/test/Yoti.Auth.Tests/DocScan/DocScanClientTests.cs +++ b/test/Yoti.Auth.Tests/DocScan/DocScanClientTests.cs @@ -686,6 +686,37 @@ public void ShouldParseIdentityProfileResponse() Assert.AreEqual(mediaId, result.IdentityProfile.Report["media"]["id"]); } + + [TestMethod] + public void ShouldParseAdvancedIdentityProfileResponse() + { + string mediaId = "c69ff2db-6caf-4e74-8386-037711bbc8d7"; + string getSessionResult; + using (StreamReader r = File.OpenText("TestData/GetSessionResultWithAdvancedIdentityProfile.json")) + { + getSessionResult = r.ReadToEnd(); + } + + var successResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(getSessionResult), + }; + + Mock handlerMock = Auth.Tests.Common.Http.SetupMockMessageHandler(successResponse); + var httpClient = new HttpClient(handlerMock.Object); + + DocScanClient docScanClient = new DocScanClient(_sdkId, _keyPair, httpClient); + + GetSessionResult result = docScanClient.GetSession("some-session-id"); + + Assert.AreEqual("DONE", result.AdvancedIdentityProfile.Result); + Assert.AreEqual("someStringHere", result.AdvancedIdentityProfile.SubjectId); + Assert.AreEqual("MANDATORY_DOCUMENT_COULD_NOT_BE_PROVIDED", result.AdvancedIdentityProfile.FailureReason.ReasonCode); + + Assert.AreEqual("UK_TFIDA", result.AdvancedIdentityProfile.Report["compliance"][0]["trust_framework"]); + Assert.AreEqual(mediaId, result.AdvancedIdentityProfile.Report["media"]["id"].Value()); + } private DocScanClient SetupDocScanClient(dynamic responseContent) { diff --git a/test/Yoti.Auth.Tests/DocScan/Session/Create/SessionSpecificationBuilderTests.cs b/test/Yoti.Auth.Tests/DocScan/Session/Create/SessionSpecificationBuilderTests.cs index 9b332b15..cc26264c 100644 --- a/test/Yoti.Auth.Tests/DocScan/Session/Create/SessionSpecificationBuilderTests.cs +++ b/test/Yoti.Auth.Tests/DocScan/Session/Create/SessionSpecificationBuilderTests.cs @@ -163,6 +163,61 @@ public void ShoudBuildWithIdentityProfileRequirements() Assert.IsTrue(sessionSpecJson.Contains("BASIC")); } + [TestMethod] + public void ShoudBuildWithAdvancedIdentityProfileRequirements() + { + 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 sessionSpec = new SessionSpecificationBuilder() + .WithIdentityProfileRequirements(advancedIdentityProfileJson) + .Build(); + + string sessionSpecJson = JsonConvert.SerializeObject(sessionSpec); + Assert.IsTrue(sessionSpecJson.Contains("UK_TFIDA")); + Assert.IsTrue(sessionSpecJson.Contains("YOTI_GLOBAL")); + Assert.IsTrue(sessionSpecJson.Contains("IDENTITY")); + + } + + [TestMethod] + public void ShouldNotImplicitlySetAValueForAdvancedIdentityProfileRequirements() + { + SessionSpecification sessionSpec = + new SessionSpecificationBuilder() + .Build(); + + Assert.IsNull(sessionSpec.AdvancedIdentityProfileRequirements); + } + [TestMethod] public void ShoudBuildWithSubject() { @@ -287,6 +342,33 @@ public void ShouldBuildWithIdentityProfilePreview() Assert.AreEqual(identityProfileRequirements, sessionSpec.IdentityProfileRequirements); } + + [TestMethod] + public void ShouldBuildWithAdvancedIdentityProfilePreview() + { + AdvancedIdentityProfile advancedIdentityProfileRequirements = IdentityProfiles.CreateStandardAdvancedIdentityProfileRequirements(); + + SessionSpecification sessionSpec = + new SessionSpecificationBuilder() + .WithAdvancedIdentityProfileRequirements(advancedIdentityProfileRequirements) + .WithCreateIdentityProfilePreview(true) + .Build(); + + Assert.AreEqual(advancedIdentityProfileRequirements, sessionSpec.AdvancedIdentityProfileRequirements); + } + + [TestMethod] + public void ShouldBuildWithAdvancedIdentityProfileRequirements() + { + AdvancedIdentityProfile advancedIdentityProfileRequirements = IdentityProfiles.CreateStandardAdvancedIdentityProfileRequirements(); + + SessionSpecification sessionSpec = + new SessionSpecificationBuilder() + .WithAdvancedIdentityProfileRequirements(advancedIdentityProfileRequirements) + .Build(); + + Assert.AreEqual(advancedIdentityProfileRequirements, sessionSpec.AdvancedIdentityProfileRequirements); + } [TestMethod] public void ShouldNotImplicitlySetAValueForIdentityProfileRequirements() diff --git a/test/Yoti.Auth.Tests/DocScan/Session/Create/Task/RequestedTextExtractionTaskBuilderTests.cs b/test/Yoti.Auth.Tests/DocScan/Session/Create/Task/RequestedTextExtractionTaskBuilderTests.cs index b3dd26bc..6dfedc94 100644 --- a/test/Yoti.Auth.Tests/DocScan/Session/Create/Task/RequestedTextExtractionTaskBuilderTests.cs +++ b/test/Yoti.Auth.Tests/DocScan/Session/Create/Task/RequestedTextExtractionTaskBuilderTests.cs @@ -78,12 +78,35 @@ public void ShouldBuildWithChipDataIgnore() { RequestedTextExtractionTask task = new RequestedTextExtractionTaskBuilder() - .WithManualCheckAlways() + .WithManualCheckFallback() .WithChipDataIgnore() .Build(); - Assert.AreEqual("ALWAYS", task.Config.ManualCheck); + Assert.AreEqual("FALLBACK", task.Config.ManualCheck); Assert.AreEqual("IGNORE", task.Config.ChipData); } + + [TestMethod] + public void ShouldBuildWithCreateExpandedDocumentFields() + { + RequestedTextExtractionTask task = + new RequestedTextExtractionTaskBuilder() + .WithManualCheckFallback() + .WithCreateExpandedDocumentFields() + .Build(); + + Assert.AreEqual(true, task.Config.CreateExpandedDocumentFields); + } + + [TestMethod] + public void ShouldBuildWithouthCreateExpandedDocumentFields() + { + RequestedTextExtractionTask task = + new RequestedTextExtractionTaskBuilder() + .WithManualCheckFallback() + .Build(); + + Assert.AreEqual(false, task.Config.CreateExpandedDocumentFields); + } } } \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DocScan/Session/Retrieve/GetSessionResultTests.cs b/test/Yoti.Auth.Tests/DocScan/Session/Retrieve/GetSessionResultTests.cs index 8f73dafc..bdf2cb56 100644 --- a/test/Yoti.Auth.Tests/DocScan/Session/Retrieve/GetSessionResultTests.cs +++ b/test/Yoti.Auth.Tests/DocScan/Session/Retrieve/GetSessionResultTests.cs @@ -4,6 +4,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; using Yoti.Auth.DocScan.Session.Retrieve; +using Yoti.Auth.DocScan.Session.Retrieve.AdvancedIdentityProfile; +using Yoti.Auth.DocScan.Session.Retrieve.AdvancedIdentityProfilePreview; using Yoti.Auth.DocScan.Session.Retrieve.Check; using Yoti.Auth.DocScan.Session.Retrieve.IdentityProfilePreview; @@ -346,6 +348,21 @@ public void CheckIdentityProfilePreviewResponseIsParsed() AssertMediaValuesCorrect(identityProfilePreviewResponse.media, response.Media, typeof(MediaResponse)); } + + [TestMethod] + public void CheckAdvancedIdentityProfilePreviewResponseIsParsed() + { + dynamic advancedIdentityProfilePreviewResponse = new + { + media = GetMediaResponse() + }; + + string json = JsonConvert.SerializeObject(advancedIdentityProfilePreviewResponse); + AdvancedIdentityProfilePreviewResponse response = + JsonConvert.DeserializeObject(json); + + AssertMediaValuesCorrect(advancedIdentityProfilePreviewResponse.media, response.Media, typeof(MediaResponse)); + } private void AssertMediaValuesCorrect(dynamic originalData, MediaResponse response, Type requiredType) { diff --git a/test/Yoti.Auth.Tests/ShareUrl/Policy/DynamicPolicyBuilderTests.cs b/test/Yoti.Auth.Tests/ShareUrl/Policy/DynamicPolicyBuilderTests.cs index 8d4f87c1..d65f4d1b 100644 --- a/test/Yoti.Auth.Tests/ShareUrl/Policy/DynamicPolicyBuilderTests.cs +++ b/test/Yoti.Auth.Tests/ShareUrl/Policy/DynamicPolicyBuilderTests.cs @@ -325,5 +325,18 @@ public void ShouldBuildWithIdentityProfileRequirements() Assert.AreEqual(identityProfileRequirements, result.IdentityProfileRequirements); } + + [TestMethod] + public void ShouldBuildWithAdvancedIdentityProfileRequirements() + { + object advancedIdentityProfileRequirements = + IdentityProfiles.CreateStandardAdvancedIdentityProfileRequirements(); + + DynamicPolicy result = new DynamicPolicyBuilder() + .WithAdvancedIdentityProfileRequirements(advancedIdentityProfileRequirements) + .Build(); + + Assert.AreEqual(advancedIdentityProfileRequirements, result.AdvancedIdentityProfileRequirements); + } } } \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/TestData/GetSessionResultWithAdvancedIdentityProfile.json b/test/Yoti.Auth.Tests/TestData/GetSessionResultWithAdvancedIdentityProfile.json new file mode 100644 index 00000000..80791cd9 --- /dev/null +++ b/test/Yoti.Auth.Tests/TestData/GetSessionResultWithAdvancedIdentityProfile.json @@ -0,0 +1,42 @@ +{ + "session_id": "a1746488-efcc-4c59-bd28-f849dcb933a2", + "client_session_token_ttl": 599, + "user_tracking_id": "user-tracking-id", + "biometric_consent": "2022-03-29T11:39:08.473Z", + "state": "COMPLETED", + "client_session_token": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "advanced_identity_profile": { + "subject_id": "someStringHere", + "result": "DONE", + "failure_reason": { + "reason_code": "MANDATORY_DOCUMENT_COULD_NOT_BE_PROVIDED" + }, + "identity_profile_report": { + "compliance": [{ + "trust_framework": "UK_TFIDA", + "schemes_compliance": [{ + "scheme": { + "type": "DBS", + "objective": "STANDARD" + }, + "requirements_met": true, + "requirements_not_met_info": "some string here" + }] + }], + "media": { + "id": "c69ff2db-6caf-4e74-8386-037711bbc8d7", + "type": "IMAGE", + "created": "2022-03-29T11:39:24Z", + "last_updated": "2022-03-29T11:39:24Z" + } + } + }, + "advanced_identity_profile_preview": { + "media": { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type": "IMAGE", + "created": "2021-06-11T11:39:24Z", + "last_updated": "2021-06-11T11:39:24Z" + } + } +} \ 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 5d81d7a7..033a042f 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 System.Collections.Generic; +using Yoti.Auth.DocScan.Session.Create; + +namespace Yoti.Auth.Tests.TestData { internal static class IdentityProfiles { @@ -22,5 +25,42 @@ public static object CreateStandardSubject() subject_id = "some_subject_id_string" }; } + + public static AdvancedIdentityProfile CreateStandardAdvancedIdentityProfileRequirements() + { + AdvancedIdentityProfile data = new AdvancedIdentityProfile + { + profiles = new List + { + new Yoti.Auth.DocScan.Session.Create.Profile + { + trust_framework = "UK_TFIDA", + schemes = new List + { + new Scheme + { + label = "LB912", + type = "RTW" + } + } + }, + new Yoti.Auth.DocScan.Session.Create.Profile + { + trust_framework = "YOTI_GLOBAL", + schemes = new List + { + new Scheme + { + label = "LB321", + type = "IDENTITY", + objective = "AL_L1" + } + } + } + } + }; + + return data; + } } } diff --git a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj index cc28ada4..828d95d6 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 @@ -67,5 +67,8 @@ PreserveNewest + + PreserveNewest + \ No newline at end of file From 657ba6093c19569274f9ced06170ea2285e04357 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Wed, 14 Feb 2024 17:37:37 +0300 Subject: [PATCH 08/10] Yoti.Auth version string update (#452) --- src/Yoti.Auth/Yoti.Auth.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Yoti.Auth/Yoti.Auth.csproj b/src/Yoti.Auth/Yoti.Auth.csproj index 36c5346e..85bf58f8 100644 --- a/src/Yoti.Auth/Yoti.Auth.csproj +++ b/src/Yoti.Auth/Yoti.Auth.csproj @@ -19,7 +19,7 @@ False latest true - 3.14.0 + 3.15.0 From 8330968d96c8906cb121c4f100810810e76339b4 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Tue, 14 May 2024 22:49:18 +0300 Subject: [PATCH 09/10] Release/3.16.0 (#465) * SDK-2238:Create Session * Update azure-pipelines.yml * SDK-2245: Retrieve Session * Sdk 2252:Create QR code * SDK-2252:Updated validation rule * SDK-2252:removed net 2.1 support for security issues * Sdk 2257:Retrieve QR code * IN-5590: Update support link (#437) * Sdk 2363: net check for and add optional attribute configuration to sdk (#458) * Bump Google.Protobuf from 3.21.3 to 3.22.0 (#425) * Bump Microsoft.CodeAnalysis.CSharp.Workspaces, Microsoft.CodeAnalysis.Common and Microsoft.CodeAnalysis.CSharp (#460) Updates `Microsoft.CodeAnalysis.CSharp.Workspaces` from 4.2.0 to 4.9.2 Updates `Microsoft.CodeAnalysis.Common` from 4.2.0 to 4.9.2 Updates `Microsoft.CodeAnalysis.CSharp` from 4.2.0 to 4.9.2 * Bump Microsoft.VisualStudio.Azure.Containers.Tools.Targets (#459) * Bumps Microsoft.VisualStudio.Azure.Containers.Tools.Targets from 1.16.1 to 1.20.1. * SDK-2264:Retrieve Receipt, updated Newtonsoft.Json 13.0.3 * SDK-2416:Add support for advanced identity profiles to Share V2 and examples --- README.md | 2 +- azure-pipelines.yml | 2 +- src/Examples/Aml/AmlExample/AmlExample.csproj | 2 +- .../DigitalIdentity/.dockerignore | 25 ++ .../DigitalIdentity/.env.example | 2 + .../AdvancedIdentityShareController.cs | 94 ++++ .../Controllers/HomeController.cs | 91 ++++ .../Controllers/SuccessController.cs | 163 +++++++ .../DigitalIdentityExample.csproj | 55 +++ .../DigitalIdentity/Dockerfile | 20 + .../DigitalIdentity/GlobalSuppressions.cs | 9 + .../Models/DisplayAttribute.cs | 62 +++ .../Models/DisplayAttributes.cs | 28 ++ .../DigitalIdentity/Program.cs | 17 + .../Properties/launchSettings.json | 26 ++ .../DigitalIdentity/DigitalIdentity/README.md | 21 + .../DigitalIdentity/Startup.cs | 87 ++++ .../AdvancedIdentityShare.cshtml | 92 ++++ .../Views/Home/DigitalIdentity.cshtml | 92 ++++ .../Views/Success/Error.cshtml | 19 + .../Views/Success/SuccessResult.cshtml | 172 +++++++ .../DigitalIdentity/Views/Web.config | 43 ++ .../appsettings.Development.json | 9 + .../DigitalIdentity/appsettings.json | 8 + .../DigitalIdentity/docker-compose.dcproj | 18 + .../docker-compose.override.yml | 18 + .../DigitalIdentity/docker-compose.yml | 8 + .../https/DigitalIdentityExample.pfx | Bin 0 -> 2700 bytes .../wwwroot/static/assets/app-store-badge.png | Bin 0 -> 4077 bytes .../static/assets/app-store-badge@2x.png | Bin 0 -> 8819 bytes .../wwwroot/static/assets/company-logo.jpg | Bin 0 -> 4682 bytes .../static/assets/google-play-badge.png | Bin 0 -> 4957 bytes .../static/assets/google-play-badge@2x.png | Bin 0 -> 11267 bytes .../wwwroot/static/assets/icons/address.svg | 3 + .../wwwroot/static/assets/icons/calendar.svg | 5 + .../static/assets/icons/chevron-down-grey.svg | 7 + .../wwwroot/static/assets/icons/document.svg | 3 + .../wwwroot/static/assets/icons/email.svg | 14 + .../wwwroot/static/assets/icons/gender.svg | 5 + .../static/assets/icons/nationality.svg | 3 + .../wwwroot/static/assets/icons/phone.svg | 3 + .../wwwroot/static/assets/icons/profile.svg | 3 + .../wwwroot/static/assets/icons/verified.svg | 6 + .../wwwroot/static/assets/logo.png | Bin 0 -> 2988 bytes .../wwwroot/static/assets/logo@2x.png | Bin 0 -> 5609 bytes .../DigitalIdentity/wwwroot/static/index.css | 152 +++++++ .../wwwroot/static/profile.css | 420 ++++++++++++++++++ .../CoreExample/Controllers/HomeController.cs | 3 +- .../Profile/CoreExample/CoreExample.csproj | 8 +- src/Yoti.Auth.sln | 7 + src/Yoti.Auth/Constants/Api.cs | 3 + src/Yoti.Auth/Conversion.cs | 2 +- src/Yoti.Auth/CryptoEngine.cs | 66 ++- .../DigitalIdentity/CreateQrResult.cs | 18 + .../DigitalIdentity/DigitalIdentityService.cs | 371 ++++++++++++++++ .../Extensions/BaseExtension.cs | 27 ++ .../Extensions/DeviceLocation.cs | 63 +++ .../DigitalIdentity/Extensions/Extension.cs | 33 ++ .../Extensions/ExtensionBuilder.cs | 25 ++ .../Extensions/LocationConstraintContent.cs | 24 + .../LocationConstraintExtensionBuilder.cs | 66 +++ .../Extensions/ThirdPartyAttributeContent.cs | 31 ++ .../ThirdPartyAttributeExtensionBuilder.cs | 67 +++ .../TransactionalFlowExtensionBuilder.cs | 25 ++ .../DigitalIdentity/GetQrCodeResult.cs | 32 ++ src/Yoti.Auth/DigitalIdentity/GetReceipt.cs | 46 ++ .../DigitalIdentity/GetSessionResult.cs | 48 ++ .../Policy/AdvancedIdentityProfile.cs | 30 ++ .../DigitalIdentity/Policy/Constraint.cs | 16 + .../DigitalIdentity/Policy/Notification.cs | 18 + .../DigitalIdentity/Policy/Policy.cs | 113 +++++ .../DigitalIdentity/Policy/PolicyBuilder.cs | 178 ++++++++ .../Policy/PreferredSources.cs | 20 + .../Policy/SourceConstraint.cs | 18 + .../Policy/SourceConstraintBuilder.cs | 72 +++ .../DigitalIdentity/Policy/WantedAnchor.cs | 19 + .../Policy/WantedAnchorBuilder.cs | 37 ++ .../DigitalIdentity/Policy/WantedAttribute.cs | 34 ++ .../Policy/WantedAttributeBuilder.cs | 69 +++ src/Yoti.Auth/DigitalIdentity/QrRequest.cs | 40 ++ .../DigitalIdentity/QrRequestBuilder.cs | 39 ++ .../DigitalIdentity/ReceiptItemKeyResponse.cs | 16 + .../DigitalIdentity/ShareSessionRequest.cs | 83 ++++ .../ShareSessionRequestBuilder.cs | 77 ++++ .../DigitalIdentity/ShareSessionResult.cs | 24 + .../DigitalIdentity/SharedReceiptResponse.cs | 29 ++ src/Yoti.Auth/DigitalIdentityClient.cs | 114 +++++ src/Yoti.Auth/DigitalIdentityClientEngine.cs | 43 ++ src/Yoti.Auth/DocScan/DocScanService.cs | 5 + .../Exceptions/DigitalIdentityException.cs | 22 + .../ShareUrl/Policy/DynamicPolicyBuilder.cs | 3 +- .../ShareUrl/Policy/WantedAttribute.cs | 8 +- .../ShareUrl/Policy/WantedAttributeBuilder.cs | 11 +- src/Yoti.Auth/Yoti.Auth.csproj | 42 +- .../Yoti.Auth.Tests.Common.csproj | 4 +- test/Yoti.Auth.Tests/CryptoEngineTests.cs | 63 ++- .../DigitalIdentityServiceTests.cs | 208 +++++++++ .../Extensions/ExtensionBuilderTests.cs | 25 ++ ...LocationConstraintExtensionBuilderTests.cs | 100 +++++ ...hirdPartyAttributeExtensionBuilderTests.cs | 132 ++++++ .../TransactionalFlowExtensionBuilderTests.cs | 44 ++ .../Policy/DynamicPolicyBuilderTests.cs | 345 ++++++++++++++ .../Policy/WantedAttributeBuilderTests.cs | 166 +++++++ .../Policy/WantedAttributeMatcher.cs | 41 ++ .../DigitalIdentity/QrRequestBuilderTests.cs | 30 ++ .../ShareSessionRequestBuilderTests.cs | 72 +++ .../DigitalIdentityClientEngineTests.cs | 106 +++++ .../DigitalIdentityClientTests.cs | 158 +++++++ .../DigitalIdentityExceptionTests.cs | 36 ++ .../ShareUrl/DynamicScenarioBuilderTests.cs | 3 +- .../Policy/WantedAttributeBuilderTests.cs | 7 +- .../TestData/DigitalIdentity.json | 52 +++ .../TestData/DynamicPolicy.json | 11 +- .../TestData/IdentityProfiles.cs | 53 ++- test/Yoti.Auth.Tests/TestTools/CreateQr.cs | 15 + .../Yoti.Auth.Tests/TestTools/ShareSession.cs | 27 ++ test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj | 9 +- 117 files changed, 5614 insertions(+), 47 deletions(-) create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/.dockerignore create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/.env.example create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Controllers/AdvancedIdentityShareController.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Controllers/SuccessController.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/DigitalIdentityExample.csproj create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Dockerfile create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/GlobalSuppressions.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Models/DisplayAttribute.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Models/DisplayAttributes.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Program.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Properties/launchSettings.json create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/README.md create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Startup.cs create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Views/AdvancedIdentityShare/AdvancedIdentityShare.cshtml create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Views/Home/DigitalIdentity.cshtml create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/Error.cshtml create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Views/Success/SuccessResult.cshtml create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/Views/Web.config create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/appsettings.Development.json create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/appsettings.json create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.dcproj create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.override.yml create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.yml create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/https/DigitalIdentityExample.pfx create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge.png create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge@2x.png create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/company-logo.jpg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/google-play-badge.png create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/google-play-badge@2x.png create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/address.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/calendar.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/chevron-down-grey.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/document.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/email.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/gender.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/nationality.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/phone.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/profile.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/verified.svg create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/logo.png create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/logo@2x.png create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/index.css create mode 100644 src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/profile.css create mode 100644 src/Yoti.Auth/DigitalIdentity/CreateQrResult.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/BaseExtension.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/DeviceLocation.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/Extension.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/ExtensionBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/LocationConstraintContent.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/LocationConstraintExtensionBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/ThirdPartyAttributeContent.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/ThirdPartyAttributeExtensionBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Extensions/TransactionalFlowExtensionBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/GetQrCodeResult.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/GetReceipt.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/AdvancedIdentityProfile.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/Constraint.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/PolicyBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/PreferredSources.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/SourceConstraint.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/SourceConstraintBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/WantedAnchor.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/WantedAnchorBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/WantedAttribute.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/Policy/WantedAttributeBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/QrRequest.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/QrRequestBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/ReceiptItemKeyResponse.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/ShareSessionRequest.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/ShareSessionResult.cs create mode 100644 src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs create mode 100644 src/Yoti.Auth/DigitalIdentityClient.cs create mode 100644 src/Yoti.Auth/DigitalIdentityClientEngine.cs create mode 100644 src/Yoti.Auth/Exceptions/DigitalIdentityException.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Extensions/ExtensionBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Extensions/LocationConstraintExtensionBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Extensions/ThirdPartyAttributeExtensionBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Extensions/TransactionalFlowExtensionBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/Policy/WantedAttributeMatcher.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/QrRequestBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentity/ShareSessionRequestBuilderTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs create mode 100644 test/Yoti.Auth.Tests/DigitalIdentityExceptionTests.cs create mode 100644 test/Yoti.Auth.Tests/TestData/DigitalIdentity.json create mode 100644 test/Yoti.Auth.Tests/TestTools/CreateQr.cs create mode 100644 test/Yoti.Auth.Tests/TestTools/ShareSession.cs diff --git a/README.md b/README.md index 64354c12..0a165086 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 d9282ce8..7c3f24ea 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -36,7 +36,7 @@ steps: extraProperties: | sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" sonar.links.scm = https://github.com/getyoti/yoti-dotnet-sdk - sonar.exclusions = src/Yoti.Auth/ProtoBuf/**,src/Examples/**,**/obj/**,**/*.dll + sonar.exclusions = src/Yoti.Auth/ProtoBuf/**,src/Examples/**,**/obj/**,**/*.dll,src/Yoti.Auth/DigitalIdentity/** displayName: SonarCloud Prepare Analysis - task: NuGetToolInstaller@1 diff --git a/src/Examples/Aml/AmlExample/AmlExample.csproj b/src/Examples/Aml/AmlExample/AmlExample.csproj index f96d7c36..ab68935f 100644 --- a/src/Examples/Aml/AmlExample/AmlExample.csproj +++ b/src/Examples/Aml/AmlExample/AmlExample.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/.dockerignore b/src/Examples/DigitalIdentity/DigitalIdentity/.dockerignore new file mode 100644 index 00000000..e7b690f1 --- /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 00000000..e8d11ad0 --- /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 00000000..b6cf7f5b --- /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 00000000..9099ec63 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/HomeController.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Yoti.Auth; +using Yoti.Auth.DigitalIdentity; +using Yoti.Auth.DigitalIdentity.Policy; + +namespace DigitalIdentityExample.Controllers +{ + public class HomeController : Controller + { + private readonly string _clientSdkId; + private readonly ILogger _logger; + public HomeController(ILogger logger) + { + _logger = logger; + + _clientSdkId = Environment.GetEnvironmentVariable("YOTI_CLIENT_SDK_ID"); + _logger.LogInformation(string.Format("Yoti Client SDK ID='{0}'", _clientSdkId)); + } + + // GET: /generate-share + [Route("generate-share")] + public IActionResult DigitalIdentity() + { + try + { + string yotiKeyFilePath = Environment.GetEnvironmentVariable("YOTI_KEY_FILE_PATH"); + _logger.LogInformation( + string.Format( + "yotiKeyFilePath='{0}'", + yotiKeyFilePath)); + + StreamReader privateKeyStream = System.IO.File.OpenText(yotiKeyFilePath); + + var yotiClient = new DigitalIdentityClient(_clientSdkId, privateKeyStream); + + var givenNamesWantedAttribute = new WantedAttributeBuilder() + .WithName("given_names") + .WithOptional(false) + .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(new Notification + { + Headers = { }, + Url = "https://example.com/webhook", + Method = "POST", + VerifyTls = true + + }) + .WithRedirectUri("https:/www.yoti.com").WithSubject(new + { + subject_id = "some_subject_id_string" + }).Build(); + + var SessionResult = yotiClient.CreateShareSession(sessionReq); + + var sharedReceiptResponse = new SharedReceiptResponse(); + ViewBag.YotiClientSdkId = _clientSdkId; + ViewBag.sessionID = SessionResult.Id; + + return View("DigitalIdentity", sharedReceiptResponse); + } + catch (Exception e) + { + _logger.LogError( + exception: e, + message: e.Message); + + TempData["Error"] = e.Message; + TempData["InnerException"] = e.InnerException?.Message; + return RedirectToAction("Error", "Success"); + } + } + } +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/SuccessController.cs b/src/Examples/DigitalIdentity/DigitalIdentity/Controllers/SuccessController.cs new file mode 100644 index 00000000..aeac61a2 --- /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 00000000..b24d78cf --- /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 00000000..bfd9c663 --- /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 00000000..87fc554a --- /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 00000000..6625d295 --- /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 00000000..417d3e7e --- /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 00000000..2e32c9ab --- /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 00000000..cb95eb61 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55000", + "sslPort": 44380 + } + }, + "profiles": { + "CoreExample": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:44344;http://localhost:44343", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": {} + } + } +} diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/README.md b/src/Examples/DigitalIdentity/DigitalIdentity/README.md new file mode 100644 index 00000000..64b310ce --- /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 00000000..04dd3919 --- /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 00000000..fe627f05 --- /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 00000000..841d32ac --- /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 00000000..9cd2d459 --- /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 00000000..a27d3c40 --- /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 00000000..9bc7eab3 --- /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 00000000..e203e940 --- /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 00000000..def9159a --- /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 00000000..86377fe6 --- /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 00000000..701f4c3f --- /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 00000000..11d8ff63 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + digitalidentityexample: + image: ${DOCKER_REGISTRY-}digitalidentityexample + build: + context: ../../../ + dockerfile: Examples/DigitalIdentity/DigitalIdentity/Dockerfile diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/https/DigitalIdentityExample.pfx b/src/Examples/DigitalIdentity/DigitalIdentity/https/DigitalIdentityExample.pfx new file mode 100644 index 0000000000000000000000000000000000000000..f1b4fc482e7af7424ba205fa425b226ccbb04705 GIT binary patch literal 2700 zcmZXUcU05a7KMKyAwU932}&^(rAVCw1f+UdP79L4yz=-5-`szZ@?=H}MdXo3zihQ6>SWx*y48Ogb+0jpB%t zOF#J(7!DM_9KuK9xaA0xjnATd!kfH)qLhCroP@!1tiR8&h&v-ftSDQq`qQ= za(9l?(_YMgD`T`Q^;IItKIn_uCNDfLYzI-X$7pZ(Pi*QO$TX{&aWgJXzL@?Yr z&ACJ{&h~DNu@!0$z9l;pFO)rP<-*XZ=bj}x@o=0xj}%~6e?2MUe_UDdG+lP_lEw%A z%JKf-B|~gv{C>2ReEWdu<4Et>ncy+=*6_vg zYJrq{m2!cKm^U$Zf0k-VR>$~^UUN5|Cnp>G5A)xsZeM$HrC)IW@b!l>3efqEGh2kE zgJ;UAyxrCqWwKtc=Xjelk3?;T6Y_$tFQb0amOaczu+Ugv_||49-xu&6|2I9oYW&pU zzzqjG6+WvsaNy;tIk$(ofbYPXGL!Kbv9UNRV|y^yQTXIYg2eP&+%~xoaVFG4J${Gl ztZ9e0x{YvNHYY<|^($FkB1CDwjx9g@{Ul%Hhn45%4!v%QP0te4jBjB1MbKd`zHPhT*)Dj zxt?!ZMBDcJ$;noTIrFY_O#Ys5P?5#(07>G&FSBW9%q6efNTa7EE5+7R+bgaw(-!Ei zioiAKbTVTcOntL4QYm7e%^Kl-h{W;dk7QPw8)4NL49Eke^IaGz(O7D#pT;O3%sc3BhIhW{6~s7kTv+XYaWf0=f%a` z!ilu7`LmHa1?#$cv3#mKx^Wu~HE<+K zEecd%2lBM0Z)H|QJF%61iecengBQRoa6V@RmXn*S5s4$8+w-W zTt9gxUu?eP8z|VAKr1Bb9lA5d!_&FFpR>hfY@GffbVA)F<~5l#n`RJFjDlg4DNqT8HPnl2kkm|x`rBB zH5`thh1yUGq~Z*g_>n~3BdYq6$ZGn91j*b&)Oq?IoDZU5Q}^L;+DTULs>K2S@$4Tf@}Qqdk!co)K9XI z6dMR&9mszj(ErDLZhWm?*I1vBFV2aJb{#5N74P@fZiaVPZ0;{iXztz> z(Vg3ITH4BvW<)OulfHEWy}b9WibrrX&R4Zj?7G*=Ua`gONb1dK%*%1W+W6bHQ(qdY z23Rod+N+)N<39C)efKi(NcA5V=iS;`c}9yFgX1K%WtG7;DCVo39OXT7dQQ(g)dXkK z;<>vgq+T3zCkn6=*zeLcBhpWHlKlH;dY&H0s>+lSx1=jtz>ONpS}SdHwEDyb(w-80 z%jdT58PskuUCLE?)8uBWk<5Fae4>J*E3~LX+QF(`#9yq0R&-&AX+4k^8iC&ztK2>e zvv_#*@v=J+o{L(JpY#j5Z1QBY+FM3wM-Dfe*#;aV4^!0YPn}MWd|Fc=K0jQgsWsJV zV5oL{qd-Nv-Yvo-IMrYW`yROUo|`$J#!1QYKcm{^ zC9>7>XRaIV>}zR;zC_aNZ!}p%Fz@Yh2mgofwTfE6yx3Th;ZIr5Mwh!=Str^iW@6t} zQP9wK9kNiRn&~KlUUl^OkOmH?Qmv^Uen=?3E5pXXS!~bMf}t^XG;!%<)Q?zo!>FSR zH?BqfP;z1OxAlsZW?%DPucN1>Hc)t*984PcA60YuBpr>IUrFvJ0$p{{VO&q6ttAbq z{Mb6Lt+m<0MX#TA0OhS1H9JE%xi}tW++UGtC zUFEEoOh>2VE!?eZlx@i&xWVce9TO25pFeMG#v6WBu_C7{jb{vg;tJgE!>B#nhv@A%H$TnxE z`ZXKdQsad+ogg|Mj{`$m``9-E!01n+v?00btMH!pOxeso%IMJ+#Tze&rY*aCf_dmST#7c3kM=yPIu*xyjvC~*`A zlwIKvm`wzN_~A@VYMT{&eDs-Uf!nTP>8*?>1PDUB;8c1G|Cae<0{c@{v1i%49Xr8) H1;&2>#y8Wj literal 0 HcmV?d00001 diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge.png b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge.png new file mode 100644 index 0000000000000000000000000000000000000000..3ec996cc6288d68279c1d735c9d627c64d8a48c6 GIT binary patch literal 4077 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 literal 0 HcmV?d00001 diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge@2x.png b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/app-store-badge@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..84b34068fc22aa74f388f6db1a583ac6592f5420 GIT binary patch literal 8819 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 MlZs4*lPCscyWU2PMP1t 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 + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/calendar.svg b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/calendar.svg new file mode 100644 index 00000000..4f6b9bb7 --- /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 00000000..6753becb --- /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 00000000..4c41271e --- /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 00000000..c4582d6e --- /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 00000000..af5c5772 --- /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 00000000..e57d7522 --- /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 00000000..b19cce04 --- /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 00000000..5c514fc1 --- /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 00000000..7ca4dbb3 --- /dev/null +++ b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/icons/verified.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/logo.png b/src/Examples/DigitalIdentity/DigitalIdentity/wwwroot/static/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c60227fabf339e9540e5daac4d2d25e121137752 GIT binary patch literal 2988 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 - - - + + + - + diff --git a/src/Yoti.Auth.sln b/src/Yoti.Auth.sln index f2630d1d..7368b448 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 4cc0cfaf..08bc4b75 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 717727db..44928f57 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 d275c21c..6951da93 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 00000000..d06d380d --- /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 00000000..fa6fc0f4 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs @@ -0,0 +1,371 @@ +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,QrRequest qrRequestPayload) + { + Validation.NotNull(httpClient, nameof(httpClient)); + Validation.NotNull(apiUrl, nameof(apiUrl)); + Validation.NotNull(sdkId, nameof(sdkId)); + Validation.NotNull(keyPair, nameof(keyPair)); + + string serializedQrCode = JsonConvert.SerializeObject( + qrRequestPayload, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + byte[] body = Encoding.UTF8.GetBytes(serializedQrCode); + + + Request 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) + .WithContent(body) + .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 + }; + + 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/Extensions/BaseExtension.cs b/src/Yoti.Auth/DigitalIdentity/Extensions/BaseExtension.cs new file mode 100644 index 00000000..71f96b96 --- /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 00000000..fac07057 --- /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 00000000..da0243e8 --- /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 00000000..c5aea619 --- /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 00000000..15e0cb32 --- /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 00000000..0a633c91 --- /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 00000000..7aa92fb8 --- /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 00000000..92df28e6 --- /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 00000000..05ac8fc8 --- /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 00000000..63ca5e45 --- /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 00000000..d1821a8f --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/GetReceipt.cs @@ -0,0 +1,46 @@ +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity +{ + public class Content + { + [JsonProperty("profile")] + public byte[] Profile { get; set; } + + [JsonProperty("extraData")] + public byte[] ExtraData { get; set; } + } + + public class ReceiptResponse + { + [JsonProperty("id")] + public string ID { get; set; } + + [JsonProperty("sessionId")] + public string SessionID { get; set; } + + [JsonProperty("timestamp")] + public string Timestamp { get; set; } + + [JsonProperty("rememberMeId")] + public string RememberMeID { get; set; } + + [JsonProperty("parentRememberMeId")] + public string ParentRememberMeID { get; set; } + + [JsonProperty("content")] + public Content Content { get; set; } + + [JsonProperty("otherPartyContent")] + public Content OtherPartyContent { get; set; } + + [JsonProperty("wrappedItemKeyId")] + public string WrappedItemKeyId { get; set; } + + [JsonProperty("wrappedKey")] + public byte[] WrappedKey { get; set; } + + [JsonProperty("error")] + public string Error { get; set; } + } +} diff --git a/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs b/src/Yoti.Auth/DigitalIdentity/GetSessionResult.cs new file mode 100644 index 00000000..f5084f6b --- /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 00000000..40896fdd --- /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 00000000..df5fc9e4 --- /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 00000000..a0e60b5c --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/Policy/Notification.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Yoti.Auth.DigitalIdentity.Policy +{ + public class Notification + { + [JsonProperty(PropertyName = "url")] + public string Url { get; set; } // Required if 'notification' is defined + [JsonProperty(PropertyName = "method")] + public string Method { get; set; } = "POST"; // Optional, defaults to 'POST' + [JsonProperty(PropertyName = "headers")] + public Dictionary Headers { get; set; } // Optional + [JsonProperty(PropertyName = "verifyTls")] + public bool VerifyTls { get; set; } = true; // Optional, defaults to 'true' if URL is HTTPS + } +} diff --git a/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs b/src/Yoti.Auth/DigitalIdentity/Policy/Policy.cs new file mode 100644 index 00000000..9bd799ec --- /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 00000000..d85c9694 --- /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 00000000..b315166b --- /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 00000000..1430c5b6 --- /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 00000000..f11dbf7f --- /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 00000000..7cf4e9da --- /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 00000000..de66c6b7 --- /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 00000000..a5319938 --- /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 00000000..34bfc1fa --- /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 00000000..70ccd7c0 --- /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 00000000..2e066d3d --- /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 00000000..da03a5ba --- /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/ShareSessionRequest.cs b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequest.cs new file mode 100644 index 00000000..c1d24ddc --- /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 00000000..1b9a8353 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/ShareSessionRequestBuilder.cs @@ -0,0 +1,77 @@ +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 00000000..eb04f88d --- /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 00000000..bc2e649a --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentity/SharedReceiptResponse.cs @@ -0,0 +1,29 @@ +using Yoti.Auth.Profile; +using Yoti.Auth.Share; + +namespace Yoti.Auth.DigitalIdentity +{ + public class SharedReceiptResponse + { + public string ID { get; set; } + public string SessionID { get; set; } + public string RememberMeID { get; set; } + public string ParentRememberMeID { get; set; } + public string Timestamp { get; set; } + public string Error { get; set; } + public UserContent UserContent { get; set; } + public ApplicationContent ApplicationContent { get; set; } + } + + public class ApplicationContent + { + public ApplicationProfile ApplicationProfile { get; set; } + public ExtraData ExtraData { get; set; } + } + + public class UserContent + { + public YotiProfile UserProfile { get; set; } + public ExtraData ExtraData { get; set; } + } +} diff --git a/src/Yoti.Auth/DigitalIdentityClient.cs b/src/Yoti.Auth/DigitalIdentityClient.cs new file mode 100644 index 00000000..e17070ca --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentityClient.cs @@ -0,0 +1,114 @@ +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; + } + + 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 00000000..2492e8c5 --- /dev/null +++ b/src/Yoti.Auth/DigitalIdentityClientEngine.cs @@ -0,0 +1,43 @@ +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; + } + } +} diff --git a/src/Yoti.Auth/DocScan/DocScanService.cs b/src/Yoti.Auth/DocScan/DocScanService.cs index 404c19e5..811b9f5f 100644 --- a/src/Yoti.Auth/DocScan/DocScanService.cs +++ b/src/Yoti.Auth/DocScan/DocScanService.cs @@ -156,6 +156,11 @@ public async Task GetMediaContent(string sdkId, AsymmetricCipherKeyP return null; } + if (response.Content.Headers.ContentType == null) + { + return null; + } + string contentType = response.Content.Headers.ContentType.MediaType; diff --git a/src/Yoti.Auth/Exceptions/DigitalIdentityException.cs b/src/Yoti.Auth/Exceptions/DigitalIdentityException.cs new file mode 100644 index 00000000..cfbe594f --- /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 78ff280e..ee476783 100644 --- a/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicyBuilder.cs +++ b/src/Yoti.Auth/ShareUrl/Policy/DynamicPolicyBuilder.cs @@ -30,6 +30,7 @@ public DynamicPolicyBuilder WithWantedAttribute(string name, List co { WantedAttribute wantedAttribute = new WantedAttributeBuilder() .WithName(name) + .WithOptional(false) .WithConstraints(constraints) .Build(); return WithWantedAttribute(wantedAttribute); @@ -175,4 +176,4 @@ public DynamicPolicy Build() return new DynamicPolicy(_wantedAttributes.Values, _wantedAuthTypes, _wantedRememberMeId, _identityProfileRequirements, _advancedIdentityProfileRequirements); } } -} \ 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 99e7b663..fdacbcf5 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 4bf1b273..f4f35cb4 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 85bf58f8..e5374455 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.15.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/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj b/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj index 8832cfb2..fc020afc 100644 --- a/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj +++ b/test/Yoti.Auth.Tests.Common/Yoti.Auth.Tests.Common.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 @@ -35,4 +35,4 @@ - \ No newline at end of file + diff --git a/test/Yoti.Auth.Tests/CryptoEngineTests.cs b/test/Yoti.Auth.Tests/CryptoEngineTests.cs index 2f2cf09b..134cdc95 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 00000000..0e43838f --- /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, _someCreateQrRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("httpClient")); + } + + [TestMethod] + public void CreateQrCodeShouldFailWithNullApiUrl() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateQrCode(_httpClient, null, _sdkID, _keyPair, _sessionID, _someCreateQrRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("apiUrl")); + } + + [TestMethod] + public void CreateQrCodeShouldFailWithNullSdkId() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateQrCode(_httpClient, _apiURL, null, _keyPair, _sessionID, _someCreateQrRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("sdkId")); + } + + [TestMethod] + public void CreateQrCodeShouldFailWithNullKeyPair() + { + var aggregateException = Assert.ThrowsException(() => + { + DigitalIdentityService.CreateQrCode(_httpClient, _apiURL, _sdkID, null, _sessionID, _someCreateQrRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("keyPair")); + } + + [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 00000000..901fb515 --- /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 00000000..eec03f9d --- /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 00000000..c610e9b1 --- /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 00000000..878a818a --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentity/Extensions/TransactionalFlowExtensionBuilderTests.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Yoti.Auth.ShareUrl.Extensions; + +namespace Yoti.Auth.Tests.DigitalIdentity.Extensions +{ + [TestClass] + public class TransactionalFlowExtensionBuilderTests + { + private readonly object _objectContent = new object(); + private readonly DateTime _dateTimeContent = new DateTime(1980, 1, 1); + + [TestMethod] + public void ShouldFailForNullContent() + { + Assert.ThrowsException(() => + { + new TransactionalFlowExtensionBuilder() + .WithContent(null) + .Build(); + }); + } + + [TestMethod] + public void ShouldBuildWithObjectContent() + { + Extension extension = new TransactionalFlowExtensionBuilder() + .WithContent(_objectContent) + .Build(); + + Assert.AreEqual(_objectContent, extension.Content); + } + + [TestMethod] + public void ShouldBuildWithDateTimeContent() + { + Extension extension = new TransactionalFlowExtensionBuilder() + .WithContent(_dateTimeContent) + .Build(); + + Assert.AreEqual(_dateTimeContent, extension.Content); + } + } +} \ No newline at end of file diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/Policy/DynamicPolicyBuilderTests.cs new file mode 100644 index 00000000..b1fec0d3 --- /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 00000000..a856536c --- /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 00000000..e3497323 --- /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 00000000..42295715 --- /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/ShareSessionRequestBuilderTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/ShareSessionRequestBuilderTests.cs new file mode 100644 index 00000000..bc52fe01 --- /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 00000000..16b73230 --- /dev/null +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs @@ -0,0 +1,106 @@ +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 void TestGetShareReceipt() + { + Uri apiUrl = new Uri("https://example.com/api"); + string receiptId = "some_receiptid"; + string refId = "NpdmVVGC-28356678-c236-4518-9de4-7a93009ccaf0-c5f92f2a-5539-453e-babc-9b06e1d6b7de"; + + Mock handlerMock = SetupMockMessageHandler( + HttpStatusCode.OK, + "{\"id\":\"" + refId + "\",\"status\":\"SOME_STATUS\",\"expiry\":\"SOME_EXPIRY\",\"created\":\"SOME_CREATED\",\"updated\":\"SOME_UPDATED\",\"qrCode\":{\"id\":\"SOME_QRCODE_ID\"},\"receipt\":{\"id\":\"SOME_RECEIPT_ID\"}}"); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + + Assert.ThrowsException(() => + { + SharedReceiptResponse response = engine.GetShareReceipt(SdkId, _keyPair, apiUrl, receiptId).Result; + }); + } + + [DataTestMethod] + [DataRow(HttpStatusCode.BadRequest)] + [DataRow(HttpStatusCode.Unauthorized)] + [DataRow(HttpStatusCode.InternalServerError)] + [DataRow(HttpStatusCode.RequestTimeout)] + [DataRow(HttpStatusCode.NotFound)] + [DataRow(HttpStatusCode.Forbidden)] + public void CreateShareSessionNonSuccessStatusCodesShouldThrowException(HttpStatusCode httpStatusCode) + { + Mock handlerMock = SetupMockMessageHandler( + httpStatusCode, + "{\"status\":\"bad\""); + + var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); + + ShareSessionRequest shareSessionRequest = TestTools.ShareSession.CreateStandardShareSessionRequest(); + + var aggregateException = Assert.ThrowsException(() => + { + engine.CreateShareSessionAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiApiUrl), shareSessionRequest).Wait(); + }); + + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + } + + private static Mock SetupMockMessageHandler(HttpStatusCode httpStatusCode, string responseContent) + { + var handlerMock = new Mock(MockBehavior.Strict); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage() + { + StatusCode = httpStatusCode, + Content = new StringContent(responseContent) + }) + .Callback((http, token) => _httpRequestMessage = http) + .Verifiable(); + return handlerMock; + } + } +} diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs new file mode 100644 index 00000000..805165de --- /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 00000000..c3bdf660 --- /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/ShareUrl/DynamicScenarioBuilderTests.cs b/test/Yoti.Auth.Tests/ShareUrl/DynamicScenarioBuilderTests.cs index 22781fb6..3bdb9fec 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 b39fead0..733871f0 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 00000000..1aa053d8 --- /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 86b8ba46..e6592288 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 033a042f..276149b2 100644 --- a/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs +++ b/test/Yoti.Auth.Tests/TestData/IdentityProfiles.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; -using Yoti.Auth.DocScan.Session.Create; +using Newtonsoft.Json; +using Yoti.Auth.DigitalIdentity.Policy; +using System.Collections.Generic; namespace Yoti.Auth.Tests.TestData { @@ -17,6 +18,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() { @@ -26,18 +63,18 @@ public static object CreateStandardSubject() }; } - public static AdvancedIdentityProfile CreateStandardAdvancedIdentityProfileRequirements() + public static Yoti.Auth.DocScan.Session.Create.AdvancedIdentityProfile CreateStandardAdvancedIdentityProfileRequirements() { - AdvancedIdentityProfile data = new AdvancedIdentityProfile + Yoti.Auth.DocScan.Session.Create.AdvancedIdentityProfile data = new Yoti.Auth.DocScan.Session.Create.AdvancedIdentityProfile { profiles = new List { new Yoti.Auth.DocScan.Session.Create.Profile { trust_framework = "UK_TFIDA", - schemes = new List + schemes = new List { - new Scheme + new Yoti.Auth.DocScan.Session.Create.Scheme { label = "LB912", type = "RTW" @@ -47,9 +84,9 @@ public static AdvancedIdentityProfile CreateStandardAdvancedIdentityProfileRequi new Yoti.Auth.DocScan.Session.Create.Profile { trust_framework = "YOTI_GLOBAL", - schemes = new List + schemes = new List { - new Scheme + new Yoti.Auth.DocScan.Session.Create.Scheme { label = "LB321", type = "IDENTITY", diff --git a/test/Yoti.Auth.Tests/TestTools/CreateQr.cs b/test/Yoti.Auth.Tests/TestTools/CreateQr.cs new file mode 100644 index 00000000..0474d430 --- /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 00000000..7be4792a --- /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 828d95d6..145462c9 100644 --- a/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj +++ b/test/Yoti.Auth.Tests/Yoti.Auth.Tests.csproj @@ -38,7 +38,6 @@ - PreserveNewest @@ -67,8 +66,14 @@ PreserveNewest + + PreserveNewest + + + + PreserveNewest - \ No newline at end of file + From 01a184f189c3f084ce2f269e585f838f52535e61 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Thu, 19 Sep 2024 21:47:43 +0100 Subject: [PATCH 10/10] SDK-2468-added-net-idv-support-brand-id-in-session-config --- .../DocScanExample/Controllers/HomeController.cs | 3 ++- .../DocScan/Session/Create/SdkConfig.cs | 9 ++++++--- .../DocScan/Session/Create/SdkConfigBuilder.cs | 16 ++++++++++++++-- .../Session/Create/SdkConfigBuilderTests.cs | 15 ++++++++++++++- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/Examples/DocScan/DocScanExample/Controllers/HomeController.cs b/src/Examples/DocScan/DocScanExample/Controllers/HomeController.cs index 1713dc4b..8b26f099 100644 --- a/src/Examples/DocScan/DocScanExample/Controllers/HomeController.cs +++ b/src/Examples/DocScan/DocScanExample/Controllers/HomeController.cs @@ -106,6 +106,7 @@ public IActionResult Index() .WithErrorUrl($"{_baseUrl}/idverify/error") .WithPrivacyPolicyUrl($"{_baseUrl}/privacy-policy") .WithAllowHandoff(false) + .WithBrandId("brand_id") .Build() ) //Add Required Documents (with builders) @@ -192,4 +193,4 @@ public IActionResult PrivacyPolicy() return View(); } } -} \ No newline at end of file +} diff --git a/src/Yoti.Auth/DocScan/Session/Create/SdkConfig.cs b/src/Yoti.Auth/DocScan/Session/Create/SdkConfig.cs index 3644020c..1fb0f7c1 100644 --- a/src/Yoti.Auth/DocScan/Session/Create/SdkConfig.cs +++ b/src/Yoti.Auth/DocScan/Session/Create/SdkConfig.cs @@ -40,7 +40,9 @@ public class SdkConfig [JsonProperty(PropertyName = "attempts_configuration")] public AttemptsConfiguration AttemptsConfiguration { get; } - + + [JsonProperty(PropertyName = "brand_id")] + public string BrandId { get; } public SdkConfig(string allowedCaptureMethods, string primaryColour, string secondaryColour, @@ -51,7 +53,7 @@ public SdkConfig(string allowedCaptureMethods, string errorUrl, string privacyPolicyUrl, bool? allowHandoff = null, - Dictionary idDocumentTextDataExtractionRetriesConfig = null) + Dictionary idDocumentTextDataExtractionRetriesConfig = null, string brandId = "") { AllowedCaptureMethods = allowedCaptureMethods; PrimaryColour = primaryColour; @@ -63,6 +65,7 @@ public SdkConfig(string allowedCaptureMethods, ErrorUrl = errorUrl; PrivacyPolicyUrl = privacyPolicyUrl; AllowHandoff = allowHandoff; + BrandId = brandId; if (idDocumentTextDataExtractionRetriesConfig != null) { @@ -73,4 +76,4 @@ public SdkConfig(string allowedCaptureMethods, } } } -} \ No newline at end of file +} diff --git a/src/Yoti.Auth/DocScan/Session/Create/SdkConfigBuilder.cs b/src/Yoti.Auth/DocScan/Session/Create/SdkConfigBuilder.cs index 00157f36..cd25a6eb 100644 --- a/src/Yoti.Auth/DocScan/Session/Create/SdkConfigBuilder.cs +++ b/src/Yoti.Auth/DocScan/Session/Create/SdkConfigBuilder.cs @@ -15,6 +15,7 @@ public class SdkConfigBuilder private string _errorUrl; private string _privacyPolicyUrl; private bool? _allowHandoff; + private string _brandId; private Dictionary _idDocumentTextDataExtractionAttemptsConfig; /// @@ -236,6 +237,17 @@ public SdkConfigBuilder WithIdDocumentTextExtractionGenericAttempts(int genericA return this; } + /// + /// Sets the Brand Id + /// + /// BrandID + /// The + public SdkConfigBuilder WithBrandId(string brandId) + { + _brandId = brandId; + return this; + } + /// /// Builds the based on values supplied to the builder /// @@ -253,7 +265,7 @@ public SdkConfig Build() _errorUrl, _privacyPolicyUrl, _allowHandoff, - _idDocumentTextDataExtractionAttemptsConfig); + _idDocumentTextDataExtractionAttemptsConfig, _brandId); } } -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/DocScan/Session/Create/SdkConfigBuilderTests.cs b/test/Yoti.Auth.Tests/DocScan/Session/Create/SdkConfigBuilderTests.cs index adac260f..e8fbdf86 100644 --- a/test/Yoti.Auth.Tests/DocScan/Session/Create/SdkConfigBuilderTests.cs +++ b/test/Yoti.Auth.Tests/DocScan/Session/Create/SdkConfigBuilderTests.cs @@ -194,6 +194,19 @@ public void AttemptsConfigurationShouldBeNullIfNotSet() Assert.IsNull(sdkConfig.AttemptsConfiguration); } + [TestMethod] + public void ShouldBuildWithBrandId() + { + string brandid = "some_brand_id"; + + SdkConfig sdkConfig = + new SdkConfigBuilder() + .WithBrandId(brandid) + .Build(); + + Assert.AreEqual(brandid, sdkConfig.BrandId); + } + [TestMethod] public void AttemptsConfigurationShouldResetSameValueWithRepeatedCalls() { @@ -232,4 +245,4 @@ public void AttemptsConfigurationShouldAllowMultipleCategories() CollectionAssert.Contains(sdkConfig.AttemptsConfiguration.IdDocumentTextDataExtraction, kvpGenericAttempts); } } -} \ No newline at end of file +}