From 3b5da7bb3d5be40f804d46640a4ff938ac914068 Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Tue, 21 Feb 2023 21:33:44 +0100 Subject: [PATCH 01/20] docs, deployment --- docs/deployment.md | 30 +++++++++++++----------------- docs/reverse-proxy.md | 10 +++++----- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index f231daa21..1b9b9e325 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -47,11 +47,9 @@ The solution is to delete (purge) the old Key Vault, which will release the name ## Upload risk passwords -You can read the number of risk passwords uploaded to FoxIDs in [FoxIDs Control Client](control.md#foxids-control-client) master tenant on the Risk Passwords tap. And you can test if a password is okay or has appeared in breaches. - -You can upload risk passwords with the FoxIDs seed tool. The seed tool is a console application. +You can increment the password security level by uploading risk passwords. -> The seed tool code can be [downloaded](https://github.com/ITfoxtec/FoxIDs/tree/master/tools/FoxIDs.SeedTool) and need to be compiled to run. +You can upload risk passwords with the FoxIDs seed tool console application. The seed tool code is [downloaded](https://github.com/ITfoxtec/FoxIDs/tree/master/tools/FoxIDs.SeedTool) and need to be compiled and [configured](#configure-the-seed-tool) to run. Download the `SHA-1` pwned passwords `ordered by prevalence` from [haveibeenpwned.com/passwords](https://haveibeenpwned.com/Passwords). @@ -60,6 +58,8 @@ Download the `SHA-1` pwned passwords `ordered by prevalence` from [haveibeenpwne The risk passwords are uploaded as bulk which has a higher consumption. Please make sure to adjust the Cosmos DB provisioned throughput (e.g. to 20000 RU/s or higher) temporarily. The throughput can be adjusted in Azure Cosmos DB --> Data Explorer --> Scale & Settings. +You can read the number of risk passwords uploaded to FoxIDs in [FoxIDs Control Client](control.md#foxids-control-client) master tenant on the Risk Passwords tap. And you can test if a password is okay or has appeared in breaches. + ### Configure the seed tool The seed tool is configured in the `appsettings.json` file. @@ -74,9 +74,9 @@ Create a seed tool OAuth 2.0 client in the [FoxIDs Control Client](control.md#fo 4. Remember the client secret. 5. In the resource and scopes section. Grant the sample seed client access to the FoxIDs Control API resource `foxids_control_api` with the scope `foxids:master`. 6. Click show advanced settings. -7. In the issue claims section. Add a claim with the name `role` and the value `foxids:tenant.admin`. This will granted the client the administrator role. +7. In the issue claims section. Add a claim with the name `role` and the value `foxids:tenant.admin`. This will grant the client the administrator role. -The seed tool client is thereby granted access to update to the master tenant. +The seed tool client is thereby granted access to update the master tenant. ![FoxIDs Control Client - seed tool client](images/upload-risk-passwords-seed-client.png) @@ -105,20 +105,20 @@ It is possible to run the sample applications after they are configured in a Fox ## Custom primary domains -The FoxIDs service and FoxIDs Control sites primary domains can be customized. +The FoxIDs service and FoxIDs Control sites primary domains can be customized. The new primary custom domains can be configured on the App Services or by using a [reverse proxy](reverse-proxy.md) > Important: change the primary domain before adding tenants. -- FoxIDs service default domain is `https://foxidsxxxx.azurewebsites.net` which can be changed to a custom primary domain like e.g., `https://somedomain.com` or `https://auth.somedomain.com` -- FoxIDs Control default domain is `https://foxidscontrolxxxx.azurewebsites.net` which can be changed to a custom primary domain like e.g., `https://control.somedomain.com` or `https://foxidscontrol.somedomain.com` +- FoxIDs service default domain is `https://foxidsxxxx.azurewebsites.net` which can be changed to a custom primary domain like e.g., `https://somedomain.com` or `https://id.somedomain.com` +- FoxIDs Control default domain is `https://foxidscontrolxxxx.azurewebsites.net` which can be changed to a custom primary domain like e.g., `https://control.somedomain.com` or `https://idcontrol.somedomain.com` The FoxIDs site support one primary domain and multiple [custom domains](custom-domain.md) which are connected to tenants, where the FoxIDs Control site only support one primary domain. Configure new primary custom domains: -1) Login to [FoxIDs Control Client](control.md#foxids-control-client) using the default/old primary domain. Select the `Parties` tab and under `Down-parties` select click `OpenID Connect - foxids_control_client` and click `Show advanced settings`. +1) Login to [FoxIDs Control Client](control.md#foxids-control-client) using the default/old primary domain. Select the `Parties` tab and `Down-parties` tap then click `OpenID Connect - foxids_control_client` and click `Show advanced settings`. - - Add the FoxIDs Control sites new primary custom domain to the `Allow CORS origins` list without a trailing slash. + - Add the FoxIDs Control sites new primary custom domain URL to the `Allow CORS origins` list without a trailing slash. - Add the FoxIDs Control Client sites new primary custom domain login and logout redirect URIs to the `Redirect URIs` list including the trailing `/master/authentication/login_callback` and `/master/authentication/logout_callback`. > If you have added tenants before changing the primary domain, the `OpenID Connect - foxids_control_client` configuration have to be done in each tenant. @@ -129,16 +129,12 @@ Depending on the reverse proxy your are using you might be required to also conf - If configured on App Services: add the custom primary domains in Azure portal on the FoxIDs App Service and the FoxIDs Control App Service production slot under the `Custom domains` tab by clicking the `Add custom domain` link. - If configured on reverse proxy: the custom primary domains are exposed through the [reverse proxy](reverse-proxy.md). -3) Then configure the FoxIDs service sites new primary custom domains in the FoxIDs App Service under the `Configuration` tab and `Applications settings` sub tab: - - - The setting `Settings:FoxIDsEndpoint` is changed to the FoxIDs service sites new primary custom domain. - -4) And configure the FoxIDs service and FoxIDs Control sites new primary custom domains in the FoxIDs Control App Service under the `Configuration` tab and `Applications settings` sub tab: +3) Then configure the FoxIDs service and FoxIDs Control sites new primary custom domains in the FoxIDs Control App Service under the `Configuration` tab and `Applications settings` sub tab: - The setting `Settings:FoxIDsEndpoint` is changed to the FoxIDs service sites new primary custom domain. - The setting `Settings:FoxIDsControlEndpoint` is changed to the FoxIDs Control sites new primary custom domain. -> You can create a `main` tenant and add the custom primary domain used on the FoxIDs service as a [custom domain](custom-domain.md) to remove the tenant element from the URL. +> Yo can achieve a shorter and prettier URL where the tenant element is removed from the URL. By creating a `main` tenant where the custom primary domain used on the FoxIDs service is set as a [custom domain](custom-domain.md). ## Reverse proxy It is recommended to place both the FoxIDs Azure App service and the FoxIDs Control Azure App service behind a [reverse proxy](reverse-proxy.md). diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index 86386851f..c56008bf4 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -1,12 +1,12 @@ # Reverse proxy It is recommended to place both the FoxIDs Azure App service and the FoxIDs Control Azure App service behind a reverse proxy. +The [custom primary domains](deployment.md#custom-primary-domains) is exposed through the reverse proxy alongside optionally [custom domains](custom-domain.md). + The FoxIDs service support [custom domains](custom-domain.md) which is handled with domain rewrite through the reverse proxy. > FoxIDs only support [custom domains](custom-domain.md) if it is behind a reverse proxy and the access is restricted by the `X-FoxIDs-Secret` HTTP header or the `Settings:TrustProxyHeaders` setting is set to `true` in the FoxIDs App Service configuration. -The [custom primary domains](deployment.md#custom-primary-domains) is exposed through the reverse proxy alongside optionally [custom domains](custom-domain.md). - ## Restrict access Both the FoxIDs service and FoxIDs Control sites can restrict access based on the `X-FoxIDs-Secret` HTTP header. The access restriction is activated by adding a secret with the name `Settings--ProxySecret` in Key Vault. @@ -31,8 +31,8 @@ FoxIDs service support reading the [custom domain](custom-domain.md) (host name) > The host header is only read if access is restricted by the `X-FoxIDs-Secret` HTTP header. -## Tested reverse proxies -FoxIDs is tested with the following reverse proxies. +## Supported and tested reverse proxies +FoxIDs generally support all reverse proxies. The following reverse proxies is tested to work with FoxIDs. ### Azure Front Door Azure Front Door can be configured as a reverse proxy with close to the default setup. Azure Front Door rewrite domains by default. @@ -41,7 +41,7 @@ The `X-FoxIDs-Secret` HTTP header can optionally be added but is required to sup > Do NOT enable caching. The `Accept-Language` header is not forwarded if caching is enabled. The header is required by FoxIDs to support cultures. ### Cloudflare -Cloudflare can be configured as a reverse proxy. But Cloudflare require a Enterprise plan to rewrite domains (host headers). The `X-FoxIDs-Secret` HTTP header should can be added. +Cloudflare can be configured as a reverse proxy. But Cloudflare require a Enterprise plan to rewrite domains (host headers). The `X-FoxIDs-Secret` HTTP header should be added. ### IIS ARR Proxy Internet Information Services (IIS) Application Request Routing (ARR) Proxy require a Windows server. ARR Proxy rewrite domains with a rewrite rule. From 840e08b83dc0e079b8aa8bc45487ded136a7f553 Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Wed, 22 Feb 2023 14:09:17 +0100 Subject: [PATCH 02/20] docs, Reverse proxy --- FoxIDs.sln | 2 ++ docs/deployment.md | 3 ++- .../configure-reverse-proxy-secret-firewall.png | Bin 0 -> 23625 bytes ...nfigure-reverse-proxy-secret-permissions.png | Bin 0 -> 43868 bytes docs/reverse-proxy.md | 9 +++++++++ 5 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 docs/images/configure-reverse-proxy-secret-firewall.png create mode 100644 docs/images/configure-reverse-proxy-secret-permissions.png diff --git a/FoxIDs.sln b/FoxIDs.sln index 24e2225bb..32753881d 100644 --- a/FoxIDs.sln +++ b/FoxIDs.sln @@ -116,6 +116,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{CB8812 docs\images\configure-plan.png = docs\images\configure-plan.png docs\images\configure-resource-scopes-client.png = docs\images\configure-resource-scopes-client.png docs\images\configure-resource-scopes-resource.png = docs\images\configure-resource-scopes-resource.png + docs\images\configure-reverse-proxy-secret-firewall.png = docs\images\configure-reverse-proxy-secret-firewall.png + docs\images\configure-reverse-proxy-secret-permissions.png = docs\images\configure-reverse-proxy-secret-permissions.png docs\images\configure-reverse-proxy-secret.png = docs\images\configure-reverse-proxy-secret.png docs\images\configure-saml-adfs-up-party.png = docs\images\configure-saml-adfs-up-party.png docs\images\configure-saml-down-party.png = docs\images\configure-saml-down-party.png diff --git a/docs/deployment.md b/docs/deployment.md index 1b9b9e325..ac1586ae7 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -134,7 +134,8 @@ Depending on the reverse proxy your are using you might be required to also conf - The setting `Settings:FoxIDsEndpoint` is changed to the FoxIDs service sites new primary custom domain. - The setting `Settings:FoxIDsControlEndpoint` is changed to the FoxIDs Control sites new primary custom domain. -> Yo can achieve a shorter and prettier URL where the tenant element is removed from the URL. By creating a `main` tenant where the custom primary domain used on the FoxIDs service is set as a [custom domain](custom-domain.md). +> Yo can achieve a shorter and prettier URL where the tenant element is removed from the URL. By creating a `main` tenant where the custom primary domain used on the FoxIDs service is set 92452093 +as a [custom domain](custom-domain.md). ## Reverse proxy It is recommended to place both the FoxIDs Azure App service and the FoxIDs Control Azure App service behind a [reverse proxy](reverse-proxy.md). diff --git a/docs/images/configure-reverse-proxy-secret-firewall.png b/docs/images/configure-reverse-proxy-secret-firewall.png new file mode 100644 index 0000000000000000000000000000000000000000..f91d3ede1b9067426e3fafa9675de6d06faefc0c GIT binary patch literal 23625 zcmb@ucT`hB7cYu}U;!)$NLNv$2uSZun)D75nu>G?HS_?Aic%Hn5Q21w2^}O5KoL<& zLJ5fwASgnBP^1eXaD#r|yYId~-n#FuH)}yoPR^OxGiT52y?^^R=aspsKFe9|vvhQH zEC%;wv@Z-ES|(a_baiRx4j!JOeLoX$ z&pw!r?o!C{--&5=fp9uHhiL;HE$eXSm8mn)HrtS+qp<3yJgHe%9ehl3_F9coE@%SS z;BHop^js?5aD&%$06(pwP&TPEl~WlyIZw0TH|lKpAYZL5u3`;;x;!>>fcqI_WSLw7 zVJEbD4lug~YpuJJ;;jnj$mG3n-ZeF10~iw|Ic?SfT~mPfT%!qfTnzRyz{dj8%4>#` z-v5p?&z`P4@$XRNYSD2YX{9tSj{D!y>y!Vx7d{mfEy5jaU3Mw}SM|v+x7_?!LD$Ez z!?=kjyIT_t!P^kB{!-F-yzKYf4#oLM>YwMk%V(WtKCCXq1jRx^iy=(^?qRKOc zs)zaG(^*WW>rrEQd1{8iO9RGbiH!M;!>!dsWqbr*ji{BQUG=aKB*d^O4>CT_ZYBFU zSCZh(p*#s<@>;TD@s{o70`3G>2_4kx#h%0USLstX2-^aod$9wN)+v&I2&)ZZN))1b zRj#i5bGfimp#xI?(kBMn|48@bSh_-3dljjguohEVLn6Mb&JJDb&gq(n8N&$F@Wj-c zU{r?BO2a>6^2UY$pSWkL^e(sB zDiGHzoxD-a_Z5h$b~O+w=brhTuvELqQ3o9&@qYLEZry}%g+S~>5&0Fd+kHp;X0aQCop|C6~}!9&~C zigO(SZyHQrL>3hOXm?)7P(*(XnO2$O+_F=Ufp&GgYgu}yQe%ZKi^x0d77*?gfe|o&|N0X!;N_y} z=byqK^^sL#hFpl)8inA`@3e_`M-~RZ+gGUp@wnLRjz(4L2P|D=pIkBo%S>G!LLylr zC1o=fV*J7%!omKzUAa8Gw};gFU54tvQMZ4bOWIy`Y47>hLfkN#Hs`8<`@&B0hpd_6 z2e}-#58jl=xV3XEt7<9X=Oab3Nxv3xMr~fCZY$6%UuNV^S$5Np=icHu+k$Fs_^DSV z36ZUkGPUXrCaA^KNG33^Qe@4aQLGX+^t9KikE2a9{i+mPfz?V{wJg2d})*7GR4(!bJR*U(iXmv z?Tg#e@@l^P?`Kb^UfM*u#OG-}=-PzNzo@+HTXXu~Pxrn^T{MTeC_Brn#bQ@_;lLh@ ztjv+m{`y*J^0(*QF^uq0I^Ju|=&_k)Ewrre*t-tl^ByMEg?WBtRs7Byt{Ud!dQxxq z>S)3}IuE@Olw3j@0h@l-Z|G)U=k7)EHW4GHz;+9|d%pfnHQxw?@N@v>LljwuGUh zpB~oZ9v5T+i8m_{f!UkHn7#md5A?wjKyIz>Ny7<2xliTsNb(Bs_sKw|+@-aweQHz6 zNP&5b@Nq@FJTyJ{TsSfnpR+aCb^0B;-e%LJc5o|=*Z9o8byKz$tK{&s4oXsIJynZ(t=b`EFKyyRKuWjDJ5nW-4TVvBHP4v$8)Ee;xpt4xeQ~QC8bR z(-bTJ=Q0(QZ>@IIA-e?zbMeS*(irjYO_D-t?vLeEs-ZNw@+ ziuBIFiQ0kag#>m^yz74I1+l#$PEgfOSR1R~1!27hdGdk%|DG4?c|PupnT=~u)>EVW zfIS3%joR1vOUv-F3FJwWJf*uz>`B%reAa)Svs3BRiKdoml?OzFbI=I=*y$!0k6ra? zNbL5Q-8O}I)G=7q+UkmWC|6Ts;q>F4)6gxmw`YBtLcwpHE>%VJTqbzrK(6nfgW1^! z39}*y{|T8%q-b{1fu%dRzJ!p>1iE+QX}veoaZq=A9m)iK|8fr9sHv zm#WYzu}}EV1^us!4M|RALeF-7@F{#Kv~b~n-|skV|TalFYWc|0uGcNS8 zF|bYL@2kBIKgVu19(A=M3CSY`%2kNR8Ew8xR+LHIRn*HRGJHFV98i-V>=@ShWBWQn z{O{Oko}FMfcc?pb0*5UEoE&>X={C_I=jv+jM{-Y?s4 zSk%L*ELSa(U^=uhRBvJ=BkN(Tc*oX79T3d;EL$I_A{uIcSLpQoduZ$J#>4WmQ=z#m z-0X7RIK#Gq+$49TgHDgocv zZIHG~N)jFG`1PQ|)Tp#gJ`X%DYehohA0u#IG}T~<%b*oVMMS=xk8Ns(>so(%AZox? z>d_j!VQObRkWr!%`>d_(QQLHNx>3Qbu8Ydw5sLdoTatF32b+$Z=>@;fCv_sSox%DF z(R;FU1#2wQLltHMZf=><8D2p@>3Pk8sF#8-c^qzSgDr$@9F-*_LNMmfGQ?uO;DTG6JdJqiJ-%yk_|61WX}pZP<&$@YSr1Yp80$6TD7ch= zBWiH9KuLnvS4Im_b}Hum9k)HU%?$oY+&FRT0nu|61+$q2sz8%J?8-YugmH*fAoR%B z0rbk`DK1w29bOfY)uw-Q;htXGNpFwBn5T*U3l(ouDfbne-XJ7BAnw`W#+rUJT~GW> zJqjbrr>=vxF?OOArFX8I%NLthN2(94dWOX>1?erMe|U0Raskxs_9V{!^*GKfh1tYe zwD^`&gzlcaxD>MKZG&&I7vH=%0jJlGfUq?6ab>~^*H79;@nEko^S%ft5k3fBx!rH0 z=R#>lEz9+wsEid{W1l>mUs zAgh)rq@|5(9v?OdVX>ttvD0fEYe7JAX@G#kq3lNyE8SbHrmwAdO{18N-*NcShV}9% zJ)ZD$449MsLVC2I@O?1lE{|1-qy}o-c~p~I;&1I;4-vFQqUeR1z#<9L0h4pk2q0-3 z{&8I)c6JU{p(a0`{>j*8{NRgGV8|>0EHhhrv^SEeq#3FBy;!_*%ujHeSB1#mX6e(S zC$BHlFF)b-%95GlF<%V?!SiOD9+M$e$sY(fSoz&PMX}e)OLi;h~2T6GMjoZaYlUmuXuEqJnb}BFm38s8J=mo%_ycV5=6jz zHCCaDa)xOO<%1p~_;V}`e{j9S_CqGtAPcJ}& zzc4r?jKKy{8FE43x*$>iShNmT*VU0YaVsr>aqT=4hP zbBZ7Y^Qj9E@>1&6iL98uuj`gJ+75Zab!%$QwGD`e*^o5BRTKz8nJTLmD=4V#RQ@+V zQ@v?@4XcSQN`U&0ur>L^er30X90W4SsvH@IH4e_EX*gFD1SKNF)(Wv1^>Fq)t8Cbh z?w>6KiL6^WZ^X0N8EFq#a*tZRjUJ`PTsC@w9Ug|sWRgQyvJmZiFgYW4S(8|T7NCG@ zp#}ah^g(U|vCKgh{MrxHr1k;gly&D%X4zsnyTcqqTZat02-ZK(Yp%IkCr{=J5y}Ua z{U{9KbH%8L2LRol4J>G#De$p#Oot`hawjOmhhJv)^wlM?Z8Ean!AHNGiXC*T6M+J2 z8y>Z{MblPpU$4^dQ7VQ$`fyzNjOL}Tx<_P3lFX2;cf{N33PosDm%zn4 zs`qg5Bh;*TO!iVh&AZ*T=eta>?)#QdQfMu{$k*t%+2gJ^-&XEErePxC1%yGz?rU>T zG#2RTEYop6YuBtce*5PYctInfF;BAw|9ChgmSk;e>}m@c8Y!I8A7dE0If-#71KuA2 zTvbopJK;<-wUZMbSO#o+S8zbc?CE)?-5hQf7cPGL4;`uwY@)@7Rl*0)`4Ip*!X%VYLW!VSC4&93tD>oSl;+5Yp@Egz8p{RILc$%Ze5~T z1GI-?(}RhJ6ZK;UKOP2fHva6s*!@^E$2Ub#Ax94uNT)ag@-#mg`^=2jWI$sTeN@EA zU6OM&4mE!RQ|Z)GWLXGW?czR_CmZ}MNI4k$)0CzRMjK^`atArbzIA}{Qig?kpsW=A zN$YCQTB79m)f9<0?zyrhsmXn3u8OP*e|atl(h~5WVUh6;R0R^Hc4YEuYi<%;(mSWI z>~Q9-H=;5l2=1}h!ZRM0xPnMO!XszMEJZw^KzWMePs}S|?rfo51d8YDF2z1MDn^QA zE3hj`1(Km%k;7n*+z=ctD=N=gO^&c9@ji3O_bBHgbtfm}({FXgTjO~9UuwQ)owjg3 zJS_Ovb^uO**Jge?4F}lM5fGe4!lALhP$A_H`oC0C6cv$@0xj&80TB-zcDc?;BoWWQ zrZ6Xq*gys}<=vg4Sa-PKyJ;80IUL{(7g2EgUZ6moA9j~2m(KXVk&)zOY6L= zhbO_Cd!&bb^mwHZ-Y}{~o)yV@ct?R}8Q(aD$&P&wKU7jAGR@b1bQPMwW&W9*#F$!? z0RRUv)+=fzMG-yN-!TKfCNVF~@?G%aKagLRF)p|E8~iZ6Y6^B_C{w46<<=e&Fa47j zvL;kfc)0s)Z&DAdboWwkBa!JMQN>S9vSX+<3UuC2V7v96BMAq!YUo((6}zwCPxwl+ z`SfAb8B}s)d2t#Wl~;!t|I&xeBO=GHrb=u_7dXo+iPUIWSW6#LpF)0#X+8)(+kn4X zJm84=8Z|jDv#L=sd?cj5t}*%!6TyRTgtc2XnFw}}%{uJ{^FyDow?^mlAEKNdw2xO_ zY8YG_gxMe7LOtq>J-W3^6_}hm)10#2?7d1TPnT<(#DDb*UHl=x<2vF%om;hT>X)eV zg*XSgK<0MkULgFpn&Ei1u4}IQwBUr7wYh14Pqx-=l|!X%?l5avYgeW0O>-uT@3%u- zY>VvpUY6R?lyuLy#3LsMj*0B0#0G$iW zR_PTZc%M@x^gSgk`|Pdx=YpD>q8}MO1#R_xB0swk0W>iKi>-n-T4ef}n6gzstgAAB zfkWmt%u63{45|s7yE3omC!?qmFh!-*5}dhW)gMKWd;KE44IhK2Tov&m<{ZH+#jn$$5eZUG?FQaADuc?9qqG-<1g- zLrQDl82f?dX%7Ww`_F2Lf~&!EYZ^<=x-s+ZRQ(v%0y4gjn%r9cWcCk$C2@s2Yva|;Ihsr@F&|=0Tn|k`o45N9e0V`=Y)+#)-pW*m#`@=nyVW!ue9Q;6k}g`f@3Je#RGNO=1pUDaHx{Z@R1?P|=n_}}G;HC= zBQm#5)m7O~HH@j&9ildD2V#5E-r|U_WA9byOAhj4-ehrn=Q5)T(rmIL|L}<@T;o{Y z*p7L-U+_`@;j`gf27{hjYJZi7b#*wd@xd+fUwEPsr8W3bql|PJ7Ve8w32Ic4{;flr zJh@o*4dQ?jH6~9~l4YyR-*qg8%3}@bPeQJrFL)D_upQ%coiQR{r{Q)$!G{8*aoTtH zQlL46JB+X=e487sr05?per-ZG7EUspThs6i$N@olch>Gp-&f7AL zeh=&o3-+C$ZcsV>N#&>#D-w3BGSvUKmiUk~DQDW-Ui5Zg;hdry2QtKo;}HiovQTVP@<)fT<7BSG|9<3%g#46Od zkSB>xzt;2BsPytLs*Q@M&7vFfDk&VJPvMo)8~iPiCT(ByQzG9V-WbHJd7R^gu9c~v zRf|B|{!yDA?h7y8JOL)C$5tnXXhszS>e!tw`VYm=hUhG& zkl>nf5kzlIM-e`YQ4*T}z`ssz2Bd;0N27bq_ms5-&zg!qM0WRVyIe>JmN;GIFAm`4Bw69lfXr7FVg+`+B)oUKlB+gJPUsV{e5Fi2? z**E_PkajAs2$=3&v8rz1^iMrF!D)Br$|VlthLF*z;#0BM&-XSaCk0N%B)Wv>nLo@{ zsYgiKn$D$|Mij^*N^c;Nc*3Qh+*J|C-qaQ@FqCn&F*uKe{0fZnP!8)lZ<*s4BzNF* zio0+&JK|&956-3-0l|^|0s+T|XmuVyD<@nXHr*UkZ8b@4(7;hsfvL&*Y`GVaG(Bp z6_SsFwhj}u(bR(=(-1n9S6X7mwSrgf%f&LBQIq65P=8r5wsu!=-Q&)*RIqfs#zcv3 zS+KIz<385{GMnVKMm3tRH5-msNaT56k9iz|EIDd-=F0u)UuYMrwo9aMDMWWN#TbV0 z0N^eo&^MmcjE5%^n%kIysT-2{KMp$MTyUizALCpUiKZFrM<%$Pp1dW-(PBL<;7VR- zrh>+ZGF98^WL2tQN1OJp-l_ifM$_0mb@8U2hrLy4ETOtfNdHwl6r{P!XlXnsTgE39LFe=rOJ<$ylbMw}&yk-Zn| z`Ny!+xjz$%OZAzk*3`r?hhEz(fn^g!hr}B`#^ne+y5`)eUt*UwBziQV+w^Q%c#~e@ z=k$lS#6&5lh|R%+Ah$nY=I2j60<%u-?-tsD(Dh=Mr(1rrA6S&yUnR zTUev@Cf7XQ0@k9LG#npTsZ8PCn2xV9q875bAnVEJb4nw`lFHI8&HxU+w3a)~4Y2(z z)nQy2;ix#tc2+~cutoLuP31HN;xJNyt*Nj9y^M%GQ=WEG@%=I4uZ=Ju7YOhI1G=-G zo!WmWebnhod6OF)_MjAw8WT;q2MIYO;a9?ZqjmaBY0m;>(+G*AtX^uVpviZ3- z&aJ$&@hio%@Rf9|3wlqdyx>%0^LxRcxK@L)u>6iXAsK!~edj7e!5F%P z8h_IINnH@j$Bw9p!)TC>C5dA*`pd&L(v%%-;w?^LCY~Csob$}LMmBQ89qROKVl-@R zgP=x)lU1eH9dCv4QPg$U)#C#@&nXX39G@-uc6QDlwXuf|6^_n?=`*iIfWsgCR$BPp z$>et~R__z><}mb4I8;1gGdUM~a0b1*!s3GL;>e3tXs=I-x2Q))elal9nHsF(&}tDS^;On&*><*_pfeB}*G* ztdl~l2rUO8I_CK{=3#kT+0O-Q)zPIs5twK_SnS8NA9Ph(TX>EO!}*#fRyZXde!Vf|kDsAI@6s)TzXd;Qe3`lK zWe;04MynM>*nO6l%NED8RFy^xfgRWMA=d$KzDu8Sz<>V1Tk=6io)2g2BYS;cDak60 zhH7Z{Zmel76ndm(A9Te2ob3JR$}M|+NWmo_H$RfwQ=AjEJRSjg8_DV5u{Q3hW&)%C zsb=@>ytRcN^i+So&f7aj z?8+-#3cG!Krt(f^%z+(A0|G06wsO9W`8K5dkVZ}}f2hM_$h(Uz&@z=R1meu?sP|@) zgx1c6_TREzW2r;+;`HY+8JNvYN$kP<2eAPvjldaugEGG_z%~>7veNc%U?vl9$;f~? zbVrSjPG5NRT`1|`^vO7!Y)ww}Ff2izAw12yTrQnJa`j-Mk&+bahwrk`1^j9NJKpr3 zW1@p}^SIT)t@UXbz?PBE1m{%JO*&p`WceuY@MRMSQNAk7+^9oK66}QAQLG!|Cd10_j$^PuM9L6G8ZWn4%zuF>9U5)zvO!*1A zxM!a47-w>H{^hPTqmHHcTmFx-RR8aJFRg3_Y++QHJu&701NeR5vW;UBAkgSq$7SX# z{^p6Ml$14O4PloGl>yIm{3p$o%II{-oF_dn&-Bl)qpe(iFYf);QD2dNw*~wnr^ijq zk#?~fot;`PYMpK3TKR{+gPmgbFx!ccl)5#gL0)hx|9<>`s7hVe0w1#)K8QCe@lda5 z@v|5rz}y!%82*#$iAzg~qsv9q^GSNC(rLRk3>Aq>~Dp2yn?Q1UFdS{V!cBpO&TDfw4qw!?Jf5MsErD z8$F`B5t#sg@;gI;=6*vaqMh?@nHsgCueQ*3+e349o{a|VDTbV2w>L5x)H9?hw!C?X zqgLm@Q9}=}Idy-&Y(k=V{Xl7Us`FMgM5bW&PjA3259-S}q60u_GicCpJCWjzdqt20 zgaCX(zy}YRKD3m?hBr-ZLbn@`tf+DO>9k%gQ?9oXlp4BLw zym>fV{+C7U?u*v+1}JH@)zRbo$bB$RW7(W5Jn|9FkuSmalFfH>H)hX3oZ-s4{VmbRR0xYKL z7D5DX0yS&6IkFcaBO9U($E_JWBv`1v{SN|&7kP~;~O%2S>zQAYml5>DXt2zQ}Rdz>!fs1Ctm;5%Eti-*+-9+4j# zZQi=>vXLA19Vd$lJa&0_WVej89lU=0BNE10H;ENB|40dI0uztCwn$N57)B2p!yrpX zJB^UH&VM#9Q6vV=S5uefrkYD!psu^k55>ntavqXPQ>R(gH^R zQUG*~X0$w&P)=xnwg!y00(p0nk9aOxtCSHcY(myAQH^SnXrjb8a6-Tkr=LTu%FZ?xBP%{^tQtGXvg_QQ0r<$_=}Jud^fl< z<**EWma(+}1ibU{1J6Kx*S%ceM8)yb{vXW#OxheO_nwN60X8QE@=llYaX615 zd&_FRaHaf*9py(3I48&ZWk;t8Z&SV)xmDP&5*uPUbNcRot<*(x zm1^9CiZnguzA(}8{)pN5?lOg6bEequvL9bNv_~_ne{Z5I{a?ws zaflWWg3$wGgMy^g7>rpldMzaisl;CX?f@q+nptd5|31JjYl#wqZtpa|{zWxevOjK$ zRh$M%#9@6Quxs&5RlYDt@5Pdc9&46DOSrqr+NlFZuYk~iyy7LbkQ(l8XPEZ>Wc~x! z--oAY{zmb&x%KYX?)HYecoy#$&{xfvR%f>-+e|9kp48LM0dkEoTS}+U4IPsX)1L*; z(3+At9!!ydnA4I~*d9(&^-{#dg|hd@{Fv$D#rP3lty_!cFa*s9Yt_v!h|Kk}c@?so zY1Iv1nh7YRv>AKi~21pN#7q$?yMI&Ol25w5=o(`)B?|=Gs+%8J?|3>^M5Ey^;R3Zk()(}RUD&2TF7Xt3~8{3#%^NzLWQ#lpap45 zgHX@lzb)if@yb0>D)eZ_%ZyNv&mb*4X(T{ZYG zO$2C^^S=MAwVmZfHaaP-CuayrO?@V$pz_RIz%pa>N#j~K<0|Q_2~dHTWq0^sRYU*m zD@w`4o>phMtx`!4N`MHw;L*;#7Aet4{L35}DB59RFqd+cqwz}gJTx}%iU(Y8)q*@o zEHHS;^MJGY8-v>yqbKBAU(G5#eI}cN$c$>m_M-wrlUk7=`_Y1Zaq)oZ2=Pn+OYMy^ z0~QubQ+Ev&0a!#2w;RKVPBy#p_8FYgH%dOX;b+j!(ZM}`$XCJ15X6dyXVSL@^*vg-B4qePttB?D@1s4$Pa{W+q! zxi_kLUqn#fd-9F2=M*l8rj_x3!LDl(yaCl6Q7aw}5QzcRG00ExbEfVtA_SzKjp*r? zP%cHwz3OD<02;~aXQJaEQf3JGd5eeRwELCqw2W#*K!i@LHPFQXoi_iu-ugc8SQud% zKrNSWdFxx-Ph6v)Jy{h2?91Ew?e99F-Xvae|2cdo*DXHTh?HRU%Q$hOv1_m8Q)$1A zu1f+rDbvcX1oRQ7W~%l{1mt|SKos|xK}IG1jQo@nPe=Ls7O#q6KnHDox)NZpbSSj< zKD=NXasI`bL6F@*^VPk9XM#t{-#g;F+;o@|A30^*6>t!ggF>l#<{gWj+`U zGH^3B1`FK36oCK+$bOeTXDbaCqAjPa6344)X^u6#ApMgjx#D)pX@)zF<umW^5hi=FA!*T2etvj16h{Xx~+y*bAi-K-$wtCleoH-L-h`O7qsF8|81 zZ=HO;^n~(gj%AN5)(6JVXGw0J*-Y~XSg5sCt=Iy~Pvsaq2el-5+GPD*)7N#I@`7rj z9;2p^cHie%6}oI9O04)H=*3ev$cB1K{S(>z`CH)4++dXAc(AT25Jq!W;=1qALa3Nd zGS`@&OF5sv|M1bFU^z;IZsy)>vrszAU7b^RSm$RLP{DGlz5eVTmg}V|ouNEr)KnhZtv(eM$R?lFUtoyWP;>0dgQz}RN558yNs#6ef~NR23Mj6 zN+ac)+lZ~&vBZh_a_^2S`PGX}(@AWlCPTZq`O0~0`O4geJqJG4qVv;?sMTeW@70*M zh=CL9fV^?+aKojRL^Q@?acZIdEUy(4L~4P&v|9zyy|1)cPv#k@fd zPv;o+W6zrOXG;19JT*V>7C16JDe%kUI-t3=U0@8p3E*rc#O)yyblX4Y(S@hv?X zmRqn}evr)r9IKHxWodGxrCCMVO~+@CiqMu1U$IE!-0SJOs}LY{%z#-kGJ2}4TS$+mWe+YtqhN!V{Pg^{ z*Wge9PX~T_GeCP-kpjNh!7+b9t=H7H0~N|oikWm0)h&Iizxgx{&XPf!lL56LVOpe| zUE$d?AJtg4?uFVsROeG1DmEL~dR6ePJaXO;fr z@N1gq4L9W`7yI{(<0aY@KF1s4GF9Q=Pd@G@4y%Wiozb-|sa#s4%#K6+<(Z?^mrm_( za+fvbu0Kr+{JvlP*!qJ;{jzS~%Y`_NGw2v0_I+?-Q;kb)sZy_Ksv=R2dl*5<|J9Hl zx7bDGbHWmFxyJ}ysiSSRaN|JhtrFWd!6u~H$DJ;zK~G4Mf-du&3nHfdy%}ry!H7-U zo&xgwO06@C>eMoJlYq6XQSu4}2 zx)S5Sa{nXBR9h$#4<*x<2v$x$?|1HZ1I3|@3_>F02D>Fl=NfJU)Pk(ZE|iga)JmCH zy-?^j%EH+4hJ)r{^rWtW7^F^Qze;ItSinA&xL8tis$#=0H03g=NFsr6^WZXHLGZez z*Y!0m;ZvtIRmNcD#o&@>Cs%%!SJ--gJ7TbWrJ_aMw6T1rVJQCdAM3}_u?s|ta1t_E z8L{mlrV4|jxuz<0EfOmJ=`ouLy58R#3>>&);ZLA#BO+_AcqwXiZX7^g&|41?beZtc zKW8^yEZsnIJm^=W=N-ttsCMqhxU@a7m6;o(5s#EFxWMgM*szGJuI)(PD%QT%aPXI8 zU=HtxL-b?IXOHw0=#?>Z@tA;$68mG8W>5$5K)omqK{`0R|Mx{w@&y~pP_aMo{$ni_Z#zDJRZ!g55GWT%g)7y0%rEO|=e(bb_0+}iD z_ev39Fycn2tO6}&cx1!QAxuPf-vNK{#YvB1Olw^qhe&UBFY3uK*W$pSP=y{jn4Lu3H2O{U8|G{GlfM(CE!Hc-P74E!E1TqqGkUCP9wI%8qJ;AE8XW zisrAV4=2wz>vblLyS&B0kNLrZEA5!AYwY^;I~ z1C4D<({8I@E=?#7;~#T02HUOZ>#NPWndqi^J_!|y3#Stfomw$c?&43u1)vFjQ39;6^z7 z^gt_fAjLE(OyW!-J(Q?C1eUnol2Y4rHw-_Qps!=}DvbYQq)uVNF0(!~-yy6*{qW^; zL?u}cbV__S;90|o6q7B5=Yp@fc&`@s1YtFggLuXI5aA-+Y-wThn0*puKev!`!3w&c zak!Wv16q2eHic(1Q?>9YJD+?XmCq3fY}S3o(=P0dg_tEAk-qb2^$Ud3cE5bu(d=^zz)H7d_Jr4*vhs|0 zvDzW3a#=X==sMZ=ZIQg96<4)QiH7a7GwsP$!vQU3QZnOMbDr8n zJI*6z$BxMGKfHF4CDECFXY@b=mBBu;(IqHbbj*^WA-zb2P!30gxUHMS$g_ozsc_Jw zy1LLGTf6#u$Wez|&SEEnC3vQyE=EYyVV*vz%g{-lU~xS! z(|KtJCXaf2$dZ-F4q?{0#?tNfHpU}KNG=&)pe@DmZ@ZL!F6${QTCu5yur#ouzrx0* z<7EK=F8%f!rX&Ptm4O7^m^XxVae1S7!XfnPl_0 zivQ5AgIi>e!H@YR9N<0dr?}q?eAp=u-cQnBNVlcGV*$ah`I(m#P8LCjUTJE~2YmVhERV62|SXrtyim?^eHnLnB;6I`~E!gTG< z@xF7K4SP=0$z+&hUdgDL#uWHya-L}$M$GzboQ4nsWkWa2Zhat&6{ppkJ{UROA{nI8 zStI-{KqcO6=`>g-bMsM%p$pICVK0p#HTnaL@)j*k#^`}`Gy-181Es4%o%2dmM(}it zKNPDVG^)IA`K!=ZLN-Ldkz42duJ$FXmSe39G}&&eNU1N)PgXNBwVV&A1N z{_RntT>`zg2SnXELxJiCl9A%*_?eMYEMgG36iY}J0@c(soi)E9lof8TOORWMLb z7PO=)_i=M9c=_?13jXH?)5($~WpZ{FcIn0S&Kl*-_oQk>>D}$aS(2*Cn|Q@P)P7xO z$z86df14aNDImon_g~C8MPdn4#cxg3oV^XKlJ?%M86T3*Or?!}pY%tH6g%}W2ud%} zGUdg9mrWm;T|3{`TC&{*2d89rggkNLhAzs+(txtCqNCa~fYsFf9DZ+Z&Pmd7npTEJ{5UomFHWyi`~*ebOzDf$FnW1%Sz4n8-he_WWOUe5AKm7 z2AYiEig?((G~>;`icuu1+Ic{0jLX6QDhz;oyF&JI+2*rQCc&@F|?Z2pN z0Eia-eP7?w*3eDRjda^Y7XvFoG>9?iMReXLKh$$ zV*lSAbBf%cz9O2VcwD;b{?{F)e^37Z_D+QV?UyI~KQ2US(kS)JBQwjb)h##I$6&V! z_Wm@gydMosrFq}0fA?7)?_)#rmsW}8`986B0iJYxB0_sPz(6(j1uFDZ2x^zxrsaSY zJiBD1z59dqk^e7*xlY?n{>5%>Xd-O+QAqB2T6A;96@kMt*)F9a@8R>J?zCzu zfv3mN^HlFTau4SR|F=g@oZf9X^*g#pQuC4G-;qQFTSl4KBnYyYw~5`#H#9-TB-*Ze zQUC57P1mM9np6=kZuP@_tVf@}P`EgW#E6V$^O`GLj!t9yzxZmDXNF zg0`CxWp-z>xR4$n1(Brckg{=`*>jUE4jna6^?E%R|6WJXmcIC&ZLaqb(C%ETIX*@( z8k!BJI4;Hm;En>oy<;rBhb~2rU6lbBH!9jIGaS6Vf&ONe zAH5^_5%(bG$UieObno)oeujTT|8>;P}OQRhbg>Sz;btG0}9bhdyTf`JH@p64Iy zLoSf7EH?5l?(osN6Nesp7Y7)B8$0@P4H#TO6#zdnDyupAZPViWC7z%ya(fLNaJ6yu z>Cvkr?Q?Viuc@w`#-8twyr$as6sHM)GQ7?HM_GdnFSPp8DP%ube>g=KEd zQ8wM>N)WNkEmkU!7;3{&IV#t2;K;pGQB(wNn5E%DKta?_ngXs=9H8>v*m>UZ$NPCc z&-Ev8-*8`x-}PPO8|qWRSEZ9e;fl67ziEd;n_}r?4hpv2R@9@#)8&wB94gz|sxiS_6h%!qtV{w?$hvm*T z5uZlWeWYCZjobpv%oYac`Dv%z>}2PHX{=B$FOTNKQXqkxV>(so)e3>s8Y4)4@Ta)$ zu9MpXJ6{JUj;Zn|9oX?kHaS)Eq+`s#R1uxG7P!_pg%4*`JGEbh~`FBtq%2sVbd` zO2fusAw(-QVpf&(xr7%QVzX*gNQ+Z=afmRlXVElzgg?WR%W;f3fJ=@JL}K*S#{T+g zlFgd$dtyX=9ByROFR#sAXw3^yI^Bamo#B}*F6+j(idCuRMYMD^rSR^Q2zZ4;Qy1*R z*jL8KfQi2!7<$^WzZ+5Gny@5t$m&(BS6hy^*Q-Hcq}MlEF2h-nGl#*ESHg|o{ul{B zb{rNC@>6RKCL4DwqhSwGS%kOMoi<6>173w4+sYRUb{4N0GwWw~&TKZvdv!_1qyMBn zKcgoxfP!qZt|6;`IzYlt@fQce;wP%U;vcmC{kBz3Pt{@nJ7h=GRXEDoaA5Fv)APvD z^pXH5;sM>FfFIyII9Ng7qt5#CmedD~0nxKENymm(E!0>UJkD>2R`u3TeittaS6Q*1 zFn=lMQV4wU)Xj1cMEE!aV;4$16}!rZ-23jJ$$ten>oj3h4=|(HpL&6!ie4F#n63$a z4}xT@D(Hwfsx>7sy^mJ?;9e$8LNkNuUR{4d{(%JEt2yl0!;Id>FS7Msf7w(VDk?gO zYDi0Si(`CL)z@)#b{`)&I5wJirDj31BDY$DJRj{GFvY36)jheg9Un*qAq1j~-cqw6 zYBk#Yy5m5%vA6=oZ;`0^a2eSC-F@x_1*+`c5U3Q2zZ(BOB`=zaxPPRgyt{`1O$hkD zm>Ak}Ne&Ydie2ik3|G9)X*)NBkA;-PRBU>0Gwo{)!(pen1#_xMXRde6H(WEOqOo+S zk5DFeFT4DnvsN!DA00ez9Y=iYMffJaw}aEVMs=XNQ@nG=Oc$e$9aCI)qZ|XJa5UPZWy$D!@}&KV(C4r&n^F z*{ZAgpKzHj(Opyf+lc5_vz>mnG=d+a&0x#%r{Z(h!O7UKVa!2qi=BBZOVqt~=$^Q} z8^Vx1US}s6TIX(LbCaFz+28Y2ut%%h9OyG&+PW;kb<39%%L-``9w+%YXh>fE+~oH1 zz)>g7%WT^m{o^55>^k*?5H*KAZH!~G>D*T(JS7k1vT|E(^OO>8#2y!6C=VNoV(9)c5aIxD4~6}MYo>`|<}h_< z(gZCe*MyA289(%N&4y0la-*faHHBBKE^XenKH6qPNpFRarR)edBsyekI>&OlX$aGK zMzqu#oQ1t0`-!lJ}9SJyQ*Aqzrk+w4&SkhVx5B4W@TkqTBBifW39hz3Aw(>pQ`n|Soygn8!3J(do`zT_n2os(cNtEgF&RSuT)#^~I+-wT9w+y;c`Gt80 zQV0~qGl_GF3|3MKXsgX-ugOAMs&*~4^*Zm4QWh!)f2TTM8h;7+Mxf~&!4tz%w?&kN zj$I_p-cLiU7x`Ht`>xUZ{8VNr7$2 z7ioEc+uF#f7yBwH7AG{z*yb-)-#VwSQBobupY=KPCmBEN8R`TXU7}TVKCr977Z!i^ z`(A51C$LUycpuFG*~2sBnP^{#kL~K!U~%OJZ0Y{;T$8fVc&otS=Hr49VVu5|Yrvy{9fF~x+ zm!h&DbI*(9NSLSg{j0tZ$5O`eO9lRVLPr%DBJZm;y?Os2Ii2^ish6dC{bY?`_Pk2Z z*}FsTkkoH^HB8GqUi%fVj>92GcfampI@MY-T%c`fpK7J#sd+bg|+3zOgD4E^)tXs<6`q`-2p zu7^MF)@>`e#xbv#xfvkR_;k`#QzTbtP@^Rttl0+e`EJkzU3+Xp}Zsv!#U_#kYI*a-gzl}qVo^cO#` zuCTlf`9Kr9hgcGyfj|yLXSSB$&J)wo{8~bA(ylsTk##)fJ!Z zZ&6PEP*Ur0H*y=fSn$_n4zA>zuBH4`kjm{%JpzBx1B+tSI62%u=Qvn7fI1~vK?d-h z!^)Wo*!fp}d{w*N_k>B>&9 z`t{=QP4QuBz5Q>^?L43!io6*>O$!`un;7;x3)%V8_*ROZ~hwoVeBu>);oC zj|~V#XCVL~ahS=eevmg~^$y{y>_VA{ZY3$ejmU@=w)8B(pe>Q||yJJa*>Ce4qu+0Nzb8+`Kx}V3+YUtNd-t@Pz z>aF7~O2`H%+)Hv>+Mb)q!Q#C#FvU`;FeNKxTNWaOFKN-J2a}p!Nms(wBqyEgAKBc$ zwOFG0mS-2Z!7U2EJgRj=z1xz=pNC1+1P@H~*}GxOdnV|R=@y!U1I2k{pqgF0)=yOf z_Z;a9CKAiC*3Yg9NQm(y2PrCHTPSdv+C`4=`uRY&OmvlgN|22okcP9%2rFDnT&pG0 zhND>u%#j)VJ+r{O5=w4rZ!I5_spfL74~+7sHHgh)i$%39jj70uCyG0&{_d*n*o%z@ z=a1U2j8^gqQhz)fWRw;TCqk1q@pmw9log;nPIg-GCeHPLpY26=Vi2)jd7zO2YEwU9 zeTvx?{MHLHsIz&p#-iJ>(hKl}@Dh1FYf!{`{w=*2AF^}KHrTG;W;Tu z-d6z1=e(Sg^W8CEVvTH0(^{|_th2vh2n-YWUDnsETXG4YT|Xw)kTqJ1U#QyU+%Xv) zL5N#zn%}-!N1D~7{X!W*SzlwpxYjQvURL{MGL~=0mED))GG{~Use=Z(c@94&H*mcR zIu0<}5vK^jktlsPEc>184v!%%+}XWn=Cw78UT)LpqPg|AlWnX*4{xzrpbXtxsB`4y zKx$JkwKC;gmeJcBOR9rM%LIy0>=6#7fqN<2B(dvbk{__q+d`oTUa@Dw>9#w`vXTI? zsV*qQQ~;v@Gt$W$Q{$_LO54IBAQG z-#S{;qFbyc0)|e9RDHos-vCsR_ZG5dH1Z%8YE2)-B(wf)QdJr1dIFGq${w|G=xdlR zqa&UuE)GqACBkx|7!D3+X!qw#UY*DsjEbjWW;n3gkB%aSl%2fqZ}c&h(Vh2eVZRTc zszIBnTF_V6SDe#`>_aaprOepC{+{1s6+V89_jv5soP#%j{V^nJ<$TuvWBrO1#4GD&oJrg0FaU z9;+lx35IxV{zZ+oui9@{+~(x7>dNoL=_7{AWLvZ{yteY)NF7*WyUqg<%CRaGi@Qv88FA{k zvxo4%|5yJh1sYbdSYh_$l(3b|DmOZ&P>IRU^cp%gJzn*AHt> z&D_vJYxSyl*p_2zLDDSpRU3;l2u{-HVJAReq*6*_JBR_5I?6wJuCXCIOkZx=^Yn$^ zr({jU3$0Y!&dKj2!``i>zv`ibdt%99kZE(b!W){<91|CQaZ$qAj@cfYe+45!%^t{~ zah$UUnH|dsWC7_ox2yr35R1%Eq3ZxPD{*V?o>KPBP7-rg2IuY<7cVl8TmhVv1`mJ2 zR1S_)7XK%k$oWwv(r-dJlQcJw1z}Ef?^b-6wET-K0uVag$CBPPsp{@8ll?eWT9Z{{ zeao2gqu|^1`(0c9=KCjf8#v32oKiP zItW;)D&R|S{p7|9CxI2Pc{Q7AW!1V4wL+={Fp_|}0p+r1+xmT;^! z;>jVvYZmBK^()uVc+AJjcvAV`zpCDGy??a%|2Ga%;a>)+?f;)petk^-!{TuLntxLo c_vDHc!Vfx$o|IoK6Mz2NCCiKD7hE3y7ikx3$p8QV literal 0 HcmV?d00001 diff --git a/docs/images/configure-reverse-proxy-secret-permissions.png b/docs/images/configure-reverse-proxy-secret-permissions.png new file mode 100644 index 0000000000000000000000000000000000000000..fa7e7ca7a5bef0edee7cef49c73e215746012f24 GIT binary patch literal 43868 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 !eb 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

    ^dwB3c@UdmSst-B+Dot5Yd;3YTu}kE}C!Kp& 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 DP2F+( literal 0 HcmV?d00001 diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index c56008bf4..7d384f055 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -11,8 +11,17 @@ The FoxIDs service support [custom domains](custom-domain.md) which is handled w Both the FoxIDs service and FoxIDs Control sites can restrict access based on the `X-FoxIDs-Secret` HTTP header. The access restriction is activated by adding a secret with the name `Settings--ProxySecret` in Key Vault. +1. Grant your IP address access through the Key Vault firewall +![Configure reverse proxy secret - firewall](images/configure-reverse-proxy-secret-firewall.png) + +2. Grant your user List and Set permissions in Access policies. +![Configure reverse proxy secret - permissions](images/configure-reverse-proxy-secret-permissions.png) + +3. Add the `Settings--ProxySecret` secret ![Configure reverse proxy secret](images/configure-reverse-proxy-secret.png) +4. After successfully configuration, remove you IP address and permissions. + > The sites needs to be restarted to read the secret. After the reverse proxy secret has been configured in Key Vault the reverse proxy needs to add the `X-FoxIDs-Secret` HTTP header in all backed calls to FoxIDs to get access. From 3a8b2f7c7c44b271cce0b53dc78ec099d4d85d9f Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Wed, 22 Feb 2023 14:14:57 +0100 Subject: [PATCH 03/20] docs, Custom primary domains --- docs/deployment.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/deployment.md b/docs/deployment.md index ac1586ae7..61eb927fc 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -129,7 +129,11 @@ Depending on the reverse proxy your are using you might be required to also conf - If configured on App Services: add the custom primary domains in Azure portal on the FoxIDs App Service and the FoxIDs Control App Service production slot under the `Custom domains` tab by clicking the `Add custom domain` link. - If configured on reverse proxy: the custom primary domains are exposed through the [reverse proxy](reverse-proxy.md). -3) Then configure the FoxIDs service and FoxIDs Control sites new primary custom domains in the FoxIDs Control App Service under the `Configuration` tab and `Applications settings` sub tab: +3) Then configure the FoxIDs service sites new primary custom domains in the FoxIDs App Service under the `Configuration` tab and `Applications settings` sub tab: + + - The setting `Settings:FoxIDsEndpoint` is changed to the FoxIDs service sites new primary custom domain. + +4) And configure the FoxIDs service and FoxIDs Control sites new primary custom domains in the FoxIDs Control App Service under the `Configuration` tab and `Applications settings` sub tab: - The setting `Settings:FoxIDsEndpoint` is changed to the FoxIDs service sites new primary custom domain. - The setting `Settings:FoxIDsControlEndpoint` is changed to the FoxIDs Control sites new primary custom domain. From 5272f68b394c560ad4a15cc82e06f6ce676ff0ca Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Wed, 22 Feb 2023 20:11:02 +0100 Subject: [PATCH 04/20] Create master track login with SessionAbsoluteLifetime = 0 --- docs/reverse-proxy.md | 2 ++ src/FoxIDs.Shared/Logic/MasterTenantLogic.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index 7d384f055..360d04a8a 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -49,6 +49,8 @@ The `X-FoxIDs-Secret` HTTP header can optionally be added but is required to sup > Do NOT enable caching. The `Accept-Language` header is not forwarded if caching is enabled. The header is required by FoxIDs to support cultures. +Disable Session affinity and Health probes. Furthermore, Load balancing is not applicable. + ### Cloudflare Cloudflare can be configured as a reverse proxy. But Cloudflare require a Enterprise plan to rewrite domains (host headers). The `X-FoxIDs-Secret` HTTP header should be added. diff --git a/src/FoxIDs.Shared/Logic/MasterTenantLogic.cs b/src/FoxIDs.Shared/Logic/MasterTenantLogic.cs index b49266c9e..5a9ca0e2e 100644 --- a/src/FoxIDs.Shared/Logic/MasterTenantLogic.cs +++ b/src/FoxIDs.Shared/Logic/MasterTenantLogic.cs @@ -66,6 +66,7 @@ public async Task CreateMasterLoginDocumentAsync(string tenantName EnableCreateUser = false, EnableCancelLogin = false, SessionLifetime = 0, + SessionAbsoluteLifetime = 0, PersistentSessionLifetimeUnlimited = false, LogoutConsent = LoginUpPartyLogoutConsent.IfRequired }; From 2a2c387d5ba87ba334f5cc4b060d5609961c37d0 Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Wed, 22 Feb 2023 21:04:18 +0100 Subject: [PATCH 05/20] docs Reverse proxy --- docs/reverse-proxy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index 360d04a8a..c08091e00 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -45,11 +45,11 @@ FoxIDs generally support all reverse proxies. The following reverse proxies is t ### Azure Front Door Azure Front Door can be configured as a reverse proxy with close to the default setup. Azure Front Door rewrite domains by default. -The `X-FoxIDs-Secret` HTTP header can optionally be added but is required to support [custom domain](custom-domain.md). +The `X-FoxIDs-Secret` HTTP header can optionally be added or access to the App Services can be restricted in Azure. > Do NOT enable caching. The `Accept-Language` header is not forwarded if caching is enabled. The header is required by FoxIDs to support cultures. -Disable Session affinity and Health probes. Furthermore, Load balancing is not applicable. +Disable Session affinity. ### Cloudflare Cloudflare can be configured as a reverse proxy. But Cloudflare require a Enterprise plan to rewrite domains (host headers). The `X-FoxIDs-Secret` HTTP header should be added. From 9634f15f3838fcca7f4c6e5cef9d106799255e4d Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Thu, 23 Feb 2023 09:27:21 +0100 Subject: [PATCH 06/20] docs Reverse proxy --- docs/reverse-proxy.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index c08091e00..108ce31eb 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -44,12 +44,18 @@ FoxIDs service support reading the [custom domain](custom-domain.md) (host name) FoxIDs generally support all reverse proxies. The following reverse proxies is tested to work with FoxIDs. ### Azure Front Door -Azure Front Door can be configured as a reverse proxy with close to the default setup. Azure Front Door rewrite domains by default. -The `X-FoxIDs-Secret` HTTP header can optionally be added or access to the App Services can be restricted in Azure. +Azure Front Door can be configured as a reverse proxy. Azure Front Door rewrite domains by default. > Do NOT enable caching. The `Accept-Language` header is not forwarded if caching is enabled. The header is required by FoxIDs to support cultures. -Disable Session affinity. +Configuration: +- Add a Azure Front Door endpoint for both the FoxIDs App Service and the FoxIDs Control App Service +- In the Networking section of the App Services. Enable access restriction to only allow traffic from Azure Front Door +- Optionally add a Front Door endpoint for both the FoxIDs App Service and the FoxIDs Control App Service test slots +- Restrict access to the App Services test slots +- Add the `Settings:TrustProxyHeaders` setting with the value `true` in the FoxIDs App Service (optionally also the test slot) configuration to support [custom domains](custom-domain.md) +- Disable Session affinity +- Optionally configure WAF policies ### Cloudflare Cloudflare can be configured as a reverse proxy. But Cloudflare require a Enterprise plan to rewrite domains (host headers). The `X-FoxIDs-Secret` HTTP header should be added. From 000f251dfcbdde943c8ecd273dc81934f8a8178a Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Thu, 23 Feb 2023 09:54:16 +0100 Subject: [PATCH 07/20] docs Enable test slots for testing --- docs/deployment.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/deployment.md b/docs/deployment.md index 61eb927fc..037860898 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -144,6 +144,16 @@ as a [custom domain](custom-domain.md). ## Reverse proxy It is recommended to place both the FoxIDs Azure App service and the FoxIDs Control Azure App service behind a [reverse proxy](reverse-proxy.md). +## Enable test slots for testing +Both the FoxIDs App Service and FoxIDs Control App service contain a test slots use for [updating](update.md) the sites without downtime. + +It is possible to do preliminary test in the test slots against the production data or create a new dataset for testing. + +Configuration to enable test with production data: +- In Key Vault. Grant the FoxIDs App Service and FoxIDs Control App service test slots access to call Key Vault with the same rights as the FoxIDs App Service and FoxIDs Control App service existing rights. +- In Log Analytics workspace. Grant the FoxIDs App Service and FoxIDs Control App service test slots read access. +- You can optionally add the two test slots behind a [reverse proxy](reverse-proxy.md) or restrict access otherwise + ## Specify default page An alternative default page can be configured for the FoxIDs site using the `Settings:WebsiteUrl` setting. If configured a full URL is required like e.g., `https://www.foxidsxxxx.com`. From f8480fed6d866474c7ba07168c2664ba5bd544ee Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Fri, 24 Feb 2023 13:20:05 +0100 Subject: [PATCH 08/20] ITfoxtec.Identity.Saml2 version 4.8.4 --- src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj | 2 +- src/FoxIDs/FoxIDs.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj b/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj index 66fe013cf..235f3b92c 100644 --- a/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj +++ b/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/FoxIDs/FoxIDs.csproj b/src/FoxIDs/FoxIDs.csproj index 5fdec4cf0..6cea6642c 100644 --- a/src/FoxIDs/FoxIDs.csproj +++ b/src/FoxIDs/FoxIDs.csproj @@ -31,7 +31,7 @@ - + From df2c8330e543e4e186e1eb7a4648e24b5d69402f Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Mon, 27 Feb 2023 20:04:32 +0100 Subject: [PATCH 09/20] Error messages in route binding middleware improved. Docs, Supported standards updated --- docs/standard-support.md | 2 +- .../Infrastructure/Hosting/RouteBindingMiddleware.cs | 8 ++++---- .../Hosting/FoxIDsRouteBindingMiddleware.cs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/standard-support.md b/docs/standard-support.md index 5f90488d9..7ac793a6a 100644 --- a/docs/standard-support.md +++ b/docs/standard-support.md @@ -16,5 +16,5 @@ - [SAML 2.0 metadata](https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf) - OAuth 2.0 limited to down-party [Client Credential Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4) - [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) -- One-Time Password (OPT) supported by MFA +- Two-factor authentication (2FA) with One-Time Password (OPT) - [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238) \ No newline at end of file diff --git a/src/FoxIDs.Shared/Infrastructure/Hosting/RouteBindingMiddleware.cs b/src/FoxIDs.Shared/Infrastructure/Hosting/RouteBindingMiddleware.cs index 1f7f7157e..821bf9f5e 100644 --- a/src/FoxIDs.Shared/Infrastructure/Hosting/RouteBindingMiddleware.cs +++ b/src/FoxIDs.Shared/Infrastructure/Hosting/RouteBindingMiddleware.cs @@ -163,17 +163,17 @@ private async Task GetTrackAsync(Track.IdKey idKey, bool hasCustomDomain) { if (hasCustomDomain && idKey.TenantName.Equals(idKey.TrackName, StringComparison.OrdinalIgnoreCase)) { - throw new RouteCreationException($"Invalid tenant and track name '{idKey.TenantName}'. The URL for a custom domain has to be without the tenant element.", ex); + throw new RouteCreationException($"Invalid tenant and track '{idKey.TenantName}'. The URL for a custom domain has to be without the tenant element.", ex); } - throw new RouteCreationException($"Invalid tenant name '{idKey.TenantName}' and track name '{idKey.TrackName}'.", ex); + throw new RouteCreationException($"Invalid tenant '{idKey.TenantName}' and track '{idKey.TrackName}'.", ex); } } if (hasCustomDomain && idKey.TenantName.Equals(idKey.TrackName, StringComparison.OrdinalIgnoreCase)) { - throw new RouteCreationException($"Error loading tenant and track name '{idKey.TenantName}'.", ex); + throw new RouteCreationException($"Error loading tenant and track '{idKey.TenantName}'.", ex); } - throw new RouteCreationException($"Error loading tenant name '{idKey.TenantName}' and track name '{idKey.TrackName}'.", ex); + throw new RouteCreationException($"Error loading tenant '{idKey.TenantName}' and track '{idKey.TrackName}'.", ex); } } } diff --git a/src/FoxIDs/Infrastructure/Hosting/FoxIDsRouteBindingMiddleware.cs b/src/FoxIDs/Infrastructure/Hosting/FoxIDsRouteBindingMiddleware.cs index b60d6a10c..9c45113a8 100644 --- a/src/FoxIDs/Infrastructure/Hosting/FoxIDsRouteBindingMiddleware.cs +++ b/src/FoxIDs/Infrastructure/Hosting/FoxIDsRouteBindingMiddleware.cs @@ -242,7 +242,7 @@ private async Task GetUpPartyAsync(Track.IdKey trackIdKey, Group upPart } catch (Exception ex) { - throw new RouteCreationException($"Invalid tenantName '{trackIdKey.TenantName}', trackName '{trackIdKey.TrackName}' and upPartyName '{upPartyGroup.Value}'.", ex); + throw new RouteCreationException($"Invalid tenant '{trackIdKey.TenantName}', track '{trackIdKey.TrackName}' and up-party '{upPartyGroup.Value}' combination.", ex); } } @@ -254,7 +254,7 @@ private async Task GetDownPartyAsync(Track.IdKey trackIdKey, Group do } catch (Exception ex) { - throw new RouteCreationException($"Invalid tenantName '{trackIdKey.TenantName}', trackName '{trackIdKey.TrackName}' and downPartyName '{downPartyGroup.Value}'.", ex); + throw new RouteCreationException($"Invalid tenant '{trackIdKey.TenantName}', track '{trackIdKey.TrackName}' and down-party '{downPartyGroup.Value}' combination.", ex); } } From c1e64466cdff52300288f2891dfed35a715b11d0 Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Tue, 28 Feb 2023 13:29:12 +0100 Subject: [PATCH 10/20] docs, Signicat and Nets eID Broker. --- FoxIDs.sln | 2 + docs/oidc.md | 4 ++ docs/up-party-howto-oidc-keycloak.md | 1 + docs/up-party-howto-oidc-nets-eid-broker.md | 27 ++++++++++++ docs/up-party-howto-oidc-signicat.md | 47 +++++++++++++++++++++ docs/up-party-oidc.md | 2 + 6 files changed, 83 insertions(+) create mode 100644 docs/up-party-howto-oidc-keycloak.md create mode 100644 docs/up-party-howto-oidc-nets-eid-broker.md create mode 100644 docs/up-party-howto-oidc-signicat.md diff --git a/FoxIDs.sln b/FoxIDs.sln index 32753881d..f26dd363e 100644 --- a/FoxIDs.sln +++ b/FoxIDs.sln @@ -73,6 +73,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{CB5D86A0-D docs\up-party-howto-oidc-azure-ad.md = docs\up-party-howto-oidc-azure-ad.md docs\up-party-howto-oidc-foxids.md = docs\up-party-howto-oidc-foxids.md docs\up-party-howto-oidc-identityserver.md = docs\up-party-howto-oidc-identityserver.md + docs\up-party-howto-oidc-nets-eid-broker.md = docs\up-party-howto-oidc-nets-eid-broker.md + docs\up-party-howto-oidc-signicat.md = docs\up-party-howto-oidc-signicat.md docs\up-party-howto-saml-2.0-adfs.md = docs\up-party-howto-saml-2.0-adfs.md docs\up-party-howto-saml-2.0-nemlogin.md = docs\up-party-howto-saml-2.0-nemlogin.md docs\up-party-howto-saml-2.0-pingone.md = docs\up-party-howto-saml-2.0-pingone.md diff --git a/docs/oidc.md b/docs/oidc.md index b5952b7a5..160584eab 100644 --- a/docs/oidc.md +++ b/docs/oidc.md @@ -16,6 +16,10 @@ How to guides: - Connect [Azure AD](up-party-howto-oidc-azure-ad.md) - Connect [Azure AD B2C](up-party-howto-oidc-azure-ad-b2c.md) - Connect [IdentityServer](up-party-howto-oidc-identityserver.md) +- Connect [Signicat](up-party-howto-oidc-signicat.md) +- Connect [Nets eID Broker](up-party-howto-oidc-nets-eid-broker.md) + +up-party-howto-oidc-nets-eid-broker ## Down-party diff --git a/docs/up-party-howto-oidc-keycloak.md b/docs/up-party-howto-oidc-keycloak.md new file mode 100644 index 000000000..94b4d0fd3 --- /dev/null +++ b/docs/up-party-howto-oidc-keycloak.md @@ -0,0 +1 @@ +# Up-party - connect Keycloak with OpenID Connect diff --git a/docs/up-party-howto-oidc-nets-eid-broker.md b/docs/up-party-howto-oidc-nets-eid-broker.md new file mode 100644 index 000000000..e7c2084bb --- /dev/null +++ b/docs/up-party-howto-oidc-nets-eid-broker.md @@ -0,0 +1,27 @@ +# Up-party - connect Nets eID Broker with OpenID Connect + +FoxIDs can be connected to Nets eID Broker with OpenID Connect and thereby authenticating end users with MitID. + +> A connection to Nets eID Broker demo can be tested with the [samples](samples.md). E.g., with the [AspNetCoreOidcAuthCodeAllUpPartiesSample](https://github.com/ITfoxtec/FoxIDs.Samples/tree/master/src/AspNetCoreOidcAuthCodeAllUpPartiesSample) in the [sample solution](https://github.com/ITfoxtec/FoxIDs.Samples). + +Nets eID Broker has a [MitID demo](https://broker.signaturgruppen.dk/en/technical-documentation/open-oidc-clients) where all clients can connect without prior registration. All redirect URIs are accepted. +Her you can find all needed to register a client with Nets eID Broker. + +This guide describes how to connect a FoxIDs up-party to Nets eID Broker demo. + +## Configuring Nets eID Broker as OpenID Provider (OP) + +This connection use OpenID Connect Authorization Code flow with PKCE, which is the recommended OpenID Connect flow. + +**Create an OpenID Connect up-party client in [FoxIDs Control Client](control.md#foxids-control-client)** + + 1. Add the name + 2. Add the Nets eID Broker demo authority in the Authority field + 3. In the scopes list add `mitid` (to support MitID) and optionally `nemid` (to support the old NemID) + 4. Add the Nets eID Broker demo secret in the Client secret field + 5. Click create + +That's it, you are done. + +> The new up-party can now be selected as an allowed up-party in a down-party. +> The down-party can read the claims from the up-party. You can optionally add a `*` in the down-party Issue claims list to issue all the claims to your application. diff --git a/docs/up-party-howto-oidc-signicat.md b/docs/up-party-howto-oidc-signicat.md new file mode 100644 index 000000000..ce98860a9 --- /dev/null +++ b/docs/up-party-howto-oidc-signicat.md @@ -0,0 +1,47 @@ +# Up-party - connect Signicat with OpenID Connect + +FoxIDs can be connected to Signicat with OpenID Connect and thereby authenticating end users with MitID and all other credentials supported by Signicat. + +> A connection to Signicat Express can be tested with the [samples](samples.md). E.g., with the [AspNetCoreOidcAuthCodeAllUpPartiesSample](https://github.com/ITfoxtec/FoxIDs.Samples/tree/master/src/AspNetCoreOidcAuthCodeAllUpPartiesSample) in the [sample solution](https://github.com/ITfoxtec/FoxIDs.Samples). + +You can create a [free account](https://www.signicat.com/sign-up/express-api-onboarding) on [Signicat Express](https://developer.signicat.com/express/docs/) and get access to the [dashbord](https://dashboard-test.signicat.io/dashboard). +Her you have access to the test environment. + +This guide describes how to connect a FoxIDs up-party to Signicat Express. + +## Configuring Signicat as OpenID Provider (OP) + +This connection use OpenID Connect Authorization Code flow with PKCE, which is the recommended OpenID Connect flow. + +**1 - Start by creating an API client in [Signicat Express dashbord](https://dashboard-test.signicat.io/dashboard)** + + 1. Navigate to Account and then API Clients + 2. Add the Client name + 3. In Auth Flow / Grant Type select Authorization code + 4. Copy the Secret + 5. Click Create + 6. Copy the Client ID + +**2 - Then create an OpenID Connect up-party client in [FoxIDs Control Client](control.md#foxids-control-client)** + + 1. Add the name + 2. Add the Signicat Express test authority `https://login-test.signicat.io` in the Authority field + 3. Copy the three URLs: `Redirect URL`, `Post logout redirect URL` and `Front channel logout URL` + 4. In the scopes list add `profile` + 5. Add the Signicat Express secret in the Client secret field + 6. Select show advanced settings + 7. Add the Signicat Express client id the Optional customer SP client ID field + 8. Click create + + **3 - Go back to [Signicat Express dashbord](https://dashboard-test.signicat.io/dashboard)** + + 1. Click OAuth / OpenID + 2. Click Edit + 3. Find the App URIs section + 4. Add the three URLs from the FoxIDs up-party client: `Redirect URL`, `Post logout redirect URL` and `Front channel logout URL` in the respectively fields + 5. Click Save + +That's it, you are done. + +> The new up-party can now be selected as an allowed up-party in a down-party. +> The down-party can read the claims from the up-party. You can optionally add a `*` in the down-party Issue claims list to issue all the claims to your application. diff --git a/docs/up-party-oidc.md b/docs/up-party-oidc.md index 387c2980d..bde326235 100644 --- a/docs/up-party-oidc.md +++ b/docs/up-party-oidc.md @@ -12,6 +12,8 @@ How to guides: - Connect [Azure AD](up-party-howto-oidc-azure-ad.md) - Connect [Azure AD B2C](up-party-howto-oidc-azure-ad-b2c.md) - Connect [IdentityServer](up-party-howto-oidc-identityserver.md) +- Connect [Signicat](up-party-howto-oidc-signicat.md) +- Connect [Nets eID Broker](up-party-howto-oidc-nets-eid-broker.md) > It is recommended to use OpenID Connect Authorization Code flow with PKCE, because it is considered a secure flow. From 81a366b75849be863fa9271ae90521b3d3fbfc01 Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Tue, 28 Feb 2023 13:30:21 +0100 Subject: [PATCH 11/20] docs, clean up --- docs/oidc.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/oidc.md b/docs/oidc.md index 160584eab..a6b11898d 100644 --- a/docs/oidc.md +++ b/docs/oidc.md @@ -19,8 +19,6 @@ How to guides: - Connect [Signicat](up-party-howto-oidc-signicat.md) - Connect [Nets eID Broker](up-party-howto-oidc-nets-eid-broker.md) -up-party-howto-oidc-nets-eid-broker - ## Down-party Configure your application as a [down-party OpenID Connect](down-party-oidc.md). From c2680a1856cfc08d7d9c4ceb38b175c88b19ab45 Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Tue, 28 Feb 2023 13:58:14 +0100 Subject: [PATCH 12/20] docs, Nets eID Broker more details --- docs/up-party-howto-oidc-nets-eid-broker.md | 12 +++++++----- docs/up-party-howto-oidc-signicat.md | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/up-party-howto-oidc-nets-eid-broker.md b/docs/up-party-howto-oidc-nets-eid-broker.md index e7c2084bb..f5ea3c40b 100644 --- a/docs/up-party-howto-oidc-nets-eid-broker.md +++ b/docs/up-party-howto-oidc-nets-eid-broker.md @@ -15,11 +15,13 @@ This connection use OpenID Connect Authorization Code flow with PKCE, which is t **Create an OpenID Connect up-party client in [FoxIDs Control Client](control.md#foxids-control-client)** - 1. Add the name - 2. Add the Nets eID Broker demo authority in the Authority field - 3. In the scopes list add `mitid` (to support MitID) and optionally `nemid` (to support the old NemID) - 4. Add the Nets eID Broker demo secret in the Client secret field - 5. Click create +1. Add the name +2. Add the Nets eID Broker demo authority `https://pp.netseidbroker.dk/op` in the Authority field +3. In the scopes list add `mitid` (to support MitID) and optionally `nemid` (to support the old NemID) +4. Add the Nets eID Broker demo secret `rnlguc7CM/wmGSti4KCgCkWBQnfslYr0lMDZeIFsCJweROTROy2ajEigEaPQFl76Py6AVWnhYofl/0oiSAgdtg==` in the Client secret field +5. Select show advanced settings +6. Add the Signicat Express client id `0a775a87-878c-4b83-abe3-ee29c720c3e7` in the Optional customer SP client ID field +7. Click create That's it, you are done. diff --git a/docs/up-party-howto-oidc-signicat.md b/docs/up-party-howto-oidc-signicat.md index ce98860a9..1d952faa5 100644 --- a/docs/up-party-howto-oidc-signicat.md +++ b/docs/up-party-howto-oidc-signicat.md @@ -30,7 +30,7 @@ This connection use OpenID Connect Authorization Code flow with PKCE, which is t 4. In the scopes list add `profile` 5. Add the Signicat Express secret in the Client secret field 6. Select show advanced settings - 7. Add the Signicat Express client id the Optional customer SP client ID field + 7. Add the Signicat Express client id in the Optional customer SP client ID field 8. Click create **3 - Go back to [Signicat Express dashbord](https://dashboard-test.signicat.io/dashboard)** From 0fc26935e173502f5c340990f41bce98cddcadbe Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Tue, 28 Feb 2023 20:21:27 +0100 Subject: [PATCH 13/20] clean up --- docs/up-party-howto-oidc-keycloak.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 docs/up-party-howto-oidc-keycloak.md diff --git a/docs/up-party-howto-oidc-keycloak.md b/docs/up-party-howto-oidc-keycloak.md deleted file mode 100644 index 94b4d0fd3..000000000 --- a/docs/up-party-howto-oidc-keycloak.md +++ /dev/null @@ -1 +0,0 @@ -# Up-party - connect Keycloak with OpenID Connect From 1cbcf6d9ac3d8560da5a6eb837513630e3e217a7 Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Tue, 7 Mar 2023 09:29:40 +0100 Subject: [PATCH 14/20] docs Nets eID Broker and how to. --- FoxIDs.sln | 2 + docs/_sidebar.md | 1 + docs/up-party-howto-oidc-nets-eid-broker.md | 64 +++++++++++++++++++-- docs/up-party-howto-oidc-signicat.md | 2 +- docs/up-party-howto.md | 29 ++++++++++ 5 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 docs/up-party-howto.md diff --git a/FoxIDs.sln b/FoxIDs.sln index f26dd363e..1bf2bc344 100644 --- a/FoxIDs.sln +++ b/FoxIDs.sln @@ -73,11 +73,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{CB5D86A0-D docs\up-party-howto-oidc-azure-ad.md = docs\up-party-howto-oidc-azure-ad.md docs\up-party-howto-oidc-foxids.md = docs\up-party-howto-oidc-foxids.md docs\up-party-howto-oidc-identityserver.md = docs\up-party-howto-oidc-identityserver.md + docs\up-party-howto-oidc-nets-eid-broker-demo.md = docs\up-party-howto-oidc-nets-eid-broker-demo.md docs\up-party-howto-oidc-nets-eid-broker.md = docs\up-party-howto-oidc-nets-eid-broker.md docs\up-party-howto-oidc-signicat.md = docs\up-party-howto-oidc-signicat.md docs\up-party-howto-saml-2.0-adfs.md = docs\up-party-howto-saml-2.0-adfs.md docs\up-party-howto-saml-2.0-nemlogin.md = docs\up-party-howto-saml-2.0-nemlogin.md docs\up-party-howto-saml-2.0-pingone.md = docs\up-party-howto-saml-2.0-pingone.md + docs\up-party-howto.md = docs\up-party-howto.md docs\up-party-oidc.md = docs\up-party-oidc.md docs\up-party-saml-2.0.md = docs\up-party-saml-2.0.md docs\update.md = docs\update.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 1493bef29..94fc9b6d9 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -2,6 +2,7 @@ - [Getting Started](getting-started.md) - [Parties](parties.md) - [Login & HRD & 2FA/MFA](login.md) + - [How to connect IdP](up-party-howto.md) - [OpenID Connect](oidc.md) - [OAuth 2.0](oauth-2.0.md) - [SAML 2.0](saml-2.0.md) diff --git a/docs/up-party-howto-oidc-nets-eid-broker.md b/docs/up-party-howto-oidc-nets-eid-broker.md index f5ea3c40b..dccba2860 100644 --- a/docs/up-party-howto-oidc-nets-eid-broker.md +++ b/docs/up-party-howto-oidc-nets-eid-broker.md @@ -1,15 +1,19 @@ # Up-party - connect Nets eID Broker with OpenID Connect -FoxIDs can be connected to Nets eID Broker with OpenID Connect and thereby authenticating end users with MitID. +FoxIDs can be connected to Nets eID Broker with OpenID Connect and thereby authenticating end users with MitID and other credentials supported by Nets eID Broker. + +How to configure Nets eID Broker in +- [test environment](#configuring-nets-eid-broker-demotest-as-openid-provider-op) using Nets eID Broker demo +- [production environment](#configuring-nets-eid-broker-as-openid-provider-op) using Nets eID Broker admin portal > A connection to Nets eID Broker demo can be tested with the [samples](samples.md). E.g., with the [AspNetCoreOidcAuthCodeAllUpPartiesSample](https://github.com/ITfoxtec/FoxIDs.Samples/tree/master/src/AspNetCoreOidcAuthCodeAllUpPartiesSample) in the [sample solution](https://github.com/ITfoxtec/FoxIDs.Samples). -Nets eID Broker has a [MitID demo](https://broker.signaturgruppen.dk/en/technical-documentation/open-oidc-clients) where all clients can connect without prior registration. All redirect URIs are accepted. -Her you can find all needed to register a client with Nets eID Broker. +## Configuring Nets eID Broker demo/test as OpenID Provider (OP) -This guide describes how to connect a FoxIDs up-party to Nets eID Broker demo. +This guide describes how to connect a FoxIDs up-party to Nets eID Broker demo in the test environment. -## Configuring Nets eID Broker as OpenID Provider (OP) +Nets eID Broker has a [MitID demo](https://broker.signaturgruppen.dk/en/technical-documentation/open-oidc-clients) where all clients can connect without prior registration. All redirect URIs are accepted. +Her you can find all needed to register a client with Nets eID Broker. This connection use OpenID Connect Authorization Code flow with PKCE, which is the recommended OpenID Connect flow. @@ -20,10 +24,58 @@ This connection use OpenID Connect Authorization Code flow with PKCE, which is t 3. In the scopes list add `mitid` (to support MitID) and optionally `nemid` (to support the old NemID) 4. Add the Nets eID Broker demo secret `rnlguc7CM/wmGSti4KCgCkWBQnfslYr0lMDZeIFsCJweROTROy2ajEigEaPQFl76Py6AVWnhYofl/0oiSAgdtg==` in the Client secret field 5. Select show advanced settings -6. Add the Signicat Express client id `0a775a87-878c-4b83-abe3-ee29c720c3e7` in the Optional customer SP client ID field +6. Add the Nets eID Broker demo client id `0a775a87-878c-4b83-abe3-ee29c720c3e7` in the Optional customer SP client ID field 7. Click create That's it, you are done. > The new up-party can now be selected as an allowed up-party in a down-party. > The down-party can read the claims from the up-party. You can optionally add a `*` in the down-party Issue claims list to issue all the claims to your application. + +## Configuring Nets eID Broker as OpenID Provider (OP) + +This guide describes how to connect a FoxIDs up-party to the Nets eID Broker in the production environment. + +You are granted access to the [Nets eID Broker admin portal](https://netseidbroker.dk/admin) by Nets. The Nets eID Broker [documentation](https://broker.signaturgruppen.dk/en/technical-documentation). + +This connection use OpenID Connect Authorization Code flow with PKCE, which is the recommended OpenID Connect flow. + +**1 - Start by creating an API client in [Nets eID Broker admin portal](https://netseidbroker.dk/admin)** + + 1. Navigate to Services & Clients + 2. Select the Service Provider + 3. Create or select a Service + 4. Click Add new client + 5. Add a Client name + 6. Select Web + 7. Click Create + 8. Copy the Client ID + 9. Click Create new Client Secret + 10. Select Based on password + 11. Add a name for the new client secret + 12. Click Generate on server + 13. Copy the Secret + 14. Click the Endpoints tab + 15. Set PKCE to Active + + +**2 - Then create an OpenID Connect up-party client in [FoxIDs Control Client](control.md#foxids-control-client)** + +1. Add the name +2. Add the Nets eID Broker demo authority `https://netseidbroker.dk/op` in the Authority field +3. Copy the two URLs: `Redirect URL` and `Post logout redirect URL` +4. In the scopes list add `mitid` (to support MitID) and optionally `nemid` (to support the old NemID) +5. Add the Nets eID Broker secret in the Client secret field +6. Select show advanced settings +7. Add the Nets eID Broker client id in the Optional customer SP client ID field +8. Click create + + **3 - Go back to [Nets eID Broker admin portal](https://netseidbroker.dk/admin)** + + 1. Click the Endpoints tab + 2. Add the two URLs from the FoxIDs up-party client: `Redirect URL` and `Post logout redirect URL` in the fields `Login redirects` and `Logout redirects`. + +That's it, you are done. + +> The new up-party can now be selected as an allowed up-party in a down-party. +> The down-party can read the claims from the up-party. You can optionally add a `*` in the down-party Issue claims list to issue all the claims to your application. \ No newline at end of file diff --git a/docs/up-party-howto-oidc-signicat.md b/docs/up-party-howto-oidc-signicat.md index 1d952faa5..beceaf4e9 100644 --- a/docs/up-party-howto-oidc-signicat.md +++ b/docs/up-party-howto-oidc-signicat.md @@ -7,7 +7,7 @@ FoxIDs can be connected to Signicat with OpenID Connect and thereby authenticati You can create a [free account](https://www.signicat.com/sign-up/express-api-onboarding) on [Signicat Express](https://developer.signicat.com/express/docs/) and get access to the [dashbord](https://dashboard-test.signicat.io/dashboard). Her you have access to the test environment. -This guide describes how to connect a FoxIDs up-party to Signicat Express. +This guide describes how to connect a FoxIDs up-party to the Signicat Express test environment. ## Configuring Signicat as OpenID Provider (OP) diff --git a/docs/up-party-howto.md b/docs/up-party-howto.md new file mode 100644 index 000000000..afee3c501 --- /dev/null +++ b/docs/up-party-howto.md @@ -0,0 +1,29 @@ +# Up-party - How to connect Identity Provider (IdP) + +An Identity Provider (IdP) can be connected with an [OpenID Connect up-party](#openid-connect-up-party) or an [SAML 2.0 up-party](#saml-20-up-party). An Identity Provider (IdP) is more precisely called an OpenID Provider (OP) if configured with OpenID Connect. + +All IdPs supporting either OpenID Connect or SAML 2.0 can be connected to FoxIDs. The following is how to guides for some IdPs, more guides will be added over time. + +## OpenID Connect up-party + +Configure [OpenID Connect up-party](up-party-oidc.md) which trust an external OpenID Provider (OP) - *an Identity Provider (IdP) is called an OpenID Provider (OP) if configured with OpenID Connect*. + +How to guides: + +- Connect [FoxIDs](up-party-howto-oidc-foxids.md) between tracks, optionally in different tenants +- Connect [Azure AD](up-party-howto-oidc-azure-ad.md) +- Connect [Azure AD B2C](up-party-howto-oidc-azure-ad-b2c.md) +- Connect [IdentityServer](up-party-howto-oidc-identityserver.md) +- Connect [Signicat](up-party-howto-oidc-signicat.md) +- Connect [Nets eID Broker](up-party-howto-oidc-nets-eid-broker.md) + +## SAML 2.0 up-party + +Configure [SAML 2.0 up-party](up-party-saml-2.0.md) which trust an external SAML 2.0 Identity Provider (IdP). + +How to guides: + +- Connect [AD FS](up-party-howto-saml-2.0-adfs.md) +- Connect [PingIdentity / PingOne](up-party-howto-saml-2.0-pingone.md) +- Connect [NemLog-in (Danish IdP)](up-party-howto-saml-2.0-nemlogin.md) +- Connect [Context Handler (Danish IdP)](howto-saml-2.0-context-handler.md#up-party---connect-to-context-handler) \ No newline at end of file From 77d72ab8d2f4606811bd9a3aa1be620cb04b8ad6 Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Tue, 7 Mar 2023 09:30:02 +0100 Subject: [PATCH 15/20] docs clean up --- FoxIDs.sln | 1 - 1 file changed, 1 deletion(-) diff --git a/FoxIDs.sln b/FoxIDs.sln index 1bf2bc344..debce1aa5 100644 --- a/FoxIDs.sln +++ b/FoxIDs.sln @@ -73,7 +73,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{CB5D86A0-D docs\up-party-howto-oidc-azure-ad.md = docs\up-party-howto-oidc-azure-ad.md docs\up-party-howto-oidc-foxids.md = docs\up-party-howto-oidc-foxids.md docs\up-party-howto-oidc-identityserver.md = docs\up-party-howto-oidc-identityserver.md - docs\up-party-howto-oidc-nets-eid-broker-demo.md = docs\up-party-howto-oidc-nets-eid-broker-demo.md docs\up-party-howto-oidc-nets-eid-broker.md = docs\up-party-howto-oidc-nets-eid-broker.md docs\up-party-howto-oidc-signicat.md = docs\up-party-howto-oidc-signicat.md docs\up-party-howto-saml-2.0-adfs.md = docs\up-party-howto-saml-2.0-adfs.md From 75d76e01eaad9bb5dbd2e880e7838e0161006c5f Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Tue, 7 Mar 2023 09:51:31 +0100 Subject: [PATCH 16/20] Control Client, create tenant - default change password and confirm. --- src/FoxIDs.Control/FoxIDs.Control.csproj | 2 +- .../FoxIDs.ControlClient.csproj | 2 +- .../Tenants/CreateTenantViewModel.cs | 18 ++++++++++++------ .../Shared/MainLayout.razor | 1 + .../FoxIDs.ControlShared.csproj | 2 +- .../Models/Api/Tenants/CreateTenantRequest.cs | 4 ++-- src/FoxIDs.Shared/FoxIDs.Shared.csproj | 2 +- src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj | 2 +- src/FoxIDs/FoxIDs.csproj | 2 +- 9 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/FoxIDs.Control/FoxIDs.Control.csproj b/src/FoxIDs.Control/FoxIDs.Control.csproj index 6b9bb531f..7ccbeae2d 100644 --- a/src/FoxIDs.Control/FoxIDs.Control.csproj +++ b/src/FoxIDs.Control/FoxIDs.Control.csproj @@ -2,7 +2,7 @@ net7.0 - 1.0.12 + 1.0.13 FoxIDs Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj b/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj index ca4bf9e80..7d5b65225 100644 --- a/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj +++ b/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj @@ -2,7 +2,7 @@ net7.0 - 1.0.12 + 1.0.13 FoxIDs.Client Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Tenants/CreateTenantViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Tenants/CreateTenantViewModel.cs index 8d5545f14..fdbe56257 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Tenants/CreateTenantViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Tenants/CreateTenantViewModel.cs @@ -23,12 +23,6 @@ public class CreateTenantViewModel [Display(Name = "Administrator email")] public string AdministratorEmail { get; set; } - /// - /// True if the administrator account should be confirmed. - /// - [Display(Name = "Confirm administrator account")] - public bool ConfirmAdministratorAccount { get; set; } - /// /// Administrator password. /// @@ -38,6 +32,18 @@ public class CreateTenantViewModel [Display(Name = "Administrator password")] public string AdministratorPassword { get; set; } + /// + /// True if the administrator account password should be changed on first login. Default true. + /// + [Display(Name = "Change administrator password")] + public bool ChangeAdministratorPassword { get; set; } = true; + + /// + /// True if the administrator account should be confirmed. Default true. + /// + [Display(Name = "Confirm administrator account")] + public bool ConfirmAdministratorAccount { get; set; } = true; + /// /// Plan (optional). /// diff --git a/src/FoxIDs.ControlClient/Shared/MainLayout.razor b/src/FoxIDs.ControlClient/Shared/MainLayout.razor index 55aa4f192..81e0a885f 100644 --- a/src/FoxIDs.ControlClient/Shared/MainLayout.razor +++ b/src/FoxIDs.ControlClient/Shared/MainLayout.razor @@ -133,6 +133,7 @@ + diff --git a/src/FoxIDs.ControlShared/FoxIDs.ControlShared.csproj b/src/FoxIDs.ControlShared/FoxIDs.ControlShared.csproj index 84519fb45..29199d555 100644 --- a/src/FoxIDs.ControlShared/FoxIDs.ControlShared.csproj +++ b/src/FoxIDs.ControlShared/FoxIDs.ControlShared.csproj @@ -2,7 +2,7 @@ net7.0 - 1.0.12 + 1.0.13 FoxIDs Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.ControlShared/Models/Api/Tenants/CreateTenantRequest.cs b/src/FoxIDs.ControlShared/Models/Api/Tenants/CreateTenantRequest.cs index f43f99ed9..5d0a65338 100644 --- a/src/FoxIDs.ControlShared/Models/Api/Tenants/CreateTenantRequest.cs +++ b/src/FoxIDs.ControlShared/Models/Api/Tenants/CreateTenantRequest.cs @@ -23,13 +23,13 @@ public class CreateTenantRequest : Tenant public string AdministratorPassword { get; set; } /// - /// True if the administrator account password should be changed on first login. + /// True if the administrator account password should be changed on first login. Default true. /// [Display(Name = "Change administrator password")] public bool ChangeAdministratorPassword { get; set; } /// - /// True if the administrator account email should be confirmed. + /// True if the administrator account email should be confirmed. Default true. /// [Display(Name = "Confirm administrator account")] public bool ConfirmAdministratorAccount { get; set; } diff --git a/src/FoxIDs.Shared/FoxIDs.Shared.csproj b/src/FoxIDs.Shared/FoxIDs.Shared.csproj index 861cbfff6..b6011b955 100644 --- a/src/FoxIDs.Shared/FoxIDs.Shared.csproj +++ b/src/FoxIDs.Shared/FoxIDs.Shared.csproj @@ -2,7 +2,7 @@ net7.0 - 1.0.12 + 1.0.13 FoxIDs Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj b/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj index 235f3b92c..1864f8c53 100644 --- a/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj +++ b/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj @@ -2,7 +2,7 @@ net7.0 - 1.0.12 + 1.0.13 FoxIDs Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs/FoxIDs.csproj b/src/FoxIDs/FoxIDs.csproj index 6cea6642c..aeb857748 100644 --- a/src/FoxIDs/FoxIDs.csproj +++ b/src/FoxIDs/FoxIDs.csproj @@ -1,7 +1,7 @@  net7.0 - 1.0.12 + 1.0.13 FoxIDs Anders Revsgaard ITfoxtec From 165b41ad337c2a1dc8477c36886df912a77499f1 Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Tue, 7 Mar 2023 14:07:11 +0100 Subject: [PATCH 17/20] docs Nets eID Broker --- docs/up-party-howto-oidc-nets-eid-broker.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/up-party-howto-oidc-nets-eid-broker.md b/docs/up-party-howto-oidc-nets-eid-broker.md index dccba2860..52799fbb9 100644 --- a/docs/up-party-howto-oidc-nets-eid-broker.md +++ b/docs/up-party-howto-oidc-nets-eid-broker.md @@ -25,7 +25,8 @@ This connection use OpenID Connect Authorization Code flow with PKCE, which is t 4. Add the Nets eID Broker demo secret `rnlguc7CM/wmGSti4KCgCkWBQnfslYr0lMDZeIFsCJweROTROy2ajEigEaPQFl76Py6AVWnhYofl/0oiSAgdtg==` in the Client secret field 5. Select show advanced settings 6. Add the Nets eID Broker demo client id `0a775a87-878c-4b83-abe3-ee29c720c3e7` in the Optional customer SP client ID field -7. Click create +7. Select use claims from ID token +8. Click create That's it, you are done. @@ -55,20 +56,22 @@ This connection use OpenID Connect Authorization Code flow with PKCE, which is t 11. Add a name for the new client secret 12. Click Generate on server 13. Copy the Secret - 14. Click the Endpoints tab - 15. Set PKCE to Active - - + 14. Click the IDP tab + 15. Select MitID and click `Add to pre-selected login options`, optionally select others + 16. Click the Advanced tab + 17. Set PKCE to Active + **2 - Then create an OpenID Connect up-party client in [FoxIDs Control Client](control.md#foxids-control-client)** 1. Add the name 2. Add the Nets eID Broker demo authority `https://netseidbroker.dk/op` in the Authority field 3. Copy the two URLs: `Redirect URL` and `Post logout redirect URL` -4. In the scopes list add `mitid` (to support MitID) and optionally `nemid` (to support the old NemID) +4. In the scopes list add `mitid` (to support MitID) and optionally other scopes like e.g, `nemid.pid` to request the NemID PID and/or `ssn` to request the CPR number 5. Add the Nets eID Broker secret in the Client secret field 6. Select show advanced settings 7. Add the Nets eID Broker client id in the Optional customer SP client ID field -8. Click create +8. Select use claims from ID token +9. Click create **3 - Go back to [Nets eID Broker admin portal](https://netseidbroker.dk/admin)** From 3519c7a0c352806d7392d1865e8e8bfa4b3ee92d Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Tue, 7 Mar 2023 16:04:27 +0100 Subject: [PATCH 18/20] Control API ReadCertificate. improve missing private key error message. --- .../Controllers/Helpers/TReadCertificateController.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/FoxIDs.Control/Controllers/Helpers/TReadCertificateController.cs b/src/FoxIDs.Control/Controllers/Helpers/TReadCertificateController.cs index 60d97895d..83ef3211b 100644 --- a/src/FoxIDs.Control/Controllers/Helpers/TReadCertificateController.cs +++ b/src/FoxIDs.Control/Controllers/Helpers/TReadCertificateController.cs @@ -39,9 +39,18 @@ public TReadCertificateController(TelemetryScopedLogger logger, IMapper mapper) false => new X509Certificate2(WebEncoders.Base64UrlDecode(certificateAndPassword.EncodeCertificate), certificateAndPassword.Password, keyStorageFlags: X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable), }; + if (!certificate.HasPrivateKey) + { + throw new ValidationException("Unable to read the certificates private key. E.g, try to convert the certificate and save the certificate with 'TripleDES-SHA1'."); + } + var jwt = await certificate.ToFTJsonWebKeyAsync(includePrivateKey: true); return Ok(mapper.Map(jwt)); } + catch (ValidationException) + { + throw; + } catch (Exception ex) { throw new ValidationException("Unable to read certificate.", ex); From 837cf2948f76fb5fc2a72404be944ea969eb2586 Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Tue, 7 Mar 2023 20:41:05 +0100 Subject: [PATCH 19/20] docs, Nets eID Broker - scope and claims --- docs/up-party-howto-oidc-nets-eid-broker.md | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/up-party-howto-oidc-nets-eid-broker.md b/docs/up-party-howto-oidc-nets-eid-broker.md index 52799fbb9..4bf3edbae 100644 --- a/docs/up-party-howto-oidc-nets-eid-broker.md +++ b/docs/up-party-howto-oidc-nets-eid-broker.md @@ -31,7 +31,7 @@ This connection use OpenID Connect Authorization Code flow with PKCE, which is t That's it, you are done. > The new up-party can now be selected as an allowed up-party in a down-party. -> The down-party can read the claims from the up-party. You can optionally add a `*` in the down-party Issue claims list to issue all the claims to your application. +> The down-party can read the claims from the up-party. You can optionally add a `*` in the down-party Issue claims list to issue all the claims to your application. Or optionally define a [scope to issue claims](#scope-and-claims). ## Configuring Nets eID Broker as OpenID Provider (OP) @@ -81,4 +81,23 @@ This connection use OpenID Connect Authorization Code flow with PKCE, which is t That's it, you are done. > The new up-party can now be selected as an allowed up-party in a down-party. -> The down-party can read the claims from the up-party. You can optionally add a `*` in the down-party Issue claims list to issue all the claims to your application. \ No newline at end of file +> The down-party can read the claims from the up-party. You can optionally add a `*` in the down-party Issue claims list to issue all the claims to your application. Or optionally define a [scope to issue claims](#scope-and-claims). + +## Scope and claims +You can optionally create a scope on the down-party with the Nets eID Broker claims as voluntary claims. The scope can then be used by a OpenID Connect client or another FoxIDs up-party acting as a OpenID Connect client. + +The name of the scope can e.g, be `nets_eid_broker` + +The most used Nets eID Broker claims: + +- `identity_type` +- `nemid.pid` +- `nemid.pid_status` +- `dk.cpr` +- `loa` +- `acr` +- `neb_sid` +- `idp` +- `idp_transaction_id` +- `transaction_id` +- `session_expiry` \ No newline at end of file From aedb65d40d5dcae04e512a1f2b9f033dfa6a9fba Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Tue, 7 Mar 2023 21:53:07 +0100 Subject: [PATCH 20/20] Resolve track and up-party cookie has custom domain bug. Improve RouteBinding load. --- .../Repository/TrackCookieRepository.cs | 59 ++++++++-------- .../Repository/UpPartyCookieRepository.cs | 67 ++++++++++--------- 2 files changed, 66 insertions(+), 60 deletions(-) diff --git a/src/FoxIDs/Repository/TrackCookieRepository.cs b/src/FoxIDs/Repository/TrackCookieRepository.cs index 936ccc8ab..4a8631527 100644 --- a/src/FoxIDs/Repository/TrackCookieRepository.cs +++ b/src/FoxIDs/Repository/TrackCookieRepository.cs @@ -42,23 +42,24 @@ public Task DeleteAsync() private TMessage Get() { - if (RouteBindingDoNotExists()) return null; - CheckRouteBinding(); + var routeBinding = GetRouteBinding(); + if (RouteBindingDoNotExists(routeBinding)) return null; + CheckRouteBinding(routeBinding); - logger.ScopeTrace(() => $"Get track cookie '{typeof(TMessage).Name}', route '{RouteBinding.Route}'."); + logger.ScopeTrace(() => $"Get track cookie '{typeof(TMessage).Name}', route '{routeBinding.Route}'."); var cookie = httpContextAccessor.HttpContext.Request.Cookies[CookieName()]; if (!cookie.IsNullOrWhiteSpace()) { try { - var envelope = CookieEnvelope.FromCookieString(CreateProtector(), cookie); + var envelope = CookieEnvelope.FromCookieString(CreateProtector(routeBinding), cookie); return envelope.Message; } catch (CryptographicException ex) { logger.Warning(ex, $"Unable to unprotect track cookie '{typeof(TMessage).Name}', deleting cookie."); - DeleteByName(CookieName()); + DeleteByName(routeBinding, CookieName()); return null; } catch (Exception ex) @@ -74,10 +75,11 @@ private TMessage Get() private void Save(TMessage message) { - CheckRouteBinding(); + var routeBinding = GetRouteBinding(); + CheckRouteBinding(routeBinding); if (message == null) new ArgumentNullException(nameof(message)); - logger.ScopeTrace(() => $"Save track cookie '{typeof(TMessage).Name}', route '{RouteBinding.Route}'."); + logger.ScopeTrace(() => $"Save track cookie '{typeof(TMessage).Name}', route '{routeBinding.Route}'."); var cookieOptions = new CookieOptions { @@ -85,7 +87,7 @@ private void Save(TMessage message) HttpOnly = true, SameSite = message.SameSite, IsEssential = true, - Path = GetPath(), + Path = GetPath(routeBinding), }; httpContextAccessor.HttpContext.Response.Cookies.Append( @@ -93,37 +95,38 @@ private void Save(TMessage message) new CookieEnvelope { Message = message, - }.ToCookieString(CreateProtector()), + }.ToCookieString(CreateProtector(routeBinding)), cookieOptions); } private void Delete() { - if (RouteBindingDoNotExists()) return; - CheckRouteBinding(); + var routeBinding = GetRouteBinding(); + if (RouteBindingDoNotExists(routeBinding)) return; + CheckRouteBinding(routeBinding); - logger.ScopeTrace(() => $"Delete track cookie '{typeof(TMessage).Name}', route '{RouteBinding.Route}'."); + logger.ScopeTrace(() => $"Delete track cookie '{typeof(TMessage).Name}', route '{routeBinding.Route}'."); - DeleteByName(CookieName()); + DeleteByName(routeBinding, CookieName()); } - private void CheckRouteBinding() + private void CheckRouteBinding(RouteBinding routeBinding) { - if (RouteBinding == null) new ArgumentNullException(nameof(RouteBinding)); - if (RouteBinding.TenantName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(RouteBinding.TenantName), RouteBinding.GetTypeName()); - if (RouteBinding.TrackName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(RouteBinding.TrackName), RouteBinding.GetTypeName()); + if (routeBinding == null) new ArgumentNullException(nameof(routeBinding)); + if (routeBinding.TenantName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(routeBinding.TenantName), routeBinding.GetTypeName()); + if (routeBinding.TrackName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(routeBinding.TrackName), routeBinding.GetTypeName()); } - private bool RouteBindingDoNotExists() + private bool RouteBindingDoNotExists(RouteBinding routeBinding) { - if (RouteBinding == null) return true; - if (RouteBinding.TenantName.IsNullOrEmpty()) return true; - if (RouteBinding.TrackName.IsNullOrEmpty()) return true; + if (routeBinding == null) return true; + if (routeBinding.TenantName.IsNullOrEmpty()) return true; + if (routeBinding.TrackName.IsNullOrEmpty()) return true; return false; } - private void DeleteByName(string name) + private void DeleteByName(RouteBinding routeBinding, string name) { httpContextAccessor.HttpContext.Response.Cookies.Append( name, @@ -135,18 +138,18 @@ private void DeleteByName(string name) HttpOnly = true, SameSite = new TMessage().SameSite, IsEssential = true, - Path = GetPath(), + Path = GetPath(routeBinding), }); } - private string GetPath() + private string GetPath(RouteBinding routeBinding) { - return $"/{RouteBinding.TenantName}/{RouteBinding.TrackName}"; + return $"{(!routeBinding.HasCustomDomain ? $"/{routeBinding.TenantName}" : string.Empty)}/{routeBinding.TrackName}"; } - private IDataProtector CreateProtector() + private IDataProtector CreateProtector(RouteBinding routeBinding) { - return dataProtection.CreateProtector(new[] { RouteBinding.TenantName, RouteBinding.TrackName }); + return dataProtection.CreateProtector(new[] { routeBinding.TenantName, routeBinding.TrackName }); } private string CookieName() @@ -154,6 +157,6 @@ private string CookieName() return typeof(TMessage).Name.ToLower(); } - private RouteBinding RouteBinding => httpContextAccessor.HttpContext.GetRouteBinding(); + private RouteBinding GetRouteBinding() => httpContextAccessor.HttpContext.GetRouteBinding(); } } diff --git a/src/FoxIDs/Repository/UpPartyCookieRepository.cs b/src/FoxIDs/Repository/UpPartyCookieRepository.cs index 3ca80359b..a61dec6d6 100644 --- a/src/FoxIDs/Repository/UpPartyCookieRepository.cs +++ b/src/FoxIDs/Repository/UpPartyCookieRepository.cs @@ -42,22 +42,23 @@ public Task DeleteAsync(UpParty party, bool tryDelete = false) private TMessage Get(UpParty party, bool delete, bool tryGet = false) { - if (tryGet && RouteBindingDoNotExists()) return null; - CheckRouteBinding(); + var routeBinding = GetRouteBinding(); + if (tryGet && RouteBindingDoNotExists(routeBinding)) return null; + CheckRouteBinding(routeBinding); - logger.ScopeTrace(() => $"Get up-party cookie '{typeof(TMessage).Name}', route '{RouteBinding.Route}', delete '{delete}'."); + logger.ScopeTrace(() => $"Get up-party cookie '{typeof(TMessage).Name}', route '{routeBinding.Route}', delete '{delete}'."); var cookie = httpContextAccessor.HttpContext.Request.Cookies[CookieName()]; if (!cookie.IsNullOrWhiteSpace()) { try { - var envelope = CookieEnvelope.FromCookieString(CreateProtector(), cookie); + var envelope = CookieEnvelope.FromCookieString(CreateProtector(routeBinding), cookie); if (delete) { - logger.ScopeTrace(() => $"Delete up-party cookie, '{typeof(TMessage).Name}', route '{RouteBinding.Route}'."); - DeleteByName(party, CookieName()); + logger.ScopeTrace(() => $"Delete up-party cookie, '{typeof(TMessage).Name}', route '{routeBinding.Route}'."); + DeleteByName(routeBinding, party, CookieName()); } return envelope.Message; @@ -65,7 +66,7 @@ private TMessage Get(UpParty party, bool delete, bool tryGet = false) catch (CryptographicException ex) { logger.Warning(ex, $"Unable to unprotect up-party cookie '{typeof(TMessage).Name}', deleting cookie."); - DeleteByName(party, CookieName()); + DeleteByName(routeBinding, party, CookieName()); return null; } catch (Exception ex) @@ -81,10 +82,11 @@ private TMessage Get(UpParty party, bool delete, bool tryGet = false) private void Save(UpParty party, TMessage message, DateTimeOffset? persistentCookieExpires) { - CheckRouteBinding(); + var routeBinding = GetRouteBinding(); + CheckRouteBinding(routeBinding); if (message == null) new ArgumentNullException(nameof(message)); - logger.ScopeTrace(() => $"Save up-party cookie '{typeof(TMessage).Name}', route '{RouteBinding.Route}'."); + logger.ScopeTrace(() => $"Save up-party cookie '{typeof(TMessage).Name}', route '{routeBinding.Route}'."); var cookieOptions = new CookieOptions { @@ -92,7 +94,7 @@ private void Save(UpParty party, TMessage message, DateTimeOffset? persistentCoo HttpOnly = true, SameSite = message.SameSite, IsEssential = true, - Path = GetPath(party), + Path = GetPath(routeBinding, party), }; if (persistentCookieExpires != null) { @@ -104,39 +106,40 @@ private void Save(UpParty party, TMessage message, DateTimeOffset? persistentCoo new CookieEnvelope { Message = message, - }.ToCookieString(CreateProtector()), + }.ToCookieString(CreateProtector(routeBinding)), cookieOptions); } private void Delete(UpParty party, bool tryDelete = false) { - if (tryDelete && RouteBindingDoNotExists()) return; - CheckRouteBinding(); + var routeBinding = GetRouteBinding(); + if (tryDelete && RouteBindingDoNotExists(routeBinding)) return; + CheckRouteBinding(routeBinding); - logger.ScopeTrace(() => $"Delete up-party cookie '{typeof(TMessage).Name}', route '{RouteBinding.Route}'."); + logger.ScopeTrace(() => $"Delete up-party cookie '{typeof(TMessage).Name}', route '{routeBinding.Route}'."); - DeleteByName(party, CookieName()); + DeleteByName(routeBinding, party, CookieName()); } - private void CheckRouteBinding() + private void CheckRouteBinding(RouteBinding routeBinding) { - if (RouteBinding == null) new ArgumentNullException(nameof(RouteBinding)); - if (RouteBinding.TenantName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(RouteBinding.TenantName), RouteBinding.GetTypeName()); - if (RouteBinding.TrackName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(RouteBinding.TrackName), RouteBinding.GetTypeName()); - if (RouteBinding.UpParty == null) throw new ArgumentNullException(nameof(RouteBinding.UpParty), RouteBinding.GetTypeName()); + if (routeBinding == null) new ArgumentNullException(nameof(routeBinding)); + if (routeBinding.TenantName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(routeBinding.TenantName), routeBinding.GetTypeName()); + if (routeBinding.TrackName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(routeBinding.TrackName), routeBinding.GetTypeName()); + if (routeBinding.UpParty == null) throw new ArgumentNullException(nameof(routeBinding.UpParty), routeBinding.GetTypeName()); } - private bool RouteBindingDoNotExists() + private bool RouteBindingDoNotExists(RouteBinding routeBinding) { - if (RouteBinding == null) return true; - if (RouteBinding.TenantName.IsNullOrEmpty()) return true; - if (RouteBinding.TrackName.IsNullOrEmpty()) return true; - if (RouteBinding.UpParty == null) return true; + if (routeBinding == null) return true; + if (routeBinding.TenantName.IsNullOrEmpty()) return true; + if (routeBinding.TrackName.IsNullOrEmpty()) return true; + if (routeBinding.UpParty == null) return true; return false; } - private void DeleteByName(UpParty party, string name) + private void DeleteByName(RouteBinding routeBinding, UpParty party, string name) { httpContextAccessor.HttpContext.Response.Cookies.Append( name, @@ -148,18 +151,18 @@ private void DeleteByName(UpParty party, string name) HttpOnly = true, SameSite = new TMessage().SameSite, IsEssential = true, - Path = GetPath(party), + Path = GetPath(routeBinding, party), }); } - private string GetPath(UpParty party) + private string GetPath(RouteBinding routeBinding, UpParty party) { - return $"/{RouteBinding.TenantName}/{RouteBinding.TrackName}/{RouteBinding.UpParty.Name.ToUpPartyBinding(party.PartyBindingPattern)}"; + return $"{(!routeBinding.HasCustomDomain ? $"/{routeBinding.TenantName}" : string.Empty)}/{routeBinding.TrackName}/{routeBinding.UpParty.Name.ToUpPartyBinding(party.PartyBindingPattern)}"; } - private IDataProtector CreateProtector() + private IDataProtector CreateProtector(RouteBinding routeBinding) { - return dataProtection.CreateProtector(new[] { RouteBinding.TenantName, RouteBinding.TrackName, RouteBinding.UpParty.Name, typeof(TMessage).Name }); + return dataProtection.CreateProtector(new[] { routeBinding.TenantName, routeBinding.TrackName, routeBinding.UpParty.Name, typeof(TMessage).Name }); } private string CookieName() @@ -167,6 +170,6 @@ private string CookieName() return typeof(TMessage).Name.ToLower(); } - private RouteBinding RouteBinding => httpContextAccessor.HttpContext.GetRouteBinding(); + private RouteBinding GetRouteBinding() => httpContextAccessor.HttpContext.GetRouteBinding(); } }