FSD0R%Q%SY^O!Ey3n{Mf9>T`Tid#`
z)B2H!N?k4LJhGu+Q;JBs7IhR+52`tW4?3N$l`TLz^H;g?`y*LuDyK%)Jmw%p&*KbR
znihv<@~dtcvb-MKeJ8HM-g$#U?7Xf+e19BXSDQNUU_dNS_J&J@of^c|fTp5mh;GJ=
zGYX0+{655nV8uT$n4Pap=lhMGhSF@)V2c*bad#q%*3sSi&D2)7DKd
z?~^-5CQbHOM#QGF#b#Zah8e$K3pgg_)drF8t6ki2k&D@%Ki!}VJ)+DkLfv2CEPC!Db
zmnQ0UAL^HE(7ouN1Pzu=6LFrzw5Q8I^9p7)AI_!&*cvyhd8e)&C?`{U(nQ&5=hC}u
zPQw0PG9NvbD0zmbqx=AAG{1PXRpiW8cXoleuDI}6K~BWie)oj0)}b+qPxw^F;w26K
zyp298&7ar6cU68!fLuGyyx(OPIVsSNbc6r`XpHo9w()a1#Z
z-P{UH1YD@FPfywE?L@)E07fLN)SQ`^)Jpfe2*SC_S;7_%7=5X)c=btXVt$aD{OEAy
zuY!fTY?sEeCHvS&q9AZLbPHNp&n0da#+v~Yt8;~E{G7(#cGD7R1y(%o<5`@ciK+Ga
zM+OniHFg*37t}~KAOq$<(AqAP_dlOb?u~FcbkftJ$4>PtU`j9Q5P8V-x?ynASYla+
z6O+XUo%15nKoQ+J+byM331Z`lV~oZbpcdOh2~XB=xKo^Bh#>xGsPO)BuqVYKI}V%@
zT`4BDtqNd#816)sg5cgMfQg1My*4|23jkK{lqH=Hd-Ryy|G<)wbneM)MWQz-FXy$q
z`@u?FH5gMDIhW^h{+?2kkd&ae4bc8C31+0xD}JMpmeX`x)P&Oaup}EH+5VC^ffUW;
zuFLg-E0sT)+RZmUaLLtox(u=f*~To`%RzLkJ-G+`Q5Z#PX~wwI`|{t!jdA@+USv>?
z!H@KB4xsTn%dAY)i6r@Zp)3p`LL)m89e>kZ=Jq)HDUB^Lb8kFRKzmMfg2BjU?K$tm
zDzR5{H-kOE$Z1D2);xZbC^AoIxT`Zpt*!vX_2p{kox+&j9z!l-x>)v
zn>W5i#W5t4;gvXzKN?M@_Djpp)+BKXObG6!$;e|V?jgxi(#c(vbQl~F+_Hr(XSweY
zLPFxcvD5B2larc=#cV&wAch)ZAo}wl2K|&Js!k|DF!kIE<^$rm_2@S<;gf$6=Rq-f
z@5w?u-kq+}DI%jc`$_o@EfR>n%;;?>>iTlAsV`SGpXeF-q5ZfN=wuR#)~
zso&=uTAx@L?cCy!6pj&wd<><-z(j_fC%5A3@2Nv4SL>;JX>1w@Y=MsEc65lY@!mwV
z5oJ3BkXJ^c8})m8^@GSJG#KWjF}OMjX!kJN$)CD%-UZ;0jPR9rHXmNx`Gs)_IXiWs
z@zl7CoDnv{B}n7+?sAl4r=os>8d>~8A7b7ts%XfFOn+2oD+a69|z-qn8NwWWMFH>9ASfD)>#`@
zTMY>LyH6MMG#1}O_u#WGr1E$5LU)GVQi7HJ3$a_BRDTQP8h4ESBa^o8JLw5upLVcb
z(>Hl&x}Qmi8D}{PlH<`bDfJ0e==5fNV7ecDU?%p&jlI>4_w3v2V5pARUvQrTfUdq2
z@2LYSq>PJb#`7xdQ#a>=F21UdAE|j!U452p7IL~R4;Z!d^#Og7B-No;QLsC^d16x*
zYvc0u=v7>T)dgf%cqq#u#sn-^#eTQbx^OJkI~mb&h2ge}t8`VpbM>85qDY0Q$EDO*
z6EVUBO|by@<|JB;kB>U`y4^PLcdL@&bxGa>#Ll=(ysp~gKq_k7+`3ZqKzt_uT*6Z*cT#JPlnS7)}{uFvfo8kWjxnX}z5H2DdiOlV&UPSLJHjH&ON^Ql(`e7RJhQ1GZmXU#ZOqJLscF
zj`$0Pe4O9o)n0K{zjbFm=2$eS;kf9*#|mQ}KntZyjl-jeW2#rihljF-;6=p|x4kLl
z2S$!EV*eV;imL}8af^yOeS!je)ANo!Pak17YF;%9_@~ege5%@CoRJZO?|<;r@ag3x
zaQmYFXzD+6BgKtnDGOY=U&`CMG6-W;L(5iVfL2Zjq_^G9HNj7m@F$c63&=*)tS62j->fIJjwQ|k3IV2
zwLd@SzwDZ!e<1evLx}y?Be*}_z+an`KJOS^){f6c-xBo-N!k%=JrTRh%DPfZM>i7C
z-YO@=&Cffu>+7NXh?Lm_Y^o6~s>-7dBAL&l%0(a`bhMNe#Xd<2{KDKEA4oj{>Y*F`
zL7b`xBDz1p<0{J&HO>Uop5C&itU2h}wP*B2I_1KayQ4I!ea&{zF1v&Fw#QR)zI%i{
zjzf{;P{(+*iVoSDI_z$E{n@i2Sf*rV^^#FIv){?63{V*&6y?I)bYBm!<@4X8pwuNt
zfg~zcI#|q`NjrHVFbudVz_tju%wQ{?GWb?0d3ba^afHZdR3Z}#P1OsAin@}1?aU>K
zWE|%GTH2EzHrxX`c6vHzz4cz?z(zyQzv#FMG5Et3fuo!GqCDJR0?-hZ8xkbePB
zC`+7#pmbqUyTDw5C)d5x>#c#1prrX^A74W(`y&oziL_cqbRuQ8(!ovEJppyS@|D
z)#!1=f<+(oLPJu*a6INUj(spZ4d)>&G0AsP?c7$&&Wl5#$y(9C?2WYKRsL5j{2hJ{P+%6}`CIuOS(Z&X6_bEHmP(N3<_`*p(
zzJ<%9%owFAZ@`Jp_C!;nNyMnMa;Vq-QZja=-6Z#yTaJp98>Pn1UE^UEtV(3!p$U7O
z+M(JK;73Q5R`$ivbgzFfwx#Yc&cxFu-Ayyz{-gy&Z1qw>o&Xw;(xJ4(AP>1zViMqI
z?kR+C{bM{Lq8Z?hF-OFX`I#=_{YTjx68Og>%FNGg|CiAJ;L;6tDCsc>{9k2$UEHzv
zG+1h(naB57Z~lFtiu(>Heyy767NZ(*J7UH$1r@43MLn4!Zi#u@YHs!B(SKurry
zI)e6`0t_a$7jDu&QQz&hsa!41*j1OuGCvNeh$O~V-`n0)_>UaW9az+husbHM0+-SU
z5e^9oXcElm!swqfvN2x^;=jmL37G`2O#+VenJusT&|>^NwWx#7L
+
+
diff --git a/_examples/digitalidentity/static/assets/icons/calendar.svg b/_examples/digitalidentity/static/assets/icons/calendar.svg
new file mode 100755
index 00000000..71ce6371
--- /dev/null
+++ b/_examples/digitalidentity/static/assets/icons/calendar.svg
@@ -0,0 +1,6 @@
+
diff --git a/_examples/digitalidentity/static/assets/icons/chevron-down-grey.svg b/_examples/digitalidentity/static/assets/icons/chevron-down-grey.svg
new file mode 100644
index 00000000..89f55a6f
--- /dev/null
+++ b/_examples/digitalidentity/static/assets/icons/chevron-down-grey.svg
@@ -0,0 +1,9 @@
+
+
diff --git a/_examples/digitalidentity/static/assets/icons/document.svg b/_examples/digitalidentity/static/assets/icons/document.svg
new file mode 100755
index 00000000..10fc1de3
--- /dev/null
+++ b/_examples/digitalidentity/static/assets/icons/document.svg
@@ -0,0 +1,4 @@
+
diff --git a/_examples/digitalidentity/static/assets/icons/email.svg b/_examples/digitalidentity/static/assets/icons/email.svg
new file mode 100755
index 00000000..67880ef3
--- /dev/null
+++ b/_examples/digitalidentity/static/assets/icons/email.svg
@@ -0,0 +1,17 @@
+
diff --git a/_examples/digitalidentity/static/assets/icons/gender.svg b/_examples/digitalidentity/static/assets/icons/gender.svg
new file mode 100755
index 00000000..94a0ed90
--- /dev/null
+++ b/_examples/digitalidentity/static/assets/icons/gender.svg
@@ -0,0 +1,6 @@
+
diff --git a/_examples/digitalidentity/static/assets/icons/nationality.svg b/_examples/digitalidentity/static/assets/icons/nationality.svg
new file mode 100755
index 00000000..40cbf76d
--- /dev/null
+++ b/_examples/digitalidentity/static/assets/icons/nationality.svg
@@ -0,0 +1,4 @@
+
diff --git a/_examples/digitalidentity/static/assets/icons/phone.svg b/_examples/digitalidentity/static/assets/icons/phone.svg
new file mode 100755
index 00000000..adbaad99
--- /dev/null
+++ b/_examples/digitalidentity/static/assets/icons/phone.svg
@@ -0,0 +1,4 @@
+
diff --git a/_examples/digitalidentity/static/assets/icons/profile.svg b/_examples/digitalidentity/static/assets/icons/profile.svg
new file mode 100755
index 00000000..62278ece
--- /dev/null
+++ b/_examples/digitalidentity/static/assets/icons/profile.svg
@@ -0,0 +1,4 @@
+
diff --git a/_examples/digitalidentity/static/assets/icons/verified.svg b/_examples/digitalidentity/static/assets/icons/verified.svg
new file mode 100755
index 00000000..f6e1d94c
--- /dev/null
+++ b/_examples/digitalidentity/static/assets/icons/verified.svg
@@ -0,0 +1,7 @@
+
diff --git a/_examples/digitalidentity/static/assets/logo.png b/_examples/digitalidentity/static/assets/logo.png
new file mode 100755
index 0000000000000000000000000000000000000000..c60227fabf339e9540e5daac4d2d25e121137752
GIT binary patch
literal 2988
zcmV;d3sdxoP)Px=W=TXrRCodHTYGR+)fxZo?%mzIkYEDgl?2c#&jJBdpehy$GAM{5Z>ZDhIR0T<
zrqk);pUyZ_ZKvZnw&RQ)|DY&n)lw083ql|vQW+vbK|*+g3C}zrktF-L{eI`>ZtmV}
z$nIuCHZwUhH@o-lIp6ut_dDP7+&Csoou;FwC4~f>Nx?-A6G{R-U?kB-(CobEdU9GV
zhrICZDzqkf|atnLVsfw<
zJE-K})_NScO(0!)+XF^dO5ZkjD>G&LJ>sny*@SF(#9pl*#xss%=(NqTA*$j_Eao=!
zxXmv#&57E2G>z7v(@vYe#bG{U27^NJ2mA>5gGlju5R5X!B)@@R0DfNpDJkB_en9$I
zo2F^XX;)QMqp7K>1rXuEQTrYA%)x*+RS3%{t8lDD_+2J40>=4bxc9#axP-R95y4^8
z;G6YFz!%#jQG-E&a(8arvK2-9OVp0_ie}88`!viQ+}6|tv9PEZ+ji_!AinoO16D3w
zDuDE91qf+XR90f+=B+3@S*}2FIwcGgfLfYp#_ei2>aPKpkHS^H9f1euATWM8^pswa
z0SSs^eDxwge+!v-zKM1tV3V@sJ@9=m^U9}yp{_V+N2Rj_!Y3NBe#2+jSF~RV-|cpz
z_{#(MRD{M*SS4c-7|=8q4t#YGo40O<-|vUV*5byEs<3hFd!pa;!Fi7q$Lcnhw>@58v8RR6$kxjrnwD3UE<^`
zOQC)73j8x)MX>k4a1HTfK<}Mrm?<)^97*%12;M=PZ#o`_q+=W}JerTli)zwWPt7#%
z68aex7!(N2$NafZ$p&zssJKM!R%(pP<-o4OLiK#klP!R-|2IdD;-mHdQRNI12>OEi
zh7H9F%a@=}AHz(M&h6U`0@`O%@@N5qAW-^k-qm0;7h
zZJ>jvro!cg<60S9#cv?^lUD$Dx*<$ah~Uu25F9v8geL%mz~C)S-c7hSzGRpg3R@ho
z@A2OQ_Y7zah78d?LRV-exQhy1IIG&wdo{u%Y=_v2;-g2tFrpyY>_+^TqFMX5ZiadKDg?G*LaX{>jT7
zj0xk%;mMiP@#7WltMoiYE2?Zgc
z5)cBj{qtQ|y>=aHYiq+aVn|*dRxDl|283cjz&LmQ0xB0nu|HIPgwH8ZUTl7U0R3dwE?cq?
zIeoJ&W<>q0{*cb4?y@5v@Ii1JYT;la)=gT6z~CAw^z@8$JUwSNva(uA927GBO1}6CnyR)M%^OXd
zLPNeY+fJ1dOFWG=FQ=KQ3%HJem@MI=yR;62!G#hKuQ+v&TQXuxqsicJcWN|f42eyl
z@ldO(Mhb6nw3?b)16Ym~5OjB%pc^S2kAwJK7?Kb$jJ3<+ZM)EZA89>w^qHBx)mYm7
z#>OT|7LP^h+U_%!P-<`9G=c|$k%821euqOyBx60`iW@^UGoY={L5Y}vjOhrT|nav){$
z+)~;fD4DyIYr0G8axmyq@^f=VJ0#+2cJ$PiHS0c*y3>xR&fl?xXyDS;9U9$vjBHK)^O&SKf(=h0V6DE3w-
zukftjuo0`@U5A^uZW*}=3w=_b@)xX-f}R8hca9(IB)jy!%faC4sS(Umj-Mz~r+aQm
z{8&<_GhegzeeBx32S1uJ8DmC`L}u^aAP7A7;Cafa(`QszY$S=|93-y@7W@>!Jjvj@
z5fGF^Qh=ct5#h5$Un}A1DJhSz{zVPpRZ)2v|N3}6)-g<05u5Rnf=kXYLMMQt5s*{3
z;OGSij9W^Q-L56nXV3v#uUMoJ5JC6J%#_l}B8(g{Tt!7@jzU1_*jW$bnLCp^kH_5%
zglGekJ8{_lhhBtl^2?H6FmH)vcJ}1}>vgxsCs;GMn<+46!9gj1m^G?sm&Ni*KdE8;
zY~^z7lM}=eF+&V-36v%@!{{+|WuT-7bwqDLp4GX}x_a^+1ykoklrN*=lV6~(Ng5%z+jCfe0pylj_%`67Z*
zV3Vw0z>y%L%_gim&;M7>?0*|JM?!suSG2;~2yQ&hG*6jgwq`u>I3_64>5LEz9eAvcZJXBC+WsuM8C-H(z6GQ
zBtoi{&tMY8Y}Vn3J(}u_buqFs_^}oI=rnm$VSC&eJS3bZRe=24TG$hYjeCpsfs($l-MB=CRu-jS!@!@-~c0000Px~qe(O+IO{fBm{`nU}JlXu>r@}X6&&uGc~CxPbCji
z$vpTa?|IK#UQ)@!B$Jv{C85e=Yz78q@G=-1n?VQxS`b>%u5N9ued)gW{v#w>;-1?r
z^`h;ZsxDpK(
ze1&Nh73GJsM%##D5|^J*2N9-#Cx&!>2N0;tic%iTsH3j!SPCd$?ATYY25v?H1q?Il
zsB1fx0ty&A_SLI_n^8ak!;CuW+K#1w0>+Mg^=jZ|6i~o0qmH_^V=173v14Do8n_t+
z6fn%Fqps~(3MgRg*jKLxZbkv~kPx#c*LZDcWSAzWCTU_~ns%3$AzsB#=)@Z9H@f|C
zcMlB=4$|JL-4q`ehbRE^FaV=E{GFSgnW6EC3F_|d6#;}jJ?61e)`m|>@4HKSYAJcs
z%ZL&(iLBB7nhE&2#obL>&mGdx#-=JMMWkm|lE?9aVYe(3{iKa{lGc3H@H(H=QHSU7
zhtlhzybuv!jJe0@!M%EN4G+=m{5;u{5=F#rOf*8U!&F+^I}qjCLOs2GG&wm%$BrH$
z8)BI>EqEsBMOgXp=m>Q_?xK;gQF6Q8G&(XyE|-fYCMQitqzyHbHrY=!+)9@II{-!w
z>GA30O#?_$9Hd)XTIF*)lV)KmY!Al7#1l$V=_FSw2DcB}uOJ3mdVzN{nOeg0T
zZEd5z{sD42hpD8vka9A!C?*Eru&j@B@LG%cxjAsYY3k^BOt)^=kkd5;jxoNv8`A$R
zZg3R-Jt;g{bbB6A@hhZ*bLh4da1eYj__vOtdl5=KJ4V_#z_*Etjt>CM$v+ypcV1jK
z?RR2!#6}cWU@$1UAkLis_;*5#@_H9jSY5tyg>Q6|+1W)l|K>hniYi
z=-TIB(eUsv&CJd$RU5j$ys~V1m@E&@6UAo`6+BP+{-2UJ&W?TtFaCwDqdTLfumEFt
zbd*}#+NtSbGr7mcu-x-0D3-`NIW+~*YJh6)+@*M1Jf$Wl2esy-8{*odM=b&vOPpe<
zE&j{PO^1d@i5E8BskuW(o;yqlHk)5SG6(AE>7|;QTETf{X6DGN%a^YDwz=kG)T_--
z5P28K(su{z1`JqPyc0(z@
zTlat-cXiVgQ($e>ZFYVmy9Q27+Y6dVmIv-1KWiG-Ba!uV`nwppt>$9ad
z0*!(5H8eF--TenNH9ZqPQ22IaeugYVEo5o>jI0CoWSJO%NhVBS^leuiFH;~az`$oY
zprV75OifSHC!bstrcM@j)>`J~=Bf7HJ^JFS>%^8Qe}yGF4|A{p`q+m0YHI5s_TBJT
zZRoGFXf@>`OUJ1ZM<{Z&IzYPm<4ptCU5UHjro
zp%}mUS7#_aB{d++(V){w?XV7A{^Oqju<7M0Wd}BhZ25V)boPzYl%A0$wTQ9xiAA$7
zzrGGJs!vv(;rVEopcA3Ll$VxJD#j5PA4k#AQ8YO*MGX9o&MtC~PrxwXzrWmoHa#kQ
z68iOjCjIq)B0ZLu#mcQ}S}d$uS=Hcd!2YuJjqk@dQ22597(YJdL*3`RXP&Grm+|F*
zg~+H_tmG+rnaGxgXV>P3(v$PyyD2fMbyJ=T3otlwLV7wAPp1Rb)pWnUUWiouwKgiO
zn6s}#>+Pl6kmX-|{wO6TCP4m|6W;|TxCP=`L*qmEDPRVa)VEr$*>t{R90l6v(qxK;eMRdzvgG9i%`0hCC%Fh@#~Bh7M)jlT}4p
zLI&x}!_$C+#eZ1GS~^}FIiFv=ra^?$+pZ!>fH5$1ybo`7HBo92aJDw5Bm-KHg&|>!
zl&}DU#~vRaPwc>Z?#N-GLyWn{Xko!v-{AXM7a4_aQd4`EN{R}=JsctlgJt=v$9JIs
zL<Y1Jj&;P_Zhu+?k!>kzrDHSmH#ff*Uv@JfbKNN^c6$BQ6LjYF*Xhs!
zd`SZ+%w3Z0b~<|GkoYw;J*0bZe*Wig{~M-YSj}W?72vRReNWc*>*UQljBt|j09sSG
ztd{~|1;$dRD$C1>eF5zqotShtO?3{Q7vtk@y85SU0vK<+afXt>eYlLl^vkmUh0iVu
zIbAx5o-`Z<5TvH0(EI1!rX1)y{!-hp)oEbRNwu}L@Yu__8;gqU5Bca4HbQv$K=By1Hp#!08thtnB9HW+8)O0TmzwgyW+Y
zEiwijTRx>Drpif!gD%v+d~(GB<0>;FT}
zT<81m{g6sw>TN~{$_y-7Mx09)HQfC{c3DoxCM=#UBMC3JmUy6)$pM70ijS
zC9)FDI)_Md-R^q@BE}#Zn8^Gta3L
z0aGAiqSSJaAday3QC3z$hY#(i`Gbs)8=SX4-HSuA9xhZ@qbjqQRk_xENM8#~%o!vDaAFkoX){
zKA`?vD+R*$hW+#;0hXo=_!de^iYW_nJ1gkxE$r*afhpGEa8Ln$v9YlM=oC*|V{SAG
zi`TqNOaqt`;LT4RZ9ibMY$;7Um_Gg~Df)O7eZ#{0l|<)Ubqy0;^$UH@zh
z0eRTJaA4manukL9_18DVq8~Gzej&mb
zJ@{3UB!omIE6s@BydLT#93?hrWS4v!TG?
z4qBlXJa=$EVc#a9bKI(_38dfR9xF`0NlA7n!|%YKP`qe;3i#=#H!{Z}1Hn}uT1u8aX%!@vNHX)HeCwv7rq)L2yGHX~KI&FqW}j7#FFN1w$~
zQU0q+$SJU4I>zdeBtooWAA#^Br>8>aFrS31pt7H$5IO~KY_o1qpd&(;g(+BAzE*b(
z{{y>1$KddiNEL1=9(oO1@r(<4Ew;*1z%ZsyaA!B3WBBx+?f8ce9VGY66n+2QEgBga
z^|`w8@Z^bC5SHPf^dQ19mTSb^nn89ADaQaHSY(UjCY=oQMEGN*7CYt?3sXR~0><)t
z4B^41;TTI}Vsm8)ywzqAuzYV3?ny%&R7eUw&(T`6lZg{mvStMok`{s{)e2;#Hklhn7i
z&>VA{qgbONGYBw&^wIYm3d!KGjCFia+HvlC(ZvL&!OOx2#)B&^a?uP_Yc
z=Y_zkFsI1ClGJF4Y1D(2hqrFmiggl^(lh3f=F!{2U?e(ax((0R!)x{SzEY5{*y{Sz%e9^H%Yijc^)Bx$X(9Lutg4S56INXpLok
zo1KmJToraMdtpJISHJ)P1;@w*$3S8OGb_)PYoAkHeFH+%XM!ZV{P7tih3i7Ll#743
zD%PJE-l+i;F$q}ry$b+Q9K(=CL7tR}WD?QFSMtX88jd_!vTtMC&WDD6s>1cu`OoS^?z<5G%
zj?(3ob37G!9}lC#YtcDf59mcou*8b;e=_w{h$BC
zE=q2C`S>x!cO5`l&V{LMTdRS~|V@$`9zmrAy|M6r#?>%a`ddcWxs!P&xhW&;N#Y
zSL_NH9PGerZfT|SpIpFp36I3?Pd;6F_7HlINWY0aGZeRYmOP<8qkyrBqP&E4@182U
zhkUz_kp|SC1~m9{qcmXt6C>RQOjB`t8p)zd-?G)eGj$D>)x+b@e`>Wb+<-
ztOLjjrxo>hW@}rshXjp6e3I^Ihzx!5iZ6T6SLG#EF|HrvVPvFr0;M;V2wLf@hhy
zwxT`cqV?wN7b27%Wo}Yl@tpc+-4sy3Fd!XIu^MNRYr>A&^iIr^^XoI1OqH
z;#9Pa^d#s$j+e*_Gp#C-p@7`{5}EclXDg8B?NFxAy!INMK6w%Wh8rikc*;(br~0Sl
z*?o3%(rTObN&&;<#U>{wiJZG1y#KDqZWytptwPaGBF~{;khl6rZH
z$MEy^IlWIB0t{aSFj#r!jWjusa29)rb0+qX6mSU6L90h0{wbxH^o$D9E6MWlnMK?3>hwEtorCa=r2GO>B=m92#)I(&R;2e|`5mu3*{WBbneEPc&KQzrP#oK#Dx^S>zHul!z%p`kZwPFWdPyc|}ube|V
zX0TshAl566h}CWB;0(LOo_6f8(=&H3?&U3(t8NS^^q6>>|IvSdtj58MB2EjS4M#+;
z&YUtSpXQ2RmcP~rMiOs2V6f!CSxml_YJzu;TtSN;hYhC#=HMpi
z!`AEqxJM=j9pblC>KmFwCJ{4W=p`pf&pkk1TZmHsEH$qFhLi#d7$GEQVqyZq(9QS7
zV?gK%6d|N~c&geMVKzKtQMD=xgp2|T7$GC2s%Se=KmlVry-hWikWoMZBV?ph6>TR9
zC}3=-x2eVwG72bQgp8D`qU}Th1&r85
zGE%CFwi5+RLqd3=zH4ZhVxptvd%eJ?CnTjg2+SbsWY~Y^|zik&xM6WfW*xV5=0&B&sM1C<^#eAj+fDZ+uUvV?_Z)0iyp0%kz
window.Yoti.Share.init({
"elements": [{
- "domId": "yoti-share-button",
+ "domId": "yoti-receipt-button",
"shareUrl": "{{.yotiShareURL}}",
"clientSdkId": "{{.yotiClientSdkID}}",
"button": {
diff --git a/_examples/profile/dynamicshare.go b/_examples/profile/dynamicshare.go
index a136238a..a80367a6 100644
--- a/_examples/profile/dynamicshare.go
+++ b/_examples/profile/dynamicshare.go
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"html/template"
- "io/ioutil"
"net/http"
"os"
@@ -38,7 +37,7 @@ func dynamicShare(w http.ResponseWriter, req *http.Request) {
func pageFromScenario(w http.ResponseWriter, req *http.Request, title string, scenario dynamic.Scenario) {
sdkID := os.Getenv("YOTI_CLIENT_SDK_ID")
- key, err := ioutil.ReadFile(os.Getenv("YOTI_KEY_FILE_PATH"))
+ key, err := os.ReadFile(os.Getenv("YOTI_KEY_FILE_PATH"))
if err != nil {
errorPage(w, req.WithContext(context.WithValue(
req.Context(),
diff --git a/_examples/profile/go.mod b/_examples/profile/go.mod
index d25fc959..dc3aef89 100644
--- a/_examples/profile/go.mod
+++ b/_examples/profile/go.mod
@@ -1,6 +1,6 @@
module profile
-go 1.17
+go 1.19
require (
github.com/getyoti/yoti-go-sdk/v3 v3.0.0
diff --git a/_examples/profile/profile.go b/_examples/profile/profile.go
index 9492d49c..15c3f307 100644
--- a/_examples/profile/profile.go
+++ b/_examples/profile/profile.go
@@ -9,7 +9,6 @@ import (
"image"
"image/jpeg"
"io"
- "io/ioutil"
"net/http"
"os"
@@ -18,7 +17,7 @@ import (
func profile(w http.ResponseWriter, r *http.Request) {
var err error
- key, err = ioutil.ReadFile(os.Getenv("YOTI_KEY_FILE_PATH"))
+ key, err = os.ReadFile(os.Getenv("YOTI_KEY_FILE_PATH"))
sdkID = os.Getenv("YOTI_CLIENT_SDK_ID")
if err != nil {
diff --git a/_examples/profilesandbox/go.mod b/_examples/profilesandbox/go.mod
index d98f6bc5..377a5a05 100644
--- a/_examples/profilesandbox/go.mod
+++ b/_examples/profilesandbox/go.mod
@@ -1,6 +1,6 @@
module profilesandbox
-go 1.17
+go 1.19
require (
github.com/getyoti/yoti-go-sdk/v3 v3.0.0
diff --git a/aml/service.go b/aml/service.go
index 962d911d..68605246 100644
--- a/aml/service.go
+++ b/aml/service.go
@@ -4,7 +4,7 @@ import (
"crypto/rsa"
"encoding/json"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"github.com/getyoti/yoti-go-sdk/v3/requests"
@@ -45,7 +45,7 @@ func PerformCheck(httpClient requests.HttpClient, profile Profile, clientSdkId,
}
var responseBytes []byte
- responseBytes, err = ioutil.ReadAll(response.Body)
+ responseBytes, err = io.ReadAll(response.Body)
if err != nil {
return
}
diff --git a/aml/service_test.go b/aml/service_test.go
index 05dd51ec..d8ef5b03 100644
--- a/aml/service_test.go
+++ b/aml/service_test.go
@@ -4,7 +4,7 @@ import (
"crypto/rsa"
"errors"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"strings"
"testing"
@@ -50,7 +50,7 @@ func TestPerformCheck_WithInvalidJSON(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
- Body: ioutil.NopCloser(strings.NewReader("Not a JSON document")),
+ Body: io.NopCloser(strings.NewReader("Not a JSON document")),
}, nil
},
}
@@ -66,7 +66,7 @@ func TestPerformCheck_Success(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
- Body: ioutil.NopCloser(strings.NewReader(`{"on_fraud_list":true,"on_pep_list":true,"on_watch_list":true}`)),
+ Body: io.NopCloser(strings.NewReader(`{"on_fraud_list":true,"on_pep_list":true,"on_watch_list":true}`)),
}, nil
},
}
@@ -87,7 +87,7 @@ func TestPerformCheck_Unsuccessful(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 503,
- Body: ioutil.NopCloser(strings.NewReader(responseBody)),
+ Body: io.NopCloser(strings.NewReader(responseBody)),
}, nil
},
}
diff --git a/yoti_client.go b/client.go
similarity index 100%
rename from yoti_client.go
rename to client.go
diff --git a/yoti_client_test.go b/client_test.go
similarity index 80%
rename from yoti_client_test.go
rename to client_test.go
index ca839dca..44ba57d5 100644
--- a/yoti_client_test.go
+++ b/client_test.go
@@ -2,7 +2,7 @@ package yoti
import (
"crypto/rsa"
- "io/ioutil"
+ "io"
"net/http"
"os"
"strings"
@@ -26,15 +26,15 @@ func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) {
}
func TestNewClient(t *testing.T) {
- key, readErr := ioutil.ReadFile("./test/test-key.pem")
- assert.NilError(t, readErr)
+ key, err := os.ReadFile("./test/test-key.pem")
+ assert.NilError(t, err)
- _, err := NewClient("some-sdk-id", key)
+ _, err = NewClient("some-sdk-id", key)
assert.NilError(t, err)
}
func TestNewClient_KeyLoad_Failure(t *testing.T) {
- key, err := ioutil.ReadFile("test/test-key-invalid-format.pem")
+ key, err := os.ReadFile("test/test-key-invalid-format.pem")
assert.NilError(t, err)
_, err = NewClient("", key)
@@ -48,17 +48,17 @@ func TestNewClient_KeyLoad_Failure(t *testing.T) {
}
func TestYotiClient_PerformAmlCheck(t *testing.T) {
- key, readErr := ioutil.ReadFile("./test/test-key.pem")
- assert.NilError(t, readErr)
+ key, err := os.ReadFile("./test/test-key.pem")
+ assert.NilError(t, err)
- client, clientErr := NewClient("some-sdk-id", key)
- assert.NilError(t, clientErr)
+ client, err := NewClient("some-sdk-id", key)
+ assert.NilError(t, err)
client.HTTPClient = &mockHTTPClient{
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
- Body: ioutil.NopCloser(strings.NewReader(`{"on_fraud_list":true}`)),
+ Body: io.NopCloser(strings.NewReader(`{"on_fraud_list":true}`)),
}, nil
},
}
@@ -78,26 +78,26 @@ func TestYotiClient_PerformAmlCheck(t *testing.T) {
}
func TestYotiClient_CreateShareURL(t *testing.T) {
- key, readErr := ioutil.ReadFile("./test/test-key.pem")
- assert.NilError(t, readErr)
+ key, err := os.ReadFile("./test/test-key.pem")
+ assert.NilError(t, err)
- client, clientErr := NewClient("some-sdk-id", key)
- assert.NilError(t, clientErr)
+ client, err := NewClient("some-sdk-id", key)
+ assert.NilError(t, err)
client.HTTPClient = &mockHTTPClient{
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 201,
- Body: ioutil.NopCloser(strings.NewReader(`{"qrcode":"https://code.yoti.com/some-qr","ref_id":"0"}`)),
+ Body: io.NopCloser(strings.NewReader(`{"qrcode":"https://code.yoti.com/some-qr","ref_id":"0"}`)),
}, nil
},
}
- policy, policyErr := (&dynamic.PolicyBuilder{}).WithFullName().WithWantedRememberMe().Build()
- assert.NilError(t, policyErr)
+ policy, err := (&dynamic.PolicyBuilder{}).WithFullName().WithWantedRememberMe().Build()
+ assert.NilError(t, err)
- scenario, scenarioErr := (&dynamic.ScenarioBuilder{}).WithPolicy(policy).Build()
- assert.NilError(t, scenarioErr)
+ scenario, err := (&dynamic.ScenarioBuilder{}).WithPolicy(policy).Build()
+ assert.NilError(t, err)
result, err := client.CreateShareURL(&scenario)
assert.NilError(t, err)
diff --git a/cryptoutil/crypto_utils.go b/cryptoutil/crypto_utils.go
index fbfacf26..de4d0f94 100644
--- a/cryptoutil/crypto_utils.go
+++ b/cryptoutil/crypto_utils.go
@@ -11,6 +11,8 @@ import (
"fmt"
"github.com/getyoti/yoti-go-sdk/v3/util"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom"
+ "google.golang.org/protobuf/proto"
)
// ParseRSAKey parses a PKCS1 private key from bytes
@@ -114,3 +116,49 @@ func UnwrapKey(wrappedKey string, key *rsa.PrivateKey) (result []byte, err error
}
return decryptRsa(cipherBytes, key)
}
+
+func decryptAESGCM(cipherText, iv, secret []byte) ([]byte, error) {
+ block, err := aes.NewCipher(secret)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create new aes cipher: %v", err)
+ }
+
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create new gcm cipher: %v", err)
+ }
+
+ plainText, err := gcm.Open(nil, iv, cipherText, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decrypt receipt key: %v", err)
+ }
+
+ return plainText, nil
+}
+
+func UnwrapReceiptKey(wrappedReceiptKey []byte, encryptedItemKey []byte, itemKeyIv []byte, key *rsa.PrivateKey) ([]byte, error) {
+ decryptedItemKey, err := decryptRsa(encryptedItemKey, key)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decrypt item key: %v", err)
+ }
+
+ plainText, err := decryptAESGCM(wrappedReceiptKey, itemKeyIv, decryptedItemKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decrypt receipt key: %v", err)
+ }
+ return plainText, nil
+}
+
+func DecryptReceiptContent(content, receiptContentKey []byte) ([]byte, error) {
+ if content == nil {
+ return nil, fmt.Errorf("failed to decrypt receipt content is nil")
+ }
+
+ decodedData := &yotiprotocom.EncryptedData{}
+ err := proto.Unmarshal(content, decodedData)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshall content: %v", content)
+ }
+
+ return DecipherAes(receiptContentKey, decodedData.Iv, decodedData.CipherText)
+}
diff --git a/cryptoutil/crypto_utils_test.go b/cryptoutil/crypto_utils_test.go
index 4b425fee..864e2f3a 100644
--- a/cryptoutil/crypto_utils_test.go
+++ b/cryptoutil/crypto_utils_test.go
@@ -6,7 +6,6 @@ import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
- "io/ioutil"
"os"
"testing"
@@ -163,7 +162,7 @@ func TestCryptoutil_UnwrapKey_InvalidBase64ShouldError(t *testing.T) {
}
func getKey() (key *rsa.PrivateKey) {
- keyBytes, err := ioutil.ReadFile("../test/test-key.pem")
+ keyBytes, err := os.ReadFile("../test/test-key.pem")
if err != nil {
panic("Error reading the test key: " + err.Error())
}
diff --git a/digital_identity_client.go b/digital_identity_client.go
new file mode 100644
index 00000000..b074712e
--- /dev/null
+++ b/digital_identity_client.go
@@ -0,0 +1,88 @@
+package yoti
+
+import (
+ "crypto/rsa"
+ "os"
+
+ "github.com/getyoti/yoti-go-sdk/v3/cryptoutil"
+ "github.com/getyoti/yoti-go-sdk/v3/digitalidentity"
+ "github.com/getyoti/yoti-go-sdk/v3/requests"
+)
+
+const DefaultURL = "https://api.yoti.com/share"
+
+// DigitalIdentityClient represents a client that can communicate with yoti and return information about Yoti users.
+type DigitalIdentityClient struct {
+ // SdkID represents the SDK ID and NOT the App ID. This can be found in the integration section of your
+ // application hub at https://hub.yoti.com/
+ SdkID string
+
+ // Key should be the security key given to you by yoti (see: security keys section of
+ // https://hub.yoti.com) for more information about how to load your key from a file see:
+ // https://github.com/getyoti/yoti-go-sdk/blob/master/README.md
+ Key *rsa.PrivateKey
+
+ apiURL string
+ HTTPClient requests.HttpClient // Mockable HTTP Client Interface
+}
+
+// NewDigitalIdentityClient constructs a Client object
+func NewDigitalIdentityClient(sdkID string, key []byte) (*DigitalIdentityClient, error) {
+ decodedKey, err := cryptoutil.ParseRSAKey(key)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &DigitalIdentityClient{
+ SdkID: sdkID,
+ Key: decodedKey,
+ }, err
+}
+
+// OverrideAPIURL overrides the default API URL for this Yoti Client
+func (client *DigitalIdentityClient) OverrideAPIURL(apiURL string) {
+ client.apiURL = apiURL
+}
+
+func (client *DigitalIdentityClient) getAPIURL() string {
+ if client.apiURL != "" {
+ return client.apiURL
+ }
+
+ if value, exists := os.LookupEnv("YOTI_API_URL"); exists && value != "" {
+ return value
+ }
+
+ return DefaultURL
+}
+
+// GetSdkID gets the Client SDK ID attached to this client instance
+func (client *DigitalIdentityClient) GetSdkID() string {
+ return client.SdkID
+}
+
+// CreateShareSession creates a sharing session to initiate a sharing process based on a policy
+func (client *DigitalIdentityClient) CreateShareSession(shareSessionRequest *digitalidentity.ShareSessionRequest) (shareSession *digitalidentity.ShareSession, err error) {
+ return digitalidentity.CreateShareSession(client.HTTPClient, shareSessionRequest, client.GetSdkID(), client.getAPIURL(), client.Key)
+}
+
+// GetShareSession retrieves the sharing session.
+func (client *DigitalIdentityClient) GetShareSession(sessionID string) (*digitalidentity.ShareSession, error) {
+ return digitalidentity.GetShareSession(client.HTTPClient, sessionID, client.GetSdkID(), client.getAPIURL(), client.Key)
+}
+
+// CreateShareQrCode generates a sharing session QR code to initiate a sharing process based on session ID
+func (client *DigitalIdentityClient) CreateShareQrCode(sessionID string) (share *digitalidentity.QrCode, err error) {
+ return digitalidentity.CreateShareQrCode(client.HTTPClient, sessionID, client.GetSdkID(), client.getAPIURL(), client.Key)
+}
+
+// Get session QR code based on generated Qr ID
+func (client *DigitalIdentityClient) GetQrCode(qrCodeId string) (share digitalidentity.ShareSessionQrCode, err error) {
+ return digitalidentity.GetShareSessionQrCode(client.HTTPClient, qrCodeId, client.GetSdkID(), client.getAPIURL(), client.Key)
+}
+
+// GetShareReceipt fetches the receipt of the share given a receipt id.
+func (client *DigitalIdentityClient) GetShareReceipt(receiptId string) (share digitalidentity.SharedReceiptResponse, err error) {
+ return digitalidentity.GetShareReceipt(client.HTTPClient, receiptId, client.GetSdkID(), client.getAPIURL(), client.Key)
+}
diff --git a/digital_identity_client_test.go b/digital_identity_client_test.go
new file mode 100644
index 00000000..3c85d93b
--- /dev/null
+++ b/digital_identity_client_test.go
@@ -0,0 +1,168 @@
+package yoti
+
+import (
+ "crypto/rsa"
+ "io"
+ "net/http"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/getyoti/yoti-go-sdk/v3/digitalidentity"
+ "github.com/getyoti/yoti-go-sdk/v3/test"
+ "gotest.tools/v3/assert"
+)
+
+func TestDigitalIDClient(t *testing.T) {
+ key, err := os.ReadFile("./test/test-key.pem")
+ assert.NilError(t, err)
+
+ _, err = NewDigitalIdentityClient("some-sdk-id", key)
+ assert.NilError(t, err)
+}
+
+func TestDigitalIDClient_KeyLoad_Failure(t *testing.T) {
+ key, err := os.ReadFile("test/test-key-invalid-format.pem")
+ assert.NilError(t, err)
+
+ _, err = NewDigitalIdentityClient("", key)
+
+ assert.ErrorContains(t, err, "invalid key: not PEM-encoded")
+
+ tempError, temporary := err.(interface {
+ Temporary() bool
+ })
+ assert.Check(t, !temporary || !tempError.Temporary())
+}
+
+func TestYotiClient_CreateShareSession(t *testing.T) {
+ key, err := os.ReadFile("./test/test-key.pem")
+ assert.NilError(t, err)
+
+ client, err := NewDigitalIdentityClient("some-sdk-id", key)
+ assert.NilError(t, err)
+
+ client.HTTPClient = &mockHTTPClient{
+ do: func(*http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: 201,
+ Body: io.NopCloser(strings.NewReader(`{"id":"SOME_ID","status":"SOME_STATUS","expiry":"SOME_EXPIRY","created":"SOME_CREATED","updated":"SOME_UPDATED","qrCode":{"id":"SOME_QRCODE_ID"},"receipt":{"id":"SOME_RECEIPT_ID"}}`)),
+ }, nil
+ },
+ }
+
+ policy, err := (&digitalidentity.PolicyBuilder{}).WithFullName().WithWantedRememberMe().Build()
+ assert.NilError(t, err)
+
+ session, err := (&digitalidentity.ShareSessionRequestBuilder{}).WithPolicy(policy).Build()
+ assert.NilError(t, err)
+
+ result, err := client.CreateShareSession(&session)
+
+ assert.NilError(t, err)
+ assert.Equal(t, result.Status, "SOME_STATUS")
+}
+
+func TestDigitalIDClient_HttpFailure_ReturnsUnKnownHttpError(t *testing.T) {
+ key := getDigitalValidKey()
+ client := DigitalIdentityClient{
+ HTTPClient: &mockHTTPClient{
+ do: func(*http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: 401,
+ }, nil
+ },
+ },
+ Key: key,
+ }
+
+ _, err := client.GetShareSession("SOME ID")
+
+ assert.ErrorContains(t, err, "unknown HTTP error")
+ tempError, temporary := err.(interface {
+ Temporary() bool
+ })
+ assert.Check(t, !temporary || !tempError.Temporary())
+}
+
+func TestDigitalIDClient_GetSession(t *testing.T) {
+ key, err := os.ReadFile("./test/test-key.pem")
+ if err != nil {
+ t.Fatalf("failed to read pem file :: %v", err)
+ }
+
+ mockSessionID := "SOME_SESSION_ID"
+ client, err := NewDigitalIdentityClient("some-sdk-id", key)
+ if err != nil {
+ t.Fatalf("failed to build the DigitalIdClient :: %v", err)
+ }
+
+ client.HTTPClient = &mockHTTPClient{
+ do: func(*http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: 200,
+ Body: io.NopCloser(strings.NewReader(`{"id":"SOME_ID","status":"SOME_STATUS","expiry":"SOME_EXPIRY","created":"SOME_CREATED","updated":"SOME_UPDATED","qrCode":{"id":"SOME_QRCODE_ID"},"receipt":{"id":"SOME_RECEIPT_ID"}}`)),
+ }, nil
+ },
+ }
+
+ result, err := client.GetShareSession(mockSessionID)
+ if err != nil {
+ t.Fatalf("failed to GetShareSesssion :: %v", err)
+ }
+
+ assert.Equal(t, result.Id, "SOME_ID")
+ assert.Equal(t, result.Status, "SOME_STATUS")
+ assert.Equal(t, result.Created, "SOME_CREATED")
+
+}
+
+func TestDigitalIDClient_OverrideAPIURL_ShouldSetAPIURL(t *testing.T) {
+ client := &DigitalIdentityClient{}
+
+ expectedURL := "expectedurl.com"
+ client.OverrideAPIURL(expectedURL)
+
+ assert.Equal(t, client.getAPIURL(), expectedURL)
+}
+
+func TestDigitalIDClient_GetAPIURLUsesOverriddenBaseUrlOverEnvVariable(t *testing.T) {
+ client := DigitalIdentityClient{}
+ client.OverrideAPIURL("overridenBaseUrl")
+
+ os.Setenv("YOTI_API_URL", "envBaseUrl")
+ result := client.getAPIURL()
+
+ assert.Equal(t, "overridenBaseUrl", result)
+}
+
+func TestDigitalIDClient_GetAPIURLUsesEnvVariable(t *testing.T) {
+ client := DigitalIdentityClient{}
+
+ os.Setenv("YOTI_API_URL", "envBaseUrl")
+ result := client.getAPIURL()
+
+ assert.Equal(t, "envBaseUrl", result)
+}
+
+func TestDigitalIDClient_GetAPIURLUsesDefaultUrlAsFallbackWithEmptyEnvValue(t *testing.T) {
+ client := DigitalIdentityClient{}
+
+ os.Setenv("YOTI_API_URL", "")
+ result := client.getAPIURL()
+
+ assert.Equal(t, "https://api.yoti.com/share", result)
+}
+
+func TestDigitalIDClient_GetAPIURLUsesDefaultUrlAsFallbackWithNoEnvValue(t *testing.T) {
+ client := DigitalIdentityClient{}
+
+ os.Unsetenv("YOTI_API_URL")
+ result := client.getAPIURL()
+
+ assert.Equal(t, "https://api.yoti.com/share", result)
+}
+
+func getDigitalValidKey() *rsa.PrivateKey {
+ return test.GetValidKey("test/test-key.pem")
+}
diff --git a/digitalidentity/address.go b/digitalidentity/address.go
new file mode 100644
index 00000000..17bfb51e
--- /dev/null
+++ b/digitalidentity/address.go
@@ -0,0 +1,52 @@
+package digitalidentity
+
+import (
+ "reflect"
+
+ "github.com/getyoti/yoti-go-sdk/v3/consts"
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+func getFormattedAddress(profile *UserProfile, formattedAddress string) *yotiprotoattr.Attribute {
+ proto := getProtobufAttribute(*profile, consts.AttrStructuredPostalAddress)
+
+ return &yotiprotoattr.Attribute{
+ Name: consts.AttrAddress,
+ Value: []byte(formattedAddress),
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: proto.Anchors,
+ }
+}
+
+func ensureAddressProfile(p *UserProfile) *attribute.StringAttribute {
+ if structuredPostalAddress, err := p.StructuredPostalAddress(); err == nil {
+ if (structuredPostalAddress != nil && !reflect.DeepEqual(structuredPostalAddress, attribute.JSONAttribute{})) {
+ var formattedAddress string
+ formattedAddress, err = retrieveFormattedAddressFromStructuredPostalAddress(structuredPostalAddress.Value())
+ if err == nil && formattedAddress != "" {
+ return attribute.NewString(getFormattedAddress(p, formattedAddress))
+ }
+ }
+ }
+
+ return nil
+}
+
+func retrieveFormattedAddressFromStructuredPostalAddress(structuredPostalAddress interface{}) (address string, err error) {
+ parsedStructuredAddressMap := structuredPostalAddress.(map[string]interface{})
+ if formattedAddress, ok := parsedStructuredAddressMap["formatted_address"]; ok {
+ return formattedAddress.(string), nil
+ }
+ return
+}
+
+func getProtobufAttribute(profile UserProfile, key string) *yotiprotoattr.Attribute {
+ for _, v := range profile.attributeSlice {
+ if v.Name == key {
+ return v
+ }
+ }
+
+ return nil
+}
diff --git a/digitalidentity/application_profile.go b/digitalidentity/application_profile.go
new file mode 100644
index 00000000..8fae7bda
--- /dev/null
+++ b/digitalidentity/application_profile.go
@@ -0,0 +1,50 @@
+package digitalidentity
+
+import (
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+// Attribute names for application attributes
+const (
+ AttrConstApplicationName = "application_name"
+ AttrConstApplicationURL = "application_url"
+ AttrConstApplicationLogo = "application_logo"
+ AttrConstApplicationReceiptBGColor = "application_receipt_bgcolor"
+)
+
+// ApplicationProfile is the profile of an application with convenience methods
+// to access well-known attributes.
+type ApplicationProfile struct {
+ baseProfile
+}
+
+func newApplicationProfile(attributes *yotiprotoattr.AttributeList) ApplicationProfile {
+ return ApplicationProfile{
+ baseProfile{
+ attributeSlice: createAttributeSlice(attributes),
+ },
+ }
+}
+
+// ApplicationName is the name of the application
+func (p ApplicationProfile) ApplicationName() *attribute.StringAttribute {
+ return p.GetStringAttribute(AttrConstApplicationName)
+}
+
+// ApplicationURL is the URL where the application is available at
+func (p ApplicationProfile) ApplicationURL() *attribute.StringAttribute {
+ return p.GetStringAttribute(AttrConstApplicationURL)
+}
+
+// ApplicationReceiptBgColor is the background colour that will be displayed on
+// each receipt the user gets as a result of a share with the application.
+func (p ApplicationProfile) ApplicationReceiptBgColor() *attribute.StringAttribute {
+ return p.GetStringAttribute(AttrConstApplicationReceiptBGColor)
+}
+
+// ApplicationLogo is the logo of the application that will be displayed to
+// those users that perform a share with it.
+func (p ApplicationProfile) ApplicationLogo() *attribute.ImageAttribute {
+ return p.GetImageAttribute(AttrConstApplicationLogo)
+}
diff --git a/digitalidentity/attribute/age_verifications.go b/digitalidentity/attribute/age_verifications.go
new file mode 100644
index 00000000..a7655d06
--- /dev/null
+++ b/digitalidentity/attribute/age_verifications.go
@@ -0,0 +1,34 @@
+package attribute
+
+import (
+ "strconv"
+ "strings"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+// AgeVerification encapsulates the result of a single age verification
+// as part of a share
+type AgeVerification struct {
+ Age int
+ CheckType string
+ Result bool
+ Attribute *yotiprotoattr.Attribute
+}
+
+// NewAgeVerification constructs an AgeVerification from a protobuffer
+func NewAgeVerification(attr *yotiprotoattr.Attribute) (verification AgeVerification, err error) {
+ split := strings.Split(attr.Name, ":")
+ verification.Age, err = strconv.Atoi(split[1])
+ verification.CheckType = split[0]
+
+ if string(attr.Value) == "true" {
+ verification.Result = true
+ } else {
+ verification.Result = false
+ }
+
+ verification.Attribute = attr
+
+ return
+}
diff --git a/digitalidentity/attribute/age_verifications_test.go b/digitalidentity/attribute/age_verifications_test.go
new file mode 100644
index 00000000..b3a6e086
--- /dev/null
+++ b/digitalidentity/attribute/age_verifications_test.go
@@ -0,0 +1,42 @@
+package attribute
+
+import (
+ "testing"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "gotest.tools/v3/assert"
+)
+
+func TestNewAgeVerification_ValueTrue(t *testing.T) {
+ attribute := &yotiprotoattr.Attribute{
+ Name: "age_over:18",
+ Value: []byte("true"),
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ ageVerification, err := NewAgeVerification(attribute)
+
+ assert.NilError(t, err)
+
+ assert.Equal(t, ageVerification.Age, 18)
+ assert.Equal(t, ageVerification.CheckType, "age_over")
+ assert.Equal(t, ageVerification.Result, true)
+}
+
+func TestNewAgeVerification_ValueFalse(t *testing.T) {
+ attribute := &yotiprotoattr.Attribute{
+ Name: "age_under:30",
+ Value: []byte("false"),
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ ageVerification, err := NewAgeVerification(attribute)
+
+ assert.NilError(t, err)
+
+ assert.Equal(t, ageVerification.Age, 30)
+ assert.Equal(t, ageVerification.CheckType, "age_under")
+ assert.Equal(t, ageVerification.Result, false)
+}
diff --git a/digitalidentity/attribute/anchor/anchor_parser.go b/digitalidentity/attribute/anchor/anchor_parser.go
new file mode 100644
index 00000000..d1476c4f
--- /dev/null
+++ b/digitalidentity/attribute/anchor/anchor_parser.go
@@ -0,0 +1,110 @@
+package anchor
+
+import (
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/asn1"
+ "errors"
+ "fmt"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom"
+ "google.golang.org/protobuf/proto"
+)
+
+type anchorExtension struct {
+ Extension string `asn1:"tag:0,utf8"`
+}
+
+var (
+ sourceOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 47127, 1, 1, 1}
+ verifierOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 47127, 1, 1, 2}
+)
+
+// ParseAnchors takes a slice of protobuf anchors, parses them, and returns a slice of Yoti SDK Anchors
+func ParseAnchors(protoAnchors []*yotiprotoattr.Anchor) []*Anchor {
+ var processedAnchors []*Anchor
+ for _, protoAnchor := range protoAnchors {
+ parsedCerts := parseCertificates(protoAnchor.OriginServerCerts)
+
+ anchorType, extension := getAnchorValuesFromCertificate(parsedCerts)
+
+ parsedSignedTimestamp, err := parseSignedTimestamp(protoAnchor.SignedTimeStamp)
+ if err != nil {
+ continue
+ }
+
+ processedAnchor := newAnchor(anchorType, parsedCerts, parsedSignedTimestamp, protoAnchor.SubType, extension)
+
+ processedAnchors = append(processedAnchors, processedAnchor)
+ }
+
+ return processedAnchors
+}
+
+func getAnchorValuesFromCertificate(parsedCerts []*x509.Certificate) (anchorType Type, extension string) {
+ defaultAnchorType := TypeUnknown
+
+ for _, cert := range parsedCerts {
+ for _, ext := range cert.Extensions {
+ var (
+ value string
+ err error
+ )
+ parsedAnchorType, value, err := parseExtension(ext)
+ if err != nil {
+ continue
+ } else if parsedAnchorType == TypeUnknown {
+ continue
+ }
+ return parsedAnchorType, value
+ }
+ }
+
+ return defaultAnchorType, ""
+}
+
+func parseExtension(ext pkix.Extension) (anchorType Type, val string, err error) {
+ anchorType = TypeUnknown
+
+ switch {
+ case ext.Id.Equal(sourceOID):
+ anchorType = TypeSource
+ case ext.Id.Equal(verifierOID):
+ anchorType = TypeVerifier
+ default:
+ return anchorType, "", nil
+ }
+
+ var ae anchorExtension
+ _, err = asn1.Unmarshal(ext.Value, &ae)
+ switch {
+ case err != nil:
+ return anchorType, "", fmt.Errorf("unable to unmarshal extension: %v", err)
+ case len(ae.Extension) == 0:
+ return anchorType, "", errors.New("empty extension")
+ default:
+ val = ae.Extension
+ }
+
+ return anchorType, val, nil
+}
+
+func parseSignedTimestamp(rawBytes []byte) (*yotiprotocom.SignedTimestamp, error) {
+ signedTimestamp := &yotiprotocom.SignedTimestamp{}
+ if err := proto.Unmarshal(rawBytes, signedTimestamp); err != nil {
+ return signedTimestamp, err
+ }
+
+ return signedTimestamp, nil
+}
+
+func parseCertificates(rawCerts [][]byte) (result []*x509.Certificate) {
+ for _, cert := range rawCerts {
+ parsedCertificate, _ := x509.ParseCertificate(cert)
+
+ result = append(result, parsedCertificate)
+ }
+
+ return result
+}
diff --git a/digitalidentity/attribute/anchor/anchor_parser_test.go b/digitalidentity/attribute/anchor/anchor_parser_test.go
new file mode 100644
index 00000000..13849a3d
--- /dev/null
+++ b/digitalidentity/attribute/anchor/anchor_parser_test.go
@@ -0,0 +1,147 @@
+package anchor
+
+import (
+ "crypto/x509/pkix"
+ "math/big"
+ "testing"
+ "time"
+
+ "github.com/getyoti/yoti-go-sdk/v3/test"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "google.golang.org/protobuf/proto"
+ "gotest.tools/v3/assert"
+)
+
+func assertServerCertSerialNo(t *testing.T, expectedSerialNo string, actualSerialNo *big.Int) {
+ expectedSerialNoBigInt := new(big.Int)
+ expectedSerialNoBigInt, ok := expectedSerialNoBigInt.SetString(expectedSerialNo, 10)
+ assert.Assert(t, ok, "Unexpected error when setting string as big int")
+
+ assert.Equal(t, expectedSerialNoBigInt.Cmp(actualSerialNo), 0) // 0 == equivalent
+}
+
+func createAnchorSliceFromTestFile(t *testing.T, filename string) []*yotiprotoattr.Anchor {
+ anchorBytes := test.DecodeTestFile(t, filename)
+
+ protoAnchor := &yotiprotoattr.Anchor{}
+ err2 := proto.Unmarshal(anchorBytes, protoAnchor)
+ assert.NilError(t, err2)
+
+ protoAnchors := append([]*yotiprotoattr.Anchor{}, protoAnchor)
+
+ return protoAnchors
+}
+
+func TestAnchorParser_parseExtension_ShouldErrorForInvalidExtension(t *testing.T) {
+ invalidExt := pkix.Extension{
+ Id: sourceOID,
+ }
+
+ _, _, err := parseExtension(invalidExt)
+
+ assert.Check(t, err != nil)
+ assert.Error(t, err, "unable to unmarshal extension: asn1: syntax error: sequence truncated")
+}
+
+func TestAnchorParser_Passport(t *testing.T) {
+ anchorSlice := createAnchorSliceFromTestFile(t, "../../../test/fixtures/test_anchor_passport.txt")
+
+ parsedAnchors := ParseAnchors(anchorSlice)
+
+ actualAnchor := parsedAnchors[0]
+
+ assert.Equal(t, actualAnchor.Type(), TypeSource)
+
+ expectedDate := time.Date(2018, time.April, 12, 13, 14, 32, 835537e3, time.UTC)
+ actualDate := actualAnchor.SignedTimestamp().Timestamp().UTC()
+ assert.Equal(t, actualDate, expectedDate)
+
+ expectedSubType := "OCR"
+ assert.Equal(t, actualAnchor.SubType(), expectedSubType)
+
+ expectedValue := "PASSPORT"
+ assert.Equal(t, actualAnchor.Value(), expectedValue)
+
+ actualSerialNo := actualAnchor.OriginServerCerts()[0].SerialNumber
+ assertServerCertSerialNo(t, "277870515583559162487099305254898397834", actualSerialNo)
+}
+
+func TestAnchorParser_DrivingLicense(t *testing.T) {
+ anchorSlice := createAnchorSliceFromTestFile(t, "../../../test/fixtures/test_anchor_driving_license.txt")
+
+ parsedAnchors := ParseAnchors(anchorSlice)
+ resultAnchor := parsedAnchors[0]
+
+ assert.Equal(t, resultAnchor.Type(), TypeSource)
+
+ expectedDate := time.Date(2018, time.April, 11, 12, 13, 3, 923537e3, time.UTC)
+ actualDate := resultAnchor.SignedTimestamp().Timestamp().UTC()
+ assert.Equal(t, actualDate, expectedDate)
+
+ expectedSubType := ""
+ assert.Equal(t, resultAnchor.SubType(), expectedSubType)
+
+ expectedValue := "DRIVING_LICENCE"
+ assert.Equal(t, resultAnchor.Value(), expectedValue)
+
+ actualSerialNo := resultAnchor.OriginServerCerts()[0].SerialNumber
+ assertServerCertSerialNo(t, "46131813624213904216516051554755262812", actualSerialNo)
+}
+
+func TestAnchorParser_UnknownAnchor(t *testing.T) {
+ anchorSlice := createAnchorSliceFromTestFile(t, "../../../test/fixtures/test_anchor_unknown.txt")
+
+ resultAnchor := ParseAnchors(anchorSlice)[0]
+
+ expectedDate := time.Date(2019, time.March, 5, 10, 45, 11, 840037e3, time.UTC)
+ actualDate := resultAnchor.SignedTimestamp().Timestamp().UTC()
+ assert.Equal(t, actualDate, expectedDate)
+
+ expectedSubType := "TEST UNKNOWN SUB TYPE"
+ expectedType := TypeUnknown
+ assert.Equal(t, resultAnchor.SubType(), expectedSubType)
+ assert.Equal(t, resultAnchor.Type(), expectedType)
+ assert.Equal(t, resultAnchor.Value(), "")
+}
+
+func TestAnchorParser_YotiAdmin(t *testing.T) {
+ anchorSlice := createAnchorSliceFromTestFile(t, "../../../test/fixtures/test_anchor_yoti_admin.txt")
+
+ resultAnchor := ParseAnchors(anchorSlice)[0]
+
+ assert.Equal(t, resultAnchor.Type(), TypeVerifier)
+
+ expectedDate := time.Date(2018, time.April, 11, 12, 13, 4, 95238e3, time.UTC)
+ actualDate := resultAnchor.SignedTimestamp().Timestamp().UTC()
+ assert.Equal(t, actualDate, expectedDate)
+
+ expectedSubType := ""
+ assert.Equal(t, resultAnchor.SubType(), expectedSubType)
+
+ expectedValue := "YOTI_ADMIN"
+ assert.Equal(t, resultAnchor.Value(), expectedValue)
+
+ actualSerialNo := resultAnchor.OriginServerCerts()[0].SerialNumber
+ assertServerCertSerialNo(t, "256616937783084706710155170893983549581", actualSerialNo)
+}
+
+func TestAnchors_None(t *testing.T) {
+ var anchorSlice []*Anchor
+
+ sources := GetSources(anchorSlice)
+ assert.Equal(t, len(sources), 0, "GetSources should not return anything with empty anchors")
+
+ verifiers := GetVerifiers(anchorSlice)
+ assert.Equal(t, len(verifiers), 0, "GetVerifiers should not return anything with empty anchors")
+}
+
+func TestAnchorParser_InvalidSignedTimestamp(t *testing.T) {
+ var protoAnchors []*yotiprotoattr.Anchor
+ protoAnchors = append(protoAnchors, &yotiprotoattr.Anchor{
+ SignedTimeStamp: []byte("invalidProto"),
+ })
+ parsedAnchors := ParseAnchors(protoAnchors)
+
+ var expectedAnchors []*Anchor
+ assert.DeepEqual(t, expectedAnchors, parsedAnchors)
+}
diff --git a/digitalidentity/attribute/anchor/anchors.go b/digitalidentity/attribute/anchor/anchors.go
new file mode 100644
index 00000000..839a6e11
--- /dev/null
+++ b/digitalidentity/attribute/anchor/anchors.go
@@ -0,0 +1,105 @@
+package anchor
+
+import (
+ "crypto/x509"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom"
+)
+
+// Anchor is the metadata associated with an attribute. It describes how an attribute has been provided
+// to Yoti (SOURCE Anchor) and how it has been verified (VERIFIER Anchor).
+// If an attribute has only one SOURCE Anchor with the value set to
+// "USER_PROVIDED" and zero VERIFIER Anchors, then the attribute
+// is a self-certified one.
+type Anchor struct {
+ anchorType Type
+ originServerCerts []*x509.Certificate
+ signedTimestamp SignedTimestamp
+ subtype string
+ value string
+}
+
+func newAnchor(anchorType Type, originServerCerts []*x509.Certificate, signedTimestamp *yotiprotocom.SignedTimestamp, subtype string, value string) *Anchor {
+ return &Anchor{
+ anchorType: anchorType,
+ originServerCerts: originServerCerts,
+ signedTimestamp: convertSignedTimestamp(signedTimestamp),
+ subtype: subtype,
+ value: value,
+ }
+}
+
+// Type Anchor type, based on the Object Identifier (OID)
+type Type int
+
+const (
+ // TypeUnknown - default value
+ TypeUnknown Type = 1 + iota
+ // TypeSource - how the anchor has been sourced
+ TypeSource
+ // TypeVerifier - how the anchor has been verified
+ TypeVerifier
+)
+
+// Type of the Anchor - most likely either SOURCE or VERIFIER, but it's
+// possible that new Anchor types will be added in future.
+func (a Anchor) Type() Type {
+ return a.anchorType
+}
+
+// OriginServerCerts are the X.509 certificate chain(DER-encoded ASN.1)
+// from the service that assigned the attribute.
+//
+// The first certificate in the chain holds the public key that can be
+// used to verify the Signature field; any following entries (zero or
+// more) are for intermediate certificate authorities (in order).
+//
+// The last certificate in the chain must be verified against the Yoti root
+// CA certificate. An extension in the first certificate holds the main artifact type,
+// e.g. “PASSPORT”, which can be retrieved with .Value().
+func (a Anchor) OriginServerCerts() []*x509.Certificate {
+ return a.originServerCerts
+}
+
+// SignedTimestamp is the time at which the signature was created. The
+// message associated with the timestamp is the marshaled form of
+// AttributeSigning (i.e. the same message that is signed in the
+// Signature field). This method returns the SignedTimestamp
+// object, the actual timestamp as a *time.Time can be called with
+// .Timestamp() on the result of this function.
+func (a Anchor) SignedTimestamp() SignedTimestamp {
+ return a.signedTimestamp
+}
+
+// SubType is an indicator of any specific processing method, or
+// subcategory, pertaining to an artifact. For example, for a passport, this would be
+// either "NFC" or "OCR".
+func (a Anchor) SubType() string {
+ return a.subtype
+}
+
+// Value identifies the provider that either sourced or verified the attribute value.
+// The range of possible values is not limited. For a SOURCE anchor, expect a value like
+// PASSPORT, DRIVING_LICENSE. For a VERIFIER anchor, expect a value like YOTI_ADMIN.
+func (a Anchor) Value() string {
+ return a.value
+}
+
+// GetSources returns the anchors which identify how and when an attribute value was acquired.
+func GetSources(anchors []*Anchor) (sources []*Anchor) {
+ return filterAnchors(anchors, TypeSource)
+}
+
+// GetVerifiers returns the anchors which identify how and when an attribute value was verified by another provider.
+func GetVerifiers(anchors []*Anchor) (sources []*Anchor) {
+ return filterAnchors(anchors, TypeVerifier)
+}
+
+func filterAnchors(anchors []*Anchor, anchorType Type) (result []*Anchor) {
+ for _, v := range anchors {
+ if v.anchorType == anchorType {
+ result = append(result, v)
+ }
+ }
+ return result
+}
diff --git a/digitalidentity/attribute/anchor/anchors_test.go b/digitalidentity/attribute/anchor/anchors_test.go
new file mode 100644
index 00000000..ed5287ed
--- /dev/null
+++ b/digitalidentity/attribute/anchor/anchors_test.go
@@ -0,0 +1,20 @@
+package anchor
+
+import (
+ "testing"
+
+ "gotest.tools/v3/assert"
+)
+
+func TestFilterAnchors_FilterSources(t *testing.T) {
+ anchorSlice := []*Anchor{
+ {subtype: "a", anchorType: TypeSource},
+ {subtype: "b", anchorType: TypeVerifier},
+ {subtype: "c", anchorType: TypeSource},
+ }
+ sources := filterAnchors(anchorSlice, TypeSource)
+ assert.Equal(t, len(sources), 2)
+ assert.Equal(t, sources[0].subtype, "a")
+ assert.Equal(t, sources[1].subtype, "c")
+
+}
diff --git a/digitalidentity/attribute/anchor/signed_timestamp.go b/digitalidentity/attribute/anchor/signed_timestamp.go
new file mode 100644
index 00000000..2081b7d6
--- /dev/null
+++ b/digitalidentity/attribute/anchor/signed_timestamp.go
@@ -0,0 +1,35 @@
+package anchor
+
+import (
+ "time"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom"
+)
+
+// SignedTimestamp is the object which contains a timestamp
+type SignedTimestamp struct {
+ version int32
+ timestamp *time.Time
+}
+
+func convertSignedTimestamp(protoSignedTimestamp *yotiprotocom.SignedTimestamp) SignedTimestamp {
+ uintTimestamp := protoSignedTimestamp.Timestamp
+ intTimestamp := int64(uintTimestamp)
+ unixTime := time.Unix(intTimestamp/1e6, (intTimestamp%1e6)*1e3)
+
+ return SignedTimestamp{
+ version: protoSignedTimestamp.Version,
+ timestamp: &unixTime,
+ }
+}
+
+// Version indicates both the version of the protobuf message in use,
+// as well as the specific hash algorithms.
+func (s SignedTimestamp) Version() int32 {
+ return s.version
+}
+
+// Timestamp is a point in time, to the nearest microsecond.
+func (s SignedTimestamp) Timestamp() *time.Time {
+ return s.timestamp
+}
diff --git a/digitalidentity/attribute/attribute_details.go b/digitalidentity/attribute/attribute_details.go
new file mode 100644
index 00000000..a380150b
--- /dev/null
+++ b/digitalidentity/attribute/attribute_details.go
@@ -0,0 +1,48 @@
+package attribute
+
+import (
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor"
+)
+
+// attributeDetails is embedded in each attribute for fields common to all
+// attributes
+type attributeDetails struct {
+ name string
+ contentType string
+ anchors []*anchor.Anchor
+ id *string
+}
+
+// Name gets the attribute name
+func (a attributeDetails) Name() string {
+ return a.name
+}
+
+// ID gets the attribute ID
+func (a attributeDetails) ID() *string {
+ return a.id
+}
+
+// ContentType gets the attribute's content type description
+func (a attributeDetails) ContentType() string {
+ return a.contentType
+}
+
+// Anchors are the metadata associated with an attribute. They describe
+// how an attribute has been provided to Yoti (SOURCE Anchor) and how
+// it has been verified (VERIFIER Anchor).
+func (a attributeDetails) Anchors() []*anchor.Anchor {
+ return a.anchors
+}
+
+// Sources returns the anchors which identify how and when an attribute value
+// was acquired.
+func (a attributeDetails) Sources() []*anchor.Anchor {
+ return anchor.GetSources(a.anchors)
+}
+
+// Verifiers returns the anchors which identify how and when an attribute value
+// was verified by another provider.
+func (a attributeDetails) Verifiers() []*anchor.Anchor {
+ return anchor.GetVerifiers(a.anchors)
+}
diff --git a/digitalidentity/attribute/attribute_test.go b/digitalidentity/attribute/attribute_test.go
new file mode 100644
index 00000000..67b6c2b2
--- /dev/null
+++ b/digitalidentity/attribute/attribute_test.go
@@ -0,0 +1,36 @@
+package attribute
+
+import (
+ "testing"
+ "time"
+
+ "gotest.tools/v3/assert"
+)
+
+func TestNewThirdPartyAttribute(t *testing.T) {
+ protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_third_party.txt")
+
+ stringAttribute := NewString(protoAttribute)
+
+ assert.Equal(t, stringAttribute.Value(), "test-third-party-attribute-0")
+ assert.Equal(t, stringAttribute.Name(), "com.thirdparty.id")
+
+ assert.Equal(t, stringAttribute.Sources()[0].Value(), "THIRD_PARTY")
+ assert.Equal(t, stringAttribute.Sources()[0].SubType(), "orgName")
+
+ assert.Equal(t, stringAttribute.Verifiers()[0].Value(), "THIRD_PARTY")
+ assert.Equal(t, stringAttribute.Verifiers()[0].SubType(), "orgName")
+}
+
+func TestAttribute_DateOfBirth(t *testing.T) {
+ protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_date_of_birth.txt")
+
+ dateOfBirthAttribute, err := NewDate(protoAttribute)
+
+ assert.NilError(t, err)
+
+ expectedDateOfBirth := time.Date(1970, time.December, 01, 0, 0, 0, 0, time.UTC)
+ actualDateOfBirth := dateOfBirthAttribute.Value()
+
+ assert.Assert(t, actualDateOfBirth.Equal(expectedDateOfBirth))
+}
diff --git a/digitalidentity/attribute/date_attribute.go b/digitalidentity/attribute/date_attribute.go
new file mode 100644
index 00000000..cdc55ce3
--- /dev/null
+++ b/digitalidentity/attribute/date_attribute.go
@@ -0,0 +1,39 @@
+package attribute
+
+import (
+ "time"
+
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+// DateAttribute is a Yoti attribute which returns a date as *time.Time for its value
+type DateAttribute struct {
+ attributeDetails
+ value *time.Time
+}
+
+// NewDate creates a new Date attribute
+func NewDate(a *yotiprotoattr.Attribute) (*DateAttribute, error) {
+ parsedTime, err := time.Parse("2006-01-02", string(a.Value))
+ if err != nil {
+ return nil, err
+ }
+
+ parsedAnchors := anchor.ParseAnchors(a.Anchors)
+
+ return &DateAttribute{
+ attributeDetails: attributeDetails{
+ name: a.Name,
+ contentType: a.ContentType.String(),
+ anchors: parsedAnchors,
+ id: &a.EphemeralId,
+ },
+ value: &parsedTime,
+ }, nil
+}
+
+// Value returns the value of the TimeAttribute as *time.Time
+func (a *DateAttribute) Value() *time.Time {
+ return a.value
+}
diff --git a/digitalidentity/attribute/date_attribute_test.go b/digitalidentity/attribute/date_attribute_test.go
new file mode 100644
index 00000000..24807c93
--- /dev/null
+++ b/digitalidentity/attribute/date_attribute_test.go
@@ -0,0 +1,44 @@
+package attribute
+
+import (
+ "testing"
+ "time"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "gotest.tools/v3/assert"
+)
+
+func TestTimeAttribute_NewDate_DateOnly(t *testing.T) {
+ proto := yotiprotoattr.Attribute{
+ Value: []byte("2011-12-25"),
+ }
+
+ timeAttribute, err := NewDate(&proto)
+ assert.NilError(t, err)
+
+ assert.Equal(t, *timeAttribute.Value(), time.Date(2011, 12, 25, 0, 0, 0, 0, time.UTC))
+}
+
+func TestTimeAttribute_DateOfBirth(t *testing.T) {
+ protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_date_of_birth.txt")
+
+ dateOfBirthAttribute, err := NewDate(protoAttribute)
+
+ assert.NilError(t, err)
+
+ expectedDateOfBirth := time.Date(1970, time.December, 01, 0, 0, 0, 0, time.UTC)
+ actualDateOfBirth := dateOfBirthAttribute.Value()
+
+ assert.Assert(t, actualDateOfBirth.Equal(expectedDateOfBirth))
+}
+
+func TestNewTime_ShouldReturnErrorForInvalidDate(t *testing.T) {
+ proto := yotiprotoattr.Attribute{
+ Name: "example",
+ Value: []byte("2006-60-20"),
+ ContentType: yotiprotoattr.ContentType_DATE,
+ }
+ attribute, err := NewDate(&proto)
+ assert.Check(t, attribute == nil)
+ assert.ErrorContains(t, err, "month out of range")
+}
diff --git a/digitalidentity/attribute/definition.go b/digitalidentity/attribute/definition.go
new file mode 100644
index 00000000..b0d4b8a4
--- /dev/null
+++ b/digitalidentity/attribute/definition.go
@@ -0,0 +1,31 @@
+package attribute
+
+import (
+ "encoding/json"
+)
+
+// Definition contains information about the attribute(s) issued by a third party.
+type Definition struct {
+ name string
+}
+
+// Name of the attribute to be issued.
+func (a Definition) Name() string {
+ return a.name
+}
+
+// MarshalJSON returns encoded json
+func (a Definition) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&struct {
+ Name string `json:"name"`
+ }{
+ Name: a.name,
+ })
+}
+
+// NewAttributeDefinition returns a new AttributeDefinition
+func NewAttributeDefinition(s string) Definition {
+ return Definition{
+ name: s,
+ }
+}
diff --git a/digitalidentity/attribute/definition_test.go b/digitalidentity/attribute/definition_test.go
new file mode 100644
index 00000000..b209e023
--- /dev/null
+++ b/digitalidentity/attribute/definition_test.go
@@ -0,0 +1,18 @@
+package attribute
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+func ExampleDefinition_MarshalJSON() {
+ exampleDefinition := NewAttributeDefinition("exampleDefinition")
+ marshalledJSON, err := json.Marshal(exampleDefinition)
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(marshalledJSON))
+ // Output: {"name":"exampleDefinition"}
+}
diff --git a/digitalidentity/attribute/document_details_attribute.go b/digitalidentity/attribute/document_details_attribute.go
new file mode 100644
index 00000000..a18ccaba
--- /dev/null
+++ b/digitalidentity/attribute/document_details_attribute.go
@@ -0,0 +1,87 @@
+package attribute
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+const (
+ documentDetailsDateFormatConst = "2006-01-02"
+)
+
+// DocumentDetails represents information extracted from a document provided by the user
+type DocumentDetails struct {
+ DocumentType string
+ IssuingCountry string
+ DocumentNumber string
+ ExpirationDate *time.Time
+ IssuingAuthority string
+}
+
+// DocumentDetailsAttribute wraps a document details with anchor data
+type DocumentDetailsAttribute struct {
+ attributeDetails
+ value DocumentDetails
+}
+
+// Value returns the document details struct attached to this attribute
+func (attr *DocumentDetailsAttribute) Value() DocumentDetails {
+ return attr.value
+}
+
+// NewDocumentDetails creates a DocumentDetailsAttribute which wraps a
+// DocumentDetails with anchor data
+func NewDocumentDetails(a *yotiprotoattr.Attribute) (*DocumentDetailsAttribute, error) {
+ parsedAnchors := anchor.ParseAnchors(a.Anchors)
+ details := DocumentDetails{}
+ err := details.Parse(string(a.Value))
+ if err != nil {
+ return nil, err
+ }
+
+ return &DocumentDetailsAttribute{
+ attributeDetails: attributeDetails{
+ name: a.Name,
+ contentType: a.ContentType.String(),
+ anchors: parsedAnchors,
+ id: &a.EphemeralId,
+ },
+ value: details,
+ }, nil
+}
+
+// Parse fills a DocumentDetails object from a raw string
+func (details *DocumentDetails) Parse(data string) error {
+ dataSlice := strings.Split(data, " ")
+
+ if len(dataSlice) < 3 {
+ return fmt.Errorf("Document Details data is invalid, %s", data)
+ }
+ for _, section := range dataSlice {
+ if section == "" {
+ return fmt.Errorf("Document Details data is invalid %s", data)
+ }
+ }
+
+ details.DocumentType = dataSlice[0]
+ details.IssuingCountry = dataSlice[1]
+ details.DocumentNumber = dataSlice[2]
+ if len(dataSlice) > 3 && dataSlice[3] != "-" {
+ expirationDateData, dateErr := time.Parse(documentDetailsDateFormatConst, dataSlice[3])
+
+ if dateErr == nil {
+ details.ExpirationDate = &expirationDateData
+ } else {
+ return dateErr
+ }
+ }
+ if len(dataSlice) > 4 {
+ details.IssuingAuthority = dataSlice[4]
+ }
+
+ return nil
+}
diff --git a/digitalidentity/attribute/document_details_attribute_test.go b/digitalidentity/attribute/document_details_attribute_test.go
new file mode 100644
index 00000000..bf2e7dee
--- /dev/null
+++ b/digitalidentity/attribute/document_details_attribute_test.go
@@ -0,0 +1,185 @@
+package attribute
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "gotest.tools/v3/assert"
+)
+
+func ExampleDocumentDetails_Parse() {
+ raw := "PASSPORT GBR 1234567 2022-09-12"
+ details := DocumentDetails{}
+ err := details.Parse(raw)
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Printf(
+ "Document Type: %s, Issuing Country: %s, Document Number: %s, Expiration Date: %s",
+ details.DocumentType,
+ details.IssuingCountry,
+ details.DocumentNumber,
+ details.ExpirationDate,
+ )
+ // Output: Document Type: PASSPORT, Issuing Country: GBR, Document Number: 1234567, Expiration Date: 2022-09-12 00:00:00 +0000 UTC
+}
+
+func ExampleNewDocumentDetails() {
+ proto := yotiprotoattr.Attribute{
+ Name: "exampleDocumentDetails",
+ Value: []byte("PASSPORT GBR 1234567 2022-09-12"),
+ ContentType: yotiprotoattr.ContentType_STRING,
+ }
+ attribute, err := NewDocumentDetails(&proto)
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Printf(
+ "Document Type: %s, With %d Anchors",
+ attribute.Value().DocumentType,
+ len(attribute.Anchors()),
+ )
+ // Output: Document Type: PASSPORT, With 0 Anchors
+}
+
+func TestDocumentDetailsShouldParseDrivingLicenceWithoutExpiry(t *testing.T) {
+ drivingLicenceGBR := "PASS_CARD GBR 1234abc - DVLA"
+
+ details := DocumentDetails{}
+ err := details.Parse(drivingLicenceGBR)
+ if err != nil {
+ t.Fail()
+ }
+ assert.Equal(t, details.DocumentType, "PASS_CARD")
+ assert.Equal(t, details.DocumentNumber, "1234abc")
+ assert.Assert(t, details.ExpirationDate == nil)
+ assert.Equal(t, details.IssuingCountry, "GBR")
+ assert.Equal(t, details.IssuingAuthority, "DVLA")
+}
+
+func TestDocumentDetailsShouldParseRedactedAadhar(t *testing.T) {
+ aadhaar := "AADHAAR IND ****1234 2016-05-01"
+ details := DocumentDetails{}
+ err := details.Parse(aadhaar)
+ if err != nil {
+ t.Fail()
+ }
+ assert.Equal(t, details.DocumentType, "AADHAAR")
+ assert.Equal(t, details.DocumentNumber, "****1234")
+ assert.Equal(t, details.ExpirationDate.Format("2006-01-02"), "2016-05-01")
+ assert.Equal(t, details.IssuingCountry, "IND")
+ assert.Equal(t, details.IssuingAuthority, "")
+}
+
+func TestDocumentDetailsShouldParseSpecialCharacters(t *testing.T) {
+ testData := [][]string{
+ {"type country **** - authority", "****"},
+ {"type country ~!@#$%^&*()-_=+[]{}|;':,./<>? - authority", "~!@#$%^&*()-_=+[]{}|;':,./<>?"},
+ {"type country \"\" - authority", "\"\""},
+ {"type country \\ - authority", "\\"},
+ {"type country \" - authority", "\""},
+ {"type country '' - authority", "''"},
+ {"type country ' - authority", "'"},
+ }
+ for _, row := range testData {
+ details := DocumentDetails{}
+ err := details.Parse(row[0])
+ if err != nil {
+ t.Fail()
+ }
+ assert.Equal(t, details.DocumentNumber, row[1])
+ }
+}
+
+func TestDocumentDetailsShouldFailOnDoubleSpace(t *testing.T) {
+ data := "AADHAAR IND ****1234"
+ details := DocumentDetails{}
+ err := details.Parse(data)
+ assert.Check(t, err != nil)
+ assert.ErrorContains(t, err, "Document Details data is invalid")
+}
+
+func TestDocumentDetailsShouldParseDrivingLicenceWithExtraAttribute(t *testing.T) {
+ drivingLicenceGBR := "DRIVING_LICENCE GBR 1234abc 2016-05-01 DVLA someThirdAttribute"
+ details := DocumentDetails{}
+ err := details.Parse(drivingLicenceGBR)
+ if err != nil {
+ t.Fail()
+ }
+ assert.Equal(t, details.DocumentType, "DRIVING_LICENCE")
+ assert.Equal(t, details.DocumentNumber, "1234abc")
+ assert.Equal(t, details.ExpirationDate.Format("2006-01-02"), "2016-05-01")
+ assert.Equal(t, details.IssuingCountry, "GBR")
+ assert.Equal(t, details.IssuingAuthority, "DVLA")
+}
+
+func TestDocumentDetailsShouldParseDrivingLicenceWithAllOptionalAttributes(t *testing.T) {
+ drivingLicenceGBR := "DRIVING_LICENCE GBR 1234abc 2016-05-01 DVLA"
+
+ details := DocumentDetails{}
+ err := details.Parse(drivingLicenceGBR)
+ if err != nil {
+ t.Fail()
+ }
+ assert.Equal(t, details.DocumentType, "DRIVING_LICENCE")
+ assert.Equal(t, details.DocumentNumber, "1234abc")
+ assert.Equal(t, details.ExpirationDate.Format("2006-01-02"), "2016-05-01")
+ assert.Equal(t, details.IssuingCountry, "GBR")
+ assert.Equal(t, details.IssuingAuthority, "DVLA")
+}
+
+func TestDocumentDetailsShouldParseAadhaar(t *testing.T) {
+ aadhaar := "AADHAAR IND 1234abc 2016-05-01"
+
+ details := DocumentDetails{}
+ err := details.Parse(aadhaar)
+ if err != nil {
+ t.Fail()
+ }
+ assert.Equal(t, details.DocumentType, "AADHAAR")
+ assert.Equal(t, details.DocumentNumber, "1234abc")
+ assert.Equal(t, details.ExpirationDate.Format("2006-01-02"), "2016-05-01")
+ assert.Equal(t, details.IssuingCountry, "IND")
+}
+
+func TestDocumentDetailsShouldParsePassportWithMandatoryFieldsOnly(t *testing.T) {
+ passportGBR := "PASSPORT GBR 1234abc"
+
+ details := DocumentDetails{}
+ err := details.Parse(passportGBR)
+ if err != nil {
+ t.Fail()
+ }
+ assert.Equal(t, details.DocumentType, "PASSPORT")
+ assert.Equal(t, details.DocumentNumber, "1234abc")
+ assert.Assert(t, details.ExpirationDate == nil)
+ assert.Equal(t, details.IssuingCountry, "GBR")
+ assert.Equal(t, details.IssuingAuthority, "")
+}
+
+func TestDocumentDetailsShouldErrorOnEmptyString(t *testing.T) {
+ empty := ""
+
+ details := DocumentDetails{}
+ err := details.Parse(empty)
+ assert.ErrorContains(t, err, "Document Details data is invalid")
+}
+
+func TestDocumentDetailsShouldErrorIfLessThan3Words(t *testing.T) {
+ corrupt := "PASS_CARD GBR"
+ details := DocumentDetails{}
+ err := details.Parse(corrupt)
+ assert.ErrorContains(t, err, "Document Details data is invalid")
+}
+
+func TestDocumentDetailsShouldErrorForInvalidExpirationDate(t *testing.T) {
+ corrupt := "PASSPORT GBR 1234abc X016-05-01"
+ details := DocumentDetails{}
+ err := details.Parse(corrupt)
+ assert.ErrorContains(t, err, "cannot parse")
+}
diff --git a/digitalidentity/attribute/generic_attribute.go b/digitalidentity/attribute/generic_attribute.go
new file mode 100644
index 00000000..c729e30b
--- /dev/null
+++ b/digitalidentity/attribute/generic_attribute.go
@@ -0,0 +1,38 @@
+package attribute
+
+import (
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+// GenericAttribute is a Yoti attribute which returns a generic value
+type GenericAttribute struct {
+ attributeDetails
+ value interface{}
+}
+
+// NewGeneric creates a new generic attribute
+func NewGeneric(a *yotiprotoattr.Attribute) *GenericAttribute {
+ value, err := parseValue(a.ContentType, a.Value)
+
+ if err != nil {
+ return nil
+ }
+
+ var parsedAnchors = anchor.ParseAnchors(a.Anchors)
+
+ return &GenericAttribute{
+ attributeDetails: attributeDetails{
+ name: a.Name,
+ contentType: a.ContentType.String(),
+ anchors: parsedAnchors,
+ id: &a.EphemeralId,
+ },
+ value: value,
+ }
+}
+
+// Value returns the value of the GenericAttribute as an interface
+func (a *GenericAttribute) Value() interface{} {
+ return a.value
+}
diff --git a/digitalidentity/attribute/generic_attribute_test.go b/digitalidentity/attribute/generic_attribute_test.go
new file mode 100644
index 00000000..e2daae8a
--- /dev/null
+++ b/digitalidentity/attribute/generic_attribute_test.go
@@ -0,0 +1,39 @@
+package attribute
+
+import (
+ "testing"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "gotest.tools/v3/assert"
+)
+
+func TestNewGeneric_ShouldParseUnknownTypeAsString(t *testing.T) {
+ value := []byte("value")
+ protoAttr := yotiprotoattr.Attribute{
+ ContentType: yotiprotoattr.ContentType_UNDEFINED,
+ Value: value,
+ }
+ parsed := NewGeneric(&protoAttr)
+
+ stringValue, ok := parsed.Value().(string)
+ assert.Check(t, ok)
+
+ assert.Equal(t, stringValue, string(value))
+}
+
+func TestGeneric_ContentType(t *testing.T) {
+ attribute := GenericAttribute{
+ attributeDetails: attributeDetails{
+ contentType: "contentType",
+ },
+ }
+
+ assert.Equal(t, attribute.ContentType(), "contentType")
+}
+
+func TestNewGeneric_ShouldReturnNilForInvalidProtobuf(t *testing.T) {
+ invalid := NewGeneric(&yotiprotoattr.Attribute{
+ ContentType: yotiprotoattr.ContentType_JSON,
+ })
+ assert.Check(t, invalid == nil)
+}
diff --git a/digitalidentity/attribute/helper_test.go b/digitalidentity/attribute/helper_test.go
new file mode 100644
index 00000000..47c28eae
--- /dev/null
+++ b/digitalidentity/attribute/helper_test.go
@@ -0,0 +1,21 @@
+package attribute
+
+import (
+ "testing"
+
+ "github.com/getyoti/yoti-go-sdk/v3/test"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "google.golang.org/protobuf/proto"
+ "gotest.tools/v3/assert"
+)
+
+func createAttributeFromTestFile(t *testing.T, filename string) *yotiprotoattr.Attribute {
+ attributeBytes := test.DecodeTestFile(t, filename)
+
+ attributeStruct := &yotiprotoattr.Attribute{}
+
+ err2 := proto.Unmarshal(attributeBytes, attributeStruct)
+ assert.NilError(t, err2)
+
+ return attributeStruct
+}
diff --git a/digitalidentity/attribute/image_attribute.go b/digitalidentity/attribute/image_attribute.go
new file mode 100644
index 00000000..fd9d7f14
--- /dev/null
+++ b/digitalidentity/attribute/image_attribute.go
@@ -0,0 +1,53 @@
+package attribute
+
+import (
+ "errors"
+
+ "github.com/getyoti/yoti-go-sdk/v3/media"
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+// ImageAttribute is a Yoti attribute which returns an image as its value
+type ImageAttribute struct {
+ attributeDetails
+ value media.Media
+}
+
+// NewImage creates a new Image attribute
+func NewImage(a *yotiprotoattr.Attribute) (*ImageAttribute, error) {
+ imageValue, err := parseImageValue(a.ContentType, a.Value)
+ if err != nil {
+ return nil, err
+ }
+
+ parsedAnchors := anchor.ParseAnchors(a.Anchors)
+
+ return &ImageAttribute{
+ attributeDetails: attributeDetails{
+ name: a.Name,
+ contentType: a.ContentType.String(),
+ anchors: parsedAnchors,
+ id: &a.EphemeralId,
+ },
+ value: imageValue,
+ }, nil
+}
+
+// Value returns the value of the ImageAttribute as media.Media
+func (a *ImageAttribute) Value() media.Media {
+ return a.value
+}
+
+func parseImageValue(contentType yotiprotoattr.ContentType, byteValue []byte) (media.Media, error) {
+ switch contentType {
+ case yotiprotoattr.ContentType_JPEG:
+ return media.JPEGImage(byteValue), nil
+
+ case yotiprotoattr.ContentType_PNG:
+ return media.PNGImage(byteValue), nil
+
+ default:
+ return nil, errors.New("cannot create Image with unsupported type")
+ }
+}
diff --git a/digitalidentity/attribute/image_attribute_test.go b/digitalidentity/attribute/image_attribute_test.go
new file mode 100644
index 00000000..2fe620f6
--- /dev/null
+++ b/digitalidentity/attribute/image_attribute_test.go
@@ -0,0 +1,106 @@
+package attribute
+
+import (
+ "encoding/base64"
+ "testing"
+
+ "github.com/getyoti/yoti-go-sdk/v3/consts"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "gotest.tools/v3/assert"
+)
+
+func TestImageAttribute_Image_Png(t *testing.T) {
+ attributeName := consts.AttrSelfie
+ byteValue := []byte("value")
+
+ var attributeImage = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: byteValue,
+ ContentType: yotiprotoattr.ContentType_PNG,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ selfie, err := NewImage(attributeImage)
+ assert.NilError(t, err)
+
+ assert.DeepEqual(t, selfie.Value().Data(), byteValue)
+}
+
+func TestImageAttribute_Image_Jpeg(t *testing.T) {
+ attributeName := consts.AttrSelfie
+ byteValue := []byte("value")
+
+ var attributeImage = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: byteValue,
+ ContentType: yotiprotoattr.ContentType_JPEG,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ selfie, err := NewImage(attributeImage)
+ assert.NilError(t, err)
+
+ assert.DeepEqual(t, selfie.Value().Data(), byteValue)
+}
+
+func TestImageAttribute_Image_Default(t *testing.T) {
+ attributeName := consts.AttrSelfie
+ byteValue := []byte("value")
+
+ var attributeImage = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: byteValue,
+ ContentType: yotiprotoattr.ContentType_PNG,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+ selfie, err := NewImage(attributeImage)
+ assert.NilError(t, err)
+
+ assert.DeepEqual(t, selfie.Value().Data(), byteValue)
+}
+
+func TestImageAttribute_Base64Selfie_Png(t *testing.T) {
+ attributeName := consts.AttrSelfie
+ imageBytes := []byte("value")
+
+ var attributeImage = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: imageBytes,
+ ContentType: yotiprotoattr.ContentType_PNG,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ selfie, err := NewImage(attributeImage)
+ assert.NilError(t, err)
+
+ base64ImageExpectedValue := base64.StdEncoding.EncodeToString(imageBytes)
+
+ expectedBase64Selfie := "data:image/png;base64," + base64ImageExpectedValue
+
+ base64Selfie := selfie.Value().Base64URL()
+
+ assert.Equal(t, base64Selfie, expectedBase64Selfie)
+}
+
+func TestImageAttribute_Base64URL_Jpeg(t *testing.T) {
+ attributeName := consts.AttrSelfie
+ imageBytes := []byte("value")
+
+ var attributeImage = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: imageBytes,
+ ContentType: yotiprotoattr.ContentType_JPEG,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ selfie, err := NewImage(attributeImage)
+ assert.NilError(t, err)
+
+ base64ImageExpectedValue := base64.StdEncoding.EncodeToString(imageBytes)
+
+ expectedBase64Selfie := "data:image/jpeg;base64," + base64ImageExpectedValue
+
+ base64Selfie := selfie.Value().Base64URL()
+
+ assert.Equal(t, base64Selfie, expectedBase64Selfie)
+}
diff --git a/digitalidentity/attribute/image_slice_attribute.go b/digitalidentity/attribute/image_slice_attribute.go
new file mode 100644
index 00000000..de507ab6
--- /dev/null
+++ b/digitalidentity/attribute/image_slice_attribute.go
@@ -0,0 +1,69 @@
+package attribute
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/getyoti/yoti-go-sdk/v3/media"
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+// ImageSliceAttribute is a Yoti attribute which returns a slice of images as its value
+type ImageSliceAttribute struct {
+ attributeDetails
+ value []media.Media
+}
+
+// NewImageSlice creates a new ImageSlice attribute
+func NewImageSlice(a *yotiprotoattr.Attribute) (*ImageSliceAttribute, error) {
+ if a.ContentType != yotiprotoattr.ContentType_MULTI_VALUE {
+ return nil, errors.New("creating an Image Slice attribute with content types other than MULTI_VALUE is not supported")
+ }
+
+ parsedMultiValue, err := parseMultiValue(a.Value)
+
+ if err != nil {
+ return nil, err
+ }
+
+ var imageSliceValue []media.Media
+ if parsedMultiValue != nil {
+ imageSliceValue, err = CreateImageSlice(parsedMultiValue)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return &ImageSliceAttribute{
+ attributeDetails: attributeDetails{
+ name: a.Name,
+ contentType: a.ContentType.String(),
+ anchors: anchor.ParseAnchors(a.Anchors),
+ id: &a.EphemeralId,
+ },
+ value: imageSliceValue,
+ }, nil
+}
+
+// CreateImageSlice takes a slice of Items, and converts them into a slice of images
+func CreateImageSlice(items []*Item) (result []media.Media, err error) {
+ for _, item := range items {
+
+ switch i := item.Value.(type) {
+ case media.PNGImage:
+ result = append(result, i)
+ case media.JPEGImage:
+ result = append(result, i)
+ default:
+ return nil, fmt.Errorf("unexpected item type %T", i)
+ }
+ }
+
+ return result, nil
+}
+
+// Value returns the value of the ImageSliceAttribute
+func (a *ImageSliceAttribute) Value() []media.Media {
+ return a.value
+}
diff --git a/digitalidentity/attribute/image_slice_attribute_test.go b/digitalidentity/attribute/image_slice_attribute_test.go
new file mode 100644
index 00000000..2c300926
--- /dev/null
+++ b/digitalidentity/attribute/image_slice_attribute_test.go
@@ -0,0 +1,61 @@
+package attribute
+
+import (
+ "testing"
+
+ "github.com/getyoti/yoti-go-sdk/v3/media"
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "gotest.tools/v3/assert"
+)
+
+func assertIsExpectedImage(t *testing.T, image media.Media, imageMIMEType string, expectedBase64URLLast10 string) {
+ assert.Equal(t, image.MIME(), imageMIMEType)
+
+ actualBase64URL := image.Base64URL()
+
+ ActualBase64URLLast10Chars := actualBase64URL[len(actualBase64URL)-10:]
+
+ assert.Equal(t, ActualBase64URLLast10Chars, expectedBase64URLLast10)
+}
+
+func assertIsExpectedDocumentImagesAttribute(t *testing.T, actualDocumentImages []media.Media, anchor *anchor.Anchor) {
+
+ assert.Equal(t, len(actualDocumentImages), 2, "This Document Images attribute should have two images")
+
+ assertIsExpectedImage(t, actualDocumentImages[0], media.ImageTypeJPEG, "vWgD//2Q==")
+ assertIsExpectedImage(t, actualDocumentImages[1], media.ImageTypeJPEG, "38TVEH/9k=")
+
+ expectedValue := "NATIONAL_ID"
+ assert.Equal(t, anchor.Value(), expectedValue)
+
+ expectedSubType := "STATE_ID"
+ assert.Equal(t, anchor.SubType(), expectedSubType)
+}
+
+func TestAttribute_NewImageSlice(t *testing.T) {
+ protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_multivalue.txt")
+
+ documentImagesAttribute, err := NewImageSlice(protoAttribute)
+
+ assert.NilError(t, err)
+
+ assertIsExpectedDocumentImagesAttribute(t, documentImagesAttribute.Value(), documentImagesAttribute.Anchors()[0])
+}
+
+func TestAttribute_ImageSliceNotCreatedWithNonMultiValueType(t *testing.T) {
+ attributeName := "attributeName"
+ attributeValueString := "value"
+ attributeValue := []byte(attributeValueString)
+
+ var attr = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: attributeValue,
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ _, err := NewImageSlice(attr)
+
+ assert.Assert(t, err != nil, "Expected error when creating image slice from attribute which isn't of multi-value type")
+}
diff --git a/digitalidentity/attribute/issuance_details.go b/digitalidentity/attribute/issuance_details.go
new file mode 100644
index 00000000..381d4e99
--- /dev/null
+++ b/digitalidentity/attribute/issuance_details.go
@@ -0,0 +1,86 @@
+package attribute
+
+import (
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoshare"
+ "google.golang.org/protobuf/proto"
+)
+
+// IssuanceDetails contains information about the attribute(s) issued by a third party
+type IssuanceDetails struct {
+ token string
+ expiryDate *time.Time
+ attributes []Definition
+}
+
+// Token is the issuance token that can be used to retrieve the user's stored details.
+// These details will be used to issue attributes on behalf of an organisation to that user.
+func (i IssuanceDetails) Token() string {
+ return i.token
+}
+
+// ExpiryDate is the timestamp at which the request for the attribute value
+// from third party will expire. Will be nil if not provided.
+func (i IssuanceDetails) ExpiryDate() *time.Time {
+ return i.expiryDate
+}
+
+// Attributes information about the attributes the third party would like to issue.
+func (i IssuanceDetails) Attributes() []Definition {
+ return i.attributes
+}
+
+// ParseIssuanceDetails takes the Third Party Attribute object and converts it into an IssuanceDetails struct
+func ParseIssuanceDetails(thirdPartyAttributeBytes []byte) (*IssuanceDetails, error) {
+ thirdPartyAttributeStruct := &yotiprotoshare.ThirdPartyAttribute{}
+ if err := proto.Unmarshal(thirdPartyAttributeBytes, thirdPartyAttributeStruct); err != nil {
+ return nil, fmt.Errorf("unable to parse ThirdPartyAttribute value: %q. Error: %q", string(thirdPartyAttributeBytes), err)
+ }
+
+ var issuingAttributesProto = thirdPartyAttributeStruct.GetIssuingAttributes()
+ var issuingAttributeDefinitions = parseIssuingAttributeDefinitions(issuingAttributesProto.GetDefinitions())
+
+ expiryDate, dateParseErr := parseExpiryDate(issuingAttributesProto.ExpiryDate)
+
+ var issuanceTokenBytes = thirdPartyAttributeStruct.GetIssuanceToken()
+
+ if len(issuanceTokenBytes) == 0 {
+ return nil, errors.New("Issuance Token is invalid")
+ }
+
+ base64EncodedToken := base64.StdEncoding.EncodeToString(issuanceTokenBytes)
+
+ return &IssuanceDetails{
+ token: base64EncodedToken,
+ expiryDate: expiryDate,
+ attributes: issuingAttributeDefinitions,
+ }, dateParseErr
+}
+
+func parseIssuingAttributeDefinitions(definitions []*yotiprotoshare.Definition) (issuingAttributes []Definition) {
+ for _, definition := range definitions {
+ attributeDefinition := Definition{
+ name: definition.Name,
+ }
+ issuingAttributes = append(issuingAttributes, attributeDefinition)
+ }
+
+ return issuingAttributes
+}
+
+func parseExpiryDate(expiryDateString string) (*time.Time, error) {
+ if expiryDateString == "" {
+ return nil, nil
+ }
+
+ parsedTime, err := time.Parse(time.RFC3339Nano, expiryDateString)
+ if err != nil {
+ return nil, err
+ }
+
+ return &parsedTime, err
+}
diff --git a/digitalidentity/attribute/issuance_details_test.go b/digitalidentity/attribute/issuance_details_test.go
new file mode 100644
index 00000000..462d863e
--- /dev/null
+++ b/digitalidentity/attribute/issuance_details_test.go
@@ -0,0 +1,145 @@
+package attribute
+
+import (
+ "encoding/base64"
+ "testing"
+ "time"
+
+ "github.com/getyoti/yoti-go-sdk/v3/test"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoshare"
+ "google.golang.org/protobuf/proto"
+
+ "gotest.tools/v3/assert"
+
+ is "gotest.tools/v3/assert/cmp"
+)
+
+func TestShouldParseThirdPartyAttributeCorrectly(t *testing.T) {
+ var thirdPartyAttributeBytes = test.GetTestFileBytes(t, "../../test/fixtures/test_third_party_issuance_details.txt")
+ issuanceDetails, err := ParseIssuanceDetails(thirdPartyAttributeBytes)
+
+ assert.NilError(t, err)
+ assert.Equal(t, issuanceDetails.Attributes()[0].Name(), "com.thirdparty.id")
+ assert.Equal(t, issuanceDetails.Token(), "c29tZUlzc3VhbmNlVG9rZW4=")
+ assert.Equal(t,
+ issuanceDetails.ExpiryDate().Format("2006-01-02T15:04:05.000Z"),
+ "2019-10-15T22:04:05.123Z")
+}
+
+func TestShouldLogWarningIfErrorInParsingExpiryDate(t *testing.T) {
+ var tokenValue = "41548a175dfaw"
+ thirdPartyAttribute := &yotiprotoshare.ThirdPartyAttribute{
+ IssuanceToken: []byte(tokenValue),
+ IssuingAttributes: &yotiprotoshare.IssuingAttributes{
+ ExpiryDate: "2006-13-02T15:04:05.000Z",
+ },
+ }
+
+ marshalled, err := proto.Marshal(thirdPartyAttribute)
+
+ assert.NilError(t, err)
+
+ var tokenBytes = []byte(tokenValue)
+ var expectedBase64Token = base64.StdEncoding.EncodeToString(tokenBytes)
+
+ result, err := ParseIssuanceDetails(marshalled)
+ assert.Equal(t, expectedBase64Token, result.Token())
+ assert.Assert(t, is.Nil(result.ExpiryDate()))
+ assert.Equal(t, "parsing time \"2006-13-02T15:04:05.000Z\": month out of range", err.Error())
+}
+
+func TestIssuanceDetails_parseExpiryDate_ShouldParseAllRFC3339Formats(t *testing.T) {
+ table := []struct {
+ Input string
+ Expected time.Time
+ }{
+ {
+ Input: "2006-01-02T22:04:05Z",
+ Expected: time.Date(2006, 01, 02, 22, 4, 5, 0, time.UTC),
+ },
+ {
+ Input: "2010-05-20T10:44:25Z",
+ Expected: time.Date(2010, 5, 20, 10, 44, 25, 0, time.UTC),
+ },
+ {
+ Input: "2006-01-02T22:04:05.1Z",
+ Expected: time.Date(2006, 1, 2, 22, 4, 5, 100e6, time.UTC),
+ },
+ {
+ Input: "2012-03-06T04:20:07.5Z",
+ Expected: time.Date(2012, 3, 6, 4, 20, 7, 500e6, time.UTC),
+ },
+ {
+ Input: "2006-01-02T22:04:05.12Z",
+ Expected: time.Date(2006, 1, 2, 22, 4, 5, 120e6, time.UTC),
+ },
+ {
+ Input: "2013-03-04T20:43:55.56Z",
+ Expected: time.Date(2013, 3, 4, 20, 43, 55, 560e6, time.UTC),
+ },
+ {
+ Input: "2006-01-02T22:04:05.123Z",
+ Expected: time.Date(2006, 1, 2, 22, 4, 5, 123e6, time.UTC),
+ },
+ {
+ Input: "2007-04-07T17:34:11.784Z",
+ Expected: time.Date(2007, 4, 7, 17, 34, 11, 784e6, time.UTC),
+ },
+ {
+ Input: "2006-01-02T22:04:05.1234Z",
+ Expected: time.Date(2006, 1, 2, 22, 4, 5, 123400e3, time.UTC),
+ },
+ {
+ Input: "2017-09-14T16:54:30.4784Z",
+ Expected: time.Date(2017, 9, 14, 16, 54, 30, 478400e3, time.UTC),
+ },
+ {
+ Input: "2006-01-02T22:04:05.12345Z",
+ Expected: time.Date(2006, 1, 2, 22, 4, 5, 123450e3, time.UTC),
+ },
+ {
+ Input: "2009-06-07T14:20:30.74622Z",
+ Expected: time.Date(2009, 6, 7, 14, 20, 30, 746220e3, time.UTC),
+ },
+ {
+ Input: "2006-01-02T22:04:05.123456Z",
+ Expected: time.Date(2006, 1, 2, 22, 4, 5, 123456e3, time.UTC),
+ },
+ {
+ Input: "2008-10-25T06:50:55.643562Z",
+ Expected: time.Date(2008, 10, 25, 6, 50, 55, 643562e3, time.UTC),
+ },
+ {
+ Input: "2002-10-02T10:00:00-05:00",
+ Expected: time.Date(2002, 10, 2, 10, 0, 0, 0, time.FixedZone("-0500", -5*60*60)),
+ },
+ {
+ Input: "2002-10-02T10:00:00+11:00",
+ Expected: time.Date(2002, 10, 2, 10, 0, 0, 0, time.FixedZone("+1100", 11*60*60)),
+ },
+ {
+ Input: "1920-03-13T19:50:53.999999Z",
+ Expected: time.Date(1920, 3, 13, 19, 50, 53, 999999e3, time.UTC),
+ },
+ {
+ Input: "1920-03-13T19:50:54.000001Z",
+ Expected: time.Date(1920, 3, 13, 19, 50, 54, 1e3, time.UTC),
+ },
+ }
+
+ for _, row := range table {
+ func(input string, expected time.Time) {
+ expiryDate, err := parseExpiryDate(input)
+ assert.NilError(t, err)
+ assert.Equal(t, expiryDate.UTC(), expected.UTC())
+ }(row.Input, row.Expected)
+ }
+}
+
+func TestInvalidProtobufThrowsError(t *testing.T) {
+ result, err := ParseIssuanceDetails([]byte("invalid"))
+
+ assert.Assert(t, is.Nil(result))
+
+ assert.ErrorContains(t, err, "unable to parse ThirdPartyAttribute value")
+}
diff --git a/digitalidentity/attribute/item.go b/digitalidentity/attribute/item.go
new file mode 100644
index 00000000..3efd2b96
--- /dev/null
+++ b/digitalidentity/attribute/item.go
@@ -0,0 +1,14 @@
+package attribute
+
+import (
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+// Item is a structure which contains information about an attribute value
+type Item struct {
+ // ContentType is the content of the item.
+ ContentType yotiprotoattr.ContentType
+
+ // Value is the underlying data of the item.
+ Value interface{}
+}
diff --git a/digitalidentity/attribute/json_attribute.go b/digitalidentity/attribute/json_attribute.go
new file mode 100644
index 00000000..be40920d
--- /dev/null
+++ b/digitalidentity/attribute/json_attribute.go
@@ -0,0 +1,58 @@
+package attribute
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+// JSONAttribute is a Yoti attribute which returns an interface as its value
+type JSONAttribute struct {
+ attributeDetails
+ // value returns the value of a JSON attribute in the form of an interface
+ value map[string]interface{}
+}
+
+// NewJSON creates a new JSON attribute
+func NewJSON(a *yotiprotoattr.Attribute) (*JSONAttribute, error) {
+ var interfaceValue map[string]interface{}
+ decoder := json.NewDecoder(bytes.NewReader(a.Value))
+ decoder.UseNumber()
+ err := decoder.Decode(&interfaceValue)
+ if err != nil {
+ err = fmt.Errorf("unable to parse JSON value: %q. Error: %q", a.Value, err)
+ return nil, err
+ }
+
+ parsedAnchors := anchor.ParseAnchors(a.Anchors)
+
+ return &JSONAttribute{
+ attributeDetails: attributeDetails{
+ name: a.Name,
+ contentType: a.ContentType.String(),
+ anchors: parsedAnchors,
+ id: &a.EphemeralId,
+ },
+ value: interfaceValue,
+ }, nil
+}
+
+// unmarshallJSON unmarshalls JSON into an interface
+func unmarshallJSON(byteValue []byte) (result map[string]interface{}, err error) {
+ var unmarshalledJSON map[string]interface{}
+ err = json.Unmarshal(byteValue, &unmarshalledJSON)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return unmarshalledJSON, err
+}
+
+// Value returns the value of the JSONAttribute as an interface.
+func (a *JSONAttribute) Value() map[string]interface{} {
+ return a.value
+}
diff --git a/digitalidentity/attribute/json_attribute_test.go b/digitalidentity/attribute/json_attribute_test.go
new file mode 100644
index 00000000..42756373
--- /dev/null
+++ b/digitalidentity/attribute/json_attribute_test.go
@@ -0,0 +1,76 @@
+package attribute
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "gotest.tools/v3/assert"
+)
+
+func ExampleNewJSON() {
+ proto := yotiprotoattr.Attribute{
+ Name: "exampleJSON",
+ Value: []byte(`{"foo":"bar"}`),
+ ContentType: yotiprotoattr.ContentType_JSON,
+ }
+ attribute, err := NewJSON(&proto)
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+ fmt.Println(attribute.Value())
+ // Output: map[foo:bar]
+}
+
+func TestNewJSON_ShouldReturnNilForInvalidJSON(t *testing.T) {
+ proto := yotiprotoattr.Attribute{
+ Name: "exampleJSON",
+ Value: []byte("Not a json document"),
+ ContentType: yotiprotoattr.ContentType_JSON,
+ }
+ attribute, err := NewJSON(&proto)
+ assert.Check(t, attribute == nil)
+ assert.ErrorContains(t, err, "unable to parse JSON value")
+}
+
+func TestYotiClient_UnmarshallJSONValue_InvalidValueThrowsError(t *testing.T) {
+ invalidStructuredAddress := []byte("invalidBool")
+
+ _, err := unmarshallJSON(invalidStructuredAddress)
+
+ assert.Assert(t, err != nil)
+}
+
+func TestYotiClient_UnmarshallJSONValue_ValidValue(t *testing.T) {
+ const (
+ countryIso = "IND"
+ nestedValue = "NestedValue"
+ )
+
+ var structuredAddress = []byte(`
+ {
+ "address_format": 2,
+ "building": "House No.86-A",
+ "state": "Punjab",
+ "postal_code": "141012",
+ "country_iso": "` + countryIso + `",
+ "country": "India",
+ "formatted_address": "House No.86-A\nRajgura Nagar\nLudhina\nPunjab\n141012\nIndia",
+ "1":
+ {
+ "1-1":
+ {
+ "1-1-1": "` + nestedValue + `"
+ }
+ }
+ }
+ `)
+
+ parsedStructuredAddress, err := unmarshallJSON(structuredAddress)
+ assert.NilError(t, err, "Failed to parse structured address")
+
+ actualCountryIso := parsedStructuredAddress["country_iso"]
+
+ assert.Equal(t, countryIso, actualCountryIso)
+}
diff --git a/digitalidentity/attribute/multivalue_attribute.go b/digitalidentity/attribute/multivalue_attribute.go
new file mode 100644
index 00000000..926141f9
--- /dev/null
+++ b/digitalidentity/attribute/multivalue_attribute.go
@@ -0,0 +1,90 @@
+package attribute
+
+import (
+ "fmt"
+
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "google.golang.org/protobuf/proto"
+)
+
+// MultiValueAttribute is a Yoti attribute which returns a multi-valued attribute
+type MultiValueAttribute struct {
+ attributeDetails
+ items []*Item
+}
+
+// NewMultiValue creates a new MultiValue attribute
+func NewMultiValue(a *yotiprotoattr.Attribute) (*MultiValueAttribute, error) {
+ attributeItems, err := parseMultiValue(a.Value)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &MultiValueAttribute{
+ attributeDetails: attributeDetails{
+ name: a.Name,
+ contentType: a.ContentType.String(),
+ anchors: anchor.ParseAnchors(a.Anchors),
+ id: &a.EphemeralId,
+ },
+ items: attributeItems,
+ }, nil
+}
+
+// parseMultiValue recursively unmarshals and converts Multi Value bytes into a slice of Items
+func parseMultiValue(data []byte) ([]*Item, error) {
+ var attributeItems []*Item
+ protoMultiValueStruct, err := unmarshallMultiValue(data)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for _, multiValueItem := range protoMultiValueStruct.Values {
+ var value *Item
+ if multiValueItem.ContentType == yotiprotoattr.ContentType_MULTI_VALUE {
+ parsedInnerMultiValueItems, err := parseMultiValue(multiValueItem.Data)
+
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse multi-value data: %v", err)
+ }
+
+ value = &Item{
+ ContentType: yotiprotoattr.ContentType_MULTI_VALUE,
+ Value: parsedInnerMultiValueItems,
+ }
+ } else {
+ itemValue, err := parseValue(multiValueItem.ContentType, multiValueItem.Data)
+
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse data within a multi-value attribute. Content type: %q, data: %q, error: %v",
+ multiValueItem.ContentType, multiValueItem.Data, err)
+ }
+
+ value = &Item{
+ ContentType: multiValueItem.ContentType,
+ Value: itemValue,
+ }
+ }
+ attributeItems = append(attributeItems, value)
+ }
+
+ return attributeItems, nil
+}
+
+func unmarshallMultiValue(bytes []byte) (*yotiprotoattr.MultiValue, error) {
+ multiValueStruct := &yotiprotoattr.MultiValue{}
+
+ if err := proto.Unmarshal(bytes, multiValueStruct); err != nil {
+ return nil, fmt.Errorf("unable to parse MULTI_VALUE value: %q. Error: %q", string(bytes), err)
+ }
+
+ return multiValueStruct, nil
+}
+
+// Value returns the value of the MultiValueAttribute as a string
+func (a *MultiValueAttribute) Value() []*Item {
+ return a.items
+}
diff --git a/digitalidentity/attribute/multivalue_attribute_test.go b/digitalidentity/attribute/multivalue_attribute_test.go
new file mode 100644
index 00000000..15a24f99
--- /dev/null
+++ b/digitalidentity/attribute/multivalue_attribute_test.go
@@ -0,0 +1,157 @@
+package attribute
+
+import (
+ "testing"
+
+ "github.com/getyoti/yoti-go-sdk/v3/media"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "google.golang.org/protobuf/proto"
+ "gotest.tools/v3/assert"
+ is "gotest.tools/v3/assert/cmp"
+)
+
+func marshallMultiValue(t *testing.T, multiValue *yotiprotoattr.MultiValue) []byte {
+ marshalled, err := proto.Marshal(multiValue)
+
+ assert.NilError(t, err)
+
+ return marshalled
+}
+
+func createMultiValueAttribute(t *testing.T, multiValueItemSlice []*yotiprotoattr.MultiValue_Value) (*MultiValueAttribute, error) {
+ var multiValueStruct = &yotiprotoattr.MultiValue{
+ Values: multiValueItemSlice,
+ }
+
+ var marshalledMultiValueData = marshallMultiValue(t, multiValueStruct)
+ attributeName := "nestedMultiValue"
+
+ var protoAttribute = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: marshalledMultiValueData,
+ ContentType: yotiprotoattr.ContentType_MULTI_VALUE,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ return NewMultiValue(protoAttribute)
+}
+
+func TestAttribute_MultiValueNotCreatedWithNonMultiValueType(t *testing.T) {
+ attributeName := "attributeName"
+ attributeValueString := "value"
+ attributeValue := []byte(attributeValueString)
+
+ var attr = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: attributeValue,
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ _, err := NewMultiValue(attr)
+
+ assert.Assert(t, err != nil, "Expected error when creating multi value from attribute which isn't of multi-value type")
+}
+
+func TestAttribute_NewMultiValue(t *testing.T) {
+ protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_multivalue.txt")
+
+ multiValueAttribute, err := NewMultiValue(protoAttribute)
+
+ assert.NilError(t, err)
+
+ documentImagesAttributeItems, err := CreateImageSlice(multiValueAttribute.Value())
+ assert.NilError(t, err)
+
+ assertIsExpectedDocumentImagesAttribute(t, documentImagesAttributeItems, multiValueAttribute.Anchors()[0])
+}
+
+func TestAttribute_InvalidMultiValueNotReturned(t *testing.T) {
+ var invalidMultiValueItem = &yotiprotoattr.MultiValue_Value{
+ ContentType: yotiprotoattr.ContentType_DATE,
+ Data: []byte("invalid"),
+ }
+
+ var stringMultiValueItem = &yotiprotoattr.MultiValue_Value{
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Data: []byte("string"),
+ }
+
+ var multiValueItemSlice = []*yotiprotoattr.MultiValue_Value{invalidMultiValueItem, stringMultiValueItem}
+
+ var multiValueStruct = &yotiprotoattr.MultiValue{
+ Values: multiValueItemSlice,
+ }
+
+ var marshalledMultiValueData = marshallMultiValue(t, multiValueStruct)
+ attributeName := "nestedMultiValue"
+
+ var protoAttribute = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: marshalledMultiValueData,
+ ContentType: yotiprotoattr.ContentType_MULTI_VALUE,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ multiValueAttr, err := NewMultiValue(protoAttribute)
+ assert.Check(t, err != nil)
+
+ assert.Assert(t, is.Nil(multiValueAttr))
+}
+
+func TestAttribute_NestedMultiValue(t *testing.T) {
+ var innerMultiValueProtoValue = createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_multivalue.txt").Value
+
+ var stringMultiValueItem = &yotiprotoattr.MultiValue_Value{
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Data: []byte("string"),
+ }
+
+ var multiValueItem = &yotiprotoattr.MultiValue_Value{
+ ContentType: yotiprotoattr.ContentType_MULTI_VALUE,
+ Data: innerMultiValueProtoValue,
+ }
+
+ var multiValueItemSlice = []*yotiprotoattr.MultiValue_Value{stringMultiValueItem, multiValueItem}
+
+ multiValueAttribute, err := createMultiValueAttribute(t, multiValueItemSlice)
+
+ assert.NilError(t, err)
+
+ for key, value := range multiValueAttribute.Value() {
+ switch key {
+ case 0:
+ value0 := value.Value
+
+ assert.Equal(t, value0.(string), "string")
+ case 1:
+ value1 := value.Value
+
+ innerItems, ok := value1.([]*Item)
+ assert.Assert(t, ok)
+
+ for innerKey, item := range innerItems {
+ switch innerKey {
+ case 0:
+ assertIsExpectedImage(t, item.Value.(media.Media), media.ImageTypeJPEG, "vWgD//2Q==")
+
+ case 1:
+ assertIsExpectedImage(t, item.Value.(media.Media), media.ImageTypeJPEG, "38TVEH/9k=")
+ }
+ }
+ }
+ }
+}
+
+func TestAttribute_MultiValueGenericGetter(t *testing.T) {
+ protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_multivalue.txt")
+ multiValueAttribute, err := NewMultiValue(protoAttribute)
+ assert.NilError(t, err)
+
+ // We need to cast, since GetAttribute always returns generic attributes
+ multiValueAttributeValue := multiValueAttribute.Value()
+ imageSlice, err := CreateImageSlice(multiValueAttributeValue)
+ assert.NilError(t, err)
+
+ assertIsExpectedDocumentImagesAttribute(t, imageSlice, multiValueAttribute.Anchors()[0])
+}
diff --git a/digitalidentity/attribute/parser.go b/digitalidentity/attribute/parser.go
new file mode 100644
index 00000000..d6635957
--- /dev/null
+++ b/digitalidentity/attribute/parser.go
@@ -0,0 +1,56 @@
+package attribute
+
+import (
+ "fmt"
+ "strconv"
+ "time"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+func parseValue(contentType yotiprotoattr.ContentType, byteValue []byte) (interface{}, error) {
+ switch contentType {
+ case yotiprotoattr.ContentType_DATE:
+ parsedTime, err := time.Parse("2006-01-02", string(byteValue))
+
+ if err == nil {
+ return &parsedTime, nil
+ }
+
+ return nil, fmt.Errorf("unable to parse date value: %q. Error: %q", string(byteValue), err)
+
+ case yotiprotoattr.ContentType_JSON:
+ unmarshalledJSON, err := unmarshallJSON(byteValue)
+
+ if err == nil {
+ return unmarshalledJSON, nil
+ }
+
+ return nil, fmt.Errorf("unable to parse JSON value: %q. Error: %q", string(byteValue), err)
+
+ case yotiprotoattr.ContentType_STRING:
+ return string(byteValue), nil
+
+ case yotiprotoattr.ContentType_MULTI_VALUE:
+ return parseMultiValue(byteValue)
+
+ case yotiprotoattr.ContentType_INT:
+ var stringValue = string(byteValue)
+ intValue, err := strconv.Atoi(stringValue)
+ if err == nil {
+ return intValue, nil
+ }
+
+ return nil, fmt.Errorf("unable to parse INT value: %q. Error: %q", string(byteValue), err)
+
+ case yotiprotoattr.ContentType_JPEG,
+ yotiprotoattr.ContentType_PNG:
+ return parseImageValue(contentType, byteValue)
+
+ case yotiprotoattr.ContentType_UNDEFINED:
+ return string(byteValue), nil
+
+ default:
+ return string(byteValue), nil
+ }
+}
diff --git a/digitalidentity/attribute/parser_test.go b/digitalidentity/attribute/parser_test.go
new file mode 100644
index 00000000..cc9f3d8b
--- /dev/null
+++ b/digitalidentity/attribute/parser_test.go
@@ -0,0 +1,16 @@
+package attribute
+
+import (
+ "testing"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "gotest.tools/v3/assert"
+)
+
+func TestParseValue_ShouldParseInt(t *testing.T) {
+ parsed, err := parseValue(yotiprotoattr.ContentType_INT, []byte("7"))
+ assert.NilError(t, err)
+ integer, ok := parsed.(int)
+ assert.Check(t, ok)
+ assert.Equal(t, integer, 7)
+}
diff --git a/digitalidentity/attribute/string_attribute.go b/digitalidentity/attribute/string_attribute.go
new file mode 100644
index 00000000..73346b9a
--- /dev/null
+++ b/digitalidentity/attribute/string_attribute.go
@@ -0,0 +1,32 @@
+package attribute
+
+import (
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+// StringAttribute is a Yoti attribute which returns a string as its value
+type StringAttribute struct {
+ attributeDetails
+ value string
+}
+
+// NewString creates a new String attribute
+func NewString(a *yotiprotoattr.Attribute) *StringAttribute {
+ parsedAnchors := anchor.ParseAnchors(a.Anchors)
+
+ return &StringAttribute{
+ attributeDetails: attributeDetails{
+ name: a.Name,
+ contentType: a.ContentType.String(),
+ anchors: parsedAnchors,
+ id: &a.EphemeralId,
+ },
+ value: string(a.Value),
+ }
+}
+
+// Value returns the value of the StringAttribute as a string
+func (a *StringAttribute) Value() string {
+ return a.value
+}
diff --git a/digitalidentity/attribute/string_attribute_test.go b/digitalidentity/attribute/string_attribute_test.go
new file mode 100644
index 00000000..828df201
--- /dev/null
+++ b/digitalidentity/attribute/string_attribute_test.go
@@ -0,0 +1,22 @@
+package attribute
+
+import (
+ "testing"
+
+ "gotest.tools/v3/assert"
+)
+
+func TestStringAttribute_NewThirdPartyAttribute(t *testing.T) {
+ protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_third_party.txt")
+
+ stringAttribute := NewString(protoAttribute)
+
+ assert.Equal(t, stringAttribute.Value(), "test-third-party-attribute-0")
+ assert.Equal(t, stringAttribute.Name(), "com.thirdparty.id")
+
+ assert.Equal(t, stringAttribute.Sources()[0].Value(), "THIRD_PARTY")
+ assert.Equal(t, stringAttribute.Sources()[0].SubType(), "orgName")
+
+ assert.Equal(t, stringAttribute.Verifiers()[0].Value(), "THIRD_PARTY")
+ assert.Equal(t, stringAttribute.Verifiers()[0].SubType(), "orgName")
+}
diff --git a/digitalidentity/base_profile.go b/digitalidentity/base_profile.go
new file mode 100644
index 00000000..693441d0
--- /dev/null
+++ b/digitalidentity/base_profile.go
@@ -0,0 +1,75 @@
+package digitalidentity
+
+import (
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+type baseProfile struct {
+ attributeSlice []*yotiprotoattr.Attribute
+}
+
+// GetAttribute retrieve an attribute by name on the Yoti profile. Will return nil if attribute is not present.
+func (p baseProfile) GetAttribute(attributeName string) *attribute.GenericAttribute {
+ for _, a := range p.attributeSlice {
+ if a.Name == attributeName {
+ return attribute.NewGeneric(a)
+ }
+ }
+ return nil
+}
+
+// GetAttributeByID retrieve an attribute by ID on the Yoti profile. Will return nil if attribute is not present.
+func (p baseProfile) GetAttributeByID(attributeID string) *attribute.GenericAttribute {
+ for _, a := range p.attributeSlice {
+ if a.EphemeralId == attributeID {
+ return attribute.NewGeneric(a)
+ }
+ }
+ return nil
+}
+
+// GetAttributes retrieve a list of attributes by name on the Yoti profile. Will return an empty list of attribute is not present.
+func (p baseProfile) GetAttributes(attributeName string) []*attribute.GenericAttribute {
+ var attributes []*attribute.GenericAttribute
+ for _, a := range p.attributeSlice {
+ if a.Name == attributeName {
+ attributes = append(attributes, attribute.NewGeneric(a))
+ }
+ }
+ return attributes
+}
+
+// GetStringAttribute retrieves a string attribute by name. Will return nil if attribute is not present.
+func (p baseProfile) GetStringAttribute(attributeName string) *attribute.StringAttribute {
+ for _, a := range p.attributeSlice {
+ if a.Name == attributeName {
+ return attribute.NewString(a)
+ }
+ }
+ return nil
+}
+
+// GetImageAttribute retrieves an image attribute by name. Will return nil if attribute is not present.
+func (p baseProfile) GetImageAttribute(attributeName string) *attribute.ImageAttribute {
+ for _, a := range p.attributeSlice {
+ if a.Name == attributeName {
+ imageAttribute, err := attribute.NewImage(a)
+
+ if err == nil {
+ return imageAttribute
+ }
+ }
+ }
+ return nil
+}
+
+// GetJSONAttribute retrieves a JSON attribute by name. Will return nil if attribute is not present.
+func (p baseProfile) GetJSONAttribute(attributeName string) (*attribute.JSONAttribute, error) {
+ for _, a := range p.attributeSlice {
+ if a.Name == attributeName {
+ return attribute.NewJSON(a)
+ }
+ }
+ return nil, nil
+}
diff --git a/digitalidentity/policy_builder.go b/digitalidentity/policy_builder.go
new file mode 100644
index 00000000..ef9688bd
--- /dev/null
+++ b/digitalidentity/policy_builder.go
@@ -0,0 +1,250 @@
+package digitalidentity
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/getyoti/yoti-go-sdk/v3/consts"
+ "github.com/getyoti/yoti-go-sdk/v3/yotierror"
+)
+
+const (
+ authTypeSelfieConst = 1
+ authTypePinConst = 2
+)
+
+// PolicyBuilder constructs a json payload specifying the dynamic policy
+// for a dynamic scenario
+type PolicyBuilder struct {
+ wantedAttributes map[string]WantedAttribute
+ wantedAuthTypes map[int]bool
+ isWantedRememberMe bool
+ err error
+ identityProfileRequirements *json.RawMessage
+}
+
+// Policy represents a dynamic policy for a share
+type Policy struct {
+ attributes []WantedAttribute
+ authTypes []int
+ rememberMeID bool
+ identityProfileRequirements *json.RawMessage
+}
+
+// WithWantedAttribute adds an attribute from WantedAttributeBuilder to the policy
+func (b *PolicyBuilder) WithWantedAttribute(attribute WantedAttribute) *PolicyBuilder {
+ if b.wantedAttributes == nil {
+ b.wantedAttributes = make(map[string]WantedAttribute)
+ }
+ var key string
+ if attribute.derivation != "" {
+ key = attribute.derivation
+ } else {
+ key = attribute.name
+ }
+ b.wantedAttributes[key] = attribute
+ return b
+}
+
+// WithWantedAttributeByName adds an attribute by its name. This is not the preferred
+// way of adding an attribute - instead use the other methods below.
+// Options allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithWantedAttributeByName(name string, options ...interface{}) *PolicyBuilder {
+ attributeBuilder := (&WantedAttributeBuilder{}).WithName(name)
+
+ for _, option := range options {
+ switch value := option.(type) {
+ case SourceConstraint:
+ attributeBuilder.WithConstraint(&value)
+ case constraintInterface:
+ attributeBuilder.WithConstraint(value)
+ default:
+ panic(fmt.Sprintf("not a valid option type, %v", value))
+ }
+ }
+
+ attribute, err := attributeBuilder.Build()
+ if err != nil {
+ b.err = yotierror.MultiError{This: err, Next: b.err}
+ }
+ b.WithWantedAttribute(attribute)
+ return b
+}
+
+// WithFamilyName adds the family name attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithFamilyName(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrFamilyName, options...)
+}
+
+// WithGivenNames adds the given names attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithGivenNames(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrGivenNames, options...)
+}
+
+// WithFullName adds the full name attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithFullName(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrFullName, options...)
+}
+
+// WithDateOfBirth adds the date of birth attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithDateOfBirth(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrDateOfBirth, options...)
+}
+
+// WithGender adds the gender attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithGender(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrGender, options...)
+}
+
+// WithPostalAddress adds the postal address attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithPostalAddress(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrAddress, options...)
+}
+
+// WithStructuredPostalAddress adds the structured postal address attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithStructuredPostalAddress(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrStructuredPostalAddress, options...)
+}
+
+// WithNationality adds the nationality attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithNationality(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrNationality, options...)
+}
+
+// WithPhoneNumber adds the phone number attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithPhoneNumber(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrMobileNumber, options...)
+}
+
+// WithSelfie adds the selfie attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithSelfie(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrSelfie, options...)
+}
+
+// WithEmail adds the email address attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithEmail(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrEmailAddress, options...)
+}
+
+// WithDocumentImages adds the document images attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithDocumentImages(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrDocumentImages, options...)
+}
+
+// WithDocumentDetails adds the document details attribute, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithDocumentDetails(options ...interface{}) *PolicyBuilder {
+ return b.WithWantedAttributeByName(consts.AttrDocumentDetails, options...)
+}
+
+// WithAgeDerivedAttribute is a helper method for setting age based derivations
+// Prefer to use WithAgeOver and WithAgeUnder instead of using this directly.
+// "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithAgeDerivedAttribute(derivation string, options ...interface{}) *PolicyBuilder {
+ var attributeBuilder WantedAttributeBuilder
+ attributeBuilder.
+ WithName(consts.AttrDateOfBirth).
+ WithDerivation(derivation)
+
+ for _, option := range options {
+ switch value := option.(type) {
+ case SourceConstraint:
+ attributeBuilder.WithConstraint(&value)
+ case constraintInterface:
+ attributeBuilder.WithConstraint(value)
+ default:
+ panic(fmt.Sprintf("not a valid option type, %v", value))
+ }
+ }
+
+ attr, err := attributeBuilder.Build()
+ if err != nil {
+ b.err = yotierror.MultiError{This: err, Next: b.err}
+ }
+ return b.WithWantedAttribute(attr)
+}
+
+// WithAgeOver sets this dynamic policy as requesting whether the user is older than a certain age.
+// "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithAgeOver(age int, options ...interface{}) *PolicyBuilder {
+ return b.WithAgeDerivedAttribute(fmt.Sprintf(consts.AttrAgeOver, age), options...)
+}
+
+// WithAgeUnder sets this dynamic policy as requesting whether the user is younger
+// than a certain age, "options" allows one or more options to be specified e.g. SourceConstraint
+func (b *PolicyBuilder) WithAgeUnder(age int, options ...interface{}) *PolicyBuilder {
+ return b.WithAgeDerivedAttribute(fmt.Sprintf(consts.AttrAgeUnder, age), options...)
+}
+
+// WithWantedRememberMe sets the Policy as requiring a "Remember Me ID"
+func (b *PolicyBuilder) WithWantedRememberMe() *PolicyBuilder {
+ b.isWantedRememberMe = true
+ return b
+}
+
+// WithWantedAuthType sets this dynamic policy as requiring a specific authentication type
+func (b *PolicyBuilder) WithWantedAuthType(wantedAuthType int) *PolicyBuilder {
+ if b.wantedAuthTypes == nil {
+ b.wantedAuthTypes = make(map[int]bool)
+ }
+ b.wantedAuthTypes[wantedAuthType] = true
+ return b
+}
+
+// WithSelfieAuth sets this dynamic policy as requiring Selfie-based authentication
+func (b *PolicyBuilder) WithSelfieAuth() *PolicyBuilder {
+ return b.WithWantedAuthType(authTypeSelfieConst)
+}
+
+// WithPinAuth sets this dynamic policy as requiring PIN authentication
+func (b *PolicyBuilder) WithPinAuth() *PolicyBuilder {
+ return b.WithWantedAuthType(authTypePinConst)
+}
+
+// WithIdentityProfileRequirements adds Identity Profile Requirements to the policy. Must be valid JSON.
+func (b *PolicyBuilder) WithIdentityProfileRequirements(identityProfile json.RawMessage) *PolicyBuilder {
+ b.identityProfileRequirements = &identityProfile
+ return b
+}
+
+// Build constructs a dynamic policy object
+func (b *PolicyBuilder) Build() (Policy, error) {
+ return Policy{
+ attributes: b.attributesAsList(),
+ authTypes: b.authTypesAsList(),
+ rememberMeID: b.isWantedRememberMe,
+ identityProfileRequirements: b.identityProfileRequirements,
+ }, b.err
+}
+
+func (b *PolicyBuilder) attributesAsList() []WantedAttribute {
+ attributeList := make([]WantedAttribute, 0)
+ for _, attr := range b.wantedAttributes {
+ attributeList = append(attributeList, attr)
+ }
+ return attributeList
+}
+
+func (b *PolicyBuilder) authTypesAsList() []int {
+ authTypeList := make([]int, 0)
+ for auth, boolValue := range b.wantedAuthTypes {
+ if boolValue {
+ authTypeList = append(authTypeList, auth)
+ }
+ }
+ return authTypeList
+}
+
+// MarshalJSON returns the JSON encoding
+func (policy *Policy) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&struct {
+ Wanted []WantedAttribute `json:"wanted"`
+ WantedAuthTypes []int `json:"wanted_auth_types"`
+ WantedRememberMe bool `json:"wanted_remember_me"`
+ IdentityProfileRequirements *json.RawMessage `json:"identity_profile_requirements,omitempty"`
+ }{
+ Wanted: policy.attributes,
+ WantedAuthTypes: policy.authTypes,
+ WantedRememberMe: policy.rememberMeID,
+ IdentityProfileRequirements: policy.identityProfileRequirements,
+ })
+}
diff --git a/digitalidentity/policy_builder_test.go b/digitalidentity/policy_builder_test.go
new file mode 100644
index 00000000..e2bc075a
--- /dev/null
+++ b/digitalidentity/policy_builder_test.go
@@ -0,0 +1,439 @@
+package digitalidentity
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/getyoti/yoti-go-sdk/v3/consts"
+ "github.com/getyoti/yoti-go-sdk/v3/yotierror"
+ "gotest.tools/v3/assert"
+)
+
+func ExamplePolicyBuilder_WithFamilyName() {
+ policy, err := (&PolicyBuilder{}).WithFamilyName().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.attributes[0].MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"name":"family_name","accept_self_asserted":false}
+}
+
+func ExamplePolicyBuilder_WithDocumentDetails() {
+ policy, err := (&PolicyBuilder{}).WithDocumentDetails().Build()
+ if err != nil {
+ return
+ }
+ data, err := policy.attributes[0].MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"name":"document_details","accept_self_asserted":false}
+}
+
+func ExamplePolicyBuilder_WithDocumentImages() {
+ policy, err := (&PolicyBuilder{}).WithDocumentImages().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.attributes[0].MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"name":"document_images","accept_self_asserted":false}
+}
+
+func ExamplePolicyBuilder_WithSelfie() {
+ policy, err := (&PolicyBuilder{}).WithSelfie().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.attributes[0].MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"name":"selfie","accept_self_asserted":false}
+}
+
+func ExamplePolicyBuilder_WithAgeOver() {
+ constraint, err := (&SourceConstraintBuilder{}).WithDrivingLicence("").Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ policy, err := (&PolicyBuilder{}).WithAgeOver(18, constraint).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.attributes[0].MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"name":"date_of_birth","derivation":"age_over:18","constraints":[{"type":"SOURCE","preferred_sources":{"anchors":[{"name":"DRIVING_LICENCE","sub_type":""}],"soft_preference":false}}],"accept_self_asserted":false}
+}
+
+func ExamplePolicyBuilder_WithSelfieAuth() {
+ policy, err := (&PolicyBuilder{}).WithSelfieAuth().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"wanted":[],"wanted_auth_types":[1],"wanted_remember_me":false}
+}
+
+func ExamplePolicyBuilder_WithWantedRememberMe() {
+ policy, err := (&PolicyBuilder{}).WithWantedRememberMe().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"wanted":[],"wanted_auth_types":[],"wanted_remember_me":true}
+}
+
+func ExamplePolicyBuilder_WithFullName() {
+ constraint, err := (&SourceConstraintBuilder{}).WithPassport("").Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ policy, err := (&PolicyBuilder{}).WithFullName(&constraint).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ marshalledJSON, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(marshalledJSON))
+ // Output: {"wanted":[{"name":"full_name","constraints":[{"type":"SOURCE","preferred_sources":{"anchors":[{"name":"PASSPORT","sub_type":""}],"soft_preference":false}}],"accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false}
+}
+
+func ExamplePolicyBuilder() {
+ policy, err := (&PolicyBuilder{}).WithFullName().
+ WithPinAuth().WithWantedRememberMe().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"wanted":[{"name":"full_name","accept_self_asserted":false}],"wanted_auth_types":[2],"wanted_remember_me":true}
+}
+
+func ExamplePolicyBuilder_WithAgeUnder() {
+ policy, err := (&PolicyBuilder{}).WithAgeUnder(18).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"wanted":[{"name":"date_of_birth","derivation":"age_under:18","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false}
+}
+
+func ExamplePolicyBuilder_WithGivenNames() {
+ policy, err := (&PolicyBuilder{}).WithGivenNames().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"wanted":[{"name":"given_names","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false}
+}
+
+func ExamplePolicyBuilder_WithDateOfBirth() {
+ policy, err := (&PolicyBuilder{}).WithDateOfBirth().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"wanted":[{"name":"date_of_birth","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false}
+}
+
+func ExamplePolicyBuilder_WithGender() {
+ policy, err := (&PolicyBuilder{}).WithGender().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"wanted":[{"name":"gender","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false}
+}
+
+func ExamplePolicyBuilder_WithPostalAddress() {
+ policy, err := (&PolicyBuilder{}).WithPostalAddress().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"wanted":[{"name":"postal_address","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false}
+}
+
+func ExamplePolicyBuilder_WithStructuredPostalAddress() {
+ policy, err := (&PolicyBuilder{}).WithStructuredPostalAddress().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"wanted":[{"name":"structured_postal_address","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false}
+}
+
+func ExamplePolicyBuilder_WithNationality() {
+ policy, err := (&PolicyBuilder{}).WithNationality().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"wanted":[{"name":"nationality","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false}
+}
+
+func ExamplePolicyBuilder_WithPhoneNumber() {
+ policy, err := (&PolicyBuilder{}).WithPhoneNumber().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"wanted":[{"name":"phone_number","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false}
+}
+
+func TestDigitalIdentityBuilder_WithWantedAttributeByName_WithSourceConstraint(t *testing.T) {
+ attributeName := "attributeName"
+ builder := &PolicyBuilder{}
+ sourceConstraint, err := (&SourceConstraintBuilder{}).Build()
+ assert.NilError(t, err)
+
+ builder.WithWantedAttributeByName(
+ attributeName,
+ sourceConstraint,
+ )
+
+ policy, err := builder.Build()
+ assert.NilError(t, err)
+ assert.Equal(t, len(policy.attributes), 1)
+ assert.Equal(t, policy.attributes[0].name, attributeName)
+ assert.Equal(t, len(policy.attributes[0].constraints), 1)
+}
+
+func TestDigitalIdentityBuilder_WithWantedAttributeByName_InvalidOptionsShouldPanic(t *testing.T) {
+ attributeName := "attributeName"
+ builder := &PolicyBuilder{}
+ invalidOption := "invalidOption"
+
+ defer func() {
+ r := recover().(string)
+ assert.Check(t, strings.Contains(r, "not a valid option type"))
+ }()
+
+ builder.WithWantedAttributeByName(
+ attributeName,
+ invalidOption,
+ )
+
+ t.Error("Expected Panic")
+
+}
+
+func TestDigitalIdentityBuilder_WithWantedAttributeByName_ShouldPropagateErrors(t *testing.T) {
+ builder := &PolicyBuilder{}
+
+ builder.WithWantedAttributeByName("")
+ builder.WithWantedAttributeByName("")
+
+ _, err := builder.Build()
+
+ assert.Error(t, err, "wanted attribute names must not be empty, wanted attribute names must not be empty")
+ assert.Error(t, err.(yotierror.MultiError).Unwrap(), "wanted attribute names must not be empty")
+}
+
+func TestDigitalIdentityBuilder_WithAgeDerivedAttribute_WithSourceConstraint(t *testing.T) {
+ builder := &PolicyBuilder{}
+ sourceConstraint, err := (&SourceConstraintBuilder{}).Build()
+ assert.NilError(t, err)
+
+ builder.WithAgeDerivedAttribute(
+ fmt.Sprintf(consts.AttrAgeOver, 18),
+ sourceConstraint,
+ )
+
+ policy, err := builder.Build()
+ assert.NilError(t, err)
+ assert.Equal(t, len(policy.attributes), 1)
+ assert.Equal(t, policy.attributes[0].name, consts.AttrDateOfBirth)
+ assert.Equal(t, len(policy.attributes[0].constraints), 1)
+}
+
+func TestDigitalIdentityBuilder_WithAgeDerivedAttribute_WithConstraintInterface(t *testing.T) {
+ builder := &PolicyBuilder{}
+ var constraint constraintInterface
+ sourceConstraint, err := (&SourceConstraintBuilder{}).Build()
+ constraint = &sourceConstraint
+ assert.NilError(t, err)
+
+ builder.WithAgeDerivedAttribute(
+ fmt.Sprintf(consts.AttrAgeOver, 18),
+ constraint,
+ )
+
+ policy, err := builder.Build()
+ assert.NilError(t, err)
+ assert.Equal(t, len(policy.attributes), 1)
+ assert.Equal(t, policy.attributes[0].name, consts.AttrDateOfBirth)
+ assert.Equal(t, len(policy.attributes[0].constraints), 1)
+}
+
+func TestDigitalIdentityBuilder_WithAgeDerivedAttribute_InvalidOptionsShouldPanic(t *testing.T) {
+ builder := &PolicyBuilder{}
+ invalidOption := "invalidOption"
+
+ defer func() {
+ r := recover().(string)
+ assert.Check(t, strings.Contains(r, "not a valid option type"))
+ }()
+
+ builder.WithAgeDerivedAttribute(
+ fmt.Sprintf(consts.AttrAgeOver, 18),
+ invalidOption,
+ )
+
+ t.Error("Expected Panic")
+
+}
+
+func TestDigitalIdentityBuilder_WithIdentityProfileRequirements_ShouldFailForInvalidJSON(t *testing.T) {
+ identityProfile := []byte(`{
+ "trust_framework": UK_TFIDA",
+ ,
+ }`)
+
+ policy, err := (&PolicyBuilder{}).WithIdentityProfileRequirements(identityProfile).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ _, err = policy.MarshalJSON()
+ if err == nil {
+ t.Error("expected an error")
+ }
+ var marshallerErr *json.MarshalerError
+ if !errors.As(err, &marshallerErr) {
+ t.Errorf("wanted err to be of type '%v', got: '%v'", reflect.TypeOf(marshallerErr), reflect.TypeOf(err))
+ }
+}
diff --git a/digitalidentity/qr_code.go b/digitalidentity/qr_code.go
new file mode 100644
index 00000000..bff20b64
--- /dev/null
+++ b/digitalidentity/qr_code.go
@@ -0,0 +1,6 @@
+package digitalidentity
+
+type QrCode struct {
+ Id string `json:"id"`
+ Uri string `json:"uri"`
+}
diff --git a/digitalidentity/receipt.go b/digitalidentity/receipt.go
new file mode 100644
index 00000000..8c9b9aa4
--- /dev/null
+++ b/digitalidentity/receipt.go
@@ -0,0 +1,19 @@
+package digitalidentity
+
+type Content struct {
+ Profile []byte `json:"profile"`
+ ExtraData []byte `json:"extraData"`
+}
+
+type ReceiptResponse struct {
+ ID string `json:"id"`
+ SessionID string `json:"sessionId"`
+ Timestamp string `json:"timestamp"`
+ RememberMeID string `json:"rememberMeId,omitempty"`
+ ParentRememberMeID string `json:"parentRememberMeId,omitempty"`
+ Content *Content `json:"content,omitempty"`
+ OtherPartyContent *Content `json:"otherPartyContent,omitempty"`
+ WrappedItemKeyId string `json:"wrappedItemKeyId"`
+ WrappedKey []byte `json:"wrappedKey"`
+ Error string `json:"error"`
+}
diff --git a/digitalidentity/receipt_item_key.go b/digitalidentity/receipt_item_key.go
new file mode 100644
index 00000000..7e805d87
--- /dev/null
+++ b/digitalidentity/receipt_item_key.go
@@ -0,0 +1,7 @@
+package digitalidentity
+
+type ReceiptItemKeyResponse struct {
+ ID string `json:"id"`
+ Iv []byte `json:"iv"`
+ Value []byte `json:"value"`
+}
diff --git a/digitalidentity/requests/client.go b/digitalidentity/requests/client.go
new file mode 100644
index 00000000..74c289e8
--- /dev/null
+++ b/digitalidentity/requests/client.go
@@ -0,0 +1,10 @@
+package requests
+
+import (
+ "net/http"
+)
+
+// HttpClient is a mockable HTTP Client Interface
+type HttpClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
diff --git a/digitalidentity/requests/request.go b/digitalidentity/requests/request.go
new file mode 100644
index 00000000..e5bbeaba
--- /dev/null
+++ b/digitalidentity/requests/request.go
@@ -0,0 +1,40 @@
+package requests
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/getyoti/yoti-go-sdk/v3/digitalidentity/yotierror"
+)
+
+// Execute makes a request to the specified endpoint, with an optional payload
+func Execute(httpClient HttpClient, request *http.Request) (response *http.Response, err error) {
+
+ if response, err = doRequest(request, httpClient); err != nil {
+
+ return
+ }
+
+ statusCodeIsFailure := response.StatusCode >= 300 || response.StatusCode < 200
+
+ if statusCodeIsFailure {
+ return response, yotierror.NewResponseError(response)
+ }
+
+ return response, nil
+}
+
+func doRequest(request *http.Request, httpClient HttpClient) (*http.Response, error) {
+ httpClient = ensureHttpClientTimeout(httpClient)
+ return httpClient.Do(request)
+}
+
+func ensureHttpClientTimeout(httpClient HttpClient) HttpClient {
+ if httpClient == nil {
+ httpClient = &http.Client{
+ Timeout: time.Second * 10,
+ }
+ }
+
+ return httpClient
+}
diff --git a/digitalidentity/requests/request_test.go b/digitalidentity/requests/request_test.go
new file mode 100644
index 00000000..420fa6b2
--- /dev/null
+++ b/digitalidentity/requests/request_test.go
@@ -0,0 +1,71 @@
+package requests
+
+import (
+ "net/http"
+ "testing"
+ "time"
+
+ "gotest.tools/v3/assert"
+)
+
+type mockHTTPClient struct {
+ do func(*http.Request) (*http.Response, error)
+}
+
+func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) {
+ if mock.do != nil {
+ return mock.do(request)
+ }
+ return nil, nil
+}
+
+func TestExecute_Success(t *testing.T) {
+ client := &mockHTTPClient{
+ do: func(*http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: 200,
+ }, nil
+ },
+ }
+
+ request := &http.Request{
+ Method: http.MethodGet,
+ }
+
+ response, err := Execute(client, request)
+
+ assert.NilError(t, err)
+ assert.Equal(t, response.StatusCode, 200)
+}
+
+func TestExecute_Failure(t *testing.T) {
+ client := &mockHTTPClient{
+ do: func(*http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: 400,
+ }, nil
+ },
+ }
+
+ request := &http.Request{
+ Method: http.MethodGet,
+ }
+
+ _, err := Execute(client, request)
+ assert.ErrorContains(t, err, "unknown HTTP error")
+}
+
+func TestEnsureHttpClientTimeout_NilHTTPClientShouldUse10sTimeout(t *testing.T) {
+ result := ensureHttpClientTimeout(nil).(*http.Client)
+
+ assert.Equal(t, 10*time.Second, result.Timeout)
+}
+
+func TestEnsureHttpClientTimeout(t *testing.T) {
+ httpClient := &http.Client{
+ Timeout: time.Minute * 12,
+ }
+ result := ensureHttpClientTimeout(httpClient).(*http.Client)
+
+ assert.Equal(t, 12*time.Minute, result.Timeout)
+}
diff --git a/digitalidentity/requests/signed_message.go b/digitalidentity/requests/signed_message.go
new file mode 100644
index 00000000..02e2116f
--- /dev/null
+++ b/digitalidentity/requests/signed_message.go
@@ -0,0 +1,233 @@
+package requests
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha256"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/getyoti/yoti-go-sdk/v3/consts"
+)
+
+// MergeHeaders merges two or more header prototypes together from left to right
+func MergeHeaders(headers ...map[string][]string) map[string][]string {
+ if len(headers) == 0 {
+ return make(map[string][]string)
+ }
+ out := headers[0]
+ for _, element := range headers[1:] {
+ for k, v := range element {
+ out[k] = v
+ }
+ }
+ return out
+}
+
+// JSONHeaders is a header prototype for JSON based requests
+func JSONHeaders() map[string][]string {
+ return map[string][]string{
+ "Content-Type": {"application/json"},
+ "Accept": {"application/json"},
+ }
+}
+
+// AuthHeader is a header prototype including the App/SDK ID
+func AuthHeader(clientSdkId string) map[string][]string {
+ return map[string][]string{
+ "X-Yoti-Auth-Id": {clientSdkId},
+ }
+}
+
+// AuthKeyHeader is a header prototype including an encoded RSA PublicKey
+func AuthKeyHeader(key *rsa.PublicKey) map[string][]string {
+ return map[string][]string{
+ "X-Yoti-Auth-Key": {
+ base64.StdEncoding.EncodeToString(
+ func(a []byte, _ error) []byte {
+ return a
+ }(x509.MarshalPKIXPublicKey(key)),
+ ),
+ },
+ }
+}
+
+// SignedRequest is a builder for constructing a http.Request with Yoti signing
+type SignedRequest struct {
+ Key *rsa.PrivateKey
+ HTTPMethod string
+ BaseURL string
+ Endpoint string
+ Headers map[string][]string
+ Params map[string]string
+ Body []byte
+ Error error
+}
+
+func (msg *SignedRequest) signDigest(digest []byte) (string, error) {
+ hash := sha256.Sum256(digest)
+ signed, err := rsa.SignPKCS1v15(rand.Reader, msg.Key, crypto.SHA256, hash[:])
+ if err != nil {
+ return "", err
+ }
+ return base64.StdEncoding.EncodeToString(signed), nil
+}
+
+func getTimestamp() string {
+ return strconv.FormatInt(time.Now().Unix()*1000, 10)
+}
+
+func getNonce() (string, error) {
+ nonce := make([]byte, 16)
+ _, err := rand.Read(nonce)
+ return fmt.Sprintf("%X-%X-%X-%X-%X", nonce[0:4], nonce[4:6], nonce[6:8], nonce[8:10], nonce[10:]), err
+}
+
+// WithPemFile loads the private key from a PEM file reader
+func (msg SignedRequest) WithPemFile(in []byte) SignedRequest {
+ block, _ := pem.Decode(in)
+ if block == nil {
+ msg.Error = errors.New("input is not PEM-encoded")
+ return msg
+ }
+ if block.Type != "RSA PRIVATE KEY" {
+ msg.Error = errors.New("input is not an RSA Private Key")
+ return msg
+ }
+
+ msg.Key, msg.Error = x509.ParsePKCS1PrivateKey(block.Bytes)
+ return msg
+}
+
+func (msg *SignedRequest) addParametersToEndpoint() (string, error) {
+ if msg.Params == nil {
+ msg.Params = make(map[string]string)
+ }
+ // Add Timestamp/Nonce
+ if _, ok := msg.Params["nonce"]; !ok {
+ nonce, err := getNonce()
+ if err != nil {
+ return "", err
+ }
+ msg.Params["nonce"] = nonce
+ }
+ if _, ok := msg.Params["timestamp"]; !ok {
+ msg.Params["timestamp"] = getTimestamp()
+ }
+
+ endpoint := msg.Endpoint
+ if !strings.Contains(endpoint, "?") {
+ endpoint = endpoint + "?"
+ } else {
+ endpoint = endpoint + "&"
+ }
+
+ var firstParam = true
+ for param, value := range msg.Params {
+ var formatString = "%s&%s=%s"
+ if firstParam {
+ formatString = "%s%s=%s"
+ }
+ endpoint = fmt.Sprintf(formatString, endpoint, param, value)
+ firstParam = false
+ }
+
+ return endpoint, nil
+}
+
+func (msg *SignedRequest) generateDigest(endpoint string) (digest string) {
+ // Generate the message digest
+ if msg.Body != nil {
+ digest = fmt.Sprintf(
+ "%s&%s&%s",
+ msg.HTTPMethod,
+ endpoint,
+ base64.StdEncoding.EncodeToString(msg.Body),
+ )
+ } else {
+ digest = fmt.Sprintf("%s&%s",
+ msg.HTTPMethod,
+ endpoint,
+ )
+ }
+ return
+}
+
+func (msg *SignedRequest) checkMandatories() error {
+ if msg.Error != nil {
+ return msg.Error
+ }
+ if msg.Key == nil {
+ return fmt.Errorf("missing private key")
+ }
+ if msg.HTTPMethod == "" {
+ return fmt.Errorf("missing HTTPMethod")
+ }
+ if msg.BaseURL == "" {
+ return fmt.Errorf("missing BaseURL")
+ }
+ if msg.Endpoint == "" {
+ return fmt.Errorf("missing Endpoint")
+ }
+ return nil
+}
+
+// Request builds a http.Request with signature headers
+func (msg SignedRequest) Request() (request *http.Request, err error) {
+ err = msg.checkMandatories()
+ if err != nil {
+ return
+ }
+
+ endpoint, err := msg.addParametersToEndpoint()
+ if err != nil {
+ return
+ }
+
+ signedDigest, err := msg.signDigest([]byte(msg.generateDigest(endpoint)))
+ if err != nil {
+ return
+ }
+
+ // Construct the HTTP Request
+ request, err = http.NewRequest(
+ msg.HTTPMethod,
+ msg.BaseURL+endpoint,
+ bytes.NewReader(msg.Body),
+ )
+ if err != nil {
+ return
+ }
+
+ request.Header.Add("X-Yoti-Auth-Digest", signedDigest)
+ request.Header.Add("X-Yoti-SDK", consts.SDKIdentifier)
+ request.Header.Add("X-Yoti-SDK-Version", consts.SDKVersionIdentifier)
+
+ for key, values := range msg.Headers {
+ for _, value := range values {
+ request.Header.Add(key, value)
+ }
+ }
+
+ return request, err
+}
+
+func Base64ToBase64URL(base64Str string) string {
+ decoded, err := base64.StdEncoding.DecodeString(base64Str)
+ if err != nil {
+ return ""
+ }
+
+ base64URL := base64.URLEncoding.EncodeToString(decoded)
+
+ return base64URL
+}
diff --git a/digitalidentity/requests/signed_message_test.go b/digitalidentity/requests/signed_message_test.go
new file mode 100644
index 00000000..301be62f
--- /dev/null
+++ b/digitalidentity/requests/signed_message_test.go
@@ -0,0 +1,169 @@
+package requests
+
+import (
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "math/rand"
+ "net/http"
+ "regexp"
+ "testing"
+
+ "gotest.tools/v3/assert"
+)
+
+const exampleKey = "MIICXgIBAAKBgQCpTiICtL+ujx8D0FquVWIaXg+ajJadN5hsTlGUXymiFAunSZjLjTsoGfSPz8PJm6pG9ax1Qb+R5UsSgTRTcpZTps2RLRWr5oPfD66bz4l38QXPSvfg5o+5kNxyCb8QANitF7Ht/DcpsGpL7anruHg/RgCLCBFRaGAodfuJCCM9zwIDAQABAoGBAIJL7GbSvjZUVVU1E6TZd0+9lhqmGf/S2o5309bxSfQ/oxxSyrHU9nMNTqcjCZXuJCTKS7hOKmXY5mbOYvvZ0xA7DXfOc+A4LGXQl0r3ZMzhHZTPKboUSh16E4WI4pr98KagFdkeB/0KBURM3x5d/6dSKip8ZpEyqVpuc9d1xtvhAkEAxabfsqfb4fgBsrhZ/qt133yB0FBHs1alRxvUXZWbVPTOegKi5KBdPptf2QfCy8WK3An/lg8cFQG78PyNll/P0QJBANtJBUHTuRDCoYLhqZLdSTQ52qOWRNutZ2fho9ZcLquokB4SFFeC2I4T+s3oSJ8SNh9vW1nNeXW6Zipx+zz8O58CQQCjV9qNGf40zDITEhmFxwt967aYgpAO3O9wScaCpM4fMsWkvaMDEKiewec/RBOvNY0hdb3ctJX/olRAv2b/vCTRAkAuLmCnDlnJR9QP5kp6HZRPJWgAT6NMyGYgoIqKmHtTt3oyewhBrdLBiT+moaa5qXIwiJkqfnV377uYcMzCeTRtAkEAwHdhM3v01GprmHqE2kvlKOXNq9CB1Z4j/vXSQxBYoSrFWLv5nW9e69ngX+n7qhvO3Gs9CBoy/oqOLatFZOuFEw=="
+
+var keyBytes, _ = base64.StdEncoding.DecodeString(exampleKey)
+var privateKey, _ = x509.ParsePKCS1PrivateKey(keyBytes)
+
+func ExampleMergeHeaders() {
+ left := map[string][]string{"A": {"Value Of A"}}
+ right := map[string][]string{"B": {"Value Of B"}}
+
+ merged := MergeHeaders(left, right)
+ fmt.Println(merged["A"])
+ fmt.Println(merged["B"])
+ // Output:
+ // [Value Of A]
+ // [Value Of B]
+}
+
+func TestMergeHeaders_HandleNullCaseGracefully(t *testing.T) {
+ assert.Equal(t, len(MergeHeaders()), 0)
+}
+
+func ExampleJSONHeaders() {
+ jsonHeaders, err := json.Marshal(JSONHeaders())
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(jsonHeaders))
+ // Output: {"Accept":["application/json"],"Content-Type":["application/json"]}
+}
+
+func ExampleAuthKeyHeader() {
+ headers, err := json.Marshal(AuthKeyHeader(&privateKey.PublicKey))
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(headers))
+ // Output: {"X-Yoti-Auth-Key":["MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpTiICtL+ujx8D0FquVWIaXg+ajJadN5hsTlGUXymiFAunSZjLjTsoGfSPz8PJm6pG9ax1Qb+R5UsSgTRTcpZTps2RLRWr5oPfD66bz4l38QXPSvfg5o+5kNxyCb8QANitF7Ht/DcpsGpL7anruHg/RgCLCBFRaGAodfuJCCM9zwIDAQAB"]}
+}
+
+func TestRequestShouldBuildForValid(t *testing.T) {
+ random := rand.New(rand.NewSource(25))
+ key, err := rsa.GenerateKey(random, 1024)
+
+ assert.NilError(t, err)
+ httpMethod := "GET"
+ baseURL := "example.com"
+ endpoint := "/"
+
+ request := SignedRequest{
+ Key: key,
+ HTTPMethod: httpMethod,
+ BaseURL: baseURL,
+ Endpoint: endpoint,
+ }
+ signed, err := request.Request()
+ assert.NilError(t, err)
+ assert.Equal(t, httpMethod, signed.Method)
+ urlCheck, err := regexp.Match(baseURL+endpoint, []byte(signed.URL.String()))
+ assert.NilError(t, err)
+ assert.Check(t, urlCheck)
+ assert.Check(t, signed.Header.Get("X-Yoti-Auth-Digest") != "")
+ assert.Equal(t, signed.Header.Get("X-Yoti-SDK"), "Go")
+ assert.Equal(t, signed.Header.Get("X-Yoti-SDK-Version"), "3.9.0")
+}
+
+func TestRequestShouldAddHeaders(t *testing.T) {
+ random := rand.New(rand.NewSource(25))
+ key, err := rsa.GenerateKey(random, 1024)
+
+ assert.NilError(t, err)
+ httpMethod := "GET"
+ baseURL := "example.com"
+ endpoint := "/"
+
+ request := SignedRequest{
+ Key: key,
+ HTTPMethod: httpMethod,
+ BaseURL: baseURL,
+ Endpoint: endpoint,
+ Headers: JSONHeaders(),
+ }
+ signed, err := request.Request()
+ assert.NilError(t, err)
+ assert.Check(t, signed.Header["X-Yoti-Auth-Digest"][0] != "")
+ assert.Equal(t, signed.Header["Accept"][0], "application/json")
+}
+
+func TestSignedRequest_checkMandatories_WhenErrorIsSetReturnIt(t *testing.T) {
+ msg := &SignedRequest{Error: fmt.Errorf("exampleError")}
+ assert.Error(t, msg.checkMandatories(), "exampleError")
+}
+
+func TestSignedRequest_checkMandatories_WhenKeyMissing(t *testing.T) {
+ msg := &SignedRequest{}
+ assert.Error(t, msg.checkMandatories(), "missing private key")
+}
+
+func TestSignedRequest_checkMandatories_WhenHTTPMethodMissing(t *testing.T) {
+ msg := &SignedRequest{Key: privateKey}
+ assert.Error(t, msg.checkMandatories(), "missing HTTPMethod")
+}
+
+func TestSignedRequest_checkMandatories_WhenBaseURLMissing(t *testing.T) {
+ msg := &SignedRequest{
+ Key: privateKey,
+ HTTPMethod: http.MethodPost,
+ }
+ assert.Error(t, msg.checkMandatories(), "missing BaseURL")
+}
+
+func TestSignedRequest_checkMandatories_WhenEndpointMissing(t *testing.T) {
+ msg := &SignedRequest{
+ Key: privateKey,
+ HTTPMethod: http.MethodPost,
+ BaseURL: "example.com",
+ }
+ assert.Error(t, msg.checkMandatories(), "missing Endpoint")
+}
+
+func ExampleSignedRequest_generateDigest() {
+ msg := &SignedRequest{
+ HTTPMethod: http.MethodPost,
+ Body: []byte("simple message body"),
+ }
+ fmt.Println(msg.generateDigest("endpoint"))
+ // Output: POST&endpoint&c2ltcGxlIG1lc3NhZ2UgYm9keQ==
+
+}
+
+func ExampleSignedRequest_WithPemFile() {
+ msg := SignedRequest{}.WithPemFile([]byte(`
+-----BEGIN RSA PRIVATE KEY-----
+` + exampleKey + `
+-----END RSA PRIVATE KEY-----`))
+ fmt.Println(AuthKeyHeader(&msg.Key.PublicKey))
+ // Output: map[X-Yoti-Auth-Key:[MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpTiICtL+ujx8D0FquVWIaXg+ajJadN5hsTlGUXymiFAunSZjLjTsoGfSPz8PJm6pG9ax1Qb+R5UsSgTRTcpZTps2RLRWr5oPfD66bz4l38QXPSvfg5o+5kNxyCb8QANitF7Ht/DcpsGpL7anruHg/RgCLCBFRaGAodfuJCCM9zwIDAQAB]]
+}
+
+func TestSignedRequest_WithPemFile_NotPemEncodedShouldError(t *testing.T) {
+ msg := SignedRequest{}.WithPemFile([]byte("not pem encoded"))
+ assert.ErrorContains(t, msg.Error, "not PEM-encoded")
+}
+
+func TestSignedRequest_WithPemFile_NotRSAKeyShouldError(t *testing.T) {
+ msg := SignedRequest{}.WithPemFile([]byte(`-----BEGIN RSA PUBLIC KEY-----
+` + exampleKey + `
+-----END RSA PUBLIC KEY-----`))
+ assert.ErrorContains(t, msg.Error, "not an RSA Private Key")
+}
diff --git a/digitalidentity/service.go b/digitalidentity/service.go
new file mode 100644
index 00000000..444a6560
--- /dev/null
+++ b/digitalidentity/service.go
@@ -0,0 +1,303 @@
+package digitalidentity
+
+import (
+ "crypto/rsa"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/getyoti/yoti-go-sdk/v3/cryptoutil"
+ "github.com/getyoti/yoti-go-sdk/v3/digitalidentity/requests"
+ "github.com/getyoti/yoti-go-sdk/v3/extra"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "google.golang.org/protobuf/proto"
+)
+
+const identitySessionCreationEndpoint = "/v2/sessions"
+const identitySessionRetrieval = "/v2/sessions/%s"
+const identitySessionQrCodeCreation = "/v2/sessions/%s/qr-codes"
+const identitySessionQrCodeRetrieval = "/v2/qr-codes/%s"
+const identitySessionReceiptRetrieval = "/v2/receipts/%s"
+const identitySessionReceiptKeyRetrieval = "/v2/wrapped-item-keys/%s"
+
+// CreateShareSession creates session using the supplied session specification
+func CreateShareSession(httpClient requests.HttpClient, shareSessionRequest *ShareSessionRequest, clientSdkId, apiUrl string, key *rsa.PrivateKey) (*ShareSession, error) {
+ endpoint := identitySessionCreationEndpoint
+
+ payload, err := shareSessionRequest.MarshalJSON()
+ if err != nil {
+ return nil, err
+ }
+
+ request, err := requests.SignedRequest{
+ Key: key,
+ HTTPMethod: http.MethodPost,
+ BaseURL: apiUrl,
+ Endpoint: endpoint,
+ Headers: requests.AuthHeader(clientSdkId),
+ Body: payload,
+ Params: map[string]string{"sdkID": clientSdkId},
+ }.Request()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get signed request: %v", err)
+ }
+
+ response, err := requests.Execute(httpClient, request)
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute request: %v", err)
+ }
+
+ defer response.Body.Close()
+ shareSession := &ShareSession{}
+
+ responseBytes, err := io.ReadAll(response.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %v", err)
+ }
+ err = json.Unmarshal(responseBytes, shareSession)
+ return shareSession, err
+}
+
+// GetShareSession get session info using the supplied sessionID parameter
+func GetShareSession(httpClient requests.HttpClient, sessionID string, clientSdkId, apiUrl string, key *rsa.PrivateKey) (*ShareSession, error) {
+ endpoint := fmt.Sprintf(identitySessionRetrieval, sessionID)
+
+ request, err := requests.SignedRequest{
+ Key: key,
+ HTTPMethod: http.MethodGet,
+ BaseURL: apiUrl,
+ Endpoint: endpoint,
+ Headers: requests.AuthHeader(clientSdkId),
+ Params: map[string]string{"sdkID": clientSdkId},
+ }.Request()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get signed request: %v", err)
+ }
+
+ response, err := requests.Execute(httpClient, request)
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute request: %v", err)
+ }
+ defer response.Body.Close()
+ shareSession := &ShareSession{}
+ responseBytes, err := io.ReadAll(response.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %v", err)
+ }
+ err = json.Unmarshal(responseBytes, shareSession)
+ return shareSession, err
+}
+
+// CreateShareQrCode generates a sharing qr code using the supplied sessionID parameter
+func CreateShareQrCode(httpClient requests.HttpClient, sessionID string, clientSdkId, apiUrl string, key *rsa.PrivateKey) (*QrCode, error) {
+ endpoint := fmt.Sprintf(identitySessionQrCodeCreation, sessionID)
+
+ request, err := requests.SignedRequest{
+ Key: key,
+ HTTPMethod: http.MethodPost,
+ BaseURL: apiUrl,
+ Endpoint: endpoint,
+ Headers: requests.AuthHeader(clientSdkId),
+ Body: nil,
+ Params: map[string]string{"sdkID": clientSdkId},
+ }.Request()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get signed request: %v", err)
+ }
+
+ response, err := requests.Execute(httpClient, request)
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute request: %v", err)
+ }
+
+ defer response.Body.Close()
+ qrCode := &QrCode{}
+ responseBytes, err := io.ReadAll(response.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %v", err)
+ }
+ err = json.Unmarshal(responseBytes, qrCode)
+ return qrCode, err
+}
+
+// GetShareSessionQrCode is used to fetch the qr code by id.
+func GetShareSessionQrCode(httpClient requests.HttpClient, qrCodeId string, clientSdkId, apiUrl string, key *rsa.PrivateKey) (fetchedQrCode ShareSessionQrCode, err error) {
+ endpoint := fmt.Sprintf(identitySessionQrCodeRetrieval, qrCodeId)
+ headers := requests.AuthHeader(clientSdkId)
+ request, err := requests.SignedRequest{
+ Key: key,
+ HTTPMethod: http.MethodGet,
+ BaseURL: apiUrl,
+ Endpoint: endpoint,
+ Headers: headers,
+ }.Request()
+ if err != nil {
+ return fetchedQrCode, fmt.Errorf("failed to get signed request: %v", err)
+ }
+
+ response, err := requests.Execute(httpClient, request)
+ if err != nil {
+ return fetchedQrCode, fmt.Errorf("failed to execute request: %v", err)
+ }
+ defer response.Body.Close()
+
+ responseBytes, err := io.ReadAll(response.Body)
+ if err != nil {
+ return fetchedQrCode, fmt.Errorf("failed to read response body: %v", err)
+ }
+
+ err = json.Unmarshal(responseBytes, &fetchedQrCode)
+
+ return fetchedQrCode, err
+}
+
+// GetReceipt fetches receipt info using a receipt id.
+func getReceipt(httpClient requests.HttpClient, receiptId string, clientSdkId, apiUrl string, key *rsa.PrivateKey) (receipt ReceiptResponse, err error) {
+ receiptUrl := requests.Base64ToBase64URL(receiptId)
+ endpoint := fmt.Sprintf(identitySessionReceiptRetrieval, receiptUrl)
+
+ headers := requests.AuthHeader(clientSdkId)
+ request, err := requests.SignedRequest{
+ Key: key,
+ HTTPMethod: http.MethodGet,
+ BaseURL: apiUrl,
+ Endpoint: endpoint,
+ Headers: headers,
+ }.Request()
+ if err != nil {
+ return receipt, fmt.Errorf("failed to get signed request: %v", err)
+ }
+
+ response, err := requests.Execute(httpClient, request)
+ if err != nil {
+ return receipt, fmt.Errorf("failed to execute request: %v", err)
+ }
+ defer response.Body.Close()
+
+ responseBytes, err := io.ReadAll(response.Body)
+ if err != nil {
+ return receipt, fmt.Errorf("failed to read response body: %v", err)
+ }
+
+ err = json.Unmarshal(responseBytes, &receipt)
+
+ return receipt, err
+}
+
+// GetReceiptItemKey retrieves the receipt item key for a receipt item key id.
+func getReceiptItemKey(httpClient requests.HttpClient, receiptItemKeyId string, clientSdkId, apiUrl string, key *rsa.PrivateKey) (receiptItemKey ReceiptItemKeyResponse, err error) {
+ endpoint := fmt.Sprintf(identitySessionReceiptKeyRetrieval, receiptItemKeyId)
+ headers := requests.AuthHeader(clientSdkId)
+ request, err := requests.SignedRequest{
+ Key: key,
+ HTTPMethod: http.MethodGet,
+ BaseURL: apiUrl,
+ Endpoint: endpoint,
+ Headers: headers,
+ }.Request()
+ if err != nil {
+ return receiptItemKey, fmt.Errorf("failed to get signed request: %v", err)
+ }
+
+ response, err := requests.Execute(httpClient, request)
+ if err != nil {
+ return receiptItemKey, err
+ }
+ defer response.Body.Close()
+
+ responseBytes, err := io.ReadAll(response.Body)
+ if err != nil {
+ return receiptItemKey, err
+ }
+
+ err = json.Unmarshal(responseBytes, &receiptItemKey)
+
+ return receiptItemKey, err
+}
+
+func GetShareReceipt(httpClient requests.HttpClient, receiptId string, clientSdkId, apiUrl string, key *rsa.PrivateKey) (receipt SharedReceiptResponse, err error) {
+ receiptResponse, err := getReceipt(httpClient, receiptId, clientSdkId, apiUrl, key)
+ if err != nil {
+ return receipt, fmt.Errorf("failed to get receipt: %v", err)
+ }
+
+ itemKeyId := receiptResponse.WrappedItemKeyId
+
+ encryptedItemKeyResponse, err := getReceiptItemKey(httpClient, itemKeyId, clientSdkId, apiUrl, key)
+ if err != nil {
+ return receipt, fmt.Errorf("failed to get receipt item key: %v", err)
+ }
+
+ receiptContentKey, err := cryptoutil.UnwrapReceiptKey(receiptResponse.WrappedKey, encryptedItemKeyResponse.Value, encryptedItemKeyResponse.Iv, key)
+ if err != nil {
+ return receipt, fmt.Errorf("failed to unwrap receipt content key: %v", err)
+ }
+
+ attrData, aextra, err := decryptReceiptContent(receiptResponse.Content, receiptContentKey)
+ if err != nil {
+ return receipt, fmt.Errorf("failed to decrypt receipt content: %v", err)
+ }
+
+ applicationProfile := newApplicationProfile(attrData)
+ extraDataValue, err := extra.NewExtraData(aextra)
+ if err != nil {
+ return receipt, fmt.Errorf("failed to build application extra data: %v", err)
+ }
+
+ uattrData, uextra, err := decryptReceiptContent(receiptResponse.OtherPartyContent, receiptContentKey)
+ if err != nil {
+ return receipt, fmt.Errorf("failed to decrypt receipt other party content: %v", err)
+ }
+
+ userProfile := newUserProfile(uattrData)
+ userExtraDataValue, err := extra.NewExtraData(uextra)
+ if err != nil {
+ return receipt, fmt.Errorf("failed to build other party extra data: %v", err)
+ }
+
+ return SharedReceiptResponse{
+ ID: receiptResponse.ID,
+ SessionID: receiptResponse.SessionID,
+ RememberMeID: receiptResponse.RememberMeID,
+ ParentRememberMeID: receiptResponse.ParentRememberMeID,
+ Timestamp: receiptResponse.Timestamp,
+ UserContent: UserContent{
+ UserProfile: userProfile,
+ ExtraData: userExtraDataValue,
+ },
+ ApplicationContent: ApplicationContent{
+ ApplicationProfile: applicationProfile,
+ ExtraData: extraDataValue,
+ },
+ Error: receiptResponse.Error,
+ }, nil
+}
+
+func decryptReceiptContent(content *Content, key []byte) (attrData *yotiprotoattr.AttributeList, aextra []byte, err error) {
+
+ if content != nil {
+ if len(content.Profile) > 0 {
+ aattr, err := cryptoutil.DecryptReceiptContent(content.Profile, key)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to decrypt content profile: %v", err)
+ }
+
+ attrData = &yotiprotoattr.AttributeList{}
+ if err := proto.Unmarshal(aattr, attrData); err != nil {
+ return nil, nil, fmt.Errorf("failed to unmarshal attribute list: %v", err)
+ }
+ }
+
+ if len(content.ExtraData) > 0 {
+ aextra, err = cryptoutil.DecryptReceiptContent(content.ExtraData, key)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to decrypt receipt content extra data: %v", err)
+ }
+ }
+
+ }
+
+ return attrData, aextra, nil
+}
diff --git a/digitalidentity/service_test.go b/digitalidentity/service_test.go
new file mode 100644
index 00000000..dbf5e133
--- /dev/null
+++ b/digitalidentity/service_test.go
@@ -0,0 +1,148 @@
+package digitalidentity
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/getyoti/yoti-go-sdk/v3/test"
+ "gotest.tools/v3/assert"
+)
+
+type mockHTTPClient struct {
+ do func(*http.Request) (*http.Response, error)
+}
+
+func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) {
+ if mock.do != nil {
+ return mock.do(request)
+ }
+ return nil, nil
+}
+
+func ExampleCreateShareSession() {
+ key := test.GetValidKey("../test/test-key.pem")
+
+ client := &mockHTTPClient{
+ do: func(*http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: 201,
+ Body: io.NopCloser(strings.NewReader(`{"id":"0","status":"success","expiry": ""}`)),
+ }, nil
+ },
+ }
+
+ policy, err := (&PolicyBuilder{}).WithFullName().WithWantedRememberMe().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ session, err := (&ShareSessionRequestBuilder{}).WithPolicy(policy).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ result, err := CreateShareSession(client, &session, "sdkId", "https://apiurl", key)
+
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Printf("Status code: %s", result.Status)
+ // Output: Status code: success
+}
+
+func TestCreateShareURL_Unsuccessful_401(t *testing.T) {
+ _, err := createShareSessionWithErrorResponse(401, `{"id":"8f6a9dfe72128de20909af0d476769b6","status":401,"error":"INVALID_REQUEST_SIGNATURE","message":"Invalid request signature"}`)
+
+ assert.ErrorContains(t, err, "INVALID_REQUEST_SIGNATURE")
+
+ tempError, temporary := err.(interface {
+ Temporary() bool
+ })
+ assert.Check(t, !temporary || !tempError.Temporary())
+}
+
+func createShareSessionWithErrorResponse(statusCode int, responseBody string) (*ShareSession, error) {
+ key := test.GetValidKey("../test/test-key.pem")
+
+ client := &mockHTTPClient{
+ do: func(*http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: statusCode,
+ Body: io.NopCloser(strings.NewReader(responseBody)),
+ }, nil
+ },
+ }
+
+ policy, err := (&PolicyBuilder{}).WithFullName().WithWantedRememberMe().Build()
+ if err != nil {
+ return nil, err
+ }
+ session, err := (&ShareSessionRequestBuilder{}).WithPolicy(policy).Build()
+ if err != nil {
+ return nil, err
+ }
+
+ return CreateShareSession(client, &session, "sdkId", "https://apiurl", key)
+}
+
+func TestGetShareSession(t *testing.T) {
+ key := test.GetValidKey("../test/test-key.pem")
+ mockSessionID := "SOME_SESSION_ID"
+ mockClientSdkId := "SOME_CLIENT_SDK_ID"
+ mockApiUrl := "https://example.com/api"
+ client := &mockHTTPClient{
+ do: func(*http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: 201,
+ Body: io.NopCloser(strings.NewReader(`{"id":"SOME_ID","status":"SOME_STATUS","expiry":"SOME_EXPIRY","created":"SOME_CREATED","updated":"SOME_UPDATED","qrCode":{"id":"SOME_QRCODE_ID"},"receipt":{"id":"SOME_RECEIPT_ID"}}`)),
+ }, nil
+ },
+ }
+
+ _, err := GetShareSession(client, mockSessionID, mockClientSdkId, mockApiUrl, key)
+ assert.NilError(t, err)
+
+}
+
+func TestCreateShareQrCode(t *testing.T) {
+ key := test.GetValidKey("../test/test-key.pem")
+ mockSessionID := "SOME_SESSION_ID"
+
+ client := &mockHTTPClient{
+ do: func(*http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: 201,
+ Body: io.NopCloser(strings.NewReader(`{}`)),
+ }, nil
+ },
+ }
+
+ _, err := CreateShareQrCode(client, mockSessionID, "sdkId", "https://apiurl", key)
+ assert.NilError(t, err)
+}
+
+func TestGetQrCode(t *testing.T) {
+ key := test.GetValidKey("../test/test-key.pem")
+ mockQrId := "SOME_QR_CODE_ID"
+ mockClientSdkId := "SOME_CLIENT_SDK_ID"
+ mockApiUrl := "https://example.com/api"
+ client := &mockHTTPClient{
+ do: func(*http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: 201,
+ Body: io.NopCloser(strings.NewReader(`{}`)),
+ }, nil
+ },
+ }
+
+ _, err := GetShareSessionQrCode(client, mockQrId, mockClientSdkId, mockApiUrl, key)
+ assert.NilError(t, err)
+
+}
diff --git a/digitalidentity/share_receipt.go b/digitalidentity/share_receipt.go
new file mode 100644
index 00000000..70c2d4cf
--- /dev/null
+++ b/digitalidentity/share_receipt.go
@@ -0,0 +1,24 @@
+package digitalidentity
+
+import "github.com/getyoti/yoti-go-sdk/v3/extra"
+
+type SharedReceiptResponse struct {
+ ID string
+ SessionID string
+ RememberMeID string
+ ParentRememberMeID string
+ Timestamp string
+ Error string
+ UserContent UserContent
+ ApplicationContent ApplicationContent
+}
+
+type ApplicationContent struct {
+ ApplicationProfile ApplicationProfile
+ ExtraData *extra.Data
+}
+
+type UserContent struct {
+ UserProfile UserProfile
+ ExtraData *extra.Data
+}
diff --git a/digitalidentity/share_retrieve_qr.go b/digitalidentity/share_retrieve_qr.go
new file mode 100644
index 00000000..fe737d9b
--- /dev/null
+++ b/digitalidentity/share_retrieve_qr.go
@@ -0,0 +1,10 @@
+package digitalidentity
+
+type ShareSessionFetchedQrCode struct {
+ ID string `json:"id"`
+ Expiry string `json:"expiry"`
+ Policy string `json:"policy"`
+ Extensions []interface{} `json:"extensions"`
+ Session ShareSessionCreated `json:"session"`
+ RedirectURI string `json:"redirectUri"`
+}
diff --git a/digitalidentity/share_session.go b/digitalidentity/share_session.go
new file mode 100644
index 00000000..e27b99fc
--- /dev/null
+++ b/digitalidentity/share_session.go
@@ -0,0 +1,21 @@
+package digitalidentity
+
+// ShareSession contains information about the session.
+type ShareSession struct {
+ Id string `json:"id"`
+ Status string `json:"status"`
+ Expiry string `json:"expiry"`
+ Created string `json:"created"`
+ Updated string `json:"updated"`
+ QrCode qrCode `json:"qrCode"`
+ Receipt *receipt `json:"receipt"`
+}
+
+type qrCode struct {
+ Id string `json:"id"`
+}
+
+// receipt containing the receipt id as a string.
+type receipt struct {
+ Id string `json:"id"`
+}
diff --git a/digitalidentity/share_session_builder.go b/digitalidentity/share_session_builder.go
new file mode 100644
index 00000000..7f644a2c
--- /dev/null
+++ b/digitalidentity/share_session_builder.go
@@ -0,0 +1,75 @@
+package digitalidentity
+
+import (
+ "encoding/json"
+)
+
+// ShareSessionRequestBuilder builds a session
+type ShareSessionRequestBuilder struct {
+ shareSessionRequest ShareSessionRequest
+ err error
+}
+
+// ShareSessionRequest represents a sharesession
+type ShareSessionRequest struct {
+ policy Policy
+ extensions []interface{}
+ subject *json.RawMessage
+ shareSessionNotification *ShareSessionNotification
+ redirectUri string
+}
+
+// WithPolicy attaches a Policy to the ShareSession
+func (builder *ShareSessionRequestBuilder) WithPolicy(policy Policy) *ShareSessionRequestBuilder {
+ builder.shareSessionRequest.policy = policy
+ return builder
+}
+
+// WithExtension adds an extension to the ShareSession
+func (builder *ShareSessionRequestBuilder) WithExtension(extension interface{}) *ShareSessionRequestBuilder {
+ builder.shareSessionRequest.extensions = append(builder.shareSessionRequest.extensions, extension)
+ return builder
+}
+
+// WithNotification sets the callback URL
+func (builder *ShareSessionRequestBuilder) WithNotification(notification *ShareSessionNotification) *ShareSessionRequestBuilder {
+ builder.shareSessionRequest.shareSessionNotification = notification
+ return builder
+}
+
+// WithRedirectUri sets redirectUri to the ShareSession
+func (builder *ShareSessionRequestBuilder) WithRedirectUri(redirectUri string) *ShareSessionRequestBuilder {
+ builder.shareSessionRequest.redirectUri = redirectUri
+ return builder
+}
+
+// WithSubject adds a subject to the ShareSession. Must be valid JSON.
+func (builder *ShareSessionRequestBuilder) WithSubject(subject json.RawMessage) *ShareSessionRequestBuilder {
+ builder.shareSessionRequest.subject = &subject
+ return builder
+}
+
+// Build constructs the ShareSession
+func (builder *ShareSessionRequestBuilder) Build() (ShareSessionRequest, error) {
+ if builder.shareSessionRequest.extensions == nil {
+ builder.shareSessionRequest.extensions = make([]interface{}, 0)
+ }
+ return builder.shareSessionRequest, builder.err
+}
+
+// MarshalJSON returns the JSON encoding
+func (shareSesssion ShareSessionRequest) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&struct {
+ Policy Policy `json:"policy"`
+ Extensions []interface{} `json:"extensions"`
+ RedirectUri string `json:"redirectUri"`
+ Subject *json.RawMessage `json:"subject,omitempty"`
+ Notification *ShareSessionNotification `json:"notification,omitempty"`
+ }{
+ Policy: shareSesssion.policy,
+ Extensions: shareSesssion.extensions,
+ RedirectUri: shareSesssion.redirectUri,
+ Subject: shareSesssion.subject,
+ Notification: shareSesssion.shareSessionNotification,
+ })
+}
diff --git a/digitalidentity/share_session_builder_test.go b/digitalidentity/share_session_builder_test.go
new file mode 100644
index 00000000..b0101e20
--- /dev/null
+++ b/digitalidentity/share_session_builder_test.go
@@ -0,0 +1,99 @@
+package digitalidentity
+
+import (
+ "fmt"
+
+ "github.com/getyoti/yoti-go-sdk/v3/extension"
+)
+
+func ExampleShareSessionRequestBuilder() {
+ shareSession, err := (&ShareSessionRequestBuilder{}).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := shareSession.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"policy":{"wanted":null,"wanted_auth_types":null,"wanted_remember_me":false},"extensions":[],"redirectUri":""}
+}
+
+func ExampleShareSessionRequestBuilder_WithPolicy() {
+ policy, err := (&PolicyBuilder{}).WithEmail().WithPinAuth().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ session, err := (&ShareSessionRequestBuilder{}).WithPolicy(policy).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := session.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"policy":{"wanted":[{"name":"email_address","accept_self_asserted":false}],"wanted_auth_types":[2],"wanted_remember_me":false},"extensions":[],"redirectUri":""}
+}
+
+func ExampleShareSessionRequestBuilder_WithExtension() {
+ policy, err := (&PolicyBuilder{}).WithFullName().Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ builtExtension, err := (&extension.TransactionalFlowExtensionBuilder{}).
+ WithContent("Transactional Flow Extension").
+ Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ session, err := (&ShareSessionRequestBuilder{}).WithExtension(builtExtension).WithPolicy(policy).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := session.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"policy":{"wanted":[{"name":"full_name","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false},"extensions":[{"type":"TRANSACTIONAL_FLOW","content":"Transactional Flow Extension"}],"redirectUri":""}
+}
+
+func ExampleShareSessionRequestBuilder_WithSubject() {
+ subject := []byte(`{
+ "subject_id": "some_subject_id_string"
+ }`)
+
+ session, err := (&ShareSessionRequestBuilder{}).WithSubject(subject).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := session.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"policy":{"wanted":null,"wanted_auth_types":null,"wanted_remember_me":false},"extensions":[],"redirectUri":"","subject":{"subject_id":"some_subject_id_string"}}
+}
diff --git a/digitalidentity/share_session_created.go b/digitalidentity/share_session_created.go
new file mode 100644
index 00000000..86d60e3f
--- /dev/null
+++ b/digitalidentity/share_session_created.go
@@ -0,0 +1,8 @@
+package digitalidentity
+
+// ShareSessionCreated Share Session QR Result
+type ShareSessionCreated struct {
+ ID string `json:"id"`
+ Satus string `json:"status"`
+ Expiry string `json:"expiry"`
+}
diff --git a/digitalidentity/share_session_notification_builder.go b/digitalidentity/share_session_notification_builder.go
new file mode 100644
index 00000000..6c873ebc
--- /dev/null
+++ b/digitalidentity/share_session_notification_builder.go
@@ -0,0 +1,62 @@
+package digitalidentity
+
+import (
+ "encoding/json"
+)
+
+// ShareSessionNotification specifies the session notification configuration.
+type ShareSessionNotification struct {
+ url string
+ method *string
+ verifyTLS *bool
+ headers map[string][]string
+}
+
+// ShareSessionNotificationBuilder builds Share Session Notification
+type ShareSessionNotificationBuilder struct {
+ shareSessionNotification ShareSessionNotification
+}
+
+// WithUrl setsUrl to Share Session Notification
+func (b *ShareSessionNotificationBuilder) WithUrl(url string) *ShareSessionNotificationBuilder {
+ b.shareSessionNotification.url = url
+ return b
+}
+
+// WithMethod set method to Share Session Notification
+func (b *ShareSessionNotificationBuilder) WithMethod(method string) *ShareSessionNotificationBuilder {
+ b.shareSessionNotification.method = &method
+ return b
+}
+
+// WithVerifyTLS sets whether TLS should be verified for notifications.
+func (b *ShareSessionNotificationBuilder) WithVerifyTls(verifyTls bool) *ShareSessionNotificationBuilder {
+ b.shareSessionNotification.verifyTLS = &verifyTls
+ return b
+}
+
+// WithHeaders set headers to Share Session Notification
+func (b *ShareSessionNotificationBuilder) WithHeaders(headers map[string][]string) *ShareSessionNotificationBuilder {
+ b.shareSessionNotification.headers = headers
+ return b
+}
+
+// Build constructs the Share Session Notification Builder
+func (b *ShareSessionNotificationBuilder) Build() (ShareSessionNotification, error) {
+ return b.shareSessionNotification, nil
+}
+
+// MarshalJSON returns the JSON encoding
+func (a *ShareSessionNotification) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&struct {
+ Url string `json:"url"`
+ Method *string `json:"method,omitempty"`
+ VerifyTls *bool `json:"verifyTls,omitempty"`
+ Headers map[string][]string `json:"headers,omitempty"`
+ }{
+ Url: a.url,
+ Method: a.method,
+ VerifyTls: a.verifyTLS,
+ Headers: a.headers,
+ })
+}
diff --git a/digitalidentity/share_session_notification_builder_test.go b/digitalidentity/share_session_notification_builder_test.go
new file mode 100644
index 00000000..a60e301e
--- /dev/null
+++ b/digitalidentity/share_session_notification_builder_test.go
@@ -0,0 +1,95 @@
+package digitalidentity
+
+import (
+ "fmt"
+)
+
+func ExampleShareSessionNotificationBuilder() {
+ shareSessionNotify, err := (&ShareSessionNotificationBuilder{}).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := shareSessionNotify.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"url":""}
+}
+
+func ExampleShareSessionNotificationBuilder_WithUrl() {
+ shareSessionNotify, err := (&ShareSessionNotificationBuilder{}).WithUrl("Custom_Url").Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := shareSessionNotify.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"url":"Custom_Url"}
+}
+
+func ExampleShareSessionNotificationBuilder_WithMethod() {
+ shareSessionNotify, err := (&ShareSessionNotificationBuilder{}).WithMethod("CUSTOMMETHOD").Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := shareSessionNotify.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"url":"","method":"CUSTOMMETHOD"}
+}
+
+func ExampleShareSessionNotificationBuilder_WithVerifyTls() {
+
+ shareSessionNotify, err := (&ShareSessionNotificationBuilder{}).WithVerifyTls(true).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := shareSessionNotify.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"url":"","verifyTls":true}
+}
+
+func ExampleShareSessionNotificationBuilder_WithHeaders() {
+
+ headers := make(map[string][]string)
+ headers["key"] = append(headers["key"], "value")
+
+ shareSessionNotify, err := (&ShareSessionNotificationBuilder{}).WithHeaders(headers).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := shareSessionNotify.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"url":"","headers":{"key":["value"]}}
+}
diff --git a/digitalidentity/share_session_qr_code.go b/digitalidentity/share_session_qr_code.go
new file mode 100644
index 00000000..5f98caa1
--- /dev/null
+++ b/digitalidentity/share_session_qr_code.go
@@ -0,0 +1,14 @@
+package digitalidentity
+
+type ShareSessionQrCode struct {
+ ID string `json:"id"`
+ Expiry string `json:"expiry"`
+ Policy string `json:"policy"`
+ Extensions []interface{} `json:"extensions"`
+ Session struct {
+ ID string `json:"id"`
+ Status string `json:"status"`
+ Expiry string `json:"expiry"`
+ } `json:"session"`
+ RedirectURI string `json:"redirectUri"`
+}
diff --git a/digitalidentity/source_constraint.go b/digitalidentity/source_constraint.go
new file mode 100644
index 00000000..079007f5
--- /dev/null
+++ b/digitalidentity/source_constraint.go
@@ -0,0 +1,105 @@
+package digitalidentity
+
+import (
+ "encoding/json"
+
+ "github.com/getyoti/yoti-go-sdk/v3/yotierror"
+)
+
+// Anchor name constants
+const (
+ AnchorDrivingLicenceConst = "DRIVING_LICENCE"
+ AnchorPassportConst = "PASSPORT"
+ AnchorNationalIDConst = "NATIONAL_ID"
+ AnchorPassCardConst = "PASS_CARD"
+)
+
+// SourceConstraint describes a requirement or preference for a particular set
+// of anchors
+type SourceConstraint struct {
+ anchors []WantedAnchor
+ softPreference bool
+}
+
+// SourceConstraintBuilder builds a source constraint
+type SourceConstraintBuilder struct {
+ sourceConstraint SourceConstraint
+ err error
+}
+
+// WithAnchorByValue is a helper method which builds an anchor and adds it to
+// the source constraint
+func (b *SourceConstraintBuilder) WithAnchorByValue(value, subtype string) *SourceConstraintBuilder {
+ anchor, err := (&WantedAnchorBuilder{}).
+ WithValue(value).
+ WithSubType(subtype).
+ Build()
+ if err != nil {
+ b.err = yotierror.MultiError{This: err, Next: b.err}
+ }
+
+ return b.WithAnchor(anchor)
+}
+
+// WithAnchor adds an anchor to the preference list
+func (b *SourceConstraintBuilder) WithAnchor(anchor WantedAnchor) *SourceConstraintBuilder {
+ b.sourceConstraint.anchors = append(b.sourceConstraint.anchors, anchor)
+ return b
+}
+
+// WithPassport adds a passport anchor
+func (b *SourceConstraintBuilder) WithPassport(subtype string) *SourceConstraintBuilder {
+ return b.WithAnchorByValue(AnchorPassportConst, subtype)
+}
+
+// WithDrivingLicence adds a Driving Licence anchor
+func (b *SourceConstraintBuilder) WithDrivingLicence(subtype string) *SourceConstraintBuilder {
+ return b.WithAnchorByValue(AnchorDrivingLicenceConst, subtype)
+}
+
+// WithNationalID adds a national ID anchor
+func (b *SourceConstraintBuilder) WithNationalID(subtype string) *SourceConstraintBuilder {
+ return b.WithAnchorByValue(AnchorNationalIDConst, subtype)
+}
+
+// WithPasscard adds a passcard anchor
+func (b *SourceConstraintBuilder) WithPasscard(subtype string) *SourceConstraintBuilder {
+ return b.WithAnchorByValue(AnchorPassCardConst, subtype)
+}
+
+// WithSoftPreference sets this constraint as a 'soft requirement' if the
+// parameter is true, and a hard requirement if it is false.
+func (b *SourceConstraintBuilder) WithSoftPreference(soft bool) *SourceConstraintBuilder {
+ b.sourceConstraint.softPreference = soft
+ return b
+}
+
+// Build builds a SourceConstraint
+func (b *SourceConstraintBuilder) Build() (SourceConstraint, error) {
+ if b.sourceConstraint.anchors == nil {
+ b.sourceConstraint.anchors = make([]WantedAnchor, 0)
+ }
+ return b.sourceConstraint, b.err
+}
+
+func (constraint *SourceConstraint) isConstraint() bool {
+ return true
+}
+
+// MarshalJSON returns the JSON encoding
+func (constraint *SourceConstraint) MarshalJSON() ([]byte, error) {
+ type PreferenceList struct {
+ Anchors []WantedAnchor `json:"anchors"`
+ SoftPreference bool `json:"soft_preference"`
+ }
+ return json.Marshal(&struct {
+ Type string `json:"type"`
+ PreferredSources PreferenceList `json:"preferred_sources"`
+ }{
+ Type: "SOURCE",
+ PreferredSources: PreferenceList{
+ Anchors: constraint.anchors,
+ SoftPreference: constraint.softPreference,
+ },
+ })
+}
diff --git a/digitalidentity/user_profile.go b/digitalidentity/user_profile.go
new file mode 100644
index 00000000..32a4d282
--- /dev/null
+++ b/digitalidentity/user_profile.go
@@ -0,0 +1,182 @@
+package digitalidentity
+
+import (
+ "strings"
+
+ "github.com/getyoti/yoti-go-sdk/v3/consts"
+ "github.com/getyoti/yoti-go-sdk/v3/profile/attribute"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+)
+
+// UserProfile represents the details retrieved for a particular user. Consists of
+// Yoti attributes: a small piece of information about a Yoti user such as a
+// photo of the user or the user's date of birth.
+type UserProfile struct {
+ baseProfile
+}
+
+// Creates a new Profile struct
+func newUserProfile(attributes *yotiprotoattr.AttributeList) UserProfile {
+ return UserProfile{
+ baseProfile{
+ attributeSlice: createAttributeSlice(attributes),
+ },
+ }
+}
+
+func createAttributeSlice(protoAttributeList *yotiprotoattr.AttributeList) (result []*yotiprotoattr.Attribute) {
+ if protoAttributeList != nil {
+ result = append(result, protoAttributeList.Attributes...)
+ }
+
+ return result
+}
+
+// Selfie is a photograph of the user. Will be nil if not provided by Yoti.
+func (p UserProfile) Selfie() *attribute.ImageAttribute {
+ return p.GetImageAttribute(consts.AttrSelfie)
+}
+
+// GetSelfieAttributeByID retrieve a Selfie attribute by ID on the Yoti profile.
+// This attribute is a photograph of the user.
+// Will return nil if attribute is not present.
+func (p UserProfile) GetSelfieAttributeByID(attributeID string) (*attribute.ImageAttribute, error) {
+ for _, a := range p.attributeSlice {
+ if a.EphemeralId == attributeID {
+ return attribute.NewImage(a)
+ }
+ }
+ return nil, nil
+}
+
+// GivenNames corresponds to secondary names in passport, and first/middle names in English. Will be nil if not provided by Yoti.
+func (p UserProfile) GivenNames() *attribute.StringAttribute {
+ return p.GetStringAttribute(consts.AttrGivenNames)
+}
+
+// FamilyName corresponds to primary name in passport, and surname in English. Will be nil if not provided by Yoti.
+func (p UserProfile) FamilyName() *attribute.StringAttribute {
+ return p.GetStringAttribute(consts.AttrFamilyName)
+}
+
+// FullName represents the user's full name.
+// If family_name/given_names are present, the value will be equal to the string 'given_names + " " family_name'.
+// Will be nil if not provided by Yoti.
+func (p UserProfile) FullName() *attribute.StringAttribute {
+ return p.GetStringAttribute(consts.AttrFullName)
+}
+
+// MobileNumber represents the user's mobile phone number, as verified at registration time.
+// The value will be a number in E.164 format (i.e. '+' for international prefix and no spaces, e.g. "+447777123456").
+// Will be nil if not provided by Yoti.
+func (p UserProfile) MobileNumber() *attribute.StringAttribute {
+ return p.GetStringAttribute(consts.AttrMobileNumber)
+}
+
+// EmailAddress represents the user's verified email address. Will be nil if not provided by Yoti.
+func (p UserProfile) EmailAddress() *attribute.StringAttribute {
+ return p.GetStringAttribute(consts.AttrEmailAddress)
+}
+
+// DateOfBirth represents the user's date of birth. Will be nil if not provided by Yoti.
+// Has an err value which will be filled if there is an error parsing the date.
+func (p UserProfile) DateOfBirth() (*attribute.DateAttribute, error) {
+ for _, a := range p.attributeSlice {
+ if a.Name == consts.AttrDateOfBirth {
+ return attribute.NewDate(a)
+ }
+ }
+ return nil, nil
+}
+
+// Address represents the user's address. Will be nil if not provided by Yoti.
+func (p UserProfile) Address() *attribute.StringAttribute {
+ addressAttribute := p.GetStringAttribute(consts.AttrAddress)
+ if addressAttribute == nil {
+ return ensureAddressProfile(&p)
+ }
+
+ return addressAttribute
+}
+
+// StructuredPostalAddress represents the user's address in a JSON format.
+// Will be nil if not provided by Yoti. This can be accessed as a
+// map[string]string{} using a type assertion, e.g.:
+// structuredPostalAddress := structuredPostalAddressAttribute.Value().(map[string]string{})
+func (p UserProfile) StructuredPostalAddress() (*attribute.JSONAttribute, error) {
+ return p.GetJSONAttribute(consts.AttrStructuredPostalAddress)
+}
+
+// Gender corresponds to the gender in the registered document; the value will be one of the strings "MALE", "FEMALE", "TRANSGENDER" or "OTHER".
+// Will be nil if not provided by Yoti.
+func (p UserProfile) Gender() *attribute.StringAttribute {
+ return p.GetStringAttribute(consts.AttrGender)
+}
+
+// Nationality corresponds to the nationality in the passport.
+// The value is an ISO-3166-1 alpha-3 code with ICAO9303 (passport) extensions.
+// Will be nil if not provided by Yoti.
+func (p UserProfile) Nationality() *attribute.StringAttribute {
+ return p.GetStringAttribute(consts.AttrNationality)
+}
+
+// DocumentImages returns a slice of document images cropped from the image in the capture page.
+// There can be multiple images as per the number of regions in the capture in this attribute.
+// Will be nil if not provided by Yoti.
+func (p UserProfile) DocumentImages() (*attribute.ImageSliceAttribute, error) {
+ for _, a := range p.attributeSlice {
+ if a.Name == consts.AttrDocumentImages {
+ return attribute.NewImageSlice(a)
+ }
+ }
+ return nil, nil
+}
+
+// GetDocumentImagesAttributeByID retrieve a Document Images attribute by ID on the Yoti profile.
+// This attribute consists of a slice of document images cropped from the image in the capture page.
+// There can be multiple images as per the number of regions in the capture in this attribute.
+// Will return nil if attribute is not present.
+func (p UserProfile) GetDocumentImagesAttributeByID(attributeID string) (*attribute.ImageSliceAttribute, error) {
+ for _, a := range p.attributeSlice {
+ if a.EphemeralId == attributeID {
+ return attribute.NewImageSlice(a)
+ }
+ }
+ return nil, nil
+}
+
+// DocumentDetails represents information extracted from a document provided by the user.
+// Will be nil if not provided by Yoti.
+func (p UserProfile) DocumentDetails() (*attribute.DocumentDetailsAttribute, error) {
+ for _, a := range p.attributeSlice {
+ if a.Name == consts.AttrDocumentDetails {
+ return attribute.NewDocumentDetails(a)
+ }
+ }
+ return nil, nil
+}
+
+// IdentityProfileReport represents the JSON object containing identity assertion and the
+// verification report. Will be nil if not provided by Yoti.
+func (p UserProfile) IdentityProfileReport() (*attribute.JSONAttribute, error) {
+ return p.GetJSONAttribute(consts.AttrIdentityProfileReport)
+}
+
+// AgeVerifications returns a slice of age verifications for the user.
+// Will be an empty slice if not provided by Yoti.
+func (p UserProfile) AgeVerifications() (out []attribute.AgeVerification, err error) {
+ ageUnderString := strings.Replace(consts.AttrAgeUnder, "%d", "", -1)
+ ageOverString := strings.Replace(consts.AttrAgeOver, "%d", "", -1)
+
+ for _, a := range p.attributeSlice {
+ if strings.HasPrefix(a.Name, ageUnderString) ||
+ strings.HasPrefix(a.Name, ageOverString) {
+ verification, err := attribute.NewAgeVerification(a)
+ if err != nil {
+ return nil, err
+ }
+ out = append(out, verification)
+ }
+ }
+ return out, err
+}
diff --git a/digitalidentity/user_profile_test.go b/digitalidentity/user_profile_test.go
new file mode 100644
index 00000000..b81b78ee
--- /dev/null
+++ b/digitalidentity/user_profile_test.go
@@ -0,0 +1,704 @@
+package digitalidentity
+
+import (
+ "encoding/base64"
+ "fmt"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/getyoti/yoti-go-sdk/v3/consts"
+ "github.com/getyoti/yoti-go-sdk/v3/file"
+ "github.com/getyoti/yoti-go-sdk/v3/media"
+ "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr"
+ "google.golang.org/protobuf/proto"
+ "gotest.tools/v3/assert"
+ is "gotest.tools/v3/assert/cmp"
+)
+
+const (
+ attributeName = "test_attribute_name"
+ attributeValueString = "value"
+
+ documentImagesAttributeID = "document-images-attribute-id-123"
+ selfieAttributeID = "selfie-attribute-id-123"
+ fullNameAttributeID = "full-name-id-123"
+)
+
+var attributeValue = []byte(attributeValueString)
+
+func getUserProfile() UserProfile {
+ userProfile := createProfileWithMultipleAttributes(
+ createDocumentImagesAttribute(documentImagesAttributeID),
+ createSelfieAttribute(yotiprotoattr.ContentType_JPEG, selfieAttributeID),
+ createStringAttribute("full_name", []byte("John Smith"), []*yotiprotoattr.Anchor{}, fullNameAttributeID))
+
+ return userProfile
+}
+
+func ExampleUserProfile_GetAttributeByID() {
+ userProfile := getUserProfile()
+ fullNameAttribute := userProfile.GetAttributeByID("full-name-id-123")
+ value := fullNameAttribute.Value().(string)
+
+ fmt.Println(value)
+ // Output: John Smith
+}
+
+func ExampleUserProfile_GetDocumentImagesAttributeByID() {
+ userProfile := getUserProfile()
+ documentImagesAttribute, err := userProfile.GetDocumentImagesAttributeByID("document-images-attribute-id-123")
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(*documentImagesAttribute.ID())
+ // Output: document-images-attribute-id-123
+}
+
+func ExampleUserProfile_GetSelfieAttributeByID() {
+ userProfile := getUserProfile()
+ selfieAttribute, err := userProfile.GetSelfieAttributeByID("selfie-attribute-id-123")
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(*selfieAttribute.ID())
+ // Output: selfie-attribute-id-123
+}
+
+func createProfileWithSingleAttribute(attr *yotiprotoattr.Attribute) UserProfile {
+ var attributeSlice []*yotiprotoattr.Attribute
+ attributeSlice = append(attributeSlice, attr)
+
+ return UserProfile{
+ baseProfile{
+ attributeSlice: attributeSlice,
+ },
+ }
+}
+
+func createAppProfileWithSingleAttribute(attr *yotiprotoattr.Attribute) ApplicationProfile {
+ var attributeSlice []*yotiprotoattr.Attribute
+ attributeSlice = append(attributeSlice, attr)
+
+ return ApplicationProfile{
+ baseProfile{
+ attributeSlice: attributeSlice,
+ },
+ }
+}
+
+func createProfileWithMultipleAttributes(list ...*yotiprotoattr.Attribute) UserProfile {
+ return UserProfile{
+ baseProfile{
+ attributeSlice: list,
+ },
+ }
+}
+
+func TestProfile_AgeVerifications(t *testing.T) {
+ ageOver14 := &yotiprotoattr.Attribute{
+ Name: "age_over:14",
+ Value: []byte("true"),
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+ ageUnder18 := &yotiprotoattr.Attribute{
+ Name: "age_under:18",
+ Value: []byte("true"),
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+ ageOver18 := &yotiprotoattr.Attribute{
+ Name: "age_over:18",
+ Value: []byte("false"),
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ profile := createProfileWithMultipleAttributes(ageOver14, ageUnder18, ageOver18)
+ ageVerifications, err := profile.AgeVerifications()
+
+ assert.NilError(t, err)
+ assert.Equal(t, len(ageVerifications), 3)
+
+ assert.Equal(t, ageVerifications[0].Age, 14)
+ assert.Equal(t, ageVerifications[0].CheckType, "age_over")
+ assert.Equal(t, ageVerifications[0].Result, true)
+
+ assert.Equal(t, ageVerifications[1].Age, 18)
+ assert.Equal(t, ageVerifications[1].CheckType, "age_under")
+ assert.Equal(t, ageVerifications[1].Result, true)
+
+ assert.Equal(t, ageVerifications[2].Age, 18)
+ assert.Equal(t, ageVerifications[2].CheckType, "age_over")
+ assert.Equal(t, ageVerifications[2].Result, false)
+}
+
+func TestProfile_GetAttribute_EmptyString(t *testing.T) {
+ emptyString := ""
+ attributeValue := []byte(emptyString)
+
+ var attr = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: attributeValue,
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ result := createProfileWithSingleAttribute(attr)
+ att := result.GetAttribute(attributeName)
+
+ assert.Equal(t, att.Name(), attributeName)
+ assert.Equal(t, att.Value().(string), emptyString)
+}
+
+func TestProfile_GetApplicationAttribute(t *testing.T) {
+ var attr = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ appProfile := createProfileWithSingleAttribute(attr)
+ applicationAttribute := appProfile.GetAttribute(attributeName)
+ assert.Equal(t, applicationAttribute.Name(), attributeName)
+}
+
+func TestProfile_GetApplicationName(t *testing.T) {
+ attributeValue := "APPLICATION NAME"
+ var attr = &yotiprotoattr.Attribute{
+ Name: "application_name",
+ Value: []byte(attributeValue),
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ appProfile := createAppProfileWithSingleAttribute(attr)
+ assert.Equal(t, attributeValue, appProfile.ApplicationName().Value())
+}
+
+func TestProfile_GetApplicationURL(t *testing.T) {
+ attributeValue := "APPLICATION URL"
+ var attr = &yotiprotoattr.Attribute{
+ Name: "application_url",
+ Value: []byte(attributeValue),
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ appProfile := createAppProfileWithSingleAttribute(attr)
+ assert.Equal(t, attributeValue, appProfile.ApplicationURL().Value())
+}
+
+func TestProfile_GetApplicationLogo(t *testing.T) {
+ attributeValue := "APPLICATION LOGO"
+ var attr = &yotiprotoattr.Attribute{
+ Name: "application_logo",
+ Value: []byte(attributeValue),
+ ContentType: yotiprotoattr.ContentType_JPEG,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ appProfile := createAppProfileWithSingleAttribute(attr)
+ assert.Equal(t, 16, len(appProfile.ApplicationLogo().Value().Data()))
+}
+
+func TestProfile_GetApplicationBGColor(t *testing.T) {
+ attributeValue := "BG VALUE"
+ var attr = &yotiprotoattr.Attribute{
+ Name: "application_receipt_bgcolor",
+ Value: []byte(attributeValue),
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ appProfile := createAppProfileWithSingleAttribute(attr)
+ assert.Equal(t, attributeValue, appProfile.ApplicationReceiptBgColor().Value())
+}
+
+func TestProfile_GetAttribute_Int(t *testing.T) {
+ intValues := [5]int{0, 1, 123, -10, -1}
+
+ for _, integer := range intValues {
+ assertExpectedIntegerIsReturned(t, integer)
+ }
+}
+
+func assertExpectedIntegerIsReturned(t *testing.T, intValue int) {
+ intAsString := strconv.Itoa(intValue)
+
+ var attr = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: []byte(intAsString),
+ ContentType: yotiprotoattr.ContentType_INT,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ result := createProfileWithSingleAttribute(attr)
+ att := result.GetAttribute(attributeName)
+
+ assert.Equal(t, att.Value().(int), intValue)
+}
+
+func TestProfile_GetAttribute_InvalidInt_ReturnsNil(t *testing.T) {
+ invalidIntValue := "1985-01-01"
+
+ var attr = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: []byte(invalidIntValue),
+ ContentType: yotiprotoattr.ContentType_INT,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ result := createProfileWithSingleAttribute(attr)
+
+ att := result.GetAttribute(attributeName)
+
+ assert.Assert(t, is.Nil(att))
+}
+
+func TestProfile_EmptyStringIsAllowed(t *testing.T) {
+ emptyString := ""
+ attrValue := []byte(emptyString)
+
+ var attr = &yotiprotoattr.Attribute{
+ Name: consts.AttrGender,
+ Value: attrValue,
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ profile := createProfileWithSingleAttribute(attr)
+ att := profile.Gender()
+
+ assert.Equal(t, att.Value(), emptyString)
+}
+
+func TestProfile_GetAttribute_Time(t *testing.T) {
+ dateStringValue := "1985-01-01"
+ expectedDate := time.Date(1985, time.January, 1, 0, 0, 0, 0, time.UTC)
+
+ attributeValueTime := []byte(dateStringValue)
+
+ var attr = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: attributeValueTime,
+ ContentType: yotiprotoattr.ContentType_DATE,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ result := createProfileWithSingleAttribute(attr)
+ att := result.GetAttribute(attributeName)
+
+ assert.Equal(t, expectedDate, att.Value().(*time.Time).UTC())
+}
+
+func TestProfile_GetAttribute_Jpeg(t *testing.T) {
+ var attr = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: attributeValue,
+ ContentType: yotiprotoattr.ContentType_JPEG,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ profile := createProfileWithSingleAttribute(attr)
+ att := profile.GetAttribute(attributeName)
+
+ expected := media.JPEGImage(attributeValue)
+ result := att.Value().(media.JPEGImage)
+
+ assert.DeepEqual(t, expected, result)
+ assert.Equal(t, expected.Base64URL(), result.Base64URL())
+}
+
+func TestProfile_GetAttribute_Png(t *testing.T) {
+ var attr = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: attributeValue,
+ ContentType: yotiprotoattr.ContentType_PNG,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ profile := createProfileWithSingleAttribute(attr)
+ att := profile.GetAttribute(attributeName)
+
+ expected := media.PNGImage(attributeValue)
+ result := att.Value().(media.PNGImage)
+
+ assert.DeepEqual(t, expected, result)
+ assert.Equal(t, expected.Base64URL(), result.Base64URL())
+}
+
+func TestProfile_GetAttribute_Bool(t *testing.T) {
+ var initialBoolValue = true
+ attrValue := []byte(strconv.FormatBool(initialBoolValue))
+
+ var attr = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: attrValue,
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ result := createProfileWithSingleAttribute(attr)
+ att := result.GetAttribute(attributeName)
+
+ boolValue, err := strconv.ParseBool(att.Value().(string))
+
+ assert.NilError(t, err)
+ assert.Equal(t, initialBoolValue, boolValue)
+}
+
+func TestProfile_GetAttribute_JSON(t *testing.T) {
+ addressFormat := "2"
+
+ var structuredAddressBytes = []byte(`
+ {
+ "address_format": "` + addressFormat + `",
+ "building": "House No.86-A"
+ }`)
+
+ var attr = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: structuredAddressBytes,
+ ContentType: yotiprotoattr.ContentType_JSON,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ result := createProfileWithSingleAttribute(attr)
+ att := result.GetAttribute(attributeName)
+
+ retrievedAttributeMap := att.Value().(map[string]interface{})
+ actualAddressFormat := retrievedAttributeMap["address_format"]
+
+ assert.Equal(t, actualAddressFormat, addressFormat)
+}
+
+func TestProfile_GetAttribute_Undefined(t *testing.T) {
+ var attr = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: attributeValue,
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ result := createProfileWithSingleAttribute(attr)
+ att := result.GetAttribute(attributeName)
+
+ assert.Equal(t, att.Name(), attributeName)
+ assert.Equal(t, att.Value().(string), attributeValueString)
+}
+
+func TestProfile_GetAttribute_ReturnsNil(t *testing.T) {
+ userProfile := UserProfile{
+ baseProfile{
+ attributeSlice: []*yotiprotoattr.Attribute{},
+ },
+ }
+
+ result := userProfile.GetAttribute("attributeName")
+
+ assert.Assert(t, is.Nil(result))
+}
+
+func TestProfile_GetAttributeByID(t *testing.T) {
+ attributeID := "att-id-123"
+
+ var attr1 = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: attributeValue,
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ EphemeralId: attributeID,
+ }
+ var attr2 = &yotiprotoattr.Attribute{
+ Name: attributeName,
+ Value: attributeValue,
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ EphemeralId: "non-matching-attribute-ID",
+ }
+
+ profile := createProfileWithMultipleAttributes(attr1, attr2)
+
+ result := profile.GetAttributeByID(attributeID)
+ assert.DeepEqual(t, result.ID(), &attributeID)
+}
+
+func TestProfile_GetAttributeByID_ReturnsNil(t *testing.T) {
+ userProfile := UserProfile{
+ baseProfile{
+ attributeSlice: []*yotiprotoattr.Attribute{},
+ },
+ }
+
+ result := userProfile.GetAttributeByID("attributeName")
+
+ assert.Assert(t, is.Nil(result))
+}
+
+func TestProfile_GetDocumentImagesAttributeByID_ReturnsNil(t *testing.T) {
+ userProfile := UserProfile{
+ baseProfile{
+ attributeSlice: []*yotiprotoattr.Attribute{},
+ },
+ }
+
+ result, err := userProfile.GetDocumentImagesAttributeByID("attributeName")
+ assert.NilError(t, err)
+ assert.Assert(t, is.Nil(result))
+}
+
+func TestProfile_GetSelfieAttributeByID_ReturnsNil(t *testing.T) {
+ userProfile := UserProfile{
+ baseProfile{
+ attributeSlice: []*yotiprotoattr.Attribute{},
+ },
+ }
+
+ result, err := userProfile.GetSelfieAttributeByID("attributeName")
+ assert.NilError(t, err)
+ assert.Assert(t, is.Nil(result))
+}
+
+func TestProfile_StringAttribute(t *testing.T) {
+ nationalityName := consts.AttrNationality
+
+ var as = &yotiprotoattr.Attribute{
+ Name: nationalityName,
+ Value: attributeValue,
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ result := createProfileWithSingleAttribute(as)
+
+ assert.Equal(t, result.Nationality().Value(), attributeValueString)
+
+ assert.Equal(t, result.Nationality().ContentType(), yotiprotoattr.ContentType_STRING.String())
+}
+
+func TestProfile_AttributeProperty_RetrievesAttribute(t *testing.T) {
+ attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_PNG, "id")
+
+ result := createProfileWithSingleAttribute(attributeImage)
+ selfie := result.Selfie()
+
+ assert.Equal(t, selfie.Name(), consts.AttrSelfie)
+ assert.DeepEqual(t, attributeValue, selfie.Value().Data())
+ assert.Equal(t, selfie.ContentType(), yotiprotoattr.ContentType_PNG.String())
+}
+
+func TestProfile_DocumentDetails_RetrievesAttribute(t *testing.T) {
+ documentDetailsName := consts.AttrDocumentDetails
+ attributeValue := []byte("PASSPORT GBR 1234567")
+
+ var protoAttribute = &yotiprotoattr.Attribute{
+ Name: documentDetailsName,
+ Value: attributeValue,
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: make([]*yotiprotoattr.Anchor, 0),
+ }
+
+ result := createProfileWithSingleAttribute(protoAttribute)
+ documentDetails, err := result.DocumentDetails()
+ assert.NilError(t, err)
+
+ assert.Equal(t, documentDetails.Value().DocumentType, "PASSPORT")
+}
+
+func TestProfile_DocumentImages_RetrievesAttribute(t *testing.T) {
+ protoAttribute := createDocumentImagesAttribute("attr-id")
+
+ result := createProfileWithSingleAttribute(protoAttribute)
+ documentImages, err := result.DocumentImages()
+ assert.NilError(t, err)
+
+ assert.Equal(t, documentImages.Name(), consts.AttrDocumentImages)
+}
+
+func TestProfile_AttributesReturnsNilWhenNotPresent(t *testing.T) {
+ documentImagesName := consts.AttrDocumentImages
+ multiValue, err := proto.Marshal(&yotiprotoattr.MultiValue{})
+ assert.NilError(t, err)
+
+ protoAttribute := &yotiprotoattr.Attribute{
+ Name: documentImagesName,
+ Value: multiValue,
+ ContentType: yotiprotoattr.ContentType_MULTI_VALUE,
+ Anchors: make([]*yotiprotoattr.Anchor, 0),
+ }
+
+ result := createProfileWithSingleAttribute(protoAttribute)
+
+ DoB, err := result.DateOfBirth()
+ assert.Check(t, DoB == nil)
+ assert.Check(t, err == nil)
+ assert.Check(t, result.Address() == nil)
+}
+
+func TestMissingPostalAddress_UsesFormattedAddress(t *testing.T) {
+ var formattedAddressText = `House No.86-A\nRajgura Nagar\nLudhina\nPunjab\n141012\nIndia`
+
+ var structuredAddressBytes = []byte(`
+ {
+ "address_format": 2,
+ "building": "House No.86-A",
+ "formatted_address": "` + formattedAddressText + `"
+ }
+ `)
+
+ var jsonAttribute = &yotiprotoattr.Attribute{
+ Name: consts.AttrStructuredPostalAddress,
+ Value: structuredAddressBytes,
+ ContentType: yotiprotoattr.ContentType_JSON,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ profile := createProfileWithSingleAttribute(jsonAttribute)
+
+ ensureAddressProfile(&profile)
+
+ escapedFormattedAddressText := strings.Replace(formattedAddressText, `\n`, "\n", -1)
+
+ profileAddress := profile.Address().Value()
+ assert.Equal(t, profileAddress, escapedFormattedAddressText, "Address does not equal the expected formatted address.")
+
+ structuredPostalAddress, err := profile.StructuredPostalAddress()
+ assert.NilError(t, err)
+ assert.Equal(t, structuredPostalAddress.ContentType(), "JSON")
+}
+
+func TestAttributeImage_Image_Png(t *testing.T) {
+ attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_PNG, "id")
+
+ result := createProfileWithSingleAttribute(attributeImage)
+ selfie := result.Selfie()
+
+ assert.DeepEqual(t, selfie.Value().Data(), attributeValue)
+}
+
+func TestAttributeImage_Image_Jpeg(t *testing.T) {
+ attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_JPEG, "id")
+
+ result := createProfileWithSingleAttribute(attributeImage)
+ selfie := result.Selfie()
+
+ assert.DeepEqual(t, selfie.Value().Data(), attributeValue)
+}
+
+func TestAttributeImage_Image_Default(t *testing.T) {
+ attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_PNG, "id")
+
+ result := createProfileWithSingleAttribute(attributeImage)
+ selfie := result.Selfie()
+
+ assert.DeepEqual(t, selfie.Value().Data(), attributeValue)
+}
+func TestAttributeImage_Base64Selfie_Png(t *testing.T) {
+ attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_PNG, "id")
+
+ result := createProfileWithSingleAttribute(attributeImage)
+ base64ImageExpectedValue := base64.StdEncoding.EncodeToString(attributeValue)
+ expectedBase64Selfie := "data:image/png;base64," + base64ImageExpectedValue
+ base64Selfie := result.Selfie().Value().Base64URL()
+
+ assert.Equal(t, base64Selfie, expectedBase64Selfie)
+}
+
+func TestAttributeImage_Base64URL_Jpeg(t *testing.T) {
+ attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_JPEG, "id")
+
+ result := createProfileWithSingleAttribute(attributeImage)
+
+ base64ImageExpectedValue := base64.StdEncoding.EncodeToString(attributeValue)
+
+ expectedBase64Selfie := "data:image/jpeg;base64," + base64ImageExpectedValue
+
+ base64Selfie := result.Selfie().Value().Base64URL()
+
+ assert.Equal(t, base64Selfie, expectedBase64Selfie)
+}
+
+func TestProfile_IdentityProfileReport_RetrievesAttribute(t *testing.T) {
+ identityProfileReportJSON, err := file.ReadFile("../test/fixtures/RTWIdentityProfileReport.json")
+ assert.NilError(t, err)
+
+ var attr = &yotiprotoattr.Attribute{
+ Name: consts.AttrIdentityProfileReport,
+ Value: identityProfileReportJSON,
+ ContentType: yotiprotoattr.ContentType_JSON,
+ Anchors: []*yotiprotoattr.Anchor{},
+ }
+
+ result := createProfileWithSingleAttribute(attr)
+ att, err := result.IdentityProfileReport()
+ assert.NilError(t, err)
+
+ retrievedIdentityProfile := att.Value()
+ gotProof := retrievedIdentityProfile["proof"]
+
+ assert.Equal(t, gotProof, "")
+}
+
+func TestProfileAllowsMultipleAttributesWithSameName(t *testing.T) {
+ firstAttribute := createStringAttribute("full_name", []byte("some_value"), []*yotiprotoattr.Anchor{}, "id")
+ secondAttribute := createStringAttribute("full_name", []byte("some_other_value"), []*yotiprotoattr.Anchor{}, "id")
+
+ var attributeSlice []*yotiprotoattr.Attribute
+ attributeSlice = append(attributeSlice, firstAttribute, secondAttribute)
+
+ var profile = UserProfile{
+ baseProfile{
+ attributeSlice: attributeSlice,
+ },
+ }
+
+ var fullNames = profile.GetAttributes("full_name")
+
+ assert.Assert(t, is.Equal(len(fullNames), 2))
+ assert.Assert(t, is.Equal(fullNames[0].Value().(string), "some_value"))
+ assert.Assert(t, is.Equal(fullNames[1].Value().(string), "some_other_value"))
+}
+
+func createStringAttribute(name string, value []byte, anchors []*yotiprotoattr.Anchor, attributeID string) *yotiprotoattr.Attribute {
+ return &yotiprotoattr.Attribute{
+ Name: name,
+ Value: value,
+ ContentType: yotiprotoattr.ContentType_STRING,
+ Anchors: anchors,
+ EphemeralId: attributeID,
+ }
+}
+
+func createSelfieAttribute(contentType yotiprotoattr.ContentType, attributeID string) *yotiprotoattr.Attribute {
+ var attributeImage = &yotiprotoattr.Attribute{
+ Name: consts.AttrSelfie,
+ Value: attributeValue,
+ ContentType: contentType,
+ Anchors: []*yotiprotoattr.Anchor{},
+ EphemeralId: attributeID,
+ }
+ return attributeImage
+}
+
+func createDocumentImagesAttribute(attributeID string) *yotiprotoattr.Attribute {
+ multiValue, err := proto.Marshal(&yotiprotoattr.MultiValue{})
+ if err != nil {
+ panic(err)
+ }
+
+ protoAttribute := &yotiprotoattr.Attribute{
+ Name: consts.AttrDocumentImages,
+ Value: multiValue,
+ ContentType: yotiprotoattr.ContentType_MULTI_VALUE,
+ Anchors: make([]*yotiprotoattr.Anchor, 0),
+ EphemeralId: attributeID,
+ }
+ return protoAttribute
+}
diff --git a/digitalidentity/wanted_anchor_builder.go b/digitalidentity/wanted_anchor_builder.go
new file mode 100644
index 00000000..855c9c3a
--- /dev/null
+++ b/digitalidentity/wanted_anchor_builder.go
@@ -0,0 +1,44 @@
+package digitalidentity
+
+import (
+ "encoding/json"
+)
+
+// WantedAnchor specifies a preferred anchor for a user's details
+type WantedAnchor struct {
+ name string
+ subType string
+}
+
+// WantedAnchorBuilder describes a desired anchor for user profile data
+type WantedAnchorBuilder struct {
+ wantedAnchor WantedAnchor
+}
+
+// WithValue sets the anchor's name
+func (b *WantedAnchorBuilder) WithValue(name string) *WantedAnchorBuilder {
+ b.wantedAnchor.name = name
+ return b
+}
+
+// WithSubType sets the anchors subtype
+func (b *WantedAnchorBuilder) WithSubType(subType string) *WantedAnchorBuilder {
+ b.wantedAnchor.subType = subType
+ return b
+}
+
+// Build constructs the anchor from the builder's specification
+func (b *WantedAnchorBuilder) Build() (WantedAnchor, error) {
+ return b.wantedAnchor, nil
+}
+
+// MarshalJSON ...
+func (a *WantedAnchor) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&struct {
+ Name string `json:"name"`
+ SubType string `json:"sub_type"`
+ }{
+ Name: a.name,
+ SubType: a.subType,
+ })
+}
diff --git a/digitalidentity/wanted_anchor_builder_test.go b/digitalidentity/wanted_anchor_builder_test.go
new file mode 100644
index 00000000..907d8e08
--- /dev/null
+++ b/digitalidentity/wanted_anchor_builder_test.go
@@ -0,0 +1,25 @@
+package digitalidentity
+
+import (
+ "fmt"
+)
+
+func ExampleWantedAnchorBuilder() {
+ aadhaarAnchor, err := (&WantedAnchorBuilder{}).
+ WithValue("NATIONAL_ID").
+ WithSubType("AADHAAR").
+ Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ aadhaarJSON, err := aadhaarAnchor.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println("Aadhaar:", string(aadhaarJSON))
+ // Output: Aadhaar: {"name":"NATIONAL_ID","sub_type":"AADHAAR"}
+}
diff --git a/digitalidentity/wanted_attribute_builder.go b/digitalidentity/wanted_attribute_builder.go
new file mode 100644
index 00000000..d004b96c
--- /dev/null
+++ b/digitalidentity/wanted_attribute_builder.go
@@ -0,0 +1,81 @@
+package digitalidentity
+
+import (
+ "encoding/json"
+ "errors"
+)
+
+type constraintInterface interface {
+ MarshalJSON() ([]byte, error)
+ isConstraint() bool // This function is not used but makes inheritance explicit
+}
+
+// WantedAttributeBuilder generates the payload for specifying a single wanted
+// attribute as part of a dynamic scenario
+type WantedAttributeBuilder struct {
+ attr WantedAttribute
+}
+
+// WantedAttribute represents a wanted attribute in a dynamic sharing policy
+type WantedAttribute struct {
+ name string
+ derivation string
+ constraints []constraintInterface
+ acceptSelfAsserted bool
+ Optional bool
+}
+
+// WithName sets the name of the wanted attribute
+func (builder *WantedAttributeBuilder) WithName(name string) *WantedAttributeBuilder {
+ builder.attr.name = name
+ return builder
+}
+
+// WithDerivation sets the derivation
+func (builder *WantedAttributeBuilder) WithDerivation(derivation string) *WantedAttributeBuilder {
+ builder.attr.derivation = derivation
+ return builder
+}
+
+// WithConstraint adds a constraint to a wanted attribute
+func (builder *WantedAttributeBuilder) WithConstraint(constraint constraintInterface) *WantedAttributeBuilder {
+ builder.attr.constraints = append(builder.attr.constraints, constraint)
+ return builder
+}
+
+// WithAcceptSelfAsserted allows self-asserted user details, such as those from Aadhar
+func (builder *WantedAttributeBuilder) WithAcceptSelfAsserted(accept bool) *WantedAttributeBuilder {
+ builder.attr.acceptSelfAsserted = accept
+ return builder
+}
+
+// Build generates the wanted attribute's specification
+func (builder *WantedAttributeBuilder) Build() (WantedAttribute, error) {
+ if builder.attr.constraints == nil {
+ builder.attr.constraints = make([]constraintInterface, 0)
+ }
+
+ var err error
+ if len(builder.attr.name) == 0 {
+ err = errors.New("wanted attribute names must not be empty")
+ }
+
+ return builder.attr, err
+}
+
+// MarshalJSON returns the JSON encoding
+func (attr *WantedAttribute) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&struct {
+ Name string `json:"name"`
+ Derivation string `json:"derivation,omitempty"`
+ Constraints []constraintInterface `json:"constraints,omitempty"`
+ AcceptSelfAsserted bool `json:"accept_self_asserted"`
+ Optional bool `json:"optional,omitempty"`
+ }{
+ Name: attr.name,
+ Derivation: attr.derivation,
+ Constraints: attr.constraints,
+ AcceptSelfAsserted: attr.acceptSelfAsserted,
+ Optional: attr.Optional,
+ })
+}
diff --git a/digitalidentity/wanted_attribute_test.go b/digitalidentity/wanted_attribute_test.go
new file mode 100644
index 00000000..0a990d1e
--- /dev/null
+++ b/digitalidentity/wanted_attribute_test.go
@@ -0,0 +1,154 @@
+package digitalidentity
+
+import (
+ "encoding/json"
+ "fmt"
+ "testing"
+
+ "gotest.tools/v3/assert"
+)
+
+func ExampleWantedAttributeBuilder_WithName() {
+ builder := (&WantedAttributeBuilder{}).WithName("TEST NAME")
+ attribute, err := builder.Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(attribute.name)
+ // Output: TEST NAME
+}
+
+func ExampleWantedAttributeBuilder_WithDerivation() {
+ attribute, err := (&WantedAttributeBuilder{}).
+ WithDerivation("TEST DERIVATION").
+ WithName("TEST NAME").
+ Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(attribute.derivation)
+ // Output: TEST DERIVATION
+}
+
+func ExampleWantedAttributeBuilder_WithConstraint() {
+ constraint, err := (&SourceConstraintBuilder{}).
+ Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ attribute, err := (&WantedAttributeBuilder{}).
+ WithName("TEST NAME").
+ WithConstraint(&constraint).
+ Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ marshalledJSON, err := attribute.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(marshalledJSON))
+ // Output: {"name":"TEST NAME","constraints":[{"type":"SOURCE","preferred_sources":{"anchors":[],"soft_preference":false}}],"accept_self_asserted":false}
+}
+
+func ExampleWantedAttributeBuilder_WithAcceptSelfAsserted() {
+ attribute, err := (&WantedAttributeBuilder{}).
+ WithName("TEST NAME").
+ WithAcceptSelfAsserted(true).
+ Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ marshalledJSON, err := attribute.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(marshalledJSON))
+ // Output: {"name":"TEST NAME","accept_self_asserted":true}
+}
+
+func ExampleWantedAttributeBuilder_WithAcceptSelfAsserted_false() {
+ attribute, err := (&WantedAttributeBuilder{}).
+ WithName("TEST NAME").
+ WithAcceptSelfAsserted(false).
+ Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ marshalledJSON, err := attribute.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(marshalledJSON))
+ // Output: {"name":"TEST NAME","accept_self_asserted":false}
+}
+
+func ExampleWantedAttributeBuilder_optional_true() {
+ attribute, err := (&WantedAttributeBuilder{}).
+ WithName("TEST NAME").
+ WithAcceptSelfAsserted(false).
+ Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ attribute.Optional = true
+
+ marshalledJSON, err := attribute.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(marshalledJSON))
+ // Output: {"name":"TEST NAME","accept_self_asserted":false,"optional":true}
+}
+
+func TestWantedAttributeBuilder_Optional_IsOmittedByDefault(t *testing.T) {
+ attribute, err := (&WantedAttributeBuilder{}).
+ WithName("TEST NAME").
+ Build()
+ if err != nil {
+ t.Errorf("error: %s", err.Error())
+ }
+
+ marshalledJSON, err := attribute.MarshalJSON()
+ if err != nil {
+ t.Errorf("error: %s", err.Error())
+ }
+
+ attributeMap := unmarshalJSONIntoMap(t, marshalledJSON)
+
+ optional := attributeMap["optional"]
+
+ if optional != nil {
+ t.Errorf("expected `optional` to be nil, but was: '%v'", optional)
+ }
+}
+
+func unmarshalJSONIntoMap(t *testing.T, byteValue []byte) (result map[string]interface{}) {
+ var unmarshalled interface{}
+ err := json.Unmarshal(byteValue, &unmarshalled)
+ assert.NilError(t, err)
+
+ return unmarshalled.(map[string]interface{})
+}
diff --git a/digitalidentity/yotierror/response.go b/digitalidentity/yotierror/response.go
new file mode 100644
index 00000000..3274e80f
--- /dev/null
+++ b/digitalidentity/yotierror/response.go
@@ -0,0 +1,56 @@
+package yotierror
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+)
+
+var (
+ defaultUnknownErrorCodeConst = "UNKNOWN_ERROR"
+ defaultUnknownErrorMessageConst = "unknown HTTP error"
+)
+
+// Error indicates errors related to the Yoti API.
+type Error struct {
+ Id string `json:"id"`
+ Status int `json:"status"`
+ ErrorCode string `json:"error"`
+ Message string `json:"message"`
+}
+
+func (e Error) Error() string {
+ return e.ErrorCode + " - " + e.Message
+}
+
+// NewResponseError creates a new Error
+func NewResponseError(response *http.Response) *Error {
+ err := &Error{
+ ErrorCode: defaultUnknownErrorCodeConst,
+ Message: defaultUnknownErrorMessageConst,
+ }
+ if response == nil {
+ return err
+ }
+ err.Status = response.StatusCode
+ if response.Body == nil {
+ return err
+ }
+ defer response.Body.Close()
+ b, e := io.ReadAll(response.Body)
+ if e != nil {
+ err.Message = fmt.Sprintf(defaultUnknownErrorMessageConst+": %q", e)
+ return err
+ }
+ e = json.Unmarshal(b, err)
+ if e != nil {
+ err.Message = fmt.Sprintf(defaultUnknownErrorMessageConst+": %q", e)
+ }
+ return err
+}
+
+// Temporary indicates this ErrorCode is a temporary ErrorCode
+func (e Error) Temporary() bool {
+ return e.Status >= 500
+}
diff --git a/digitalidentity/yotierror/response_test.go b/digitalidentity/yotierror/response_test.go
new file mode 100644
index 00000000..be2225ca
--- /dev/null
+++ b/digitalidentity/yotierror/response_test.go
@@ -0,0 +1,68 @@
+package yotierror
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+)
+
+var (
+ expectedErr = Error{
+ Id: "8f6a9dfe72128de20909af0d476769b6",
+ Status: 401,
+ ErrorCode: "INVALID_REQUEST_SIGNATURE",
+ Message: "Invalid request signature",
+ }
+)
+
+func TestError_ShouldReturnFormattedError(t *testing.T) {
+ jsonBytes := json.RawMessage(`{"id":"8f6a9dfe72128de20909af0d476769b6","status":401,"error":"INVALID_REQUEST_SIGNATURE","message":"Invalid request signature"}`)
+
+ err := NewResponseError(
+ &http.Response{
+ StatusCode: 401,
+ Body: io.NopCloser(bytes.NewReader(jsonBytes)),
+ },
+ )
+
+ assert.ErrorIs(t, *err, expectedErr)
+}
+
+func TestError_ShouldReturnFormattedError_ReturnWrappedErrorWhenInvalidJSON(t *testing.T) {
+ response := &http.Response{
+ StatusCode: 400,
+ Body: io.NopCloser(strings.NewReader("some invalid JSON")),
+ }
+ err := NewResponseError(
+ response,
+ )
+
+ assert.ErrorContains(t, err, "unknown HTTP error")
+}
+
+func TestError_ShouldReturnTemporaryForServerError(t *testing.T) {
+ response := &http.Response{
+ StatusCode: 500,
+ }
+ err := NewResponseError(
+ response,
+ )
+
+ assert.Check(t, err.Temporary())
+}
+
+func TestError_ShouldNotReturnTemporaryForClientError(t *testing.T) {
+ response := &http.Response{
+ StatusCode: 400,
+ }
+ err := NewResponseError(
+ response,
+ )
+
+ assert.Check(t, !err.Temporary())
+}
diff --git a/digitalidentity/yotierror/signed_requests.go b/digitalidentity/yotierror/signed_requests.go
new file mode 100644
index 00000000..3f91d381
--- /dev/null
+++ b/digitalidentity/yotierror/signed_requests.go
@@ -0,0 +1,8 @@
+package yotierror
+
+const (
+ // InvalidRequestSignature can be returned by any endpoint that requires a signed request.
+ InvalidRequestSignature = "INVALID_REQUEST_SIGNATURE"
+ // InvalidAuthHeader can be returned by any endpoint that requires a signed request.
+ InvalidAuthHeader = "INVALID_AUTH_HEADER"
+)
diff --git a/docscan/client.go b/docscan/client.go
index b0cf5ed3..c4aa82dc 100644
--- a/docscan/client.go
+++ b/docscan/client.go
@@ -5,7 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"os"
"strconv"
@@ -96,7 +96,7 @@ func (c *Client) CreateSession(sessionSpec *create.SessionSpecification) (*creat
}
var responseBytes []byte
- responseBytes, err = ioutil.ReadAll(response.Body)
+ responseBytes, err = io.ReadAll(response.Body)
if err != nil {
return nil, err
}
@@ -131,7 +131,7 @@ func (c *Client) GetSession(sessionID string) (*retrieve.GetSessionResult, error
}
var responseBytes []byte
- responseBytes, err = ioutil.ReadAll(response.Body)
+ responseBytes, err = io.ReadAll(response.Body)
if err != nil {
return nil, err
}
@@ -199,7 +199,7 @@ func (c *Client) GetMediaContent(sessionID, mediaID string) (media.Media, error)
}
var responseBytes []byte
- responseBytes, err = ioutil.ReadAll(response.Body)
+ responseBytes, err = io.ReadAll(response.Body)
if err != nil {
return nil, err
}
@@ -269,7 +269,7 @@ func (c *Client) GetSupportedDocumentsWithNonLatin(includeNonLatin bool) (*suppo
}
var responseBytes []byte
- responseBytes, err = ioutil.ReadAll(response.Body)
+ responseBytes, err = io.ReadAll(response.Body)
if err != nil {
return nil, err
}
diff --git a/docscan/client_test.go b/docscan/client_test.go
index a52f82d2..5f158711 100644
--- a/docscan/client_test.go
+++ b/docscan/client_test.go
@@ -7,7 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"os"
"strings"
@@ -44,7 +44,7 @@ func TestClient_CreateSession(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusCreated,
- Body: ioutil.NopCloser(strings.NewReader(jsonResponse)),
+ Body: io.NopCloser(strings.NewReader(jsonResponse)),
}, nil
},
}
@@ -122,7 +122,7 @@ func TestClient_GetSession(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
- Body: ioutil.NopCloser(strings.NewReader(jsonResponse)),
+ Body: io.NopCloser(strings.NewReader(jsonResponse)),
}, nil
},
}
@@ -181,7 +181,7 @@ func TestClient_GetSession_ShouldReturnJsonError(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
- Body: ioutil.NopCloser(strings.NewReader("some-invalid-json")),
+ Body: io.NopCloser(strings.NewReader("some-invalid-json")),
}, nil
},
}
@@ -258,7 +258,7 @@ func TestClient_GetMediaContent(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
- Body: ioutil.NopCloser(bytes.NewReader(jpegImage)),
+ Body: io.NopCloser(bytes.NewReader(jpegImage)),
Header: map[string][]string{"Content-Type": {media.ImageTypeJPEG}},
}, nil
},
@@ -312,7 +312,7 @@ func TestClient_GetMediaContent_NoContentType(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
- Body: ioutil.NopCloser(bytes.NewReader(jpegImage)),
+ Body: io.NopCloser(bytes.NewReader(jpegImage)),
Header: map[string][]string{},
}, nil
},
@@ -438,7 +438,7 @@ func TestClient_GetSupportedDocuments(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
- Body: ioutil.NopCloser(bytes.NewReader(jsonBytes)),
+ Body: io.NopCloser(bytes.NewReader(jsonBytes)),
}, nil
},
}
@@ -488,7 +488,7 @@ func TestClient_GetSupportedDocuments_ShouldReturnResponseError(t *testing.T) {
}
func TestNewClient(t *testing.T) {
- key, err := ioutil.ReadFile("../test/test-key.pem")
+ key, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
client, err := NewClient("sdkID", key)
@@ -504,7 +504,7 @@ func TestNewClient_EmptySdkID(t *testing.T) {
}
func TestClient_GetSession_EmptySessionID(t *testing.T) {
- key, err := ioutil.ReadFile("../test/test-key.pem")
+ key, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
client, err := NewClient("sdkID", key)
@@ -515,7 +515,7 @@ func TestClient_GetSession_EmptySessionID(t *testing.T) {
}
func TestClient_DeleteSession_EmptySessionID(t *testing.T) {
- key, err := ioutil.ReadFile("../test/test-key.pem")
+ key, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
client, err := NewClient("sdkID", key)
@@ -526,7 +526,7 @@ func TestClient_DeleteSession_EmptySessionID(t *testing.T) {
}
func TestClient_GetMediaContent_EmptySessionID(t *testing.T) {
- key, err := ioutil.ReadFile("../test/test-key.pem")
+ key, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
client, err := NewClient("sdkID", key)
@@ -537,7 +537,7 @@ func TestClient_GetMediaContent_EmptySessionID(t *testing.T) {
}
func TestClient_GetMediaContent_EmptyMediaID(t *testing.T) {
- key, err := ioutil.ReadFile("../test/test-key.pem")
+ key, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
client, err := NewClient("sdkID", key)
@@ -548,7 +548,7 @@ func TestClient_GetMediaContent_EmptyMediaID(t *testing.T) {
}
func TestClient_DeleteMediaContent_EmptySessionID(t *testing.T) {
- key, err := ioutil.ReadFile("../test/test-key.pem")
+ key, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
client, err := NewClient("sdkID", key)
@@ -559,7 +559,7 @@ func TestClient_DeleteMediaContent_EmptySessionID(t *testing.T) {
}
func TestClient_DeleteMediaContent_EmptyMediaID(t *testing.T) {
- key, err := ioutil.ReadFile("../test/test-key.pem")
+ key, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
client, err := NewClient("sdkID", key)
@@ -570,7 +570,7 @@ func TestClient_DeleteMediaContent_EmptyMediaID(t *testing.T) {
}
func Test_EmptySdkID(t *testing.T) {
- key, err := ioutil.ReadFile("../test/test-key.pem")
+ key, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
client, err := NewClient("sdkID", key)
@@ -581,7 +581,7 @@ func Test_EmptySdkID(t *testing.T) {
}
func TestNewClient_KeyLoad_Failure(t *testing.T) {
- key, err := ioutil.ReadFile("../test/test-key-invalid-format.pem")
+ key, err := os.ReadFile("../test/test-key-invalid-format.pem")
assert.NilError(t, err)
_, err = NewClient("sdkID", key)
@@ -595,7 +595,7 @@ func TestNewClient_KeyLoad_Failure(t *testing.T) {
}
func TestClient_UsesDefaultApiUrl(t *testing.T) {
- key, err := ioutil.ReadFile("../test/test-key.pem")
+ key, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
client, err := NewClient("sdkID", key)
@@ -605,7 +605,7 @@ func TestClient_UsesDefaultApiUrl(t *testing.T) {
}
func TestClient_UsesEnvVariable(t *testing.T) {
- key, err := ioutil.ReadFile("../test/test-key.pem")
+ key, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
os.Setenv("YOTI_DOC_SCAN_API_URL", "envBaseUrl")
@@ -617,7 +617,7 @@ func TestClient_UsesEnvVariable(t *testing.T) {
}
func TestClient_UsesOverrideApiUrlOverEnvVariable(t *testing.T) {
- key, err := ioutil.ReadFile("../test/test-key.pem")
+ key, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
os.Setenv("YOTI_DOC_SCAN_API_URL", "envBaseUrl")
diff --git a/docscan/sandbox/client_test.go b/docscan/sandbox/client_test.go
index 607558ac..a4876c27 100644
--- a/docscan/sandbox/client_test.go
+++ b/docscan/sandbox/client_test.go
@@ -6,7 +6,7 @@ import (
"crypto/rsa"
"encoding/json"
"errors"
- "io/ioutil"
+ "io"
"net/http"
"os"
"strings"
@@ -33,7 +33,7 @@ func TestClient_ConfigureSessionResponse_ShouldReturnErrorIfNotCreated(t *testin
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 400,
- Body: ioutil.NopCloser(strings.NewReader("")),
+ Body: io.NopCloser(strings.NewReader("")),
}, nil
},
},
@@ -54,7 +54,7 @@ func TestClient_ConfigureSessionResponse_ShouldReturnFormattedErrorWithResponse(
response := &http.Response{
StatusCode: 400,
- Body: ioutil.NopCloser(bytes.NewReader(jsonBytes)),
+ Body: io.NopCloser(bytes.NewReader(jsonBytes)),
}
client := Client{
@@ -71,7 +71,7 @@ func TestClient_ConfigureSessionResponse_ShouldReturnFormattedErrorWithResponse(
errorResponse := err.(*yotierror.Error).Response
assert.Equal(t, response, errorResponse)
- body, err := ioutil.ReadAll(errorResponse.Body)
+ body, err := io.ReadAll(errorResponse.Body)
assert.NilError(t, err)
assert.Equal(t, string(body), string(jsonBytes))
}
@@ -95,7 +95,7 @@ func TestClient_ConfigureSessionResponse_ShouldReturnJsonError(t *testing.T) {
}
func TestNewClient_ConfigureSessionResponse_Success(t *testing.T) {
- key, err := ioutil.ReadFile("../../test/test-key.pem")
+ key, err := os.ReadFile("../../test/test-key.pem")
assert.NilError(t, err)
client, err := NewClient("ClientSDKID", key)
@@ -114,7 +114,7 @@ func TestNewClient_ConfigureSessionResponse_Success(t *testing.T) {
}
func TestNewClient_KeyLoad_Failure(t *testing.T) {
- key, err := ioutil.ReadFile("../../test/test-key-invalid-format.pem")
+ key, err := os.ReadFile("../../test/test-key-invalid-format.pem")
assert.NilError(t, err)
_, err = NewClient("", key)
@@ -171,7 +171,7 @@ func TestClient_ConfigureApplicationResponse_ShouldReturnErrorIfNotCreated(t *te
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 401,
- Body: ioutil.NopCloser(strings.NewReader("")),
+ Body: io.NopCloser(strings.NewReader("")),
}, nil
},
},
@@ -287,7 +287,7 @@ func TestClient_ConfigureSessionResponseUsesDefaultUrlAsFallbackWithNoEnvValue(t
}
func createSandboxClient(t *testing.T, constructorApiURL string) (client Client) {
- keyBytes, fileErr := ioutil.ReadFile("../../test/test-key.pem")
+ keyBytes, fileErr := os.ReadFile("../../test/test-key.pem")
assert.NilError(t, fileErr)
pemFile, parseErr := cryptoutil.ParseRSAKey(keyBytes)
diff --git a/dynamic/policy_builder.go b/dynamic/policy_builder.go
index a97ffd18..e7f89308 100644
--- a/dynamic/policy_builder.go
+++ b/dynamic/policy_builder.go
@@ -61,7 +61,7 @@ func (b *PolicyBuilder) WithWantedAttributeByName(name string, options ...interf
case constraintInterface:
attributeBuilder.WithConstraint(value)
default:
- panic(fmt.Sprintf("Not a valid option type, %v", value))
+ panic(fmt.Sprintf("not a valid option type, %v", value))
}
}
@@ -154,7 +154,7 @@ func (b *PolicyBuilder) WithAgeDerivedAttribute(derivation string, options ...in
case constraintInterface:
attributeBuilder.WithConstraint(value)
default:
- panic(fmt.Sprintf("Not a valid option type, %v", value))
+ panic(fmt.Sprintf("not a valid option type, %v", value))
}
}
diff --git a/dynamic/policy_builder_test.go b/dynamic/policy_builder_test.go
index a902cd16..7402e675 100644
--- a/dynamic/policy_builder_test.go
+++ b/dynamic/policy_builder_test.go
@@ -338,7 +338,7 @@ func TestDynamicPolicyBuilder_WithWantedAttributeByName_InvalidOptionsShouldPani
defer func() {
r := recover().(string)
- assert.Check(t, strings.Contains(r, "Not a valid option type"))
+ assert.Check(t, strings.Contains(r, "not a valid option type"))
}()
builder.WithWantedAttributeByName(
@@ -404,7 +404,7 @@ func TestDynamicPolicyBuilder_WithAgeDerivedAttribute_InvalidOptionsShouldPanic(
defer func() {
r := recover().(string)
- assert.Check(t, strings.Contains(r, "Not a valid option type"))
+ assert.Check(t, strings.Contains(r, "not a valid option type"))
}()
builder.WithAgeDerivedAttribute(
diff --git a/dynamic/service.go b/dynamic/service.go
index 58db44fe..ed44cdb7 100644
--- a/dynamic/service.go
+++ b/dynamic/service.go
@@ -4,7 +4,7 @@ import (
"crypto/rsa"
"encoding/json"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"github.com/getyoti/yoti-go-sdk/v3/requests"
@@ -44,7 +44,7 @@ func CreateShareURL(httpClient requests.HttpClient, scenario *Scenario, clientSd
return share, err
}
- responseBytes, err := ioutil.ReadAll(response.Body)
+ responseBytes, err := io.ReadAll(response.Body)
if err != nil {
return
}
diff --git a/dynamic/service_test.go b/dynamic/service_test.go
index 8e926360..9057bc18 100644
--- a/dynamic/service_test.go
+++ b/dynamic/service_test.go
@@ -2,7 +2,7 @@ package dynamic
import (
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"strings"
"testing"
@@ -29,7 +29,7 @@ func ExampleCreateShareURL() {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 201,
- Body: ioutil.NopCloser(strings.NewReader(`{"qrcode":"https://code.yoti.com/CAEaJDQzNzllZDc0LTU0YjItNDkxMy04OTE4LTExYzM2ZDU2OTU3ZDAC","ref_id":"0"}`)),
+ Body: io.NopCloser(strings.NewReader(`{"qrcode":"https://code.yoti.com/CAEaJDQzNzllZDc0LTU0YjItNDkxMy04OTE4LTExYzM2ZDU2OTU3ZDAC","ref_id":"0"}`)),
}, nil
},
}
@@ -97,7 +97,7 @@ func createShareUrlWithErrorResponse(statusCode int, responseBody string) (share
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: statusCode,
- Body: ioutil.NopCloser(strings.NewReader(responseBody)),
+ Body: io.NopCloser(strings.NewReader(responseBody)),
}, nil
},
}
diff --git a/file/file.go b/file/file.go
index e9fc43a6..a002b96c 100644
--- a/file/file.go
+++ b/file/file.go
@@ -1,7 +1,7 @@
package file
import (
- "io/ioutil"
+ "io"
"os"
)
@@ -12,7 +12,7 @@ func ReadFile(filename string) ([]byte, error) {
return nil, err
}
- buffer, err := ioutil.ReadAll(file)
+ buffer, err := io.ReadAll(file)
if err != nil {
return nil, err
}
diff --git a/go.mod b/go.mod
index ca66f5fb..c1c87abb 100644
--- a/go.mod
+++ b/go.mod
@@ -7,4 +7,4 @@ require (
require github.com/google/go-cmp v0.5.5 // indirect
-go 1.17
+go 1.19
diff --git a/profile/sandbox/client.go b/profile/sandbox/client.go
index ce6b71ce..9cbde3b6 100644
--- a/profile/sandbox/client.go
+++ b/profile/sandbox/client.go
@@ -4,7 +4,7 @@ import (
"crypto/rsa"
"encoding/json"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"os"
@@ -65,7 +65,7 @@ func (client *Client) SetupSharingProfile(tokenRequest TokenRequest) (token stri
return
}
if response.StatusCode != http.StatusCreated {
- body, _ := ioutil.ReadAll(response.Body)
+ body, _ := io.ReadAll(response.Body)
return "", fmt.Errorf("Sharing Profile not created (HTTP %d) %s", response.StatusCode, string(body))
}
diff --git a/profile/sandbox/client_test.go b/profile/sandbox/client_test.go
index d4bdb9eb..05070833 100644
--- a/profile/sandbox/client_test.go
+++ b/profile/sandbox/client_test.go
@@ -3,7 +3,7 @@ package sandbox
import (
"crypto/rand"
"crypto/rsa"
- "io/ioutil"
+ "io"
"net/http"
"os"
"strings"
@@ -24,7 +24,7 @@ func TestClient_SetupSharingProfile_ShouldReturnErrorIfProfileNotCreated(t *test
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 401,
- Body: ioutil.NopCloser(strings.NewReader("")),
+ Body: io.NopCloser(strings.NewReader("")),
}, nil
},
},
@@ -45,7 +45,7 @@ func TestClient_SetupSharingProfile_Success(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 201,
- Body: ioutil.NopCloser(strings.NewReader(`{"token":"` + expectedToken + `"}`)),
+ Body: io.NopCloser(strings.NewReader(`{"token":"` + expectedToken + `"}`)),
}, nil
},
},
@@ -99,7 +99,7 @@ func TestClient_SetupSharingProfileUsesDefaultUrlAsFallbackWithNoEnvValue(t *tes
}
func createSandboxClient(t *testing.T, constructorBaseUrl string) (client Client) {
- keyBytes, fileErr := ioutil.ReadFile("../../test/test-key.pem")
+ keyBytes, fileErr := os.ReadFile("../../test/test-key.pem")
assert.NilError(t, fileErr)
pemFile, parseErr := cryptoutil.ParseRSAKey(keyBytes)
@@ -138,7 +138,7 @@ func mockHttpClientCreatedResponse() *mockHTTPClient {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 201,
- Body: ioutil.NopCloser(strings.NewReader(`{"token":"tokenValue"}`)),
+ Body: io.NopCloser(strings.NewReader(`{"token":"tokenValue"}`)),
}, nil
},
}
diff --git a/profile/service.go b/profile/service.go
index 60aa07d4..3f9ae500 100644
--- a/profile/service.go
+++ b/profile/service.go
@@ -5,7 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"strconv"
"time"
@@ -53,7 +53,7 @@ func GetActivityDetails(httpClient requests.HttpClient, token, clientSdkId, apiU
return activity, err
}
- responseBytes, err := ioutil.ReadAll(response.Body)
+ responseBytes, err := io.ReadAll(response.Body)
if err != nil {
return
}
diff --git a/profile/service_test.go b/profile/service_test.go
index e9040bd3..cc07c0f6 100644
--- a/profile/service_test.go
+++ b/profile/service_test.go
@@ -7,9 +7,10 @@ import (
"crypto/rand"
"crypto/rsa"
"encoding/base64"
- "io/ioutil"
+ "io"
"net/http"
"net/url"
+ "os"
"strings"
"testing"
"time"
@@ -109,7 +110,7 @@ func TestProfileService_GetActivityDetails(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
- Body: ioutil.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + test.WrappedReceiptKey + `","other_party_profile_content": "` + otherPartyProfileContent + `","remember_me_id":"` + rememberMeID + `", "sharing_outcome":"SUCCESS", "timestamp":"2006-01-02T15:04:05.999999Z"}}`)),
+ Body: io.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + test.WrappedReceiptKey + `","other_party_profile_content": "` + otherPartyProfileContent + `","remember_me_id":"` + rememberMeID + `", "sharing_outcome":"SUCCESS", "timestamp":"2006-01-02T15:04:05.999999Z"}}`)),
}, nil
},
}
@@ -164,7 +165,7 @@ func TestProfileService_SharingFailure_ReturnsSpecificFailure(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
- Body: ioutil.NopCloser(strings.NewReader(`{"session_data":"session_data","receipt":{"receipt_id": null,"other_party_profile_content": null,"policy_uri":null,"personal_key":null,"remember_me_id":null, "sharing_outcome":"FAILURE","timestamp":"2016-09-23T13:04:11Z"},"error_details":{"error_code":"SOME_ERROR","description":"SOME_DESCRIPTION"}}`)),
+ Body: io.NopCloser(strings.NewReader(`{"session_data":"session_data","receipt":{"receipt_id": null,"other_party_profile_content": null,"policy_uri":null,"personal_key":null,"remember_me_id":null, "sharing_outcome":"FAILURE","timestamp":"2016-09-23T13:04:11Z"},"error_details":{"error_code":"SOME_ERROR","description":"SOME_DESCRIPTION"}}`)),
}, nil
},
}
@@ -197,7 +198,7 @@ func TestProfileService_SharingFailure_ReturnsGenericErrorWhenErrorCodeIsNull(t
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
- Body: ioutil.NopCloser(strings.NewReader(`{"session_data":"session_data","receipt":{"receipt_id": null,"other_party_profile_content": null,"policy_uri":null,"personal_key":null,"remember_me_id":null, "sharing_outcome":"FAILURE","timestamp":"2016-09-23T13:04:11Z"},"error_details":{}}`)),
+ Body: io.NopCloser(strings.NewReader(`{"session_data":"session_data","receipt":{"receipt_id": null,"other_party_profile_content": null,"policy_uri":null,"personal_key":null,"remember_me_id":null, "sharing_outcome":"FAILURE","timestamp":"2016-09-23T13:04:11Z"},"error_details":{}}`)),
}, nil
},
}
@@ -226,7 +227,7 @@ func TestProfileService_SharingFailure_ReturnsGenericFailure(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
- Body: ioutil.NopCloser(strings.NewReader(`{"session_data":"session_data","receipt":{"receipt_id": null,"other_party_profile_content": null,"policy_uri":null,"personal_key":null,"remember_me_id":null, "sharing_outcome":"FAILURE","timestamp":"2016-09-23T13:04:11Z"}}`)),
+ Body: io.NopCloser(strings.NewReader(`{"session_data":"session_data","receipt":{"receipt_id": null,"other_party_profile_content": null,"policy_uri":null,"personal_key":null,"remember_me_id":null, "sharing_outcome":"FAILURE","timestamp":"2016-09-23T13:04:11Z"}}`)),
}, nil
},
}
@@ -276,7 +277,7 @@ func TestProfileService_ParentRememberMeID(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
- Body: ioutil.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + test.WrappedReceiptKey +
+ Body: io.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + test.WrappedReceiptKey +
`","other_party_profile_content": "` + otherPartyProfileContent +
`","parent_remember_me_id":"` + parentRememberMeID +
`", "sharing_outcome":"SUCCESS", "timestamp":"2006-01-02T15:04:05.999999Z"}}`)),
@@ -310,7 +311,7 @@ func TestProfileService_ParseWithoutProfile_Success(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
- Body: ioutil.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + test.WrappedReceiptKey + `",` +
+ Body: io.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + test.WrappedReceiptKey + `",` +
otherPartyProfileContent + `"remember_me_id":"` + rememberMeID + `", "sharing_outcome":"SUCCESS", "timestamp":"` + timestampString + `", "receipt_id":"` + receiptID + `"}}`)),
}, nil
},
@@ -329,7 +330,7 @@ func TestProfileService_ShouldParseAndDecryptExtraDataContent(t *testing.T) {
otherPartyProfileContent := "ChCZAib1TBm9Q5GYfFrS1ep9EnAwQB5shpAPWLBgZgFgt6bCG3S5qmZHhrqUbQr3yL6yeLIDwbM7x4nuT/MYp+LDXgmFTLQNYbDTzrEzqNuO2ZPn9Kpg+xpbm9XtP7ZLw3Ep2BCmSqtnll/OdxAqLb4DTN4/wWdrjnFC+L/oQEECu646"
rememberMeID := "remember_me_id0123456789"
- pemBytes, err := ioutil.ReadFile("../test/test-key.pem")
+ pemBytes, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
dataEntries := make([]*yotiprotoshare.DataEntry, 0)
@@ -347,7 +348,7 @@ func TestProfileService_ShouldParseAndDecryptExtraDataContent(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
- Body: ioutil.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` +
+ Body: io.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` +
test.WrappedReceiptKey + `","other_party_profile_content": "` + otherPartyProfileContent + `","extra_data_content": "` +
extraDataContent + `","remember_me_id":"` + rememberMeID + `", "sharing_outcome":"SUCCESS", "timestamp":"2006-01-02T15:04:05.999999Z"}}`)),
}, nil
@@ -374,7 +375,7 @@ func TestProfileService_ShouldCarryOnProcessingIfIssuanceTokenIsNotPresent(t *te
List: dataEntries,
}
- pemBytes, err := ioutil.ReadFile("../test/test-key.pem")
+ pemBytes, err := os.ReadFile("../test/test-key.pem")
assert.NilError(t, err)
extraDataContent := createExtraDataContent(t, protoExtraData)
@@ -387,7 +388,7 @@ func TestProfileService_ShouldCarryOnProcessingIfIssuanceTokenIsNotPresent(t *te
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
- Body: ioutil.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` +
+ Body: io.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` +
test.WrappedReceiptKey + `","other_party_profile_content": "` + otherPartyProfileContent + `","extra_data_content": "` +
extraDataContent + `","remember_me_id":"` + rememberMeID + `", "sharing_outcome":"SUCCESS"}}`)),
}, nil
@@ -417,7 +418,7 @@ func TestProfileService_ParseWithoutRememberMeID_Success(t *testing.T) {
do: func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
- Body: ioutil.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + test.WrappedReceiptKey + `",` +
+ Body: io.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + test.WrappedReceiptKey + `",` +
otherPartyProfileContent + `"sharing_outcome":"SUCCESS", "timestamp":"2006-01-02T15:04:05.999999Z"}}`)),
}, nil
},
diff --git a/requests/signed_message.go b/requests/signed_message.go
index f390a395..8a1715f2 100644
--- a/requests/signed_message.go
+++ b/requests/signed_message.go
@@ -41,6 +41,13 @@ func JSONHeaders() map[string][]string {
}
}
+// AuthHeader is a header prototype including the App/SDK ID
+func AuthHeader(clientSdkId string, key *rsa.PublicKey) map[string][]string {
+ return map[string][]string{
+ "X-Yoti-Auth-Id": {clientSdkId},
+ }
+}
+
// AuthKeyHeader is a header prototype including an encoded RSA PublicKey
func AuthKeyHeader(key *rsa.PublicKey) map[string][]string {
return map[string][]string{
@@ -185,6 +192,7 @@ func (msg SignedRequest) Request() (request *http.Request, err error) {
if err != nil {
return
}
+
signedDigest, err := msg.signDigest([]byte(msg.generateDigest(endpoint)))
if err != nil {
return
@@ -199,6 +207,7 @@ func (msg SignedRequest) Request() (request *http.Request, err error) {
if err != nil {
return
}
+
request.Header.Add("X-Yoti-Auth-Digest", signedDigest)
request.Header.Add("X-Yoti-SDK", consts.SDKIdentifier)
request.Header.Add("X-Yoti-SDK-Version", consts.SDKVersionIdentifier)
@@ -208,5 +217,6 @@ func (msg SignedRequest) Request() (request *http.Request, err error) {
request.Header.Add(key, value)
}
}
+
return request, err
}
diff --git a/sh/go-build-modtidy.sh b/sh/go-build-modtidy.sh
index 46b63f5a..bef8763e 100755
--- a/sh/go-build-modtidy.sh
+++ b/sh/go-build-modtidy.sh
@@ -2,5 +2,5 @@
go build ./...
for d in _examples/*/; do
- (cd "$d" && go mod tidy -compat=1.17)
+ (cd "$d" && go mod tidy -compat=1.19)
done
diff --git a/sonar-project.properties b/sonar-project.properties
index 961c49c5..39857580 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -12,3 +12,4 @@ sonar.go.tests.reportPaths = sonar-report.json
sonar.tests = .
sonar.test.inclusions = **/*_test.go
sonar.coverage.exclusions = test/**/*,_examples/**/*
+sonar.cpd.exclusions = digitalidentity/**
diff --git a/test/key.go b/test/key.go
index d70ceb74..54e3f494 100644
--- a/test/key.go
+++ b/test/key.go
@@ -2,14 +2,14 @@ package test
import (
"crypto/rsa"
- "io/ioutil"
+ "os"
"github.com/getyoti/yoti-go-sdk/v3/cryptoutil"
)
// GetValidKey returns a parsed RSA Private Key from a test key
func GetValidKey(filepath string) (key *rsa.PrivateKey) {
- keyBytes, err := ioutil.ReadFile(filepath)
+ keyBytes, err := os.ReadFile(filepath)
if err != nil {
panic("Error reading the test key: " + err.Error())
}
diff --git a/yotierror/response.go b/yotierror/response.go
index e15806cd..75c689d2 100644
--- a/yotierror/response.go
+++ b/yotierror/response.go
@@ -4,7 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"strings"
)
@@ -65,8 +65,8 @@ func formatResponseMessage(response *http.Response, httpErrorMessages ...map[int
return defaultMessage
}
- body, _ := ioutil.ReadAll(response.Body)
- response.Body = ioutil.NopCloser(bytes.NewBuffer(body))
+ body, _ := io.ReadAll(response.Body)
+ response.Body = io.NopCloser(bytes.NewBuffer(body))
var errorDO DataObject
jsonError := json.Unmarshal(body, &errorDO)
@@ -104,8 +104,8 @@ func formatHTTPError(message string, statusCode int, body []byte) string {
func handleHTTPError(response *http.Response, errorMessages ...map[int]string) string {
var body []byte
if response.Body != nil {
- body, _ = ioutil.ReadAll(response.Body)
- response.Body = ioutil.NopCloser(bytes.NewBuffer(body))
+ body, _ = io.ReadAll(response.Body)
+ response.Body = io.NopCloser(bytes.NewBuffer(body))
} else {
body = make([]byte, 0)
}
diff --git a/yotierror/response_test.go b/yotierror/response_test.go
index 55be0426..f94810e1 100644
--- a/yotierror/response_test.go
+++ b/yotierror/response_test.go
@@ -3,7 +3,7 @@ package yotierror
import (
"bytes"
"encoding/json"
- "io/ioutil"
+ "io"
"net/http"
"strings"
"testing"
@@ -27,7 +27,7 @@ func TestError_ShouldReturnFormattedError(t *testing.T) {
err = NewResponseError(
&http.Response{
StatusCode: 401,
- Body: ioutil.NopCloser(bytes.NewReader(jsonBytes)),
+ Body: io.NopCloser(bytes.NewReader(jsonBytes)),
},
)
@@ -44,7 +44,7 @@ func TestError_ShouldReturnFormattedErrorCodeAndMessageOnly(t *testing.T) {
err = NewResponseError(
&http.Response{
StatusCode: 400,
- Body: ioutil.NopCloser(bytes.NewReader(jsonBytes)),
+ Body: io.NopCloser(bytes.NewReader(jsonBytes)),
},
)
@@ -64,7 +64,7 @@ func TestError_ShouldReturnFormattedError_ReturnWrappedErrorByDefault(t *testing
func TestError_ShouldReturnFormattedError_ReturnWrappedErrorWhenInvalidJSON(t *testing.T) {
response := &http.Response{
StatusCode: 400,
- Body: ioutil.NopCloser(strings.NewReader("some invalid JSON")),
+ Body: io.NopCloser(strings.NewReader("some invalid JSON")),
}
err := NewResponseError(
response,
@@ -75,7 +75,7 @@ func TestError_ShouldReturnFormattedError_ReturnWrappedErrorWhenInvalidJSON(t *t
errorResponse := err.Response
assert.Equal(t, response, errorResponse)
- body, readErr := ioutil.ReadAll(errorResponse.Body)
+ body, readErr := io.ReadAll(errorResponse.Body)
assert.NilError(t, readErr)
assert.Equal(t, string(body), "some invalid JSON")
@@ -85,7 +85,7 @@ func TestError_ShouldReturnFormattedError_IgnoreUnknownErrorItems(t *testing.T)
jsonString := "{\"message\": \"some message\", \"code\": \"SOME_CODE\", \"error\": [{\"some_key\": \"some value\"}]}"
response := &http.Response{
StatusCode: 400,
- Body: ioutil.NopCloser(strings.NewReader(jsonString)),
+ Body: io.NopCloser(strings.NewReader(jsonString)),
}
err := NewResponseError(
response,
@@ -96,7 +96,7 @@ func TestError_ShouldReturnFormattedError_IgnoreUnknownErrorItems(t *testing.T)
errorResponse := err.Response
assert.Equal(t, response, errorResponse)
- body, readErr := ioutil.ReadAll(errorResponse.Body)
+ body, readErr := io.ReadAll(errorResponse.Body)
assert.NilError(t, readErr)
assert.Equal(t, string(body), jsonString)
@@ -105,7 +105,7 @@ func TestError_ShouldReturnFormattedError_IgnoreUnknownErrorItems(t *testing.T)
func TestError_ShouldReturnCustomErrorForCode(t *testing.T) {
response := &http.Response{
StatusCode: 404,
- Body: ioutil.NopCloser(strings.NewReader("some body")),
+ Body: io.NopCloser(strings.NewReader("some body")),
}
err := NewResponseError(
response,
@@ -118,7 +118,7 @@ func TestError_ShouldReturnCustomErrorForCode(t *testing.T) {
func TestError_ShouldReturnCustomDefaultError(t *testing.T) {
response := &http.Response{
StatusCode: 500,
- Body: ioutil.NopCloser(strings.NewReader("some body")),
+ Body: io.NopCloser(strings.NewReader("some body")),
}
err := NewResponseError(
response,
From 35a17a0ae5cede560a7db1e86eb8e8d34151c9b2 Mon Sep 17 00:00:00 2001
From: mehmet-yoti
Date: Thu, 4 Jan 2024 16:52:51 +0000
Subject: [PATCH 04/11] SDK:2230-Expose-share-v2-api-version-update
---
digitalidentity/requests/signed_message_test.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/digitalidentity/requests/signed_message_test.go b/digitalidentity/requests/signed_message_test.go
index 301be62f..e4cbff58 100644
--- a/digitalidentity/requests/signed_message_test.go
+++ b/digitalidentity/requests/signed_message_test.go
@@ -80,7 +80,7 @@ func TestRequestShouldBuildForValid(t *testing.T) {
assert.Check(t, urlCheck)
assert.Check(t, signed.Header.Get("X-Yoti-Auth-Digest") != "")
assert.Equal(t, signed.Header.Get("X-Yoti-SDK"), "Go")
- assert.Equal(t, signed.Header.Get("X-Yoti-SDK-Version"), "3.9.0")
+ assert.Equal(t, signed.Header.Get("X-Yoti-SDK-Version"), "3.11.0")
}
func TestRequestShouldAddHeaders(t *testing.T) {
From 7dc79f00996d90c19814d4adeb3240b64bdf7d88 Mon Sep 17 00:00:00 2001
From: Iancu Fofiu <34542028+fofiuiancu@users.noreply.github.com>
Date: Mon, 8 Jan 2024 16:42:27 +0000
Subject: [PATCH 05/11] =?UTF-8?q?SDK-2370:=20Added=20support=20for=20Advan?=
=?UTF-8?q?ced=20Identity=20Profiles=20Requirements=20f=E2=80=A6=20(#302)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* SDK-2370: Added support for Advanced Identity Profiles Requirements for Share V2
* SDK-2370: renamed file and fixed spelling
---------
Co-authored-by: System Administrator <>
---
.gitignore | 3 +
.../advanced_identity_profile.go | 70 +++
_examples/digitalidentity/login.html | 4 +-
_examples/digitalidentity/main.go | 27 +-
_examples/digitalidentity/receipt.go | 136 +++++
_examples/digitalidentity/receipt.html | 151 +++++
_examples/idv/main.go | 2 +-
_examples/profile/profile.go | 2 +-
_examples/profile/profile.html | 2 +-
digitalidentity/policy_builder.go | 53 +-
digitalidentity/policy_builder_test.go | 69 +++
.../AdvancedIdentityProfileReport.json | 541 ------------------
12 files changed, 470 insertions(+), 590 deletions(-)
create mode 100644 _examples/digitalidentity/advanced_identity_profile.go
create mode 100644 _examples/digitalidentity/receipt.go
create mode 100644 _examples/digitalidentity/receipt.html
delete mode 100644 test/fixtures/AdvancedIdentityProfileReport.json
diff --git a/.gitignore b/.gitignore
index 842d1127..a1fb98b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,7 @@ report.json
# idea files
.idea
+# DS_Store files
+.DS_Store
+
diff --git a/_examples/digitalidentity/advanced_identity_profile.go b/_examples/digitalidentity/advanced_identity_profile.go
new file mode 100644
index 00000000..86152404
--- /dev/null
+++ b/_examples/digitalidentity/advanced_identity_profile.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/getyoti/yoti-go-sdk/v3/digitalidentity"
+)
+
+var advancedIdentityProfile = []byte(`{
+ "profiles": [
+ {
+ "trust_framework": "YOTI_GLOBAL",
+ "schemes": [
+ {
+ "label": "LB321",
+ "type": "IDENTITY",
+ "objective": "AL_L1"
+ }
+ ]
+ }
+ ]
+ }`)
+
+func buildAdvancedIdentitySessionReq() (sessionSpec *digitalidentity.ShareSessionRequest, err error) {
+ policy, err := (&digitalidentity.PolicyBuilder{}).WithAdvancedIdentityProfileRequirements(advancedIdentityProfile).Build()
+ if err != nil {
+ return nil, fmt.Errorf("failed to build Advanced Identity Requirements policy: %v", err)
+ }
+
+ subject := []byte(`{
+ "subject_id": "unique-user-id-for-examples"
+ }`)
+
+ sessionReq, err := (&digitalidentity.ShareSessionRequestBuilder{}).WithPolicy(policy).WithRedirectUri("https://localhost:8080/v2/receipt-info").WithSubject(subject).Build()
+ if err != nil {
+ return nil, fmt.Errorf("failed to build create session request: %v", err)
+ }
+ return &sessionReq, nil
+}
+
+func generateAdvancedIdentitySession(w http.ResponseWriter, r *http.Request) {
+ didClient, err := initialiseDigitalIdentityClient()
+ if err != nil {
+ fmt.Fprintf(w, "Client could't be generated: %v", err)
+ return
+ }
+
+ sessionReq, err := buildAdvancedIdentitySessionReq()
+ if err != nil {
+ fmt.Fprintf(w, "failed to build session request: %v", err)
+ return
+ }
+
+ shareSession, err := didClient.CreateShareSession(sessionReq)
+ if err != nil {
+ fmt.Fprintf(w, "failed to create share session: %v", err)
+ return
+ }
+
+ output, err := json.Marshal(shareSession)
+ if err != nil {
+ fmt.Fprintf(w, "failed to marshall share session: %v", err)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprintf(w, string(output))
+
+}
diff --git a/_examples/digitalidentity/login.html b/_examples/digitalidentity/login.html
index b3d2729d..c866a122 100644
--- a/_examples/digitalidentity/login.html
+++ b/_examples/digitalidentity/login.html
@@ -52,7 +52,9 @@
+
+
+
+
+ {{ if eq .Prop.Name "document_details" }}
+
+
+ Document Type |
+ {{ .Prop.Value.DocumentType }} |
+
+
+ Issuing Country |
+ {{ .Prop.Value.IssuingCountry }} |
+
+
+ Document Number |
+ {{ .Prop.Value.DocumentNumber }} |
+
+
+ Expiration Date |
+ {{ .Prop.Value.ExpirationDate }} |
+
+
+ Issuing Authority |
+ {{ .Prop.Value.IssuingAuthority }} |
+
+
+ {{ else if eq .Prop.Name "document_images" }}
+ {{ range .Prop.Value }}
+
![]({{ .Base64URL | escapeURL }})
+ {{ end }}
+ {{ else if eq .Prop.Name "structured_postal_address" }}
+
+ {{ range $key, $value := .Prop.Value }}
+
+ {{ $key }} |
+ {{ $value }} |
+
+ {{ end }}
+
+ {{ else if eq .Prop.Name "identity_profile_report" }}
+
+ {{ range $key, $value := .Prop.Value }}
+
+ {{ $key }} |
+ {{ jsonMarshalIndent $value }} |
+
+ {{ end }}
+
+ {{ else }}
+ {{ .Prevalue }}
+ {{ .Prop.Value }}
+ {{ end }}
+
+
+
+
S / V
+
Value
+
Sub type
+ {{ range .Prop.Sources }}
+
Source
+
{{ .Value }}
+
{{ .SubType }}
+ {{ end }}
+ {{ range .Prop.Verifiers }}
+
Verifier
+
{{ .Value }}
+
{{ .SubType }}
+ {{ end }}
+
+
+ {{ end }}
+{{end}}
+
+
+
+
+
+
+
+ Yoti client example
+
+
+
+
+
+
+
+
+
Powered by
+
![Yoti](/static/assets/logo.png)
+
+ {{ if .profile.Selfie }}
+
+
![Yoti]({{ .selfieBase64URL }})
+
+
+ {{ end }}
+
+ {{ if .profile.FullName }}
+
+ {{ .profile.FullName.Value }}
+
+ {{ end }}
+
+
+
+
+
+
+
+
+
+ {{ if .profile.GivenNames }} {{ template "attribute" marshalAttribute "Given names" "yoti-icon-profile" .profile.GivenNames "" }} {{ end }}
+ {{ if .profile.FamilyName }} {{ template "attribute" marshalAttribute "Family names" "yoti-icon-profile" .profile.FamilyName "" }} {{ end }}
+ {{ if .profile.MobileNumber }} {{ template "attribute" marshalAttribute "Mobile number" "yoti-icon-phone" .profile.MobileNumber "" }} {{ end }}
+ {{ if .profile.EmailAddress }} {{ template "attribute" marshalAttribute "Email address" "yoti-icon-email" .profile.EmailAddress "" }} {{ end }}
+ {{ if .profile.DateOfBirth }} {{ template "attribute" marshalAttribute "Date of birth" "yoti-icon-calendar" .profile.DateOfBirth "" }} {{ end }}
+ {{ if .profile.GetAttribute "age_over:18"}} {{ template "attribute" marshalAttribute "Age verified" "yoti-icon-verified" (.profile.GetAttribute "age_over:18") "Age Verification/" }} {{ end }}
+ {{ if .profile.Address }} {{ template "attribute" marshalAttribute "Address" "yoti-icon-address" .profile.Address "" }} {{ end }}
+ {{ if .profile.StructuredPostalAddress }} {{ template "attribute" marshalAttribute "Structured Address" "yoti-icon-address" .profile.StructuredPostalAddress "" }} {{ end }}
+ {{ if .profile.Gender }} {{ template "attribute" marshalAttribute "Gender" "yoti-icon-gender" .profile.Gender "" }} {{ end }}
+ {{ if .profile.DocumentDetails }} {{ template "attribute" marshalAttribute "Document Details" "yoti-icon-document" .profile.DocumentDetails "" }} {{ end }}
+ {{ if .profile.DocumentImages }} {{ template "attribute" marshalAttribute "Document Images" "yoti-icon-profile" .profile.DocumentImages "" }} {{ end }}
+ {{ if .profile.IdentityProfileReport }} {{ template "attribute" marshalAttribute "Identity Profile Report" "yoti-icon-profile" .profile.IdentityProfileReport "" }} {{ end }}
+
+
+
+
+
+
+
diff --git a/_examples/idv/main.go b/_examples/idv/main.go
index 3c2d254a..937fafb3 100644
--- a/_examples/idv/main.go
+++ b/_examples/idv/main.go
@@ -21,7 +21,7 @@ func main() {
router = gin.Default()
router.SetFuncMap(template.FuncMap{
- "jsonMarshallIndent": func(data interface{}) string {
+ "jsonMarshalIndent": func(data interface{}) string {
json, err := json.MarshalIndent(data, "", "\t")
if err != nil {
fmt.Println(err)
diff --git a/_examples/profile/profile.go b/_examples/profile/profile.go
index 15c3f307..113bd327 100644
--- a/_examples/profile/profile.go
+++ b/_examples/profile/profile.go
@@ -103,7 +103,7 @@ func profile(w http.ResponseWriter, r *http.Request) {
prevalue,
}
},
- "jsonMarshallIndent": func(data interface{}) string {
+ "jsonMarshalIndent": func(data interface{}) string {
json, err := json.MarshalIndent(data, "", "\t")
if err != nil {
fmt.Println(err)
diff --git a/_examples/profile/profile.html b/_examples/profile/profile.html
index 8a54cf2f..21ca7d21 100644
--- a/_examples/profile/profile.html
+++ b/_examples/profile/profile.html
@@ -51,7 +51,7 @@
{{ range $key, $value := .Prop.Value }}
{{ $key }} |
- {{ jsonMarshallIndent $value }} |
+ {{ jsonMarshalIndent $value }} |
{{ end }}
diff --git a/digitalidentity/policy_builder.go b/digitalidentity/policy_builder.go
index ef9688bd..4dec515e 100644
--- a/digitalidentity/policy_builder.go
+++ b/digitalidentity/policy_builder.go
@@ -16,19 +16,21 @@ const (
// PolicyBuilder constructs a json payload specifying the dynamic policy
// for a dynamic scenario
type PolicyBuilder struct {
- wantedAttributes map[string]WantedAttribute
- wantedAuthTypes map[int]bool
- isWantedRememberMe bool
- err error
- identityProfileRequirements *json.RawMessage
+ wantedAttributes map[string]WantedAttribute
+ wantedAuthTypes map[int]bool
+ isWantedRememberMe bool
+ err error
+ identityProfileRequirements *json.RawMessage
+ advancedIdentityProfileRequirements *json.RawMessage
}
// Policy represents a dynamic policy for a share
type Policy struct {
- attributes []WantedAttribute
- authTypes []int
- rememberMeID bool
- identityProfileRequirements *json.RawMessage
+ attributes []WantedAttribute
+ authTypes []int
+ rememberMeID bool
+ identityProfileRequirements *json.RawMessage
+ advancedIdentityProfileRequirements *json.RawMessage
}
// WithWantedAttribute adds an attribute from WantedAttributeBuilder to the policy
@@ -206,13 +208,20 @@ func (b *PolicyBuilder) WithIdentityProfileRequirements(identityProfile json.Raw
return b
}
+// WithAdvancedIdentityProfileRequirements adds Advanced Identity Profile Requirements to the policy. Must be valid JSON.
+func (b *PolicyBuilder) WithAdvancedIdentityProfileRequirements(advancedIdentityProfile json.RawMessage) *PolicyBuilder {
+ b.advancedIdentityProfileRequirements = &advancedIdentityProfile
+ return b
+}
+
// Build constructs a dynamic policy object
func (b *PolicyBuilder) Build() (Policy, error) {
return Policy{
- attributes: b.attributesAsList(),
- authTypes: b.authTypesAsList(),
- rememberMeID: b.isWantedRememberMe,
- identityProfileRequirements: b.identityProfileRequirements,
+ attributes: b.attributesAsList(),
+ authTypes: b.authTypesAsList(),
+ rememberMeID: b.isWantedRememberMe,
+ identityProfileRequirements: b.identityProfileRequirements,
+ advancedIdentityProfileRequirements: b.advancedIdentityProfileRequirements,
}, b.err
}
@@ -237,14 +246,16 @@ func (b *PolicyBuilder) authTypesAsList() []int {
// MarshalJSON returns the JSON encoding
func (policy *Policy) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
- Wanted []WantedAttribute `json:"wanted"`
- WantedAuthTypes []int `json:"wanted_auth_types"`
- WantedRememberMe bool `json:"wanted_remember_me"`
- IdentityProfileRequirements *json.RawMessage `json:"identity_profile_requirements,omitempty"`
+ Wanted []WantedAttribute `json:"wanted"`
+ WantedAuthTypes []int `json:"wanted_auth_types"`
+ WantedRememberMe bool `json:"wanted_remember_me"`
+ IdentityProfileRequirements *json.RawMessage `json:"identity_profile_requirements,omitempty"`
+ AdvancedIdentityProfileRequirements *json.RawMessage `json:"advanced_identity_profile_requirements,omitempty"`
}{
- Wanted: policy.attributes,
- WantedAuthTypes: policy.authTypes,
- WantedRememberMe: policy.rememberMeID,
- IdentityProfileRequirements: policy.identityProfileRequirements,
+ Wanted: policy.attributes,
+ WantedAuthTypes: policy.authTypes,
+ WantedRememberMe: policy.rememberMeID,
+ IdentityProfileRequirements: policy.identityProfileRequirements,
+ AdvancedIdentityProfileRequirements: policy.advancedIdentityProfileRequirements,
})
}
diff --git a/digitalidentity/policy_builder_test.go b/digitalidentity/policy_builder_test.go
index e2bc075a..a7ae6e1a 100644
--- a/digitalidentity/policy_builder_test.go
+++ b/digitalidentity/policy_builder_test.go
@@ -437,3 +437,72 @@ func TestDigitalIdentityBuilder_WithIdentityProfileRequirements_ShouldFailForInv
t.Errorf("wanted err to be of type '%v', got: '%v'", reflect.TypeOf(marshallerErr), reflect.TypeOf(err))
}
}
+
+func ExamplePolicyBuilder_WithAdvancedIdentityProfileRequirements() {
+ advancedIdentityProfile := []byte(`{
+ "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": {}
+ }
+ ]
+ }
+ ]
+ }`)
+
+ policy, err := (&PolicyBuilder{}).WithAdvancedIdentityProfileRequirements(advancedIdentityProfile).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ data, err := policy.MarshalJSON()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ fmt.Println(string(data))
+ // Output: {"wanted":[],"wanted_auth_types":[],"wanted_remember_me":false,"advanced_identity_profile_requirements":{"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":{}}]}]}}
+}
+
+func TestPolicyBuilder_WithAdvancedIdentityProfileRequirements_ShouldFailForInvalidJSON(t *testing.T) {
+ advancedIdentityProfile := []byte(`{
+ "trust_framework": UK_TFIDA",
+ ,
+ }`)
+
+ policy, err := (&PolicyBuilder{}).WithAdvancedIdentityProfileRequirements(advancedIdentityProfile).Build()
+ if err != nil {
+ fmt.Printf("error: %s", err.Error())
+ return
+ }
+
+ _, err = policy.MarshalJSON()
+ if err == nil {
+ t.Error("expected an error")
+ }
+ var marshallerErr *json.MarshalerError
+ if !errors.As(err, &marshallerErr) {
+ t.Errorf("wanted err to be of type '%v', got: '%v'", reflect.TypeOf(marshallerErr), reflect.TypeOf(err))
+ }
+}
diff --git a/test/fixtures/AdvancedIdentityProfileReport.json b/test/fixtures/AdvancedIdentityProfileReport.json
deleted file mode 100644
index 160eeacc..00000000
--- a/test/fixtures/AdvancedIdentityProfileReport.json
+++ /dev/null
@@ -1,541 +0,0 @@
-{
- "identity_assertion": {
- "current_name": {
- "given_names": "LAURENCE GUY",
- "first_name": "LAURENCE",
- "middle_name": "GUY",
- "family_name": "WITHERS",
- "full_name": "LAURENCE GUY WITHERS"
- },
- "date_of_birth": "1981-10-05",
- "current_address": {
- "address": {
- "address_format": 1,
- "care_of": "",
- "sub_building": "",
- "building_number": "25",
- "building": "",
- "street": "25 Test Street",
- "landmark": "",
- "address_line1": "25 Test Street",
- "address_line2": "London",
- "address_line3": "EC3M 5LY",
- "address_line4": "",
- "address_line5": "",
- "address_line6": "",
- "locality": "",
- "town_city": "London",
- "subdistrict": "",
- "district": "",
- "state": "",
- "postal_code": "EC3M 5LY",
- "post_office": "",
- "country_iso": "GBR",
- "country": "United Kingdom",
- "formatted_address": "25 Test Street\\nLondon\\nEC3M 5LY\\nLondon\\nEC3M 5LY\\nUnited Kingdom",
- "udprn": ""
- },
- "move_in": ""
- }
- },
- "verification_report": {
- "report_id": "7fd51c9f-4131-4665-b44d-59f8aa888003",
- "timestamp": "2023-10-04T11:31:15Z",
- "subject_id": "ITEST",
- "address_verification": {
- "current_address_verified": true,
- "evidence_links": [
- "5df924ad-904e-4a88-9d34-889599d29795"
- ]
- },
- "trust_framework": "UK_TFIDA",
- "schemes_compliance": [
- {
- "scheme": {
- "type": "DBS",
- "objective": "STANDARD",
- "label": "",
- "config": null
- },
- "requirements_met": true,
- "requirements_not_met_info": "",
- "requirements_not_met_details": []
- }
- ],
- "assurance_process": {
- "level_of_assurance": "HIGH",
- "policy": "GPG45",
- "procedure": "H1A",
- "assurance": [
- {
- "type": "EVIDENCE_STRENGTH",
- "classification": "4",
- "evidence_links": [
- "0c3e309e-0b14-4207-8190-109c7cf7cb78"
- ]
- },
- {
- "type": "EVIDENCE_VALIDITY",
- "classification": "3",
- "evidence_links": [
- "0c3e309e-0b14-4207-8190-109c7cf7cb78"
- ]
- },
- {
- "type": "IDENTITY_FRAUD",
- "classification": "1",
- "evidence_links": [
- "22542183-8363-41bf-af6e-c5e099eb26d3"
- ]
- },
- {
- "type": "VERIFICATION",
- "classification": "3",
- "evidence_links": [
- "0c3e309e-0b14-4207-8190-109c7cf7cb78",
- "2bc147ed-e23e-4df1-9dec-61295e04770a"
- ]
- }
- ]
- },
- "evidence": {
- "face": {
- "evidence_id": "2bc147ed-e23e-4df1-9dec-61295e04770a",
- "initial_liveness": {
- "type": "ZOOM",
- "timestamp": "2023-10-04T11:31:15Z"
- },
- "last_matched_liveness": {
- "type": "ZOOM",
- "timestamp": "2023-10-04T11:31:15Z"
- },
- "verifying_org": "",
- "resource_ids": [],
- "check_ids": [],
- "user_activity_ids": [
- "DID_TRACKING_ID"
- ],
- "selfie_attribute_id": ""
- },
- "documents": [
- {
- "evidence_id": "0c3e309e-0b14-4207-8190-109c7cf7cb78",
- "timestamp": "2023-10-04T11:31:15Z",
- "document_fields": {
- "full_name": "LAURENCE GUY WITHERS",
- "date_of_birth": "1981-10-05",
- "nationality": "GBR",
- "given_names": "LAURENCE GUY",
- "first_name": "",
- "middle_name": "",
- "family_name": "WITHERS",
- "place_of_birth": "",
- "country_of_birth": "",
- "gender": "MALE",
- "name_prefix": "",
- "name_suffix": "",
- "first_name_alias": "",
- "middle_name_alias": "",
- "family_name_alias": "",
- "weight": "",
- "height": "",
- "eye_color": "",
- "structured_postal_address": null,
- "document_type": "PASSPORT",
- "issuing_country": "GBR",
- "document_number": "546697970",
- "expiration_date": "2027-05-06",
- "date_of_issue": "2017-03-06",
- "issuing_authority": "HMPO",
- "mrz": {
- "type": 2,
- "line1": "P",
- "authentication_report": {
- "report_id": "68a952f2-d675-405c-a4fa-adea7424414b",
- "timestamp": "2023-10-04T11:31:15Z",
- "level": "HIGH",
- "policy": "GPG44",
- "trust_framework": "UK_TFIDA"
- },
- "profile_match_report": null,
- "verification_reports": [
- {
- "report_id": "7fd51c9f-4131-4665-b44d-59f8aa888003",
- "timestamp": "2023-10-04T11:31:15Z",
- "subject_id": "ITEST",
- "address_verification": {
- "current_address_verified": true,
- "evidence_links": [
- "5df924ad-904e-4a88-9d34-889599d29795"
- ]
- },
- "trust_framework": "UK_TFIDA",
- "schemes_compliance": [
- {
- "scheme": {
- "type": "DBS",
- "objective": "STANDARD",
- "label": "",
- "config": null
- },
- "requirements_met": true,
- "requirements_not_met_info": "",
- "requirements_not_met_details": []
- }
- ],
- "assurance_process": {
- "level_of_assurance": "HIGH",
- "policy": "GPG45",
- "procedure": "H1A",
- "assurance": [
- {
- "type": "EVIDENCE_STRENGTH",
- "classification": "4",
- "evidence_links": [
- "0c3e309e-0b14-4207-8190-109c7cf7cb78"
- ]
- },
- {
- "type": "EVIDENCE_VALIDITY",
- "classification": "3",
- "evidence_links": [
- "0c3e309e-0b14-4207-8190-109c7cf7cb78"
- ]
- },
- {
- "type": "IDENTITY_FRAUD",
- "classification": "1",
- "evidence_links": [
- "22542183-8363-41bf-af6e-c5e099eb26d3"
- ]
- },
- {
- "type": "VERIFICATION",
- "classification": "3",
- "evidence_links": [
- "0c3e309e-0b14-4207-8190-109c7cf7cb78",
- "2bc147ed-e23e-4df1-9dec-61295e04770a"
- ]
- }
- ]
- },
- "evidence": {
- "face": {
- "evidence_id": "2bc147ed-e23e-4df1-9dec-61295e04770a",
- "initial_liveness": {
- "type": "ZOOM",
- "timestamp": "2023-10-04T11:31:15Z"
- },
- "last_matched_liveness": {
- "type": "ZOOM",
- "timestamp": "2023-10-04T11:31:15Z"
- },
- "verifying_org": "",
- "resource_ids": [],
- "check_ids": [],
- "user_activity_ids": [
- "DID_TRACKING_ID"
- ],
- "selfie_attribute_id": ""
- },
- "documents": [
- {
- "evidence_id": "0c3e309e-0b14-4207-8190-109c7cf7cb78",
- "timestamp": "2023-10-04T11:31:15Z",
- "document_fields": {
- "full_name": "LAURENCE GUY WITHERS",
- "date_of_birth": "1981-10-05",
- "nationality": "GBR",
- "given_names": "LAURENCE GUY",
- "first_name": "",
- "middle_name": "",
- "family_name": "WITHERS",
- "place_of_birth": "",
- "country_of_birth": "",
- "gender": "MALE",
- "name_prefix": "",
- "name_suffix": "",
- "first_name_alias": "",
- "middle_name_alias": "",
- "family_name_alias": "",
- "weight": "",
- "height": "",
- "eye_color": "",
- "structured_postal_address": null,
- "document_type": "PASSPORT",
- "issuing_country": "GBR",
- "document_number": "546697970",
- "expiration_date": "2027-05-06",
- "date_of_issue": "2017-03-06",
- "issuing_authority": "HMPO",
- "mrz": {
- "type": 2,
- "line1": "P
Date: Tue, 9 Jan 2024 09:18:56 +0300
Subject: [PATCH 06/11] SDK-2422 fixed code smells,updated version for release
---
digitalidentity/service.go | 35 ++++++++++++++++++---------------
requests/signed_message_test.go | 2 +-
2 files changed, 20 insertions(+), 17 deletions(-)
diff --git a/digitalidentity/service.go b/digitalidentity/service.go
index 444a6560..7215dcb1 100644
--- a/digitalidentity/service.go
+++ b/digitalidentity/service.go
@@ -20,6 +20,9 @@ const identitySessionQrCodeCreation = "/v2/sessions/%s/qr-codes"
const identitySessionQrCodeRetrieval = "/v2/qr-codes/%s"
const identitySessionReceiptRetrieval = "/v2/receipts/%s"
const identitySessionReceiptKeyRetrieval = "/v2/wrapped-item-keys/%s"
+const errorFailedToGetSignedReceipt = "failed to get signed request: %v"
+const errorFailedToExecuteRequest = "failed to execute request: %v"
+const errorFailedToReadBody = "failed to read response body: %v"
// CreateShareSession creates session using the supplied session specification
func CreateShareSession(httpClient requests.HttpClient, shareSessionRequest *ShareSessionRequest, clientSdkId, apiUrl string, key *rsa.PrivateKey) (*ShareSession, error) {
@@ -40,12 +43,12 @@ func CreateShareSession(httpClient requests.HttpClient, shareSessionRequest *Sha
Params: map[string]string{"sdkID": clientSdkId},
}.Request()
if err != nil {
- return nil, fmt.Errorf("failed to get signed request: %v", err)
+ return nil, fmt.Errorf(errorFailedToGetSignedReceipt, err)
}
response, err := requests.Execute(httpClient, request)
if err != nil {
- return nil, fmt.Errorf("failed to execute request: %v", err)
+ return nil, fmt.Errorf(errorFailedToExecuteRequest, err)
}
defer response.Body.Close()
@@ -53,7 +56,7 @@ func CreateShareSession(httpClient requests.HttpClient, shareSessionRequest *Sha
responseBytes, err := io.ReadAll(response.Body)
if err != nil {
- return nil, fmt.Errorf("failed to read response body: %v", err)
+ return nil, fmt.Errorf(errorFailedToReadBody, err)
}
err = json.Unmarshal(responseBytes, shareSession)
return shareSession, err
@@ -72,19 +75,19 @@ func GetShareSession(httpClient requests.HttpClient, sessionID string, clientSdk
Params: map[string]string{"sdkID": clientSdkId},
}.Request()
if err != nil {
- return nil, fmt.Errorf("failed to get signed request: %v", err)
+ return nil, fmt.Errorf(errorFailedToGetSignedReceipt, err)
}
response, err := requests.Execute(httpClient, request)
if err != nil {
- return nil, fmt.Errorf("failed to execute request: %v", err)
+ return nil, fmt.Errorf(errorFailedToExecuteRequest, err)
}
defer response.Body.Close()
shareSession := &ShareSession{}
responseBytes, err := io.ReadAll(response.Body)
if err != nil {
- return nil, fmt.Errorf("failed to read response body: %v", err)
+ return nil, fmt.Errorf(errorFailedToReadBody, err)
}
err = json.Unmarshal(responseBytes, shareSession)
return shareSession, err
@@ -104,19 +107,19 @@ func CreateShareQrCode(httpClient requests.HttpClient, sessionID string, clientS
Params: map[string]string{"sdkID": clientSdkId},
}.Request()
if err != nil {
- return nil, fmt.Errorf("failed to get signed request: %v", err)
+ return nil, fmt.Errorf(errorFailedToGetSignedReceipt, err)
}
response, err := requests.Execute(httpClient, request)
if err != nil {
- return nil, fmt.Errorf("failed to execute request: %v", err)
+ return nil, fmt.Errorf(errorFailedToExecuteRequest, err)
}
defer response.Body.Close()
qrCode := &QrCode{}
responseBytes, err := io.ReadAll(response.Body)
if err != nil {
- return nil, fmt.Errorf("failed to read response body: %v", err)
+ return nil, fmt.Errorf(errorFailedToReadBody, err)
}
err = json.Unmarshal(responseBytes, qrCode)
return qrCode, err
@@ -134,18 +137,18 @@ func GetShareSessionQrCode(httpClient requests.HttpClient, qrCodeId string, clie
Headers: headers,
}.Request()
if err != nil {
- return fetchedQrCode, fmt.Errorf("failed to get signed request: %v", err)
+ return fetchedQrCode, fmt.Errorf(errorFailedToGetSignedReceipt, err)
}
response, err := requests.Execute(httpClient, request)
if err != nil {
- return fetchedQrCode, fmt.Errorf("failed to execute request: %v", err)
+ return fetchedQrCode, fmt.Errorf(errorFailedToExecuteRequest, err)
}
defer response.Body.Close()
responseBytes, err := io.ReadAll(response.Body)
if err != nil {
- return fetchedQrCode, fmt.Errorf("failed to read response body: %v", err)
+ return fetchedQrCode, fmt.Errorf(errorFailedToReadBody, err)
}
err = json.Unmarshal(responseBytes, &fetchedQrCode)
@@ -167,18 +170,18 @@ func getReceipt(httpClient requests.HttpClient, receiptId string, clientSdkId, a
Headers: headers,
}.Request()
if err != nil {
- return receipt, fmt.Errorf("failed to get signed request: %v", err)
+ return receipt, fmt.Errorf(errorFailedToGetSignedReceipt, err)
}
response, err := requests.Execute(httpClient, request)
if err != nil {
- return receipt, fmt.Errorf("failed to execute request: %v", err)
+ return receipt, fmt.Errorf(errorFailedToExecuteRequest, err)
}
defer response.Body.Close()
responseBytes, err := io.ReadAll(response.Body)
if err != nil {
- return receipt, fmt.Errorf("failed to read response body: %v", err)
+ return receipt, fmt.Errorf(errorFailedToReadBody, err)
}
err = json.Unmarshal(responseBytes, &receipt)
@@ -198,7 +201,7 @@ func getReceiptItemKey(httpClient requests.HttpClient, receiptItemKeyId string,
Headers: headers,
}.Request()
if err != nil {
- return receiptItemKey, fmt.Errorf("failed to get signed request: %v", err)
+ return receiptItemKey, fmt.Errorf(errorFailedToGetSignedReceipt, err)
}
response, err := requests.Execute(httpClient, request)
diff --git a/requests/signed_message_test.go b/requests/signed_message_test.go
index e4cbff58..9fbc0e23 100644
--- a/requests/signed_message_test.go
+++ b/requests/signed_message_test.go
@@ -80,7 +80,7 @@ func TestRequestShouldBuildForValid(t *testing.T) {
assert.Check(t, urlCheck)
assert.Check(t, signed.Header.Get("X-Yoti-Auth-Digest") != "")
assert.Equal(t, signed.Header.Get("X-Yoti-SDK"), "Go")
- assert.Equal(t, signed.Header.Get("X-Yoti-SDK-Version"), "3.11.0")
+ assert.Equal(t, signed.Header.Get("X-Yoti-SDK-Version"), "3.12.0")
}
func TestRequestShouldAddHeaders(t *testing.T) {
From 90f43ba7964cfc7727d1f3f0d8f05be865f55a21 Mon Sep 17 00:00:00 2001
From: mehmet-yoti
Date: Tue, 9 Jan 2024 09:20:50 +0300
Subject: [PATCH 07/11] SDK-2422 fixed code smells,updated version for release
---
README.md | 2 +-
consts/version.go | 2 +-
digitalidentity/requests/signed_message_test.go | 2 +-
sonar-project.properties | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 8fbea9a9..eb8392dd 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ import "github.com/getyoti/yoti-go-sdk/v3"
or add the following line to your go.mod file (check https://github.com/getyoti/yoti-go-sdk/releases for the latest version)
```
-require github.com/getyoti/yoti-go-sdk/v3 v3.11.0
+require github.com/getyoti/yoti-go-sdk/v3 v3.12.0
```
## Setup
diff --git a/consts/version.go b/consts/version.go
index 553971b5..50085a5e 100644
--- a/consts/version.go
+++ b/consts/version.go
@@ -2,5 +2,5 @@ package consts
const (
SDKIdentifier = "Go"
- SDKVersionIdentifier = "3.11.0"
+ SDKVersionIdentifier = "3.12.0"
)
diff --git a/digitalidentity/requests/signed_message_test.go b/digitalidentity/requests/signed_message_test.go
index e4cbff58..9fbc0e23 100644
--- a/digitalidentity/requests/signed_message_test.go
+++ b/digitalidentity/requests/signed_message_test.go
@@ -80,7 +80,7 @@ func TestRequestShouldBuildForValid(t *testing.T) {
assert.Check(t, urlCheck)
assert.Check(t, signed.Header.Get("X-Yoti-Auth-Digest") != "")
assert.Equal(t, signed.Header.Get("X-Yoti-SDK"), "Go")
- assert.Equal(t, signed.Header.Get("X-Yoti-SDK-Version"), "3.11.0")
+ assert.Equal(t, signed.Header.Get("X-Yoti-SDK-Version"), "3.12.0")
}
func TestRequestShouldAddHeaders(t *testing.T) {
diff --git a/sonar-project.properties b/sonar-project.properties
index 39857580..a64c85b4 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -1,7 +1,7 @@
sonar.organization = getyoti
sonar.projectKey = getyoti:go
sonar.projectName = Go SDK
-sonar.projectVersion = 3.11.0
+sonar.projectVersion = 3.12.0
sonar.exclusions = **/yotiprotoattr/*.go,**/yotiprotocom/*.go,**/yotiprotoshare/*.go,**/**_test.go,_examples/**/*
sonar.links.scm = https://github.com/getyoti/yoti-go-sdk
sonar.host.url = https://sonarcloud.io
From 90be4ef33389357300f4078c395a37edee1d6112 Mon Sep 17 00:00:00 2001
From: mehmet-yoti
Date: Tue, 9 Jan 2024 09:52:39 +0300
Subject: [PATCH 08/11] SDK-2422 added test
---
requests/signed_message_test.go | 32 ++++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/requests/signed_message_test.go b/requests/signed_message_test.go
index 9fbc0e23..f23dae2a 100644
--- a/requests/signed_message_test.go
+++ b/requests/signed_message_test.go
@@ -167,3 +167,35 @@ func TestSignedRequest_WithPemFile_NotRSAKeyShouldError(t *testing.T) {
-----END RSA PUBLIC KEY-----`))
assert.ErrorContains(t, msg.Error, "not an RSA Private Key")
}
+
+// TestAuthHeader tests the AuthHeader function.
+func TestAuthHeader(t *testing.T) {
+ tests := []struct {
+ name string
+ clientSdkId string
+ expected map[string][]string
+ }{
+ {
+ name: "valid client SDK ID",
+ clientSdkId: "testSdkId",
+ expected: map[string][]string{"X-Yoti-Auth-Id": {"testSdkId"}},
+ },
+ {
+ name: "empty client SDK ID",
+ clientSdkId: "",
+ expected: map[string][]string{"X-Yoti-Auth-Id": {""}},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+
+ got := AuthHeader(tt.clientSdkId, &privateKey.PublicKey)
+
+ for key, value := range tt.expected {
+ if gotValue, exists := got[key]; !exists || len(gotValue) != 1 || gotValue[0] != value[0] {
+ t.Errorf("AuthHeader() = %v, want %v", got, tt.expected)
+ }
+ }
+ })
+ }
+}
From a0deee2e9812d5aa2a52aeccb7bf8a9f6e852bfa Mon Sep 17 00:00:00 2001
From: mehmet-yoti
Date: Tue, 9 Jan 2024 13:28:22 +0300
Subject: [PATCH 09/11] SDK-2422 removed unused function and its test
---
digitalidentity/service.go | 2 +-
requests/signed_message.go | 7 -------
requests/signed_message_test.go | 32 --------------------------------
3 files changed, 1 insertion(+), 40 deletions(-)
diff --git a/digitalidentity/service.go b/digitalidentity/service.go
index 7215dcb1..243c06e3 100644
--- a/digitalidentity/service.go
+++ b/digitalidentity/service.go
@@ -20,7 +20,7 @@ const identitySessionQrCodeCreation = "/v2/sessions/%s/qr-codes"
const identitySessionQrCodeRetrieval = "/v2/qr-codes/%s"
const identitySessionReceiptRetrieval = "/v2/receipts/%s"
const identitySessionReceiptKeyRetrieval = "/v2/wrapped-item-keys/%s"
-const errorFailedToGetSignedReceipt = "failed to get signed request: %v"
+const errorFailedToGetSignedRequest = "failed to get signed request: %v"
const errorFailedToExecuteRequest = "failed to execute request: %v"
const errorFailedToReadBody = "failed to read response body: %v"
diff --git a/requests/signed_message.go b/requests/signed_message.go
index 8a1715f2..e90fc54f 100644
--- a/requests/signed_message.go
+++ b/requests/signed_message.go
@@ -41,13 +41,6 @@ func JSONHeaders() map[string][]string {
}
}
-// AuthHeader is a header prototype including the App/SDK ID
-func AuthHeader(clientSdkId string, key *rsa.PublicKey) map[string][]string {
- return map[string][]string{
- "X-Yoti-Auth-Id": {clientSdkId},
- }
-}
-
// AuthKeyHeader is a header prototype including an encoded RSA PublicKey
func AuthKeyHeader(key *rsa.PublicKey) map[string][]string {
return map[string][]string{
diff --git a/requests/signed_message_test.go b/requests/signed_message_test.go
index f23dae2a..9fbc0e23 100644
--- a/requests/signed_message_test.go
+++ b/requests/signed_message_test.go
@@ -167,35 +167,3 @@ func TestSignedRequest_WithPemFile_NotRSAKeyShouldError(t *testing.T) {
-----END RSA PUBLIC KEY-----`))
assert.ErrorContains(t, msg.Error, "not an RSA Private Key")
}
-
-// TestAuthHeader tests the AuthHeader function.
-func TestAuthHeader(t *testing.T) {
- tests := []struct {
- name string
- clientSdkId string
- expected map[string][]string
- }{
- {
- name: "valid client SDK ID",
- clientSdkId: "testSdkId",
- expected: map[string][]string{"X-Yoti-Auth-Id": {"testSdkId"}},
- },
- {
- name: "empty client SDK ID",
- clientSdkId: "",
- expected: map[string][]string{"X-Yoti-Auth-Id": {""}},
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
-
- got := AuthHeader(tt.clientSdkId, &privateKey.PublicKey)
-
- for key, value := range tt.expected {
- if gotValue, exists := got[key]; !exists || len(gotValue) != 1 || gotValue[0] != value[0] {
- t.Errorf("AuthHeader() = %v, want %v", got, tt.expected)
- }
- }
- })
- }
-}
From f3a1654e5dfb8d616bbb52759575696db8f1ea07 Mon Sep 17 00:00:00 2001
From: mehmet-yoti
Date: Tue, 9 Jan 2024 13:31:01 +0300
Subject: [PATCH 10/11] SDK-2422 removed unused function and its test
---
digitalidentity/service.go | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/digitalidentity/service.go b/digitalidentity/service.go
index 243c06e3..3cac3933 100644
--- a/digitalidentity/service.go
+++ b/digitalidentity/service.go
@@ -43,7 +43,7 @@ func CreateShareSession(httpClient requests.HttpClient, shareSessionRequest *Sha
Params: map[string]string{"sdkID": clientSdkId},
}.Request()
if err != nil {
- return nil, fmt.Errorf(errorFailedToGetSignedReceipt, err)
+ return nil, fmt.Errorf(errorFailedToGetSignedRequest, err)
}
response, err := requests.Execute(httpClient, request)
@@ -75,7 +75,7 @@ func GetShareSession(httpClient requests.HttpClient, sessionID string, clientSdk
Params: map[string]string{"sdkID": clientSdkId},
}.Request()
if err != nil {
- return nil, fmt.Errorf(errorFailedToGetSignedReceipt, err)
+ return nil, fmt.Errorf(errorFailedToGetSignedRequest, err)
}
response, err := requests.Execute(httpClient, request)
@@ -107,7 +107,7 @@ func CreateShareQrCode(httpClient requests.HttpClient, sessionID string, clientS
Params: map[string]string{"sdkID": clientSdkId},
}.Request()
if err != nil {
- return nil, fmt.Errorf(errorFailedToGetSignedReceipt, err)
+ return nil, fmt.Errorf(errorFailedToGetSignedRequest, err)
}
response, err := requests.Execute(httpClient, request)
@@ -137,7 +137,7 @@ func GetShareSessionQrCode(httpClient requests.HttpClient, qrCodeId string, clie
Headers: headers,
}.Request()
if err != nil {
- return fetchedQrCode, fmt.Errorf(errorFailedToGetSignedReceipt, err)
+ return fetchedQrCode, fmt.Errorf(errorFailedToGetSignedRequest, err)
}
response, err := requests.Execute(httpClient, request)
@@ -170,7 +170,7 @@ func getReceipt(httpClient requests.HttpClient, receiptId string, clientSdkId, a
Headers: headers,
}.Request()
if err != nil {
- return receipt, fmt.Errorf(errorFailedToGetSignedReceipt, err)
+ return receipt, fmt.Errorf(errorFailedToGetSignedRequest, err)
}
response, err := requests.Execute(httpClient, request)
@@ -201,7 +201,7 @@ func getReceiptItemKey(httpClient requests.HttpClient, receiptItemKeyId string,
Headers: headers,
}.Request()
if err != nil {
- return receiptItemKey, fmt.Errorf(errorFailedToGetSignedReceipt, err)
+ return receiptItemKey, fmt.Errorf(errorFailedToGetSignedRequest, err)
}
response, err := requests.Execute(httpClient, request)
From 709b992dd876ba86ea6fd2669fed8a32e5264959 Mon Sep 17 00:00:00 2001
From: Mehmet Ali Sepici
Date: Tue, 27 Feb 2024 02:18:02 +0300
Subject: [PATCH 11/11] SDK-2422 resolved selfie image problem
---
_examples/digitalidentity/receipt.go | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/_examples/digitalidentity/receipt.go b/_examples/digitalidentity/receipt.go
index 63fb4e6e..77063760 100644
--- a/_examples/digitalidentity/receipt.go
+++ b/_examples/digitalidentity/receipt.go
@@ -30,13 +30,10 @@ func receipt(w http.ResponseWriter, r *http.Request) {
userProfile := receiptValue.UserContent.UserProfile
selfie := userProfile.Selfie()
+
var base64URL string
if selfie != nil {
base64URL = selfie.Value().Base64URL()
-
- decodedImage := decodeImage(selfie.Value().Data())
- file := createImage()
- saveImage(decodedImage, file)
}
dob, err := userProfile.DateOfBirth()