diff -pruN 2.0.13+ds-3/AUTHORS 2.0.14+ds-1/AUTHORS
--- 2.0.13+ds-3/AUTHORS	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/AUTHORS	2022-02-21 12:04:41.000000000 +0000
@@ -27,4 +27,4 @@ Past and present contributors:
   * Mame Dieynaba SENE
   * Habib ZITOUNI
 
-See http://lemonldap-ng.org/contact#the_team
+See https://lemonldap-ng.org/team.html
diff -pruN 2.0.13+ds-3/changelog 2.0.14+ds-1/changelog
--- 2.0.13+ds-3/changelog	2021-08-20 16:43:32.000000000 +0000
+++ 2.0.14+ds-1/changelog	2022-02-19 16:49:18.000000000 +0000
@@ -1,3 +1,100 @@
+lemonldap-ng (2.0.14) focal; urgency=medium
+
+  * Bugs:
+    * #2519: first authentication returns 500 code after inactivity period
+    * #2566: No configuration available in fresh LemonLDAP 2.0.12
+    * #2594: Double slashes in _pdata->{_url} when LLNG is OIDC RP
+    * #2595: Portal does not run correctly with portalRequireOldPassword=0
+    * #2596: [security:low] open redirect in CAS gateway mode
+    * #2597: External password reset URL is called with skin= and url= parameters
+    * #2600: RESTProxy authentication does not work with AuthChoice-enabled internal Portal
+    * #2603: Saving configuration drops OIDC scope rules
+    * #2606: FindUser plugin: SpoofId field is not updated if a value has been already set before the Ajax request
+    * #2612: [Security: low, CVE-2021-40874] RESTServer pwdConfirm always returns true with Combination + Kerberos
+    * #2613: ProxyAuth cookie name can not be modified
+    * #2616: Login is not remembered when password is incorrect
+    * #2618: DevOps handler does not work if RULES_URL uWSGI/FastCGI parameter is set
+    * #2620: Net::LDAP::Control::PasswordPolicy is not always loaded
+    * #2622: Fail oauth2 grants when resulting scope is empty
+    * #2626: Portal fatal errors cause "Conflict detected between 2 extensions, aborting 1 route" message to appear in logs
+    * #2632: Handler::Server::Nginx does not use logger config from lemonldap-ng.ini
+    * #2637: Error with default locationRules
+    * #2645: importMetadata does not set NameIDFormat to "persistent" for new providers
+    * #2648: "Authentication module succeed but has not set $req->user" when using SAML Artifact mode with some, but not all IDPs
+    * #2655: 'afterData' plugins loaded after Impersonation will be never executed
+    * #2656: CAS: multiple proxies is not correctly implemented
+    * #2658: Macros based on '_XXX' and authenticationLevel attributes are not computed by refresh function
+    * #2660: Combination is not compatible with LDAP password policies
+    * #2663: Radius authentication fails when radius used as authentication module
+    * #2671: xss attack detected on a relayState parameter
+    * #2675: Auth::Custom calls module init twice
+    * #2676: UserDB::Custom and Password::Custom loads module twice and calls init three times
+    * #2677: *::Custom do not allow config overrides
+    * #2678: Auth::Custom getDisplayType is broken with choice
+    * #2682: Fails to create password-protected X509 certificates with OpenSSL 3.0
+    * #2689: REST server: 400 bad request with DELETE /session/my
+    * #2691: Error when using has2f in a manager rule
+    * #2693: "Status: Unknown command line -> " log line for each SKIP and EXPIRED accesses
+    * #2703: OIDC RP menu attributes name do not refresh live
+
+  * New features:
+    * #1411: Web Authentication API (webauthn)
+    * #2325: "Warn on new network location" plugin
+    * #2679: CheckDevOps: Append an option to check if used attributes are existing
+    * #2686: Web service for application list
+
+  * Improvements:
+    * #1714: Check logLevel value
+    * #2277: pdata cookie is not removed if SAML flow fails
+    * #2457: Do not translate OIDC RP exported attributes
+    * #2476: $groups is not initialize  for  at least LDAP authentication
+    * #2508: Look configuration timestamp to dismiss cache
+    * #2558: Add a new portal error code for Auth::OIDC issues
+    * #2565: Adding per-request information in logs
+    * #2570: RGAA: Adding a role attribute into messages
+    * #2577: RGAA: placeholder only should not be used as label
+    * #2591: stayconnected plugin: allow to disable browser fingerprint check and update documentation
+    * #2593: Contextual / Adaptive authentication / Risk-based authentication
+    * #2599: Certificate reset templates are not translated
+    * #2601: RESTProxy authentication does not support Impersonation
+    * #2602: Export OIDC grant type in rules
+    * #2604: Append an option to normalize HTTP headers with CheckDevOps plugin
+    * #2605: llnglanguage cookie will be rejected if sameSite attribute is not set
+    * #2609: Better history management for plugins
+    * #2614: display precise error while sending direct SOAP SAML message
+    * #2617: SafeJail must be enabled with CheckDevOps plugin
+    * #2619: Brazilian translation
+    * #2621: SAML: HTTP-Artifact mode should be discouraged
+    * #2625: Add an option to encrypt TOTP secrets
+    * #2627: Append an option in Manager to be able to set RULES_URL param
+    * #2638: Redirect to 2fregisters is missing a slash
+    * #2644: No error displayed in logs in DevOps Handler when rules file can't be downloaded
+    * #2646: bruteForceProtectionMaxAge and bruteForceProtectionMaxLockTime missing from manager
+    * #2647: Display logins history with CheckUser plugin
+    * #2649: Portal plugins should not require an "init" method
+    * #2651: Hebrew Translation
+    * #2654: CAS temporary tickets should have a short expiration time
+    * #2657: Hidden attributes, custom functions and plugins declarations are inconsistent
+    * #2662: CheckUser plugin: Append a rule to allow some users to display hidden attributes
+    * #2664: impossible to use getModule in the Password modules
+    * #2667: Add RP confkey to oidcGenerateUserInfoResponse plugin hook
+    * #2668: CheckDevOps: prevent portal crash/loop if a bad rules.json file is provided
+    * #2672: DBI password hash list is too restrictive
+    * #2673: Allow to configure multiple service URL per CAS application
+    * #2679: CheckDevOps: Append an option to check if used attributes are existing
+    * #2683: Possibility to set an activation rule for "remember me" option
+    * #2685: DevOps handler uses default HTTPS redirection if no VH is defined
+    * #2694: Chrome warns about compromised data when using form replay
+    * #2698: Avoid useless warning messages in log
+
+  * Templates:
+    * #2325: "Warn on new network location" plugin
+    * #2570: RGAA: Adding a role attribute into messages
+    * #2577: RGAA: placeholder only should not be used as label
+    * #2597: External password reset URL is called with skin= and url= parameters
+
+ -- Clément <clem.oudot@gmail.com>  Sat, 19 Feb 2022 17:49:18 +0100
+
 lemonldap-ng (2.0.13) focal; urgency=medium
 
   * Bugs:
@@ -2041,7 +2138,7 @@ lemonldap-ng (1.0.6) stable; urgency=low
     * [LEMONLDAP-304] - Cannot use spaces between values of Multi
       authentication
       parameter
-    * [LEMONLDAP-305] - Parameters are not overriden in the first Multi module
+    * [LEMONLDAP-305] - Parameters are not overridden in the first Multi module
     * [LEMONLDAP-307] - Base64 encoded IDs can contain more than one "/", but
       only the first is escaped
 
@@ -2049,7 +2146,7 @@ lemonldap-ng (1.0.5) stable; urgency=low
 
     * [LEMONLDAP-292] - Application menu is not well displayed with multiple
       users having differents rights
-    * [LEMONLDAP-294] - Subroutines can not be overriden in lemonldap-ng.ini
+    * [LEMONLDAP-294] - Subroutines can not be overridden in lemonldap-ng.ini
     * [LEMONLDAP-293] - Password Manager - Sending Mail
 
 lemonldap-ng (1.0.4) stable; urgency=low
diff -pruN 2.0.13+ds-3/CONTRIBUTING.md 2.0.14+ds-1/CONTRIBUTING.md
--- 2.0.13+ds-3/CONTRIBUTING.md	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/CONTRIBUTING.md	2022-02-21 12:04:41.000000000 +0000
@@ -3,4 +3,4 @@
 * Repository, issues,... : https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng
 * Translations :
   * software : https://www.transifex.com/lemonldapng/lemonldapng/
-  * documentation : since 2.0, LLNG community supports only english doc
\ No newline at end of file
+  * documentation : since 2.0, LL::NG community supports only english doc
\ No newline at end of file
diff -pruN 2.0.13+ds-3/COPYING 2.0.14+ds-1/COPYING
--- 2.0.13+ds-3/COPYING	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/COPYING	2022-02-21 12:04:41.000000000 +0000
@@ -4,22 +4,22 @@ Upstream-Contact: https://gitlab.ow2.org
 Source: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/tags?sort=updated_desc
 
 Files: *
-Copyright: 2005-2020, Xavier Guimard <yadd@debian.org>
- 2006-2020, Clement Oudot <clem.oudot@gmail.com>
+Copyright: 2005-2022, Xavier Guimard <yadd@debian.org>
+ 2006-2022, Clement Oudot <clem.oudot@gmail.com>
+ 2018-2022, Christophe Maudoux <chrmdx@gmail.com>
+ 2019-2022, Maxime Besson <maxime.besson@worteks.com>
  2008, Mikael Ates <mikael.ates@univ-st-etienne.fr>
  2008-2011, Thomas Chemineau <thomas.chemineau@gmail.com>
  2012-2013, Sandro Cazzaniga <cazzaniga.sandro@gmail.com>
  2012-2015, François-Xavier Deltombe <fxdeltombe@gmail.com>
- 2012-2019, David Coutadeur <david.coutadeur@gmail.com>
- 2018-2020, Christophe Maudoux <chrmdx@gmail.com>
- 2019-2020, Maxime Besson <maxime.besson@worteks.com>
+ 2012-2021, David Coutadeur <david.coutadeur@gmail.com>
  2019, Soisik Frogier <soisik.froger@worteks.com>
  2019, Mame Dieynaba Sene <msene@linagora.com>
- 2019, Antoine Rosier <lemonldap@mon-refuge.fr>
- 2005-2020, Gendarmerie nationale <https://www.gendarmerie.interieur.gouv.fr>
+ 2019-2021, Antoine Rosier <lemonldap@mon-refuge.fr>
+ 2005-2022, Gendarmerie nationale <https://www.gendarmerie.interieur.gouv.fr>
  2006-2019, LINAGORA <info@linagora.com>
  2015-2018, Savoir-faire Linux <contact@savoirfairelinux.com>
- 2018-2020, Worteks <info@worteks.com>
+ 2018-2022, Worteks <info@worteks.com>
 License: GPL-2+
 
 Files: lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/PAM.pm
@@ -33,17 +33,23 @@ Copyright: 2011, Tatsuhiko Miyagawa <miy
 License: Artistic or GPL-1+
 
 Files: *.js
-Copyright: 2005-2019, Xavier Guimard <yadd@debian.org>
- 2006-2019, Clement Oudot <clem.oudot@gmail.com>
+Copyright: 2005-2022, Xavier Guimard <yadd@debian.org>
+ 2006-2022, Clement Oudot <clem.oudot@gmail.com>
  2008-2012, Thomas Chemineau <thomas.chemineau@gmail.com>
- 2018-2019, Christophe Maudoux <chrmdx@gmail.com>
+ 2018-2022, Christophe Maudoux <chrmdx@gmail.com>
+ 2019-2022, Maxime Besson <maxime.besson@worteks.com>
 License: GPL-2+
 
+Files: lemonldap-ng-portal/site/htdocs/static/bootstrap/webauthn.png
+Copyright: James Cullum <https://github.com/JamesCullum>
+License: WebAuthnLogoLicense
+
 Files: lemonldap-ng-portal/site/htdocs/static/common/js/portal.js
-Copyright: 2005-2019, Xavier Guimard <yadd@debian.org>
- 2006-2019, Clement Oudot <clem.oudot@gmail.com>
+Copyright: 2005-2022, Xavier Guimard <yadd@debian.org>
+ 2006-2022, Clement Oudot <clem.oudot@gmail.com>
  2008-2012, Thomas Chemineau <thomas.chemineau@gmail.com>
- 2018-2019, Christophe Maudoux <chrmdx@gmail.com>
+ 2018-2022, Christophe Maudoux <chrmdx@gmail.com>
+ 2019-2022, Maxime Besson <maxime.besson@worteks.com>
 License: GPL-2+
 Comment: a little part of it comes from JQuery-UI examples
  (https://snipplr.com/view/29434/)
@@ -1268,3 +1274,26 @@ License: BSD-3-clause
  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
  OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+License: WebAuthnLogoLicense
+ How to Use These Logos
+ .
+ Do these awesome things:
+ .
+  * Use the WebAuthn logo to link to WebAuthn specs or webauthn.org
+  * Use the WebAuthn logo to show that your product or project has built-in WebAuthn integration
+  * Use the WebAuthn logo in a blog post or news article about WebAuthn
+ .
+ Please don't do these things:
+ .
+  x Use the WebAuthn logo for your application’s icon
+  x Create a modified version of the WebAuthn logo
+  x Integrate the WebAuthn logo into your logo
+  x Use any WebAuthn artwork without permission
+  x Sell any WebAuthn artwork without permission
+  x Change the colors, dimensions or add your own text/images
+ .
+ Please contact me
+ .
+  * If you want to use artwork not included in this repository
+  * If you want to use these images in a video/mainstream media
diff -pruN 2.0.13+ds-3/debian/changelog 2.0.14+ds-1/debian/changelog
--- 2.0.13+ds-3/debian/changelog	2021-10-09 07:06:19.000000000 +0000
+++ 2.0.14+ds-1/debian/changelog	2022-02-23 11:30:14.000000000 +0000
@@ -1,3 +1,29 @@
+lemonldap-ng (2.0.14+ds-1) unstable; urgency=medium
+
+  [ lintian-brush ]
+  * Remove 1 obsolete maintscript entry
+
+  [ Yadd ]
+  * New upstream version 2.0.14+ds
+  * Unfuzz patches
+  * Update copyright
+
+ -- Yadd <yadd@debian.org>  Wed, 23 Feb 2022 12:30:14 +0100
+
+lemonldap-ng (2.0.14~exp+ds-1) experimental; urgency=medium
+
+  * Add FastCGI server test
+  * Update excluded files list
+  * New upstream version 2.0.14~exp+ds (Closes: #1005302, CVE-2021-40874)
+  * Drop quick-fix.patch
+  * Update install
+  * Add optional dependencies to liblist-moreutils-perl,
+    libauthen-webauthn-perl and libauthen-radius-perl
+  * Update lintian overrides
+  * Update copyright
+
+ -- Yadd <yadd@debian.org>  Thu, 17 Feb 2022 14:43:59 +0100
+
 lemonldap-ng (2.0.13+ds-3) unstable; urgency=medium
 
   * Install /var/lib/lemonldap-ng/cache (Closes: #995949)
diff -pruN 2.0.13+ds-3/debian/control 2.0.14+ds-1/debian/control
--- 2.0.13+ds-3/debian/control	2021-10-09 06:56:16.000000000 +0000
+++ 2.0.14+ds-1/debian/control	2022-02-23 05:18:32.000000000 +0000
@@ -8,8 +8,10 @@ Build-Depends: debhelper-compat (= 13),
 Build-Depends-Indep: gpg <!nocheck>,
                      gsfonts <!nocheck>,
                      libapache-session-perl <!nocheck>,
-                     libauthen-oath-perl <!nocheck>,
                      libauth-yubikey-webclient-perl <!nocheck>,
+                     libauthen-oath-perl <!nocheck>,
+                     libauthen-radius-perl <!nocheck>,
+                     libauthen-webauthn-perl <!nocheck>,
                      libauthen-u2f-tester-perl <!nocheck>,
                      libcache-cache-perl <!nocheck>,
                      libclone-perl <!nocheck>,
@@ -33,6 +35,7 @@ Build-Depends-Indep: gpg <!nocheck>,
                      libimage-magick-perl <!nocheck>,
                      libio-string-perl <!nocheck>,
                      libipc-run-perl <!nocheck>,
+                     liblist-moreutils-perl <!nocheck>,
                      libjson-perl <!nocheck>,
                      libjson-xs-perl <!nocheck>,
                      liblasso-perl <!nocheck>,
@@ -41,6 +44,7 @@ Build-Depends-Indep: gpg <!nocheck>,
                      libmouse-perl <!nocheck>,
                      libnet-cidr-lite-perl <!nocheck>,
                      libnet-ldap-perl <!nocheck>,
+                     libio-socket-timeout-perl <!nocheck>,
                      libnet-openid-consumer-perl <!nocheck>,
                      libnet-openid-server-perl <!nocheck>,
                      libplack-perl <!nocheck>,
@@ -298,8 +302,10 @@ Depends: ${misc:Depends},
          libjs-jquery-cookie,
          libtext-unidecode-perl,
          libregexp-assemble-perl,
+         liblist-moreutils-perl,
          libemail-date-format-perl
 Recommends: gsfonts,
+            libauthen-webauthn-perl,
             libcrypt-openssl-bignum-perl,
             libconvert-base32-perl,
             libio-string-perl,
@@ -310,6 +316,7 @@ Recommends: gsfonts,
             libio-socket-timeout-perl,
             libunicode-string-perl
 Suggests: gpg,
+          libauthen-radius-perl,
           libcrypt-u2f-server-perl,
           libdbi-perl,
           libglib-perl,
diff -pruN 2.0.13+ds-3/debian/copyright 2.0.14+ds-1/debian/copyright
--- 2.0.13+ds-3/debian/copyright	2021-10-09 06:14:04.000000000 +0000
+++ 2.0.14+ds-1/debian/copyright	2022-02-23 05:33:59.000000000 +0000
@@ -10,8 +10,6 @@ Files-Excluded: lemonldap-ng-manager/sit
  lemonldap-ng-manager/site/htdocs/static/bwr/angular-ui-tree/dist/*.min.css
  lemonldap-ng-manager/site/htdocs/static/bwr/angular-aria/*.min.js*
  lemonldap-ng-manager/site/htdocs/static/bwr/angular-bootstrap/*.min.css
- lemonldap-ng-manager/site/htdocs/static/bwr/jquery
- lemonldap-ng-manager/site/htdocs/static/bwr/jquery-ui
  lemonldap-ng-manager/site/htdocs/static/bwr/bootstrap
  lemonldap-ng-manager/site/htdocs/static/bwr/es5-shim
  lemonldap-ng-manager/site/htdocs/static/*/*.min.js
@@ -39,32 +37,31 @@ Files-Excluded: lemonldap-ng-manager/sit
  lemonldap-ng-portal/site/htdocs/static/bwr/fingerprintjs2/fingerprint2.min.js.map
  fastcgi-server/man/llng-fastcgi-server.8p
  doc/pages/documentation/current/.buildinfo
- doc/pages/documentation/current/lib/tpl
- doc/pages/documentation/current/lib/scripts
  .gitlab
  .gitlab-ci.yml
  .gitignore
  rpm
  .vscode
+ debian
  doc/pages/manager-api
 
 Files: *
-Copyright: 2005-2020, Yadd <yadd@debian.org>
- 2006-2020, Clement Oudot <clem.oudot@gmail.com>
+Copyright: 2005-2022, Yadd <yadd@debian.org>
+ 2006-2022, Clement Oudot <clem.oudot@gmail.com>
+ 2018-2022, Christophe Maudoux <chrmdx@gmail.com>
+ 2019-2022, Maxime Besson <maxime.besson@worteks.com>
  2008, Mikael Ates <mikael.ates@univ-st-etienne.fr>
  2008-2011, Thomas Chemineau <thomas.chemineau@gmail.com>
  2012-2013, Sandro Cazzaniga <cazzaniga.sandro@gmail.com>
  2012-2015, François-Xavier Deltombe <fxdeltombe@gmail.com>
- 2012-2019, David Coutadeur <david.coutadeur@gmail.com>
- 2018-2020, Christophe Maudoux <chrmdx@gmail.com>
- 2019-2020, Maxime Besson <maxime.besson@worteks.com>
+ 2012-2021, David Coutadeur <david.coutadeur@gmail.com>
  2019, Soisik Frogier <soisik.froger@worteks.com>
  2019, Mame Dieynaba Sene <msene@linagora.com>
- 2019, Antoine Rosier <lemonldap@mon-refuge.fr>
- 2005-2020, Gendarmerie nationale <https://www.gendarmerie.interieur.gouv.fr>
+ 2019-2021, Antoine Rosier <lemonldap@mon-refuge.fr>
+ 2005-2022, Gendarmerie nationale <https://www.gendarmerie.interieur.gouv.fr>
  2006-2019, LINAGORA <info@linagora.com>
  2015-2018, Savoir-faire Linux <contact@savoirfairelinux.com>
- 2018-2020, Worteks <info@worteks.com>
+ 2018-2022, Worteks <info@worteks.com>
 License: GPL-2+
 
 Files: lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/PAM.pm
@@ -74,17 +71,23 @@ Comment: idea taken from Authen::Simple:
  <chansen@cpan.org>
 
 Files: *.js
-Copyright: 2005-2019, Yadd <yadd@debian.org>
- 2006-2019, Clement Oudot <clem.oudot@gmail.com>
+Copyright: 2005-2022, Xavier Guimard <yadd@debian.org>
+ 2006-2022, Clement Oudot <clem.oudot@gmail.com>
  2008-2012, Thomas Chemineau <thomas.chemineau@gmail.com>
- 2018-2019, Christophe Maudoux <chrmdx@gmail.com>
+ 2018-2022, Christophe Maudoux <chrmdx@gmail.com>
+ 2019-2022, Maxime Besson <maxime.besson@worteks.com>
 License: GPL-2+
 
+Files: lemonldap-ng-portal/site/htdocs/static/bootstrap/webauthn.png
+Copyright: James Cullum <https://github.com/JamesCullum>
+License: WebAuthnLogoLicense
+
 Files: lemonldap-ng-portal/site/htdocs/static/common/js/portal.js
-Copyright: 2005-2019, Yadd <yadd@debian.org>
- 2006-2019, Clement Oudot <clem.oudot@gmail.com>
+Copyright: 2005-2022, Xavier Guimard <yadd@debian.org>
+ 2006-2022, Clement Oudot <clem.oudot@gmail.com>
  2008-2012, Thomas Chemineau <thomas.chemineau@gmail.com>
- 2018-2019, Christophe Maudoux <chrmdx@gmail.com>
+ 2018-2022, Christophe Maudoux <chrmdx@gmail.com>
+ 2019-2022, Maxime Besson <maxime.besson@worteks.com>
 License: GPL-2+
 Comment: a little part of it comes from JQuery-UI examples
  (https://snipplr.com/view/29434/)
@@ -228,7 +231,7 @@ Copyright: 2004, Entr'ouvert <https://ww
 License: GPL-2+
 
 Files: debian/*
-Copyright: 2005-2020, Yadd <yadd@debian.org>
+Copyright: 2005-2022, Yadd <yadd@debian.org>
 License: GPL-2+
 
 License: Apache-2.0
@@ -1147,3 +1150,26 @@ License: BSD-3-clause
  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
  OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+License: WebAuthnLogoLicense
+ How to Use These Logos
+ .
+ Do these awesome things:
+ .
+  * Use the WebAuthn logo to link to WebAuthn specs or webauthn.org
+  * Use the WebAuthn logo to show that your product or project has built-in WebAuthn integration
+  * Use the WebAuthn logo in a blog post or news article about WebAuthn
+ .
+ Please don't do these things:
+ .
+  x Use the WebAuthn logo for your application’s icon
+  x Create a modified version of the WebAuthn logo
+  x Integrate the WebAuthn logo into your logo
+  x Use any WebAuthn artwork without permission
+  x Sell any WebAuthn artwork without permission
+  x Change the colors, dimensions or add your own text/images
+ .
+ Please contact me
+ .
+  * If you want to use artwork not included in this repository
+  * If you want to use these images in a video/mainstream media
diff -pruN 2.0.13+ds-3/debian/lemonldap-ng-doc.maintscript 2.0.14+ds-1/debian/lemonldap-ng-doc.maintscript
--- 2.0.13+ds-3/debian/lemonldap-ng-doc.maintscript	2020-04-23 12:34:46.000000000 +0000
+++ 2.0.14+ds-1/debian/lemonldap-ng-doc.maintscript	1970-01-01 00:00:00.000000000 +0000
@@ -1 +0,0 @@
-symlink_to_dir /usr/share/doc/lemonldap-ng-doc/pages/documentation/current 1.3 1.9.7-3~
diff -pruN 2.0.13+ds-3/debian/liblemonldap-ng-common-perl.install 2.0.14+ds-1/debian/liblemonldap-ng-common-perl.install
--- 2.0.13+ds-3/debian/liblemonldap-ng-common-perl.install	2021-08-29 06:32:39.000000000 +0000
+++ 2.0.14+ds-1/debian/liblemonldap-ng-common-perl.install	2022-02-17 13:26:27.000000000 +0000
@@ -2,6 +2,7 @@ etc/lemonldap-ng/lemonldap-ng.ini
 etc/lemonldap-ng/for_etc_hosts
 usr/share/man/man1/convertConfig.1p
 usr/share/man/man1/convertSessions.1p
+usr/share/man/man1/encryptTotpSecrets.1p
 usr/share/man/man1/importMetadata.1p
 usr/share/man/man1/lemonldap-ng-cli.1p
 usr/share/man/man1/lemonldap-ng-sessions.1p
@@ -11,6 +12,7 @@ usr/share/perl5/Lemonldap/NG/Common*
 usr/share/lemonldap-ng/ressources
 usr/share/lemonldap-ng/bin/convertConfig
 usr/share/lemonldap-ng/bin/convertSessions
+usr/share/lemonldap-ng/bin/encryptTotpSecrets
 usr/share/lemonldap-ng/bin/importMetadata
 usr/share/lemonldap-ng/bin/lemonldap-ng-sessions
 usr/share/lemonldap-ng/bin/lmMigrateConfFiles2ini
diff -pruN 2.0.13+ds-3/debian/liblemonldap-ng-manager-perl.lintian-overrides 2.0.14+ds-1/debian/liblemonldap-ng-manager-perl.lintian-overrides
--- 2.0.13+ds-3/debian/liblemonldap-ng-manager-perl.lintian-overrides	2021-10-09 06:38:28.000000000 +0000
+++ 2.0.14+ds-1/debian/liblemonldap-ng-manager-perl.lintian-overrides	2022-02-17 13:41:04.000000000 +0000
@@ -1,4 +1,4 @@
 # Script not executable by default. Launched by FastCGI engine
-script-not-executable usr/share/lemonldap-ng/manager/htdocs/manager.psgi
+script-not-executable *usr/share/lemonldap-ng/manager/htdocs/manager.psgi*
 # HTML forms
 package-contains-documentation-outside-usr-share-doc usr/share/lemonldap-ng/manager/htdocs/static/forms*
diff -pruN 2.0.13+ds-3/debian/patches/fix-for-pod2man.diff 2.0.14+ds-1/debian/patches/fix-for-pod2man.diff
--- 2.0.13+ds-3/debian/patches/fix-for-pod2man.diff	2021-08-29 06:32:39.000000000 +0000
+++ 2.0.14+ds-1/debian/patches/fix-for-pod2man.diff	2022-02-23 05:32:02.000000000 +0000
@@ -5,7 +5,7 @@ Last-Update: 2020-03-29
 
 --- a/Makefile
 +++ b/Makefile
-@@ -337,6 +337,7 @@
+@@ -339,6 +339,7 @@
  
  fastcgi-server/man/llng-fastcgi-server.8p: fastcgi-server/sbin/llng-fastcgi-server
  	@echo Update FastCGI server man page
diff -pruN 2.0.13+ds-3/debian/patches/quick-fix.patch 2.0.14+ds-1/debian/patches/quick-fix.patch
--- 2.0.13+ds-3/debian/patches/quick-fix.patch	2021-09-02 06:11:16.000000000 +0000
+++ 2.0.14+ds-1/debian/patches/quick-fix.patch	1970-01-01 00:00:00.000000000 +0000
@@ -1,18 +0,0 @@
-Description: fix "Portal does not run correctly with portalRequireOldPassword=0"
-Author: Clément Oudot <clement@oodo.net>
-Origin: upstream, https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/commit/835d54c
-Bug: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/2595
-Forwarded: not-needed
-Reviewed-By: Yadd <yadd@debian.org>
-Last-Update: 2021-08-27
-
---- a/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Reload.pm
-+++ b/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Reload.pm
-@@ -639,7 +639,6 @@
- 
- sub substitute {
-     my ( $class, $expr ) = @_;
--    $expr ||= '';
- 
-     # substitute special vars, just for retro-compatibility
-     $expr =~ s/\$date\b/&date/sg;
diff -pruN 2.0.13+ds-3/debian/patches/series 2.0.14+ds-1/debian/patches/series
--- 2.0.13+ds-3/debian/patches/series	2021-09-02 06:11:16.000000000 +0000
+++ 2.0.14+ds-1/debian/patches/series	2022-02-17 13:26:27.000000000 +0000
@@ -2,4 +2,3 @@ javascript-path.patch
 Avoid-developer-tests.patch
 fix-for-pod2man.diff
 replace-api-doc-by-link.diff
-quick-fix.patch
diff -pruN 2.0.13+ds-3/debian/source/lintian-overrides 2.0.14+ds-1/debian/source/lintian-overrides
--- 2.0.13+ds-3/debian/source/lintian-overrides	2021-10-09 06:29:25.000000000 +0000
+++ 2.0.14+ds-1/debian/source/lintian-overrides	2022-02-17 13:42:28.000000000 +0000
@@ -2,6 +2,13 @@
 team/pkg-perl/testsuite/no-team-tests
 
 # False positive: these long lines are in source files (and deleted during build) with USEDEBIANLIBS=yes
-source-is-missing lemonldap-ng-manager/site/htdocs/static/bwr/angular-bootstrap/ui-bootstrap-tpls.js line 6182 is 3131 characters long (>512)
-source-contains-prebuilt-javascript-object lemonldap-ng-manager/site/htdocs/static/bwr/angular-bootstrap/ui-bootstrap-tpls.js line 6182 is 3131 characters long (>512)
-very-long-line-length-in-source-file lemonldap-ng-manager/site/htdocs/static/bwr/angular-bootstrap/ui-bootstrap-tpls.js line 6752 is 3131 characters long (>512)
+source-is-missing lemonldap-ng-manager/site/htdocs/static/bwr/angular-bootstrap/ui-bootstrap-tpls.js
+source-contains-prebuilt-javascript-object lemonldap-ng-manager/site/htdocs/static/bwr/angular-bootstrap/ui-bootstrap-tpls.js
+very-long-line-length-in-source-file lemonldap-ng-manager/site/htdocs/static/bwr/angular-bootstrap/ui-bootstrap-tpls.js line *
+very-long-line-length-in-source-file scripts/*.pl *
+
+# Doc
+very-long-line-length-in-source-file doc/*.rst *
+
+# Regexps regenerated
+very-long-line-length-in-source-file lemonldap-ng-*.pm *
diff -pruN 2.0.13+ds-3/debian/tests/control 2.0.14+ds-1/debian/tests/control
--- 2.0.13+ds-3/debian/tests/control	2021-08-29 06:32:39.000000000 +0000
+++ 2.0.14+ds-1/debian/tests/control	2022-02-17 13:26:01.000000000 +0000
@@ -1,3 +1,14 @@
+# FastCGI server
+Tests: fastcgiserver
+Depends: @
+ , libfcgi-client-perl
+ , libstring-random-perl
+ , libemail-sender-perl
+ , libmime-tools-perl
+ , libgd-securityimage-perl
+ , libimage-magick-perl
+Restrictions: allow-stderr, needs-root
+
 # debian/tests/runner launch pkg-perl-autopkgtest tests for each library
 Test-Command: ./debian/tests/runner build-deps lemonldap-ng-common
 Depends: liblemonldap-ng-common-perl, @builddeps@, pkg-perl-autopkgtest
diff -pruN 2.0.13+ds-3/debian/tests/fastcgiserver 2.0.14+ds-1/debian/tests/fastcgiserver
--- 2.0.13+ds-3/debian/tests/fastcgiserver	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/debian/tests/fastcgiserver	2022-02-17 13:26:01.000000000 +0000
@@ -0,0 +1,29 @@
+#!/usr/bin/perl
+#
+use IO::Socket::UNIX;
+use FCGI::Client;
+use Test::More tests => 1;
+
+my $sock = IO::Socket::UNIX->new(
+    Type => SOCK_STREAM(),
+    Peer => '/var/run/llng-fastcgi-server/llng-fastcgi.sock',
+);
+my $client = FCGI::Client::Connection->new( sock => $sock );
+my $env    = {
+    HTTP_HOST       => 'auth.example.com',
+    HTTP_ACCEPT     => 'text/html',
+    REMOTE_ADDR     => '127.0.0.1',
+    QUERY_STRING    => '',
+    REQUEST_URI     => '/',
+    PATH_INFO       => '/',
+    SERVER_PORT     => 80,
+    REQUEST_METHOD  => 'GET',
+    LLTYPE          => 'psgi',
+    SCRIPT_FILENAME => '/usr/share/lemonldap-ng/portal/htdocs/index.psgi',
+    'FCGI_ROLE'     => 'RESPONDER',
+};
+
+my ( $stdout, $stderr, $status ) = $client->request($env);
+
+ok( $stdout =~ /^Status: 200 OK/s, 'Portal responded 200' )
+ or diag "STDOUT: $stdout\nSTDERR: $stderr";
diff -pruN 2.0.13+ds-3/doc/sources/admin/adaptativeauthenticationlevel.rst 2.0.14+ds-1/doc/sources/admin/adaptativeauthenticationlevel.rst
--- 2.0.13+ds-3/doc/sources/admin/adaptativeauthenticationlevel.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/adaptativeauthenticationlevel.rst	2022-02-07 19:06:14.000000000 +0000
@@ -39,7 +39,7 @@ You can then create rules with these fie
 
 .. tip::
 
-    By example, to add 3 to authentication level for users from 192.168.0.0/24 network:
+    By example, to add 3 to authentication level for users from 192.168.0.0/16 network:
     
     - Rule: ``$env->{REMOTE_ADDR} =~ /^192\.168\./``
     - Value: ``+3``
Binary files 2.0.13+ds-3/doc/sources/admin/applications/confluence.png and 2.0.14+ds-1/doc/sources/admin/applications/confluence.png differ
diff -pruN 2.0.13+ds-3/doc/sources/admin/applications/confluence.rst 2.0.14+ds-1/doc/sources/admin/applications/confluence.rst
--- 2.0.13+ds-3/doc/sources/admin/applications/confluence.rst	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/applications/confluence.rst	2022-02-07 19:06:14.000000000 +0000
@@ -0,0 +1,65 @@
+Confluence
+==========
+
+Presentation
+------------
+
+Confluence is a web-based corporate wiki developed by Atlassian.
+
+It is compatible with SAML and OpenID Connect. This tutorial will focus on SAML.
+
+Configuration
+-------------
+
+You must first configure LemonLDAP::NG as a :doc:`SAML Identity Provider<../idpsaml>`.
+
+Configure SAML in Confluence
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In the SSO configuration page, choose SAML as the authentication method. And set the following parameters.
+
+Don't forget to replace ``auth.example.com`` with your actual domain.
+
+* Single sign on issuer: ``https://auth.example.com/saml/metadata``
+* Identity provider single sign on URL: ``https://auth.example.com/saml/singleSignOn``
+* X.509 certificate: You can find this certificate in the manager: SAML2 Service » Security » Signature » Public key
+* Username mapping attribute: ``${uid}``
+
+.. danger:: Make sure the certificate you copy into Confluence starts with BEGIN CERTIFICATE and not with BEGIN PRIVATE KEY
+
+Write down the *Assertion Consumer Service URL* and the *Audience URL*, that Confluence is showing you, you will need it to configure LemonLDAP::NG
+
+Configure LemonLDAP::NG
+~~~~~~~~~~~~~~~~~~~~~~~
+
+In the LemonLDAP::NG Manager, create a new *SAML Service Provider*
+
+In *Metadata*, copy the following XML document, and don't forget to change ``AUDIENCE_URL`` and ``CONSUMER_SERVICE_URL`` the URLs with the values given by Confluence.
+
+::
+
+	<?xml version="1.0"?>
+	<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
+		entityID="AUDIENCE_URL">
+	  <md:SPSSODescriptor
+		AuthnRequestsSigned="false"
+		WantAssertionsSigned="false"
+		protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+		  <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
+		  <md:AssertionConsumerService
+			Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+			Location="CONSUMER_SERVICE_URL"
+			index="1"/>
+	  </md:SPSSODescriptor>
+	</md:EntityDescriptor>
+
+In *Exported Attributes*, add a new attribute:
+
+* Variable name: the session variable containing user logins
+* Attribute name: ``uid``
+* Mandatory: ``On``
+
+Finally, in *Options* » *Signature*, set
+
+* Check SSO message signature: Off
+* Check SLO message signature: Off
Binary files 2.0.13+ds-3/doc/sources/admin/applications/gitea_logo.png and 2.0.14+ds-1/doc/sources/admin/applications/gitea_logo.png differ
Binary files 2.0.13+ds-3/doc/sources/admin/applications/gitea_oidc_config.png and 2.0.14+ds-1/doc/sources/admin/applications/gitea_oidc_config.png differ
diff -pruN 2.0.13+ds-3/doc/sources/admin/applications/gitea.rst 2.0.14+ds-1/doc/sources/admin/applications/gitea.rst
--- 2.0.13+ds-3/doc/sources/admin/applications/gitea.rst	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/applications/gitea.rst	2022-01-22 14:30:19.000000000 +0000
@@ -0,0 +1,67 @@
+Gitea
+=====
+
+|logo|
+
+Presentation
+------------
+
+`Gitea <https://gitea.io/>`__ is a community managed lightweight
+code hosting solution written in Go. It is published under the MIT license.
+
+It can be configured to authenticate users with :doc:`OpenID Connect <../idpopenidconnect>`.
+
+Configuration
+--------------
+
+LL:NG
+~~~~~
+
+Make sure you have already
+:doc:`enabled OpenID Connect<../idpopenidconnect>` on your LemonLDAP::NG
+server
+
+Make sure you have generated a set of signing keys in
+``OpenID Connect Service`` » ``Security`` » ``Keys``
+
+You also need to set a Signing key ID to a non-empty value of your choice.
+
+Then, add a Relaying Party with the following configuration:
+
+- Options » Basic » Client ID : choose a client ID, such as ``gitea``
+- Options » Basic » Client Secret : choose a client secret, such as ``xxxx``
+- Options » Basic » Allowed redirection address : ``https://git.example.com/user/oauth2/NAME/callback``
+- Options » ID Token Signature Algorithm : ``RS256``
+- No Exported Attributes needed
+
+.. note::
+
+   The redirection address is built like this: ``<Gitea service URL>`` ``/user/oauth2/`` ``<Name of the OIDC authentication source in Gitea>`` ``/callback``
+
+Gitea
+~~~~~
+
+Go in administration panel and create a new authentication source:
+
+|screenshot_admin|
+
+Configure settings:
+
+- Authentication name: set here the value used for the redirection address
+- OAuth2 Provider: set OpenID Connect
+- Client ID: the Client ID configured on LL::NG side
+- Client Secret: the Client Secret configured on LL::NG side
+- OpenID Connect Auto Discovery URL: use the default OIDC configuration URL of your LL::NG server
+- Enable the authentication source
+
+Usage
+-----
+
+In Gitea login screen, a new OpenID logo appears at the bottom. Click on it to authenticate.
+
+At first connection, the user must associate his account to an existing one (local or LDAP). The assocation is then remembered for further connections.
+
+.. |logo| image:: /applications/gitea_logo.png
+   :class: align-center
+.. |screenshot_admin| image:: /applications/gitea_oidc_config.png
+   :class: align-center
diff -pruN 2.0.13+ds-3/doc/sources/admin/applications/grafana.rst 2.0.14+ds-1/doc/sources/admin/applications/grafana.rst
--- 2.0.13+ds-3/doc/sources/admin/applications/grafana.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/applications/grafana.rst	2022-01-22 14:30:19.000000000 +0000
@@ -54,9 +54,9 @@ Then, add a Relaying Party with the foll
 
 If you want to transmit extra user attributes to Grafana, you also need to configure:
 
--  Extra Claims »
+-  Scope values content »
 
-   -  add a key named ``profile``
+   -  add a key named ``profile`` to override the default claim list
    -  set a value of ``name username display_name upn``
 
 -  Exported Attributes (not all of them are mandatory)
Binary files 2.0.13+ds-3/doc/sources/admin/applications/matrix_logo.png and 2.0.14+ds-1/doc/sources/admin/applications/matrix_logo.png differ
diff -pruN 2.0.13+ds-3/doc/sources/admin/applications/matrix.rst 2.0.14+ds-1/doc/sources/admin/applications/matrix.rst
--- 2.0.13+ds-3/doc/sources/admin/applications/matrix.rst	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/applications/matrix.rst	2022-01-22 14:30:19.000000000 +0000
@@ -0,0 +1,56 @@
+Synapse Matrix home server
+==========================
+
+|image0|
+
+Presentation
+------------
+
+Synapse is the reference implementation of a Matrix home server, written in Python.
+
+Configuring Synapse
+-------------------
+
+See `The official Synapse documentation <https://matrix-org.github.io/synapse/latest/openid.html>`__ for details
+
+
+.. code:: yaml
+
+    oidc_providers:
+      - idp_id: lemonldap
+        idp_name: lemonldap
+        discover: true
+        issuer: "https://auth.example.com/" # TO BE FILLED: replace with your domain
+        client_id: "your client id" # TO BE FILLED
+        client_secret: "your client secret" # TO BE FILLED
+        scopes:
+          - "openid"
+          - "profile"
+          - "email"
+        user_mapping_provider:
+          config:
+            localpart_template: "{{ user.preferred_username }}}"
+            # TO BE FILLED: If your users have names in LemonLDAP::NG and you want those in Synapse, this should be replaced with user.name|capitalize or any valid filter.
+            display_name_template: "{{ user.preferred_username|capitalize }}"
+
+
+Configuring LemonLDAP
+~~~~~~~~~~~~~~~~~~~~~
+
+Add a :doc:`new OpenID Connect relaying party<..//idpopenidconnect>`
+with the following parameters:
+
+* **Options/Basic**
+    * **Client ID**: same as ``client_id`` configuration in Synapse
+    * **Client Secret**: same as ``client_secret`` configuration in Synapse
+    * **Allowed redirection addresses**: ``[synapse public baseurl]/_synapse/client/oidc/callback``
+* **Options/Security**
+   * **ID Token signature algorithm**:: ``RS256``
+* **Exported Attributes**
+   * ``preferred_username``: ``uid``
+
+(adjust if you don't store your username attribute in the ``uid`` session variable
+
+.. |image0| image:: /applications/matrix_logo.png
+   :class: align-center
+
Binary files 2.0.13+ds-3/doc/sources/admin/applications/odoo_logo.png and 2.0.14+ds-1/doc/sources/admin/applications/odoo_logo.png differ
diff -pruN 2.0.13+ds-3/doc/sources/admin/applications/odoo.rst 2.0.14+ds-1/doc/sources/admin/applications/odoo.rst
--- 2.0.13+ds-3/doc/sources/admin/applications/odoo.rst	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/applications/odoo.rst	2022-01-22 14:30:19.000000000 +0000
@@ -0,0 +1,88 @@
+Odoo
+====
+
+|image0|
+
+Presentation
+------------
+
+Odoo is a suite of business management software tools including, for example, CRM, e-commerce, billing, accounting, manufacturing, warehouse, project management, and inventory management. 
+
+Requirements
+------------
+
+This guide explains how to authenticate your Odoo users using LemonLDAP::NG 's SAML provider.
+
+Make sure you have :doc:`set up LemonLDAP::NG a SAML IDP <../samlservice>` 
+
+.. warning::
+   Odoo requires your public SAML Signature key to be in `BEGIN CERTIFICATE`
+   format, if this is not the case, you need to :ref:`convert your SAML key to
+   a certificate<samlservice-convert-certificate>`)
+
+.. warning::
+   Odoo requires LemonLDAP::NG 2.0.14 in order to handle RelayState correctly
+
+Configuring Odoo
+----------------
+
+Pre-requisites
+~~~~~~~~~~~~~~
+
+On the Odoo side, you need to install the ``auth_saml`` module from OCA:
+
+* https://github.com/OCA/server-auth/tree/14.0/auth_saml 
+* https://odoo-community.org/shop/product/saml2-authentication-3211
+
+This module requires the ``pysaml2`` and ``xmlsec1`` python dependencies.
+
+Configuration
+~~~~~~~~~~~~~
+
+After installing the module, you will see two new menus in the Odoo admin:
+
+
+* Settings » Users & Companies » SAML Providers
+* And a new *SAML* tab in Settings » Users & Companies » Users
+
+
+Creating a new SAML Provider
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create a new SAML provider in Settings » Users & Companies » SAML Providers
+
+* Choose a name
+* Copy the metadata from https://auth.example.com/saml/metadata/idp in the *Identity Provider Metadata* field
+* Import a certificate and a private key in the *Odoo Public Certificate* and *Odoo Private Key* fields
+
+To generate a key/certificate pair, you can run the following command::
+
+    openssl req -x509 -newkey rsa:4096 -keyout odoo-key.pem -out odoo-cert.pem -sha256 -days 3650 -nodes
+
+* Select a signature method in the *Signature Algorithm*, such as *SIG_RSA_SHA256*
+* If you do not want to use the email address to match between LLNG and Odoo accounts, set the *Identity Provider matching attribute* to a different value
+* All other fields may be left to default values
+
+Configuring users
+~~~~~~~~~~~~~~~~~
+
+For each user you want to enable SAML on, you need to edit them in Settings » Users & Companies » Users
+
+In the *SAML* tab, set the SAML provider you just created, and their email address as the identifier.
+
+Configuring LemonLDAP
+---------------------
+
+Add a new :ref:`new SAML Service Provider to the LemonLDAP::NG configuration<samlidp-register-sp>`
+with the following parameters:
+
+* **Metadata**
+  * Copy the Metadata found at the URL referenced in Odoo's Settings » Users & Companies » SAML Providers menu » Your provider » Metadata URL
+* **Exported Attributes**
+   * Declare the attribute that you set in Odoo's *Identity Provider matching attribute*
+   * If you are using the email, you don't need to declare anything
+
+
+.. |image0| image:: /applications/odoo_logo.png
+   :class: align-center
+
Binary files 2.0.13+ds-3/doc/sources/admin/applications/prosanteconnect_logo.png and 2.0.14+ds-1/doc/sources/admin/applications/prosanteconnect_logo.png differ
diff -pruN 2.0.13+ds-3/doc/sources/admin/applications/wekan.rst 2.0.14+ds-1/doc/sources/admin/applications/wekan.rst
--- 2.0.13+ds-3/doc/sources/admin/applications/wekan.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/applications/wekan.rst	2022-01-22 14:30:25.000000000 +0000
@@ -30,6 +30,7 @@ theses :
 * **OAUTH2_USERNAME_MAP**: ``sub``
 * **OAUTH2_FULLNAME_MAP**: ``name``
 * **OAUTH2_EMAIL_MAP**: ``email``
+* **OAUTH2_REQUEST_PERMISSIONS**: ``openid profile email``
 
 
 .. danger::
diff -pruN 2.0.13+ds-3/doc/sources/admin/applications.rst 2.0.14+ds-1/doc/sources/admin/applications.rst
--- 2.0.13+ds-3/doc/sources/admin/applications.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/applications.rst	2022-02-07 19:06:14.000000000 +0000
@@ -10,6 +10,7 @@ Applications
    applications/awx
    applications/bugzilla
    applications/bigbluebutton
+   applications/confluence
    applications/cornerstone
    applications/discourse
    applications/django
@@ -17,6 +18,7 @@ Applications
    applications/drupal
    applications/fusiondirectory
    applications/gerrit
+   applications/gitea
    applications/gitlab
    applications/glpi
    applications/googleapps
@@ -28,11 +30,13 @@ Applications
    applications/jitsimeet
    applications/liferay
    applications/limesurvey
+   applications/matrix
    applications/mattermost
    applications/mediawiki
    applications/mobilizon
    applications/nextcloud
    applications/obm
+   applications/odoo
    applications/office365
    applications/publik
    applications/phpldapadmin
@@ -91,6 +95,7 @@ Application
 .. image:: applications/logo-awx.png                              :doc:`AWX (Ansible Tower)<applications/awx>`                                           ✔
 .. image:: applications/bigbluebutton-logo.png                    :doc:`BigBlueButton<applications/bigbluebutton>`                                            ✔
 .. image:: applications/bugzilla_logo.png                         :doc:`Bugzilla<applications/bugzilla>`               ✔
+.. image:: applications/confluence.png                            :doc:`Confluence<applications/confluence>`                                             ✔    ✔
 .. image:: applications/csod_logo.png                             :doc:`Cornerstone<applications/cornerstone>`                                           ✔
 .. image:: applications/discourse.jpg                             :doc:`Discourse<applications/discourse>`                                               ✔    ✔
 .. image:: applications/django_logo.png                           :doc:`Django<applications/django>`                   ✔
@@ -99,6 +104,7 @@ Application
 .. image:: applications/fusiondirectory-logo.jpg                  :doc:`FusionDirectory<applications/fusiondirectory>` ✔
 .. image:: applications/gerrit_logo.png                           :doc:`Gerrit<applications/gerrit>`                                                          ✔
 .. image:: applications/gitlab_logo.png                           :doc:`Gitlab<applications/gitlab>`                                                     ✔    ✔
+.. image:: applications/gitea_logo.png                            :doc:`Gitea<applications/gitea>`                                                            ✔
 .. image:: applications/glpi_logo.png                             :doc:`GLPI<applications/glpi>`                       ✔
 .. image:: applications/googleapps_logo.png                       :doc:`Google Apps<applications/googleapps>`                                            ✔
 .. image:: applications/grafana_logo.png                          :doc:`Grafana<applications/grafana>`                                                        ✔
@@ -109,11 +115,13 @@ Application
 .. image:: applications/logo-jitsimeet.png                        :doc:`Jitsi Meet<applications/jitsimeet>`            ✔
 .. image:: applications/liferay_logo.png                          :doc:`Liferay<applications/liferay>`                 ✔
 .. image:: applications/limesurvey_logo.png                       :doc:`LimeSurvey<applications/limesurvey>`           ✔
+.. image:: applications/matrix_logo.png                           :doc:`Matrix<applications/matrix>`                                                          ✔
 .. image:: applications/mattermost_logo.png                       :doc:`Mattermost<applications/mattermost>`                                                  ✔
 .. image:: applications/mediawiki_logo.png                        :doc:`Mediawiki<applications/mediawiki>`             ✔
 .. image:: applications/mobilizon_logo.jpg                        :doc:`Mobilizon<applications/mobilizon>`             ✔
 .. image:: applications/nextcloud-logo.png                        :doc:`NextCloud<applications/nextcloud>`                                               ✔
 .. image:: applications/obm_logo.png                              :doc:`OBM<applications/obm>`                         ✔
+.. image:: applications/odoo_logo.png                             :doc:`Odoo<applications/odoo>`                                                         ✔
 .. image:: applications/logo_office_365.png                       :doc:`Office 365<applications/office365>`                                              ✔
 .. image:: applications/logo-publik.png                           :doc:`Publik<applications/publik>`                                                          ✔
 .. image:: applications/phpldapadmin_logo.png                     :doc:`phpLDAPAdmin<applications/phpldapadmin>`       ✔
diff -pruN 2.0.13+ds-3/doc/sources/admin/authbasichandler.rst 2.0.14+ds-1/doc/sources/admin/authbasichandler.rst
--- 2.0.13+ds-3/doc/sources/admin/authbasichandler.rst	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authbasichandler.rst	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,75 @@
+AuthBasic Handler
+=================
+
+Presentation
+------------
+
+The AuthBasic Handler is a special Handler using AuthBasic method to
+authenticate and grante access to a virtual host.
+
+The Handler sends a WWW-Authenticate header to the client, to request
+user id and password. Then it checks credentials by using LL::NG REST
+web service (REST session service must be enabled in the manager). Once
+session is granted, the Handler will check authorizations like the
+standard Handler.
+
+This feature can be useful to allow a third party application to access
+a virtual host with user credentials by sending a Basic challenge to it.
+
+Configuration
+-------------
+
+Portal
+~~~~~~
+
+:doc:`REST server<restservices>` must be enabled on portal.
+
+Virtual host
+~~~~~~~~~~~~
+
+You just have to set "Type: AuthBasic" in the virtualHost options in the
+manager.
+
+If you want to protect only a virtualHost part, keep type on "Main" and
+set type in your configuration file:
+
+-  Apache: use simply a ``PerlSetVar VHOSTTYPE AuthBasic``
+-  Nginx: create another FastCGI with a
+   ``fastcgi_param VHOSTTYPE AuthBasic;`` *(and remove error_page 401)*
+
+Handler parameters
+~~~~~~~~~~~~~~~~~~
+
+No parameters needed. But you have to allow REST sessions web services,
+see :doc:`REST sessions backend<restsessionbackend>`, enable local cache
+(enabled by default in lemonldap-ng.ini) and allow source IP addresses
+to access required locations in Portal Virtual Host.
+
+
+.. danger::
+
+    With AuthBasic Handler, you have to disable CSRF token by
+    setting a special rule based on source IP addresses like this :
+
+    requireToken => $env->{REMOTE_ADDR} !~ /^127\.0\.[1-3]\.1$/
+
+    With :doc:`authchoice`, you have to declare which authentication module is
+    requested by the AuthBasic Handler to create global session.
+
+    Go to:
+    ``General Parameters > Authentication parameters > Choice parameters``
+
+    and set authentication module's name :
+
+    **Choice used for password authentication** => 2_LDAP (by example)
+
+
+.. attention::
+
+    With HTTPS, you may have to set **LWP::UserAgent object**
+    with ``verify_hostname => 0`` and ``SSL_verify_mode => 0``.
+
+    Go to:
+
+    ``General Parameters > Advanced Parameters > Security > SSL options for server requests``
+
diff -pruN 2.0.13+ds-3/doc/sources/admin/authchoice.rst 2.0.14+ds-1/doc/sources/admin/authchoice.rst
--- 2.0.13+ds-3/doc/sources/admin/authchoice.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authchoice.rst	2022-02-07 19:06:14.000000000 +0000
@@ -51,7 +51,7 @@ Then, go in ``Choice Parameters``:
    ``lmAuth``)
 -  **Allowed modules**: click on ``New chain`` to add a choice.
 -  **Choice used for password authentication**: authentication module used by
-   :doc:`AuthBasic handler<handlerauthbasic>` and :ref:`OAuth2.0 Password Grant <resource-owner-password-grant>`
+   :doc:`AuthBasic handler<authbasichandler>` and :ref:`OAuth2.0 Password Grant <resource-owner-password-grant>`
 -  **FindUser plugin parameter**: authentication module called by
    Find user plugin (:doc:`Find user plugin<finduser>`)
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/authdbi.rst 2.0.14+ds-1/doc/sources/admin/authdbi.rst
--- 2.0.13+ds-3/doc/sources/admin/authdbi.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authdbi.rst	2022-01-23 15:41:18.000000000 +0000
@@ -38,12 +38,8 @@ LL::NG can use two tables:
 
     Authentication table and user table can be the same.
 
-The password can be in plain text, or encoded with a standard SQL
-method:
-
--  SHA
--  SHA1
--  MD5
+The password can be in plain text, or encoded with a SQL method (for example
+``SHA``, ``SHA1``, ``MD5`` or any method valid on database side).
 
 Example 1: two tables
 ^^^^^^^^^^^^^^^^^^^^^
@@ -159,7 +155,8 @@ Password
 ~~~~~~~~
 
 -  **Hash schema**: SQL method for hashing password. Can be left blank
-   for plain text passwords.
+   for plain text passwords. The method will be forced to uppercase in
+   SQL statement.
 -  **Dynamic hash activation**: Activate dynamic hashing. With dynamic
    hashing, the hash scheme is recovered from the user password in the
    database during authentication.
diff -pruN 2.0.13+ds-3/doc/sources/admin/authkerberos.rst 2.0.14+ds-1/doc/sources/admin/authkerberos.rst
--- 2.0.13+ds-3/doc/sources/admin/authkerberos.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authkerberos.rst	2022-02-19 16:04:21.000000000 +0000
@@ -11,14 +11,14 @@ Presentation
 ------------
 
 `Kerberos <https://en.wikipedia.org/wiki/Kerberos_(protocol)>`__ is a
-network authentication protocol used to authenticate users based on
+network authentication protocol used for authenticating users based on
 their desktop session.
 
 LL::NG uses GSSAPI module to validate Kerberos ticket against a local
 keytab.
 
-LLNG Configuration
-------------------
+LL::NG Configuration
+--------------------
 
 In Manager, go in ``General Parameters`` > ``Authentication modules``
 and choose Kerberos for authentication. Then go to "Kerberos parameters"
@@ -34,13 +34,15 @@ and configure the following parameters:
    Kerberos code to validate Kerberos ticket
 -  **Remove domain in username**: set to "enabled" to strip username
    value and remove the '@domain'.
--  **Allowed domains**: if set, tickets will only be accepted if they come from one of the domains listed here. This is a space-separated list. This feature can be useful when using :doc:`combination<authcombination>` and cross-realm Kerberos trusts.
+-  **Allowed domains**: if set, tickets will only be accepted if they come
+   from one of the domains listed here. This is a space-separated list.
+   This feature can be useful when using :doc:`combination<authcombination>`
+   and cross-realm Kerberos trusts.
 
 
 .. attention::
 
 
-
     -  Due to a perl GSSAPI issue, you may need to copy the keytab in
        /etc/krb5.keytab which is the default location hardcoded in the
        library
diff -pruN 2.0.13+ds-3/doc/sources/admin/authldap.rst 2.0.14+ds-1/doc/sources/admin/authldap.rst
--- 2.0.13+ds-3/doc/sources/admin/authldap.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authldap.rst	2022-02-19 16:04:21.000000000 +0000
@@ -109,7 +109,7 @@ Connection
 
 .. attention::
 
-    LemonLDAP::NG need anonymous access to LDAP Directory
+    LL::NG needs anonymous access to LDAP Directory
     RootDSE in order to check LDAP connection.
 
 Filters
diff -pruN 2.0.13+ds-3/doc/sources/admin/authopenidconnect_franceconnect.rst 2.0.14+ds-1/doc/sources/admin/authopenidconnect_franceconnect.rst
--- 2.0.13+ds-3/doc/sources/admin/authopenidconnect_franceconnect.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authopenidconnect_franceconnect.rst	2022-01-22 14:30:19.000000000 +0000
@@ -26,7 +26,7 @@ Use the following form:
 https://doc.integ01.dev-franceconnect.fr/inscription.
 
 You need to provide the callback URLs, for example
-https://auth.domain.com/?openidcallback=1.
+https://auth.domain.com/?openidconnectcallback=1.
 
 You will then get a ``client_id`` and a ``client_secret``.
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/authopenidconnect_google.rst 2.0.14+ds-1/doc/sources/admin/authopenidconnect_google.rst
--- 2.0.13+ds-3/doc/sources/admin/authopenidconnect_google.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authopenidconnect_google.rst	2022-01-22 14:30:19.000000000 +0000
@@ -28,7 +28,7 @@ Here you can go in API Manager and get n
 and ``client_secret``).
 
 You need to provide the callback URLs, for example
-https://auth.domain.com/?openidcallback=1.
+https://auth.domain.com/?openidconnectcallback=1.
 
 Declare Google in your LL::NG server
 ------------------------------------
diff -pruN 2.0.13+ds-3/doc/sources/admin/authopenidconnect_prosanteconnect.rst 2.0.14+ds-1/doc/sources/admin/authopenidconnect_prosanteconnect.rst
--- 2.0.13+ds-3/doc/sources/admin/authopenidconnect_prosanteconnect.rst	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authopenidconnect_prosanteconnect.rst	2022-01-22 14:30:19.000000000 +0000
@@ -0,0 +1,209 @@
+Pro Santé Connect
+=================
+
+|logo|
+
+Presentation
+------------
+
+`Pro Santé Connect <https://tech.esante.gouv.fr/outils-services/pro-sante-connect-e-cps/presentation-generale>`__ is
+a French identity provider for healthcare professionals. It relies on OpenID Connect protocol.
+
+Register on Pro Santé Connect
+-----------------------------
+
+Once :doc:`OpenID Connect service<openidconnectservice>` is configured,
+you need to register to Pro Santé Connect.
+
+Go on https://integrateurs-cps.asipsante.fr.
+
+You need to provide the callback URLs, for example
+https://auth.domain.com/?openidconnectcallback=1.
+
+And also a logout URL, for example
+https://auth.domain.com/?logout=1.
+
+You will then get a ``client_id`` and a ``client_secret``.
+
+Declare Pro Santé Connect in your LL::NG server
+-----------------------------------------------
+
+Go in Manager and create a new OpenID Connect provider. You can call it
+``psc-connect`` for example.
+
+Click on ``Metadata`` and set manually the metadata of the service.
+
+For the sandbox server:
+
+.. code-block:: javascript
+
+   {
+     "issuer": "https://auth.bas.esw.esante.gouv.fr/auth/realms/esante-wallet",
+     "authorization_endpoint": "https://wallet.bas.esw.esante.gouv.fr/auth",
+     "token_endpoint": "https://auth.bas.esw.esante.gouv.fr/auth/realms/esante-wallet/protocol/openid-connect/token",
+     "introspection_endpoint": "https://auth.bas.esw.esante.gouv.fr/auth/realms/esante-wallet/protocol/openid-connect/token/introspect",
+     "userinfo_endpoint": "https://auth.bas.esw.esante.gouv.fr/auth/realms/esante-wallet/protocol/openid-connect/userinfo",
+     "end_session_endpoint": "https://auth.bas.esw.esante.gouv.fr/auth/realms/esante-wallet/protocol/openid-connect/logout",
+     "jwks_uri": "https://auth.bas.esw.esante.gouv.fr/auth/realms/esante-wallet/protocol/openid-connect/certs",
+     "check_session_iframe": "https://auth.bas.esw.esante.gouv.fr/auth/realms/esante-wallet/protocol/openid-connect/login-status-iframe.html",
+     "grant_types_supported": [
+       "authorization_code",
+       "implicit",
+       "refresh_token",
+       "password",
+       "client_credentials"
+     ],
+     "response_types_supported": [
+       "code",
+       "none",
+       "id_token",
+       "token",
+       "id_token token",
+       "code id_token",
+       "code token",
+       "code id_token token"
+     ],
+     "subject_types_supported": [
+       "public",
+       "pairwise"
+     ],
+     "id_token_signing_alg_values_supported": [
+       "PS384",
+       "ES384",
+       "RS384",
+       "HS256",
+       "HS512",
+       "ES256",
+       "RS256",
+       "HS384",
+       "ES512",
+       "PS256",
+       "PS512",
+       "RS512"
+     ],
+     "id_token_encryption_alg_values_supported": [
+       "RSA-OAEP",
+       "RSA1_5"
+     ],
+     "id_token_encryption_enc_values_supported": [
+       "A256GCM",
+       "A192GCM",
+       "A128GCM",
+       "A128CBC-HS256",
+       "A192CBC-HS384",
+       "A256CBC-HS512"
+     ],
+     "userinfo_signing_alg_values_supported": [
+       "PS384",
+       "ES384",
+       "RS384",
+       "HS256",
+       "HS512",
+       "ES256",
+       "RS256",
+       "HS384",
+       "ES512",
+       "PS256",
+       "PS512",
+       "RS512",
+       "none"
+     ],
+     "request_object_signing_alg_values_supported": [
+       "PS384",
+       "ES384",
+       "RS384",
+       "HS256",
+       "HS512",
+       "ES256",
+       "RS256",
+       "HS384",
+       "ES512",
+       "PS256",
+       "PS512",
+       "RS512",
+       "none"
+     ],
+     "response_modes_supported": [
+       "query",
+       "fragment",
+       "form_post"
+     ],
+     "registration_endpoint": "https://auth.bas.esw.esante.gouv.fr/auth/realms/esante-wallet/clients-registrations/openid-connect",
+     "token_endpoint_auth_methods_supported": [
+       "private_key_jwt",
+       "client_secret_basic",
+       "client_secret_post",
+       "tls_client_auth",
+       "client_secret_jwt"
+     ],
+     "token_endpoint_auth_signing_alg_values_supported": [
+       "PS384",
+       "ES384",
+       "RS384",
+       "HS256",
+       "HS512",
+       "ES256",
+       "RS256",
+       "HS384",
+       "ES512",
+       "PS256",
+       "PS512",
+       "RS512"
+     ],
+     "claims_supported": [
+       "aud",
+       "sub",
+       "iss",
+       "auth_time",
+       "name",
+       "given_name",
+       "family_name",
+       "preferred_username",
+       "email",
+       "acr"
+     ],
+     "claim_types_supported": [
+       "normal"
+     ],
+     "claims_parameter_supported": false,
+     "scopes_supported": [
+       "openid",
+       "address",
+       "email",
+       "identity",
+       "microprofile-jwt",
+       "offline_access",
+       "phone",
+       "profile",
+       "roles",
+       "scope_1",
+       "scope_2",
+       "scope_all",
+       "web-origins",
+       "eidas2"
+     ],
+     "request_parameter_supported": true,
+     "request_uri_parameter_supported": true,
+     "code_challenge_methods_supported": [
+       "plain",
+       "S256"
+     ],
+     "tls_client_certificate_bound_access_tokens": true
+   }
+
+You should alos import JWKS data from https://auth.bas.esw.esante.gouv.fr/auth/realms/esante-wallet/protocol/openid-connect/certs
+directly in configuration to avoid requests to reload them.
+
+Go in ``Exported attributes`` to choose which attributes you want to collect.
+Read the technical documentation to know available attributes:
+https://tech.esante.gouv.fr/outils-services/pro-sante-connect-e-cps/documentation-technique
+
+Now go in ``Options``:
+
+- Register the ``client_id`` and ``client_secret`` given by Pro Santé Connect
+- In ``Scopes`` set ``openid scope_all``
+- In ``ACR values`` set ``eidas2``
+- You can also set the name and the logo
+
+.. |logo| image:: /applications/prosanteconnect_logo.png
+   :class: align-center
diff -pruN 2.0.13+ds-3/doc/sources/admin/authopenidconnect.rst 2.0.14+ds-1/doc/sources/admin/authopenidconnect.rst
--- 2.0.13+ds-3/doc/sources/admin/authopenidconnect.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authopenidconnect.rst	2022-02-19 16:04:21.000000000 +0000
@@ -31,7 +31,7 @@ As an RP, LL::NG supports a lot of OpenI
 -  Logout on EndSession end point
 
 You can use this authentication module to link your LL::NG server to any
-OpenID Connect Provider. Here are some examples, witch their specific
+OpenID Connect Provider. Here are some examples, with their specific
 documentation:
 
 
@@ -40,13 +40,14 @@ documentation:
 
    authopenidconnect_google
    authopenidconnect_franceconnect
+   authopenidconnect_prosanteconnect
 
 
-=============== ==================
-Google          France Connect
-=============== ==================
-|google|        |franceconnect|
-=============== ==================
+=============== ================== ==================
+Google          France Connect     Pro Santé Connect
+=============== ================== ==================
+|google|        |franceconnect|    |prosanteconnect|
+=============== ================== ==================
 
 .. |google| image:: applications/google_logo.png
    :target: authopenidconnect_google.html
@@ -54,11 +55,14 @@ Google          France Connect
 .. |franceconnect| image:: applications/franceconnect_logo.png
    :target: authopenidconnect_franceconnect.html
 
+.. |prosanteconnect| image:: applications/prosanteconnect_logo.png
+   :target: authopenidconnect_prosanteconnect.html
+
 .. attention::
 
-    OpenID-Connect specification is not finished for logout
+    OpenID Connect specification is not finished for logout
     propagation. So logout initiated by relaying-party will be forward to
-    OpenID-Connect provider but logout initiated by the provider (or another
+    OpenID Connect provider but logout initiated by the provider (or another
     RP) will not be propagated. LLNG will implement this when spec will be
     published.
 
@@ -68,7 +72,7 @@ Configuration
 OpenID Connect Service
 ~~~~~~~~~~~~~~~~~~~~~~
 
-See :doc:`OpenIDConnect service<openidconnectservice>` configuration
+See :doc:`OpenID Connect service<openidconnectservice>` configuration
 chapter.
 
 Authentication and UserDB
@@ -101,11 +105,10 @@ In ``General Parameters`` > ``Authentica
 Then in ``General Parameters`` > ``Authentication modules`` >
 ``OpenID Connect parameters``, you can set:
 
--  **Authentication level**: level of authentication to associate to
-   this module
--  **Callback GET parameter**: name of GET parameter used to intercept
+-  **Authentication level**: Authentication level associated to this module
+-  **Callback GET parameter**: Name of the GET parameter used for intercepting
    callback (default: openidconnectcallback)
--  **State session timeout**: duration of a state session (used to keep
+-  **State session timeout**: Duration of a state session (used for keeping
    state information between authentication request and authentication
    response) in seconds (default: 600)
 
@@ -115,11 +118,12 @@ Register LL::NG to an OpenID Connect Pro
 To register LL::NG, you will need to give some information like
 application name or logo.
 
-You will be asked to provide a *Redirect URI* for LemonLDAP::NG, which is constructed by appending the ``openidcallback=1`` parameter to the Portal URL.
+You will be asked to provide a *Redirect URI* for LL::NG, which is constructed 
+by appending the ``openidconnectcallback=1`` parameter to the Portal URL.
 
 For example:
 
--  https://auth.example.com/?openidcallback=1
+-  https://auth.example.com/?openidconnectcallback=1
 
 
 .. attention::
@@ -128,15 +132,15 @@ For example:
     you need to set SameSite cookie value to "Lax" or "None".
     See :doc:`SSO cookie parameters<ssocookie>`
 
-After registration, the OP must give you a client ID and a client
-secret, that will be used to configure the OP in LL::NG.
+After registration, the OP must give you a *Client ID* and a *Client
+secret* required to configure the OP in LL::NG.
 
 Declare the OpenID Connect Provider in LL::NG
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-In the Manager, select node ``OpenID Connect Providers`` and click on
-``Add OpenID Connect Provider``. Give a technical name (no spaces, no
-special characters), like "sample-op";
+In Manager, select node ``OpenID Connect Providers`` and click on
+``Add OpenID Connect Provider``. Set a technical name (without space or
+special character) like "sample-op".
 
 You can then access to the configuration of this OP.
 
@@ -179,28 +183,24 @@ JWKS data
 ^^^^^^^^^
 
 JWKS is a JSON file containing public keys. LL::NG can grab them
-automatically if jwks_uri is defined in metadata. Else you can paste the
-content of the JSON file in the textarea.
+automatically if jwks_uri is defined in metadata. Else you can paste
+the JSON file content in the textarea.
 
 
 .. tip::
 
     If the OpenID Connect provider only uses symmetric encryption,
-    JWKS data is not useful.
+    JWKS data are useless.
 
 Exported attributes
 ^^^^^^^^^^^^^^^^^^^
 
-Define here the mapping between the LL::NG session content and the
-fields provided in UserInfo response. The fields are defined in `OpenID
-Connect
-standard <http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims>`__,
-and depends on the scope requested by LL::NG (see options in next
-chapter).
-
-.. include:: openidconnectclaims.rst
+Define here mapping between LL::NG session content and fields
+provided in UserInfo endpoint response. These fields are defined in
+`OpenID Connect standard <http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims>`__,
+and depends on the scope requested by LL::NG (see options below).
 
-So you can define for example:
+So you can define by example:
 
 -  cn => name
 -  sn => family_name
@@ -217,7 +217,7 @@ Options
       to get a fresh version of JWKS data. Set to 0 to disable it.
    -  **Client ID**: Client ID given by OP
    -  **Client secret**: Client secret given by OP
-   -  **Store ID token**: Allows one to store the ID token (JWT) inside
+   -  **Store ID token**: Allows one to store the ID Token (JWT) inside
       user session. Do not enable it unless you need to replay this token
       on an application, or if you need the id_token_hint parameter when
       using logout.
@@ -236,8 +236,8 @@ Options
       ``client_secret_post`` and ``client_secret_basic``
    -  **Check JWT signature**: Set to 0 to disable JWT signature
       checking
-   -  **ID Token max age**: If defined, LL::NG will check the date of ID
-      token and refuse it if it is too old
+   -  **ID Token max age**: If defined, LL::NG will check the ID Token
+      date and reject it if too old
    -  **Use Nonce**: If enabled, a nonce will be sent, and verified from
       the ID Token
 
@@ -246,3 +246,12 @@ Options
    -  **Display name**: Name of the application
    -  **Logo**: Logo of the application
    -  **Order**: Number to sort buttons
+
+
+.. attention::
+
+    With HTTPS authorization endpoint, you may have to set **LWP::UserAgent object**
+    with ``verify_hostname => 0`` and ``SSL_verify_mode => 0``.
+
+
+    Go to: ``General Parameters > Advanced Parameters > Security > SSL options for server requests``
\ No newline at end of file
diff -pruN 2.0.13+ds-3/doc/sources/admin/authopenid.rst 2.0.14+ds-1/doc/sources/admin/authopenid.rst
--- 2.0.13+ds-3/doc/sources/admin/authopenid.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authopenid.rst	2022-02-19 16:04:21.000000000 +0000
@@ -27,7 +27,7 @@ least version 1.0.
     LL::NG can also act as :doc:`OpenID server<idpopenid>`, that
     allows one to interconnect two LL::NG systems.
 
-LL::NG will then display a form with an OpenID input, wher users will
+LL::NG will then display a form with an OpenID input, where users will
 type their OpenID login.
 
 
@@ -81,12 +81,12 @@ See also :doc:`exported variables config
 
 .. attention::
 
-    Browser implementations of formAction directive are
-    inconsistent (e.g. Firefox doesn't block the redirects whereas Chrome
+    Browser implementations of formAction directive are inconsistent
+    (e.g. Firefox doesn't block the redirects whereas Chrome
     does). Administrators may have to modify formAction value with wildcard
     likes \*.
 
-    In Manager, go in :
+    In Manager, go in:
 
     ``General Parameters`` > ``Advanced Parameters`` > ``Security`` >
     ``Content Security Policy`` > ``Form destination``
diff -pruN 2.0.13+ds-3/doc/sources/admin/authproxy.rst 2.0.14+ds-1/doc/sources/admin/authproxy.rst
--- 2.0.13+ds-3/doc/sources/admin/authproxy.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authproxy.rst	2022-02-19 16:04:21.000000000 +0000
@@ -15,7 +15,7 @@ credentials to another LL::NG portal, li
 
 The difference with :doc:`remote authentication<authremote>` is that the
 client will never be redirect to the main LL::NG portal. This
-configuration is usable if you want to expose your internal SSO portal
+configuration is useful if you want to expose your internal SSO portal
 to another network (DMZ).
 
 Configuration
@@ -29,23 +29,33 @@ and choose Proxy for authentication and
 
 Then, go in ``Proxy parameters``:
 
--  **Internal portal URL**: URL of internal portal
--  **Session service URL** (optional): Session service URL (default:
-   same as previous for SOAP, same with "/session/my" for REST)
--  **Cookie name** (optional): name of the cookie of internal portal, if
-   different from external portal
 -  **Authentication level**: authentication level for Proxy module
 -  **Use SOAP instead of REST**: use a deprecated SOAP server instead of
    a REST one (you must set it if internal portal version is < 2.0). In
    this case, "Portal URL" parameter must contain SOAP endpoint
    (generally http://auth.example.com/index.pl/sessions for 1.9 and
    earlier, http://auth.example.com/sessions for 2.0)
+-  **URL**: URL of internal portal
+-  **Session service URL** (optional): session service URL (default:
+   same as previous for SOAP, same with "/session/my" for REST)
+-  **Choice parameter** (optional): choice parameter of the internal portal if applicable
+-  **Choice value** (optional): value of the choice parameter of the internal portal
+-  **Cookie name** (optional): internal portal cookie name,
+   if different from external portal
+-  **Impersonation** (optional) : can be enabled if the internal portal provides impersonation
+
+.. note::
+
+    If the internal portal uses :doc:`Choice Authentication<authchoice>`,
+    you have to specify 'Internal portal choice parameter' and
+    'Internal portal choice value' depending on its configuration. 
+    This feature needs at least LL::NG version 2.0.14.
 
 Internal portal
 ~~~~~~~~~~~~~~~
 
 The portal must be configured to accept REST or SOAP authentication
-requests if you chose to use SOAP. See:
+requests. See:
 :doc:`REST server plugin<restservices>` or
 :doc:`SOAP session backend<soapsessionbackend>` *(deprecated)*.
 
@@ -59,7 +69,6 @@ in your lemonldap-ng.ini:
 
    soapProxyUrn = urn:Lemonldap/NG/Common/CGI/SOAPService
 
-
 .. attention::
 
-    This feature needs at least LLNG version 2.0.8
+    This feature needs at least LL::NG version 2.0.8
diff -pruN 2.0.13+ds-3/doc/sources/admin/authradius.rst 2.0.14+ds-1/doc/sources/admin/authradius.rst
--- 2.0.13+ds-3/doc/sources/admin/authradius.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authradius.rst	2022-02-19 16:04:21.000000000 +0000
@@ -37,8 +37,8 @@ In Debian/Ubuntu, install the library th
 
    apt-get install libauthen-radius-perl
 
-Configuration of LemonLDAP::NG
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Configuration of LL::NG
+~~~~~~~~~~~~~~~~~~~~~~~
 
 In Manager, go in ``General Parameters`` > ``Authentication modules``
 and choose Radius for authentication.
diff -pruN 2.0.13+ds-3/doc/sources/admin/authsaml.rst 2.0.14+ds-1/doc/sources/admin/authsaml.rst
--- 2.0.13+ds-3/doc/sources/admin/authsaml.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/authsaml.rst	2022-01-22 14:30:19.000000000 +0000
@@ -18,8 +18,8 @@ Several IDPs are allowed, in this case t
 wants. You can preselect IDP with an IDP resolution rule.
 
 For each IDP, you can configure attributes that are collected. Some can
-be mandatory, so if they are not returned by IDP, the session will not
-open.
+be mandatory, so if they are not returned by IDP, the session will not be
+opened.
 
 
 .. tip::
@@ -91,7 +91,7 @@ between your server and the IDP):
 
 .. tip::
 
-    You can also edit the metadata directly in the textarea
+    You can also edit the metadata directly in the textarea.
 
 Exported attributes
 ^^^^^^^^^^^^^^^^^^^
@@ -102,8 +102,8 @@ For each attribute, you can set:
    "uid" will then be used as $uid in access rules
 -  **Attribute name**: name of the SAML attribute coming from the remote IDP
 -  **Friendly Name**: optional, SAML attribute friendly name.
--  **Mandatory**: if set to On, then session will not open if this
-   attribute is not given by IDP.
+-  **Mandatory**: if set to On, session will not be created if this
+   attribute is not sent by IDP.
 -  **Format** (optional): SAML attribute format.
 
 |image1|
@@ -192,8 +192,8 @@ Binding
 
 .. note::
 
-    If no binding defined, the default binding in IDP metadata will be
-    used.
+    If no binding is defined, the default binding in IDP metadata
+    will be used.
 
 Security
 ''''''''
@@ -208,11 +208,11 @@ Security
 Display
 '''''''
 
-Used only if you have more than 1 SAML Identity Provider declared
+Used only if at least 2 SAML Identity Providers are declared
 
 -  **Display name**: Name of the IDP
 -  **Logo**: Logo of the IDP
--  **Order**: Number to sort IDP display
+-  **Order**: Number used for sorting IDP display
 
 
 .. tip::
diff -pruN 2.0.13+ds-3/doc/sources/admin/bruteforceprotection.rst 2.0.14+ds-1/doc/sources/admin/bruteforceprotection.rst
--- 2.0.13+ds-3/doc/sources/admin/bruteforceprotection.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/bruteforceprotection.rst	2022-01-22 14:30:19.000000000 +0000
@@ -34,6 +34,8 @@ set to ``On``.
    -  **Allowed failed login**: Number of failed login attempts allowed before account is locked
    -  **Incremental lock**: Enable/disable incremental lock times
    -  **Incremental lock times**: List of comma separated lock time values in seconds
+   -  **Maximum lock time**: Lock time values can not be higher than max lock time
+   -  **Maximum age**: Delta between current and last stored failed login 
 
 
 Incremental lock time enabled
@@ -70,17 +72,8 @@ Lock time increases between each failed
 Incremental lock time disabled
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-After allowed failed login attempts, user must
-wait the lock time before trying to log in again.
-To modify delta (MaxAge) between current and last stored
-failed login (300 seconds by default) edit ``lemonldap-ng.ini`` in [portal] section:
-
-.. code-block:: ini
-
-   [portal]
-   bruteForceProtectionTempo = 30
-   bruteForceProtectionMaxAge = 300
-   bruteForceProtectionMaxFailed = 3
+After allowed failed login attempts, user must wait
+the lock time before trying to log in again.
 
 
 .. attention::
diff -pruN 2.0.13+ds-3/doc/sources/admin/checkdevops.rst 2.0.14+ds-1/doc/sources/admin/checkdevops.rst
--- 2.0.13+ds-3/doc/sources/admin/checkdevops.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/checkdevops.rst	2022-01-22 14:30:19.000000000 +0000
@@ -11,15 +11,19 @@ Just enable it in the manager (section 
 -  **Parameters**:
 
    -  **Activation**: Enable / Disable this plugin
-   -  **Download file**: Allow users to download DevOps file from a remote server by
-      providing an URL (By example: http://myapp.example.com:8080). Plugin will
-      try to retrieve remote file by sending a request (i.e.
-      http://myapp.example.com:8080/rules.json)
+   -  **Download file**: Allow users to download DevOps file from a
+      remote server by providing an URL
+      (By example: http://myapp.example.com:8080). Plugin will
+      try to retrieve remote file by sending a request
+      (i.e. http://myapp.example.com:8080/rules.json)
+   -  **Display normalized headers**: Display headers as they are sent
+   -  **Check session attributes**: Check if used attributes are existing
 
 Usage
 -----
 When enabled, ``/checkdevops`` URL path is handled by this plugin.
-Then, you can paste a file to test your rules and headers.
+Then, you can paste a file to test your rules and headers or
+provide an URL to download the ``rules.json`` file.
 
 Example
 ~~~~~~~
@@ -48,7 +52,7 @@ access rules and headers:
 
     By example: ``$groups =~ /\bdevops\b/``
 
-.. attention::
+.. danger::
 
     Be careful to not display secret attributes.
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/checkstate.rst 2.0.14+ds-1/doc/sources/admin/checkstate.rst
--- 2.0.13+ds-3/doc/sources/admin/checkstate.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/checkstate.rst	2022-01-22 14:30:19.000000000 +0000
@@ -25,6 +25,23 @@ GET Parameter Need     Value
 ``password``  optional
 ============= ======== ============================================================
 
+Response
+--------
+
+The plugin will respond to the HTTP request with:
+
+* HTTP code 500 if something went wrong
+* HTTP code 200 and the following JSON content if something went right
+
+```
+{"result":1,"version":"2.0.14"}
+```
+
+.. versionadded:: 2.0.14
+   The *version* key is returned
+
+
+
 Example
 ~~~~~~~
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/checkuser.rst 2.0.14+ds-1/doc/sources/admin/checkuser.rst
--- 2.0.13+ds-3/doc/sources/admin/checkuser.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/checkuser.rst	2022-01-22 14:30:19.000000000 +0000
@@ -17,29 +17,30 @@ Just enable it in the manager (section 
    -  **Identities use rule**: Rule to define which profiles can be
       displayed (by example: ``!$anonymous``)
    -  **Unrestricted users rule**: Rule to define which users can check
-      ALL users. ``Identities use rule`` is bypassed.
-   -  **Hidden attributes**: Session attributes not displayed
+      ALL users and attributes.
+   -  **Hidden attributes**: Session attributes not displayed except for unrestricted users
    -  **Attributes used for searching sessions**: User's attributes used
       for searching sessions in backend if ``whatToTrace`` fails. Useful
       to look for sessions by mail or givenName. Let it blank to search
       by ``whatToTrace`` only
    -  **Hidden headers**: Sent headers whose value is masked except for unrestricted users.
-      Key is a Virtualhost name and value represents a space-separated headers list.
-      A blank value obfuscates ALL relative Virtualhost sent headers.
+      Key is a VirtualHost name and value represents a space-separated headers list.
+      A blank value obfuscates ALL relative VirtualHost sent headers.
       Note that just valued hearders are masked.
 
 -  **Display**:
 
    -  **Computed sessions**: Rule to define which users can display a
       computed session if no SSO session is found
-   -  **Empty headers**: Rule to define which users can display ALL headers
-      appended by LemonLDAP::NG including empty ones
-   -  **Normalized headers**: Rule to define which users can see headers name sent by
-      the web server (see RFC3875)
-   -  **Empty values**: Rule to define which users can display ALL attributes
-      even empty ones
    -  **Persistent session data**: Rule to define which users can display
       persistent session data
+   -  **Normalized headers**: Rule to define which users can see headers name sent by
+      the web server (see RFC3875)
+   -  **Empty headers**: Rule to define which users can display ALL headers
+      sent by LemonLDAP::NG including empty ones
+   -  **Empty values**: Rule to define which users can display empty values
+   -  **Hidden attributes**: Rule to define which users can display hidden attributes
+   -  **History**: Rule to define which users can display logins history
 
 .. note::
 
@@ -57,7 +58,7 @@ Just enable it in the manager (section 
 
     By example:
 
-    \* Search attributes => ``mail uid givenName``
+    \* Search attributes => ``mail, uid, givenName``
 
     If ``whatToTrace`` fails, sessions are searched by ``mail``, next
     ``uid`` if none session is found and so on...
diff -pruN 2.0.13+ds-3/doc/sources/admin/configvhost.rst 2.0.14+ds-1/doc/sources/admin/configvhost.rst
--- 2.0.13+ds-3/doc/sources/admin/configvhost.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/configvhost.rst	2022-02-07 19:06:14.000000000 +0000
@@ -38,7 +38,7 @@ Example of a protected virtual host for
 
    </VirtualHost>
 
-Reverse proxy
+Reverse-Proxy
 ~~~~~~~~~~~~~
 
 Example of a protected virtual host with LemonLDAP::NG as reverse proxy:
@@ -258,7 +258,7 @@ Example of a protected virtual host for
 
 .. _reverse-proxy-1:
 
-Reverse proxy
+Reverse-Proxy
 ~~~~~~~~~~~~~
 
 - Example of a protected reverse-proxy:
@@ -452,7 +452,7 @@ A virtual host contains:
 -  Access rules: check user's right on URL patterns
 -  HTTP headers: forge information sent to protected applications
 -  POST data: use form replay
--  Options: redirection port and protocol
+-  Options: redirection port, protocol, Handler type, aliases,required authentication level,...
 
 Access rules and HTTP headers
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -464,7 +464,7 @@ application by LL::NG.
 
 .. attention::
 
-    With **Nginx**-based ReverseProxy, header directives can
+    With **Nginx**-based Reverse-Proxy, header directives can
     be appended by a LUA script.
 
     To send more than **15** headers to protected applications,
@@ -507,14 +507,19 @@ Some options are available:
    Provide a comma separated parameters list with custom function path and args.
    Args can be vars or session attributes, macros, ...
    By example: My::accessToTrace, Doctor, Who, _whatToTrace
--  **Type**: handler type (normal,
-   :doc:`ServiceToken Handler<servertoserver>`,
-   :doc:`DevOps Handler<devopshandler>`,...)
+-  **Type**: handler type (:ref:`Main<presentation-kinematics>`,
+   :doc:`AuthBasic<authbasichandler>`,
+   :doc:`ServiceToken<servertoserver>`,
+   :doc:`DevOps<devopshandler>`,
+   :doc:`DevOpsST<devopssthandler>`,
+   :doc:`OAuth2<oauth2handler>`,...)
 -  **Required authentication level**: this option avoids to reject user with
    a rule based on ``$_authenticationLevel``. When user has not got the
    required level, he is redirected to an upgrade page in the portal.
    This default level is required for ALL locations relative to this virtual host.
    It can be overrided for each locations.
+-  **DevOps rules file URL**: option to define URL to retreive DevOps rules file.
+   This option can be overridden with ``uwsgi_param/fastcgi_param RULES_URL`` parameter.
 -  **ServiceToken timeout**: by default, ServiceToken is just valid during 30
    seconds. This TTL can be customized for each virtual host.
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/conf.py 2.0.14+ds-1/doc/sources/admin/conf.py
--- 2.0.13+ds-3/doc/sources/admin/conf.py	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/conf.py	2022-01-22 14:30:19.000000000 +0000
@@ -174,6 +174,9 @@ html_css_files = [
     'css/custom.css',
 ]
 
+html_favicon = 'logos/favicon.ico'
+html_logo = 'logos/lemonldap-ng-logo.png'
+
 # Add any extra paths that contain custom files (such as robots.txt or
 # .htaccess) here, relative to this directory. These files are copied
 # directly to the root of the documentation.
diff -pruN 2.0.13+ds-3/doc/sources/admin/customfunctions.rst 2.0.14+ds-1/doc/sources/admin/customfunctions.rst
--- 2.0.13+ds-3/doc/sources/admin/customfunctions.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/customfunctions.rst	2022-02-22 17:45:22.000000000 +0000
@@ -12,14 +12,14 @@ Custom functions allow one to extend LL:
 Implementation
 --------------
 
-Your perl custom function must be declared on appropriate server when
-separating :
+Your perl custom functions must be declared on appropriate server when
+separating:
 
-portal type : declare custom function here when using it in rules,
-macros, menu
+**Portal type**: declare custom functions here when using it in rules,
+macros or menu.
 
-reverse-proxy type : declare custom function here when using it in
-headers
+**Reverse-proxy type**: declare custom functions here when using it in
+headers.
 
 Write custom functions library
 ------------------------------
@@ -51,77 +51,24 @@ as you want, for example ``SSOExtensions
 Import custom functions in LemonLDAP::NG
 ----------------------------------------
 
-Load relevant code in handler server
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+LemonLDAP::NG Configuration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-New method
-^^^^^^^^^^
-
-Just declare files or Perl module that must be loaded:
+Edit ``lemonldap-ng.ini`` to load the Perl module(s)
 
 ::
 
    [all]
-   require = /path/to/functions.pl, /path/to/SSOExtensions.pm
-   # OR
-   require = SSOExtensions::function1, SSOExtensions::function2
+   require = /path/to/SSOExtensions.pm
    ; Prevent Portal to crash if Perl module is not found
    ;requireDontDie = 1
 
-Old method
-^^^^^^^^^^
-
-
-.. danger::
-
-    This method is available but unusable by Portal under
-    Apache. So if your rule may be used by the menu, use the new
-    method.
-
-Apache
-''''''
-
-Your module has to be loaded by Apache (for example after Handler load):
-
-.. code-block:: apache
-
-   # Perl environment
-   PerlRequire Lemonldap::NG::Handler
-   PerlRequire /path/to/SSOExtensions.pm
-   PerlOptions +GlobalRequest
-
-FastCGI server (Nginx)
-''''''''''''''''''''''
-
-You've just to incicate to :doc:`LLNG FastCGI server<fastcgiserver>` the
-file to read using either ``-f`` option or ``CUSTOM_FUNCTIONS_FILE``
-environment variable. Using packages, you just have to modify your
-``/etc/default/llng-fastcgi-server`` (or
-``/etc/default/lemonldap-ng-fastcgi-server``) file:
-
-.. code-block:: sh
-
-   # Number of process (default: 7)
-   #NPROC = 7
-
-   # Unix socket to listen to
-   SOCKET=/var/run/llng-fastcgi-server/llng-fastcgi.sock
-
-   # Pid file
-   PID=/var/run/llng-fastcgi-server/llng-fastcgi-server.pid
-
-   # User and GROUP
-   USER=www-data
-   GROUP=www-data
-
-   # Custom functions file
-   CUSTOM_FUNCTIONS_FILE=/path/to/SSOExtensions.pm
 
 Declare custom functions
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
 Go in Manager, ``General Parameters`` » ``Advanced Parameters`` »
-``Custom functions`` and set:
+``Custom functions`` and declare your function names, separated by a space:
 
 ::
 
@@ -133,13 +80,13 @@ Go in Manager, ``General Parameters`` »
     If your function is not compliant with
     :doc:`Safe jail<safejail>`, you will need to disable the jail.
 
-Use it
-------
+Usage
+-----
 
 You can now use your function in a macro, an header or an access rule,
 for example:
 
 ::
 
-   SSOExtensions::function1( $uid, $ENV{REMOTE_ADDR} )
+   function1( $uid, $ENV{REMOTE_ADDR} )
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/devopshandler.rst 2.0.14+ds-1/doc/sources/admin/devopshandler.rst
--- 2.0.13+ds-3/doc/sources/admin/devopshandler.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/devopshandler.rst	2022-02-07 19:06:14.000000000 +0000
@@ -1,8 +1,8 @@
 DevOps Handler
 ==============
 
-This handler is designed to read vhost configuration from the website
-itself not from LL:NG configuration. Rules and headers are set in a
+This Handler is designed to retrieve VHost configuration from the website
+itself, not from LL:NG configuration. Rules and headers are set in a
 **rules.json** file stored at the website root directory (ie
 ``http://website/rules.json``). This file looks like:
 
@@ -23,17 +23,19 @@ If this file is not found, the default r
 
 No specific configuration is required except that:
 
--  you have to choose this specific handler (directly by using
-   ``VHOSTTYPE`` environment variable)
--  you can set the loopback URL needed by the DevOps handler to get
-   ``/rules.json`` or use ``RULES_URL`` parameter to set JSON file path
-   (see :doc:`SSO as a Service<ssoaas>`). Default to
-   ``http://127.0.0.1:<server-port>``
+-  you have to select ``DevOps`` handler type either with
+   ``VHOSTTYPE`` environment variable or in VHost options
+-  you can set in VHost options the loopback URL requested by 
+   the DevOps handler to retrieve ``/rules.json`` or use
+   ``RULES_URL`` environment variable to set JSON file location.
+   Default to ``http://127.0.0.1:<server-port>``
+-  HTTPS or redirection port can be set by using
+   ``HTTP_REDIRECT`` or ``PORT_REDIRECT`` environment variables.
 
 
 .. attention::
 
-    Note that DevOps handler will refuse to compile
-    rules.json if :doc:`Safe Jail<safejail>` isn't enabled.
+    Note that DevOps handler will not compile
+    rules.json if :doc:`Safe Jail<safejail>` is not enabled.
 
-See :doc:`SSO as a Service<ssoaas>` for more
+See :doc:`SSO as a Service<ssoaas>` for more.
diff -pruN 2.0.13+ds-3/doc/sources/admin/documentation.rst 2.0.14+ds-1/doc/sources/admin/documentation.rst
--- 2.0.13+ds-3/doc/sources/admin/documentation.rst	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/documentation.rst	2022-02-19 16:04:21.000000000 +0000
@@ -31,7 +31,7 @@ Installation and configuration
    -  `Version 2.0 </documentation/2.0/>`__ (stable)
    -  `Version 1.9 </documentation/1.9/>`__ (oldstable)
 
--  Archived versions (unmaintained by LLNG Team )
+-  Archived versions (unmaintained by LL::NG Team )
 
    -  `Version 1.4 </documentation/1.4/>`__
    -  `Version 1.3 </documentation/1.3/>`__
@@ -42,33 +42,31 @@ Installation and configuration
 Packaged versions
 ~~~~~~~~~~~~~~~~~
 
-These versions are maintained under distribution umbrella following
-their policy.
+These versions are maintained under distribution umbrella following their policy.
 
 Debian
 ^^^^^^
 
 .. tip::
 
-   Following Debian Policy, LLNG packages are never upgraded in published distributions. However, security patches are backported by maintenance teams *(except some inor ones)*.
+   Following Debian Policy, LL::NG packages are never upgraded in published distributions. However, security patches are backported by maintenance teams *(except some minor ones)*.
    See `Security tracker <https://security-tracker.debian.org/tracker/source-package/lemonldap-ng>`__
 
-=========== ========================== ======================================== ===================================================== ============================================================ =============================== =============================================================
-Debian dist                            LLNG version                             Secured                                               Maintenance                                                  LTS Limit                       `Extended LTS <https://wiki.debian.org/LTS/Extended>`__ Limit
-=========== ========================== ======================================== ===================================================== ============================================================ =============================== =============================================================
-*6*         *Squeeze*                  *0.9.4.1*                                |maybe| No known vulnerability                        *None*                                                       *February 2016*                 *April 2019*
-*7*         *Wheezy*                   `1.1.2 </documentation/1.1/>`__          |maybe| No known vulnerability                        *None*                                                       *May 2018*                      *June 2020*
-**8**       Jessie                     `1.3.3 </documentation/1.3/>`__          |clean| CVE-2019-19791 tagged as minor                **None**  [1]_                                               June 2020                       June 2022
-**9**       Stretch                    `1.9.7 </documentation/1.9/>`__          |clean| CVE-2019-19791 tagged as minor                `Debian LTS Team <https://www.debian.org/lts/>`__            June 2022                       Probably 2024
+=========== ========================== ======================================== ===================================================== ============================================================ =================================== =============================================================
+Debian dist                            LL::NG version                             Secured                                               Maintenance                                                  LTS Limit                           `Extended LTS <https://wiki.debian.org/LTS/Extended>`__ Limit
+=========== ========================== ======================================== ===================================================== ============================================================ =================================== =============================================================
+*6*         *Squeeze*                  *0.9.4.1*                                |maybe| No known vulnerability                        *None*                                                       *February 2016*                     *April 2019*
+*7*         *Wheezy*                   `1.1.2 </documentation/1.1/>`__          |maybe| No known vulnerability                        *None*                                                       *May 2018*                          *June 2020*
+**8**       Jessie                     `1.3.3 </documentation/1.3/>`__          |clean| CVE-2019-19791 tagged as minor                **None**  [1]_                                               June 2020                           June 2022
+**9**       Stretch                    `1.9.7 </documentation/1.9/>`__          |clean| CVE-2019-19791 tagged as minor                `Debian LTS Team <https://www.debian.org/lts/>`__            June 2022                           Probably 2024
 \           *Stretch-backports*        `2.0.2 </documentation/2.0/>`__          |bad| CVE-2019-12046, CVE-2019-13031, CVE-2019-15941  *None*                                                       *June 2019*
 \           *Stretch-backports-sloppy* `2.0.11 </documentation/2.0/>`__         |maybe|                                               *None*                                                       *August 2021*
-**10**      Buster                     `2.0.2 </documentation/2.0/>`__          |clean| CVE-2019-19791 tagged as minor                `Debian Security Team <https://security-team.debian.org/>`__ June 2024                       Probably 2026
-\           *Buster-backports*         `2.0.11 </documentation/2.0/>`__         |clean|                                               *None*                                                       *August 2021*
-\           Buster-backports-sloppy    `2.0.11 </documentation/2.0/>`__         |clean|                                               LLNG Team, "best effort" [3]_                                Until Debian 12 release [4]_
-**11**      Bullseye                   `2.0.11 </documentation/2.0/>`__         |clean|                                               `Debian Security Team <https://security-team.debian.org/>`__ July 2026                       Probably 2028
-\           Bullseye-backports         `2.0.11 </documentation/2.0/>`__         |clean|                                               LLNG Team, "best effort" [3]_                                Until Debian 12 release [4]_
-**Next**    Testing/Unstable           Latest  [5]_                             |clean|                                               LLNG Team
-=========== ========================== ======================================== ===================================================== ============================================================ =============================== =============================================================
+**10**      Buster                     `2.0.2 </documentation/2.0/>`__          |clean| CVE-2019-19791 tagged as minor                `Debian Security Team <https://security-team.debian.org/>`__ June 2024                           Probably 2026
+\           Buster-backports           `2.0.13 </documentation/2.0/>`__         |clean|                                               LL::NG Team, "best effort" [3]_                              Maybe until Debian 12 release [4]_
+**11**      Bullseye                   `2.0.11 </documentation/2.0/>`__         |clean|                                               `Debian Security Team <https://security-team.debian.org/>`__ July 2026                           Probably 2028
+\           Bullseye-backports         `2.0.13 </documentation/2.0/>`__         |clean|                                               LL::NG Team, "best effort" [3]_                              Maybe until Debian 13 release [4]_
+**Next**    Testing/Unstable           Latest  [5]_                             |clean|                                               LL::NG Team
+=========== ========================== ======================================== ===================================================== ============================================================ =================================== =============================================================
 
 See `Debian Security
 Tracker <https://security-tracker.debian.org/tracker/source-package/lemonldap-ng>`__
@@ -83,7 +81,7 @@ Ubuntu
    Ubuntu version are included in "universe" branch [8]_, so not really security maintained. Prefer to use our repositories or Debian ones
 
 =========== ============= ================================ ==================================================================== ===========
-Ubuntu dist               LLNG version                     Secured                                                              Maintenance
+Ubuntu dist               LL::NG version                     Secured                                                              Maintenance
 =========== ============= ================================ ==================================================================== ===========
 12.04       Precise       `1.1.2 </documentation/1.1/>`__  |maybe| No known vulnerability                                       None
 14.04       Trusty        `1.2.5 </documentation/1.2/>`__  |maybe| No known vulnerability                                       None
@@ -108,7 +106,7 @@ Development
 -  `Source
    code <https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/tree/master>`__
 -  `Nightly trunk builds <http://lemonldap-ng.ow2.io/lemonldap-ng/>`__
-   *(for Debian or Ubuntu,*\ **really unstable**\ *)*
+   *(for Debian or Ubuntu, *\ **really unstable**\ *)*
 -  Git access:
 
 ::
@@ -139,7 +137,7 @@ Other
    Possible `Extended LTS <https://wiki.debian.org/LTS/Extended>`__
 
 .. [3]
-   updated by `LLNG Team </team>`__ until dependencies are compatible.
+   updated by `LL::NG Team </team>`__ until dependencies are compatible.
    Don't use backports unless you plan to update your system because
    backports are not covered by Debian Security Policy
 
@@ -151,7 +149,7 @@ Other
 
 .. [8]
    Ubuntu universe/multiverse branches are community maintained *(so not
-   maintained by Canonical)*, but in fact nobody considers LLNG security
+   maintained by Canonical)*, but in fact nobody considers LL::NG security
    issues. See `this
    issue <https://bugs.launchpad.net/ubuntu/+source/lemonldap-ng/+bug/1829016>`__
    for example
diff -pruN 2.0.13+ds-3/doc/sources/admin/download.rst 2.0.14+ds-1/doc/sources/admin/download.rst
--- 2.0.13+ds-3/doc/sources/admin/download.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/download.rst	1970-01-01 00:00:00.000000000 +0000
@@ -1,107 +0,0 @@
-Download
-========
-
-Release notes
--------------
-
-Release notes for latest version:
-https://projects.ow2.org/view/lemonldap-ng/lemonldap-ng-2-0-9-is-out
-
-Go on https://projects.ow2.org/bin/view/lemonldap-ng/ for older
-versions.
-
-See also :doc:`upgrade notes<upgrade>`.
-
-Packages and archives
----------------------
-
-Stable version (2.0.9)
-~~~~~~~~~~~~~~~~~~~~~~
-
-Tarball
-^^^^^^^
-
--  `Tarball <https://release.ow2.org/lemonldap/lemonldap-ng-2.0.9.tar.gz>`__
-
-RPM
-^^^
-
-
-.. tip::
-
-    You can:
-    -  Use :ref:`our own YUM repository<installrpm-yum-repository>`.
-    -  Download them here and :ref:`install pre-required packages<prereq-yum>`.
-
-
-RHEL/CentOS 7
-'''''''''''''
-
--  `RPM
-   bundle <https://release.ow2.org/lemonldap/lemonldap-ng-2.0.9_el7.rpm.tar.gz>`__
--  `Source
-   RPM <https://release.ow2.org/lemonldap/lemonldap-ng-2.0.9-1.el7.src.rpm>`__
-
-RHEL/CentOS 8
-'''''''''''''
-
--  `RPM
-   bundle <https://release.ow2.org/lemonldap/lemonldap-ng-2.0.9_el8.rpm.tar.gz>`__
--  `Source
-   RPM <https://release.ow2.org/lemonldap/lemonldap-ng-2.0.9-1.el8.src.rpm>`__
-
-Debian
-^^^^^^
-
-
-.. tip::
-
-    You can:
-
-    -  Use
-       :ref:`packages provided by Debian<installdeb-official-repository>`.
-    -  Use
-       :ref:`our own Debian repository<installdeb-llng-repository>`.
-    -  Download them here and
-       :ref:`install pre-required packages<prereq-apt-get>`.
-
-
--  `DEB
-   bundle <https://release.ow2.org/lemonldap/lemonldap-ng-2.0.9_deb.tar.gz>`__
-
-Docker
-^^^^^^
-
-See https://hub.docker.com/r/coudot/lemonldap-ng/
-
-::
-
-   docker pull coudot/lemonldap-ng
-
-Nightly builds from master branch
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Debian repository of master branch, rebuilt every night:
-http://lemonldap-ng.ow2.io/lemonldap-ng/
-
-Older versions
-~~~~~~~~~~~~~~
-
-You can find all versions on `OW2
-releases <https://release.ow2.org/lemonldap/>`__.
-
-Contributions
--------------
-
-See https://github.com/LemonLDAPNG
-
-.. _download-getting-sources-from-svn-repository:
-
-Git repository
---------------
-
-See https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng
-
-::
-
-   git clone git@gitlab.ow2.org:lemonldap-ng/lemonldap-ng.git
diff -pruN 2.0.13+ds-3/doc/sources/admin/error_codes.rst 2.0.14+ds-1/doc/sources/admin/error_codes.rst
--- 2.0.13+ds-3/doc/sources/admin/error_codes.rst	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/error_codes.rst	2022-02-07 19:06:14.000000000 +0000
@@ -0,0 +1,113 @@
+Error codes list
+================
+
+.. note::
+
+    This page references all Portal error codes.
+
+
+```
+PE_IDPCHOICE                       => -5,
+PE_SENDRESPONSE                      => -4,
+PE_INFO                              => -3,
+PE_REDIRECT                          => -2,
+PE_DONE                              => -1,
+PE_OK                                => 0,
+PE_SESSIONEXPIRED                    => 1,
+PE_FORMEMPTY                          => 2,
+PE_WRONGMANAGERACCOUNT                => 3,
+PE_USERNOTFOUND                      => 4,
+PE_BADCREDENTIALS                     => 5,
+PE_LDAPCONNECTFAILED                  => 6,
+PE_LDAPERROR                         => 7,
+PE_APACHESESSIONERROR                 => 8,
+PE_FIRSTACCESS                        => 9,
+PE_BADCERTIFICATE                    => 10,
+PE_NO_PASSWORD_BE                    => 20,
+PE_PP_ACCOUNT_LOCKED                 => 21,
+PE_PP_PASSWORD_EXPIRED               => 22,
+PE_CERTIFICATEREQUIRED               => 23,
+PE_ERROR                             => 24,
+PE_PP_CHANGE_AFTER_RESET             => 25,
+PE_PP_PASSWORD_MOD_NOT_ALLOWED       => 26,
+PE_PP_MUST_SUPPLY_OLD_PASSWORD       => 27,
+PE_PP_INSUFFICIENT_PASSWORD_QUALITY  => 28,
+PE_PP_PASSWORD_TOO_SHORT             => 29,
+PE_PP_PASSWORD_TOO_YOUNG             => 30,
+PE_PP_PASSWORD_IN_HISTORY            => 31,
+PE_PP_GRACE                          => 32,
+PE_PP_EXP_WARNING                    => 33,
+PE_PASSWORD_MISMATCH                 => 34,
+PE_PASSWORD_OK                       => 35,
+PE_NOTIFICATION                      => 36,
+PE_BADURL                            => 37,
+PE_NOSCHEME                          => 38,
+PE_BADOLDPASSWORD                    => 39,
+PE_MALFORMEDUSER                     => 40,
+PE_SESSIONNOTGRANTED                 => 41,
+PE_CONFIRM                           => 42,
+PE_MAILFORMEMPTY                     => 43,
+PE_BADMAILTOKEN                      => 44,
+PE_MAILERROR                         => 45,
+PE_MAILOK                            => 46,
+PE_LOGOUT_OK                         => 47,
+PE_SAML_ERROR                        => 48,
+PE_SAML_LOAD_SERVICE_ERROR           => 49,
+PE_SAML_LOAD_IDP_ERROR               => 50,
+PE_SAML_SSO_ERROR                    => 51,
+PE_SAML_UNKNOWN_ENTITY               => 52,
+PE_SAML_DESTINATION_ERROR            => 53,
+PE_SAML_CONDITIONS_ERROR             => 54,
+PE_SAML_IDPSSOINITIATED_NOTALLOWED   => 55,
+PE_SAML_SLO_ERROR                    => 56,
+PE_SAML_SIGNATURE_ERROR              => 57,
+PE_SAML_ART_ERROR                    => 58,
+PE_SAML_SESSION_ERROR                => 59,
+PE_SAML_LOAD_SP_ERROR                => 60,
+PE_SAML_ATTR_ERROR                   => 61,
+PE_OPENID_EMPTY                      => 62,
+PE_OPENID_BADID                      => 63,
+PE_MISSINGREQATTR                    => 64,
+PE_BADPARTNER                        => 65,
+PE_MAILCONFIRMATION_ALREADY_SENT     => 66,
+PE_PASSWORDFORMEMPTY                 => 67,
+PE_CAS_SERVICE_NOT_ALLOWED           => 68,
+PE_MAILFIRSTACCESS                   => 69,
+PE_MAILNOTFOUND                      => 70,
+PE_PASSWORDFIRSTACCESS               => 71,
+PE_MAILCONFIRMOK                     => 72,
+PE_RADIUSCONNECTFAILED               => 73,
+PE_MUST_SUPPLY_OLD_PASSWORD          => 74,
+PE_FORBIDDENIP                       => 75,
+PE_CAPTCHAERROR                      => 76,
+PE_CAPTCHAEMPTY                      => 77,
+PE_REGISTERFIRSTACCESS               => 78,
+PE_REGISTERFORMEMPTY                 => 79,
+PE_REGISTERALREADYEXISTS             => 80,
+PE_NOTOKEN                           => 81,
+PE_TOKENEXPIRED                      => 82,
+PE_U2FFAILED                         => 83,
+PE_UNAUTHORIZEDPARTNER               => 84,
+PE_RENEWSESSION                      => 85,
+PE_WAIT                              => 86,
+PE_MUSTAUTHN                         => 87,
+PE_MUSTHAVEMAIL                      => 88,
+PE_SAML_SERVICE_NOT_ALLOWED          => 89,
+PE_OIDC_SERVICE_NOT_ALLOWED          => 90,
+PE_OID_SERVICE_NOT_ALLOWED           => 91,
+PE_GET_SERVICE_NOT_ALLOWED           => 92,
+PE_IMPERSONATION_SERVICE_NOT_ALLOWED => 93,
+PE_ISSUERMISSINGREQATTR              => 94,
+PE_DECRYPTVALUE_SERVICE_NOT_ALLOWED  => 95,
+PE_BADOTP                            => 96,
+PE_RESETCERTIFICATE_INVALID          => 97,
+PE_RESETCERTIFICATE_FORMEMPTY        => 98,
+PE_RESETCERTIFICATE_FIRSTACCESS      => 99,
+PE_PP_NOT_ALLOWED_CHARACTER          => 100,
+PE_PP_NOT_ALLOWED_CHARACTERS         => 101,
+PE_UPGRADESESSION                 => 102,
+PE_NO_SECOND_FACTORS                 => 103,
+PE_BAD_DEVOPS_FILE                   => 104,
+PE_FILENOTFOUND                   => 105,
+PE_OIDC_AUTH_ERROR                   => 106
+```
\ No newline at end of file
diff -pruN 2.0.13+ds-3/doc/sources/admin/error.rst 2.0.14+ds-1/doc/sources/admin/error.rst
--- 2.0.13+ds-3/doc/sources/admin/error.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/error.rst	2022-02-07 19:06:14.000000000 +0000
@@ -4,8 +4,8 @@ Error messages
 
 .. note::
 
-    This page do not reference all error messages, but only the
-    most common
+    This page does not reference all error messages,
+    but only the most common ones
 
 Lemonldap::NG::Common
 ---------------------
@@ -140,3 +140,10 @@ set ``*`` in trustedDomains to accept al
    XSS attack detected
 
 → Some URL parameters contain forbidden characters.
+
+::
+   
+   Detailled error codes list
+
+→ Corresponding error codes can be found in
+   :doc:`Portal error codes<error_codes>`
diff -pruN 2.0.13+ds-3/doc/sources/admin/extendedfunctions.rst 2.0.14+ds-1/doc/sources/admin/extendedfunctions.rst
--- 2.0.13+ds-3/doc/sources/admin/extendedfunctions.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/extendedfunctions.rst	2022-01-22 14:30:19.000000000 +0000
@@ -267,7 +267,7 @@ Simple usage example:
 groupMatch
 ~~~~~~~~~~
 
-this function allows one to parse the ``$hGroups`` variable to check if
+This function allows one to parse the ``$hGroups`` variable to check if
 a value is present inside a group attribute.
 
 Function parameter:
diff -pruN 2.0.13+ds-3/doc/sources/admin/external2f.rst 2.0.14+ds-1/doc/sources/admin/external2f.rst
--- 2.0.13+ds-3/doc/sources/admin/external2f.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/external2f.rst	2022-01-22 14:30:19.000000000 +0000
@@ -19,7 +19,7 @@ All parameters are configured in "Genera
 » Extensions » External 2nd Factor".
 
 -  **Activation**
--  **Code RegEx**: regular expression to create an OTP code. Let this
+-  **Code regex**: regular expression to create an OTP code. Let this
    option blank to delegate code Generation / Verification to an
    external provider
 -  **Send command**: define your command using *$attribute* like in
@@ -33,9 +33,9 @@ All parameters are configured in "Genera
 -  **Authentication level** (Optional): if you want to overwrite the
    value sent by your authentication module, you can define here the new
    authentication level. Example: 5
--  **Logo** (Optional): logo file (in static/<skin> directory)
 -  **Label** (Optional): label that should be displayed to the user on
    the choice screen
+-  **Logo** (Optional): logo file (in static/<skin> directory)
 
 
 .. attention::
diff -pruN 2.0.13+ds-3/doc/sources/admin/fastcgiserver.rst 2.0.14+ds-1/doc/sources/admin/fastcgiserver.rst
--- 2.0.13+ds-3/doc/sources/admin/fastcgiserver.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/fastcgiserver.rst	2022-01-22 14:30:19.000000000 +0000
@@ -32,7 +32,7 @@ Configuration
 FastCGI server has few parameters. They can be set by environment
 variables (read by startup script) or by command line options. A default
 configuration file can be found in
-``/usr/local/lemonlda-ng/etc/default/llng-fastcgi-server`` (or
+``/usr/local/lemonldap-ng/etc/default/llng-fastcgi-server`` (or
 ``/etc/default/lemonldap-ng-fastcgi-server`` in Debian package).
 
 The FastCGI server reads also ``LLTYPE`` parameter in FastCGI requests
diff -pruN 2.0.13+ds-3/doc/sources/admin/features.rst 2.0.14+ds-1/doc/sources/admin/features.rst
--- 2.0.13+ds-3/doc/sources/admin/features.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/features.rst	2022-02-09 19:10:40.000000000 +0000
@@ -28,11 +28,13 @@ Unifying authentications (Identity Feder
 
 LL::NG can easily exchange with other authentication systems by using
 SAML, OpenID or CAS protocoles. It may be the backbone of a
-heterogeneous architecture. LL:NG can be set as Identity provider,
+heterogeneous architecture.
+
+LL:NG can be set as Identity provider,
 Service Provider or Protocol Proxy
 (:doc:`LL::NG as federation protocol proxy<federationproxy>`).
 
-Its SOAP API can also be used to dialogue directly with your custom
+Its REST / SOAP API can also be used to dialogue directly with your custom
 applications.
 
 Sessions
@@ -48,8 +50,7 @@ opened sessions:
 
 -  by users
 -  by IP *(IPv4 and IPv6)*
--  by double IP (sessions opened by the same user from multiple
-   computers)
+-  by double IP (sessions opened by the same user from multiple computers)
 -  by date
 
 It can be used to delete a session
@@ -59,9 +60,8 @@ It can be used to delete a session
 Session restrictions
 ~~~~~~~~~~~~~~~~~~~~
 
-By default, a user can open several
-:doc:`sessions<sessions>`. LL::NG can restrict
-the following:
+By default, a user can open several :doc:`sessions<sessions>`.
+LL::NG can restrict the following:
 
 -  Allow only one session per user
 -  Allow only one IP address per user
@@ -72,17 +72,17 @@ Those capabilities can be used simultane
 Double cookie
 ~~~~~~~~~~~~~
 
-LL::NG can be configured to provides
-:doc:`2 cookies<ssocookie>`:
+LL::NG can be configured to provides :doc:`2 cookies<ssocookie>`:
 
 -  one secured (SSL only) for sensitive applications
 -  one unsecured for other applications
 
 So that if the http cookie is stolen, sensitive applications remain secured.
 
+
 Notifications
 -------------
 
 LL::NG can be used to notify users with a message when authenticating. This can be used to
-inform of a change in access rights, the publication of a new IT charter, etc. (See
-:doc:`notifications<notifications>` for more details)
+inform of a change in access rights, the publication of a new IT charter, etc...
+(See :doc:`notifications<notifications>` for more details)
diff -pruN 2.0.13+ds-3/doc/sources/admin/federationproxy.rst 2.0.14+ds-1/doc/sources/admin/federationproxy.rst
--- 2.0.13+ds-3/doc/sources/admin/federationproxy.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/federationproxy.rst	2022-02-09 19:10:40.000000000 +0000
@@ -1,8 +1,7 @@
 LL::NG as federation protocol proxy
 ===================================
 
-LL::NG can use federation protocols (SAML, CAS, OpenID) independently
-to:
+LL::NG can use federation protocols (SAML, CAS, OpenID) independently to:
 
 -  authenticate users
 -  provide identities to other systems
@@ -11,7 +10,7 @@ So you can configure it to authenticate
 protocol and simultaneously to provide identities using other(s)
 federation protocols.
 
-Schemes tested:
+Tested schemes:
 
 -  SAML / OpenID-Connect:
 
@@ -30,8 +29,8 @@ Schemes tested:
       :doc:`CAS<idpcas>`/:doc:`SAML<authsaml>` proxy **<=>** SAML
       Identity Provider
 
-Note that OpenID-Connect consortium hasn't already defined single-logout
-initiated by OpenID-Connect Provider. LLNG will implement it when this
+Note that OpenID-Connect consortium has not already defined single-logout
+initiated by OpenID-Connect Provider. LL::NG will implement it when this
 standard will be published.
 
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/finduser.rst 2.0.14+ds-1/doc/sources/admin/finduser.rst
--- 2.0.13+ds-3/doc/sources/admin/finduser.rst	2021-08-21 17:42:59.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/finduser.rst	2022-01-22 14:30:19.000000000 +0000
@@ -19,7 +19,7 @@ Just enable it in the Manager (section 
    -  **Character used as wildcard**: Character that can be used by users as wildcard. An empty value disable wildcarded search requests
    -  **Parameters control**: Regular expression used for checking searching values syntax
    -  **User accounts URL**: User database URL to search on if REST backend is used. Let it blank to use default user data URL.
-   -  **Searching attributes**: For each attribute, you have to set a key (attribute as defined in UserBD) and a value that will be display in login form (placeholder). A value can be a multivalued list separated by multiValuesSeparator parameter (General Parameters > Advanced parameters > Separator). See note below.
+   -  **Searching attributes**: For each attribute, you have to set a key (attribute as defined in UserBD) and a value that will be display in login form (placeholder). A value can be a multivalued list separated by multiValuesSeparator parameter (General Parameters > Advanced parameters > Separator). Attibutes can be sorted by adding ``#_`` before their name (where ``#`` is a number). See note below. 
    -  **Excluding attributes**: You can defined here attributes used for excluding accounts. Set keys corresponding to UserBD attributes and values to exclude. A value can be a multivalued list separated by multiValuesSeparator parameter (General Parameters > Advanced parameters > Separator)
 
 .. note::
@@ -41,7 +41,9 @@ Just enable it in the Manager (section 
 
          uid#Identity#1 => dwho; Dr Who; rtyler; Rose Tyler (allow empty value)
 
-   Entries are sorted by alphabetical order.
+         1_uid#Identity#1 => 2_dwho; Dr Who; 1_rtyler; Rose Tyler; dalek; Dalek 
+         (The attributes will be sorted by number, those without a number will appear at the end of the list)
+
 
 .. attention::
 
@@ -49,6 +51,14 @@ Just enable it in the Manager (section 
 
     request => searchAttr1=value && searchAttr2=value && not excludeAttr1=value && not excludeAttr2=value
 
+
+.. attention::
+
+    In some cases (like Choice authentication with SSL and Ajax), FindUser Ajax request can be blocked by Content Security Policy.
+    
+    You may have to allow <Portal>/finduser in CSP ``General Parameters > Advanced Parameters > Security > Content security policy``
+
+
 .. danger::
 
     This plugin works only with a users backend and of course if the searching or excluding attributes are existing.
diff -pruN 2.0.13+ds-3/doc/sources/admin/handlerarch.rst 2.0.14+ds-1/doc/sources/admin/handlerarch.rst
--- 2.0.13+ds-3/doc/sources/admin/handlerarch.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/handlerarch.rst	2022-02-07 19:06:14.000000000 +0000
@@ -6,9 +6,9 @@ Handlers are build on rows of modules:
 -  Applications or launchers that get the request and choose the right
    type *(Main, AuthBasic, ZimbraPreAuth,...)* and launch it *(may not
    inherits from other Handler::\* modules)*
--  Wrappers that call "type" library and platform "Main" //(may all
+-  Wrappers that call "type" library and platform "Main" (may all
    inherits from Platform::Main
--  library types if needed *(may inherits from Main)*
+-  Library types if needed *(may inherit from Main)*
 -  Main: the main handler library
 
 Overview of Handler packages
@@ -25,7 +25,7 @@ Plack servers protection or Nginx/\ :doc
 Types are:
 
 -  *(Main)*: link between Main and platform
--  :doc:`AuthBasic<handlerauthbasic>`
+-  :doc:`AuthBasic<authbasichandler>`
 -  :doc:`CDA<cda>`
 -  :doc:`DevOps<devopshandler>`
 -  :doc:`DevOps+ServiceToken<devopssthandler>`
diff -pruN 2.0.13+ds-3/doc/sources/admin/handlerauthbasic.rst 2.0.14+ds-1/doc/sources/admin/handlerauthbasic.rst
--- 2.0.13+ds-3/doc/sources/admin/handlerauthbasic.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/handlerauthbasic.rst	1970-01-01 00:00:00.000000000 +0000
@@ -1,77 +0,0 @@
-AuthBasic Handler
-=================
-
-Presentation
-------------
-
-The AuthBasic Handler is a special Handler using AuthBasic method to
-authenticate and grante access to a virtual host.
-
-The Handler sends a WWW-Authenticate header to the client, to request
-user id and password. Then it checks credentials by using LL::NG REST
-web service (REST session service must be enabled in the manager). Once
-session is granted, the Handler will check authorizations like the
-standard Handler.
-
-This feature can be useful to allow a third party application to access
-a virtual host with user credentials by sending a Basic challenge to it.
-
-Configuration
--------------
-
-Portal
-~~~~~~
-
-:doc:`REST server<restservices>` must be enabled on portal.
-
-Virtual host
-~~~~~~~~~~~~
-
-You just have to set "Type: AuthBasic" in the virtualHost options in the
-manager.
-
-If you want to protect only a virtualHost part, keep type on "Main" and
-set type in your configuration file:
-
--  Apache: use simply a ``PerlSetVar VHOSTTYPE AuthBasic``
--  Nginx: create another FastCGI with a
-   ``fastcgi_param VHOSTTYPE AuthBasic;`` *(and remove error_page 401)*
-
-Handler parameters
-~~~~~~~~~~~~~~~~~~
-
-No parameters needed. But you have to allow REST sessions web services,
-see :doc:`REST sessions backend<restsessionbackend>`, enable local cache
-(enabled by default in lemonldap-ng.ini) and allow source IP addresses
-to access required locations in Portal Virtual Host.
-
-
-.. danger::
-
-    With AuthBasic handler, you have to disable CSRF token by
-    setting a special rule based on source IP addresses like this :
-
-    requireToken => $env->{REMOTE_ADDR} !~ /^127\.0\.[1-3]\.1$/
-
-    With :doc:`authchoice`, you have to declare which authentication module is
-    requested by handler to create global session.
-
-    Go to:
-    ``General Parameters > Authentication parameters > Choice parameters``
-
-    and set authentication module's name :
-
-    **Choice used for password authentication** => 2_LDAP (by example)
-
-
-
-
-.. attention::
-
-    With HTTPS, you may have to set **LWP::UserAgent
-    object** with ``verify_hostname => 0`` and ``SSL_verify_mode => 0``.
-
-    Go to:
-
-    ``General Parameters > Advanced Parameters > Security > SSL options for server requests``
-
diff -pruN 2.0.13+ds-3/doc/sources/admin/hooks.rst 2.0.14+ds-1/doc/sources/admin/hooks.rst
--- 2.0.13+ds-3/doc/sources/admin/hooks.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/hooks.rst	2022-01-22 14:30:19.000000000 +0000
@@ -1,6 +1,8 @@
 Available plugin hooks
 ======================
 
+This page shows the list of hooks that you can use in your :doc:`custom plugins <plugincustom>`. Read the :doc:`plugincustom` page for full details on how to create and enable custom plugins.
+
 OpenID Connect Issuer hooks
 ---------------------------
 
@@ -94,7 +96,7 @@ Sample code::
    };
 
    sub addClaimToUserInfo {
-       my ( $self, $req, $userinfo ) = @_;
+       my ( $self, $req, $userinfo, $rp) = @_;
        $userinfo->{"userinfo_hook"} = 1;
        return PE_OK;
    }
@@ -192,7 +194,7 @@ Sample code::
    };
 
    sub gotRequest {
-       my ( $self, $res, $login ) = @_;
+       my ( $self, $req, $login ) = @_;
 
        # Your code here
    }
@@ -213,7 +215,7 @@ Sample code::
    };
 
    sub buildResponse {
-       my ( $self, $res, $login ) = @_;
+       my ( $self, $req, $login ) = @_;
 
        # Your code here
    }
@@ -234,7 +236,7 @@ Sample code::
    };
 
    sub gotLogout {
-       my ( $self, $res, $logout ) = @_;
+       my ( $self, $req, $logout ) = @_;
 
        # Your code here
    }
@@ -255,7 +257,7 @@ Sample code::
    };
 
    sub gotLogoutResponse {
-       my ( $self, $res, $logout ) = @_;
+       my ( $self, $req, $logout ) = @_;
 
        # Your code here
    }
@@ -276,7 +278,7 @@ Sample code::
    };
 
    sub buildLogoutResponse {
-       my ( $self, $res, $logout ) = @_;
+       my ( $self, $req, $logout ) = @_;
 
        # Your code here
    }
@@ -416,6 +418,6 @@ Sample code::
    sub logPasswordChange {
        my ( $self, $req, $user, $password, $old ) = @_;
        $old ||= "";
-       $self->userLogger->info("Password changed for $user: $old -> $password")
+       $self->userLogger->info("Password changed for $user: $old -> $password");
        return PE_OK;
    }
diff -pruN 2.0.13+ds-3/doc/sources/admin/idpcas.rst 2.0.14+ds-1/doc/sources/admin/idpcas.rst
--- 2.0.13+ds-3/doc/sources/admin/idpcas.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/idpcas.rst	2022-02-19 16:04:21.000000000 +0000
@@ -47,7 +47,7 @@ Configuring the CAS Service
 Then go in ``CAS Service`` to define:
 
 -  **CAS login**: the session key transmitted to CAS client as the main
-   identifier (CAS Principal). This setting can be overriden
+   identifier (CAS Principal). This setting can be overridden
    per-application.
 -  **Access control policy**: define if access control should be done on
    CAS service. Three options:
@@ -61,16 +61,20 @@ Then go in ``CAS Service`` to define:
       and the user is redirected to CAS service. Then CAS service has to
       show a correct error when service ticket validation will fail.
 
+-  **Use strict URL matching**: (since *2.0.12*) enforces a stricter URL
+   matching. By default, LemonLDAP::NG will try to find a declared CAS
+   Application matching the hostname of the requested application if it cannot
+   find a match using the full path. See :ref:`idpcas-url-matching` for details
+-  **Temporary ticket lifetime**: (since *2.0.14*): restricts how long Service
+   and Proxy tickets are valid after being generated. For compatibility, the
+   default value of ``0`` means they are valid for the entire session duration.
+   But the CAS spefications recommends ``300`` (5 minutes).
 -  **CAS session module name and options**: choose a specific module if
    you do not want to mix CAS sessions and normal sessions (see
    :ref:`why<samlservice-saml-sessions-module-name-and-options>`).
 -  **CAS attributes**: list of attributes that will be transmitted by
    default in the validate response. Keys are the name of attribute in
    the CAS response, values are the name of session key.
--  **Use strict URL matching**: (since *2.0.12*) enforces a stricter URL
-   matching. By default, LemonLDAP::NG will try to find a declared CAS
-   Application matching the hostname of the requested application if it cannot
-   find a match using the full path. See :ref:`idpcas-url-matching` for details
 
 
 .. tip::
diff -pruN 2.0.13+ds-3/doc/sources/admin/idpopenidconnect.rst 2.0.14+ds-1/doc/sources/admin/idpopenidconnect.rst
--- 2.0.13+ds-3/doc/sources/admin/idpopenidconnect.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/idpopenidconnect.rst	2022-02-19 16:04:21.000000000 +0000
@@ -10,11 +10,11 @@ Presentation
     OpenID Connect is a protocol based on REST, OAuth 2.0 and JOSE
     stacks. It is described here: http://openid.net/connect/.
 
-LL::NG can act as an OpenID Connect Provider (OP). It will answer to
+LL::NG can act as an OpenID Connect Provider (OP). It will reply to
 OpenID Connect requests to give user identity (through ID Token) and
-information (through User Info end point).
+information (through UserInfo endpoint).
 
-As an OP, LL::NG supports a lot of OpenID Connect features:
+As an OP, LL::NG supports many OpenID Connect features:
 
 -  Authorization Code, Implicit and Hybrid flows
 -  Publication of JSON metadata and JWKS data (Discovery)
@@ -30,12 +30,11 @@ As an OP, LL::NG supports a lot of OpenI
 -  Session management
 -  FrontChannel Logout
 -  BackChannel Logout
--  PKCE (Since ``2.0.4``) - See `RFC
-   7636 <https://tools.ietf.org/html/rfc7636>`__
--  Introspection endpoint (Since ``2.0.6``) - See `RFC
-   7662 <https://tools.ietf.org/html/rfc7662>`__
+-  PKCE (Since ``2.0.4``) - See :rfc:`7636`
+-  Introspection endpoint (Since ``2.0.6``) - See :rfc:`7662`
 -  Offline access (Since ``2.0.7``)
 -  Refresh Tokens (Since ``2.0.7``)
+-  Optional JWT Access Tokens (Since ``2.0.12``) - See :rfc:`9068`
 
 Configuration
 -------------
@@ -52,10 +51,10 @@ IssuerDB
 Go in ``General Parameters`` » ``Issuer modules`` » ``OpenID Connect``
 and configure:
 
--  **Activation**: set to ``On``.
--  **Path**: keep ``^/oauth2/`` unless you need to use another path
--  **Use rule**: a rule to allow user to use this module, set to ``1``
-   to always allow.
+-  **Activation**: set to ``On``
+-  **Path**: Keep ``^/oauth2/`` unless you need to use another path
+-  **Use rule**: Rule to allow user to use this module, set to ``1``
+   to always allow
 
 
 .. tip::
@@ -72,13 +71,13 @@ and configure:
 Configuration of LL::NG in Relying Party
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Each Relying Party has its own configuration way. LL::NG publish its
-OpenID Connect metadata to ease the configuration of client.
+Each Relying Party has its own configuration way. LL::NG exposes
+its OpenID Connect metadata to help clients configuration.
 
-The metadata can be found at the standard "Well Known" URL:
+Metadata can be downloaded at the standard "Well Known" URL:
 http://auth.example.com/.well-known/openid-configuration
 
-An example of its content:
+OIDC metadata example:
 
 .. code-block:: javascript
 
@@ -156,23 +155,22 @@ spaces, no special characters), like “
 
 You can then access to the configuration of this RP.
 
+.. _oidcexportedattr:
+
 Exported attributes
 ^^^^^^^^^^^^^^^^^^^
 
-You can map here the attribute names from the LL::NG session to an
-`OpenID Connect
-claim <http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims>`__.
+.. warning::
 
-.. include:: openidconnectclaims.rst
+   By default, only `standard OpenID Connect claims <http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims>`__
+   are exposed to applications. If you want to add non-standard attributes, you have to create a new scope in the *Scope values content* section and your application must request it.
 
-.. _oidcexportedattr:
+For each OpenID Connect attribute you want to release to applications, you can define:
 
-For each OpenID Connect claim you want to release to applications, you can define:
-
-* **Claim name**: the name of the claim as it will appear in Userinfo responses
-* **Variable name**: the name of the LemonLDAP::NG session variable containing the claim value
-* **Type**: the data type of the attribute. By default, a string. Choosing integer or boolean will make the claim appear as the corresponding JSON type.
-* **Array**: choose how to process multi-valued attributes
+* **Claim name**: Name of the attribute as it will appear in Userinfo responses
+* **Variable name**: Name of the LemonLDAP::NG session variable containing the attribute value
+* **Type**: Attribute data type. By default, it is a string. Choosing integer or boolean will make the attribute appear as the corresponding JSON type.
+* **Array**: Select how multi-valued attributes are processed
 
   * **Auto**: If the session key contains a single value, it will be released as a JSON number, string or boolean, depending on the previously specified type. If the session key contains multiple values, it will be released as an array of numbers, strings or booleans.
   * **Always**: Return an array even if the attribute only contains one value
@@ -181,41 +179,49 @@ For each OpenID Connect claim you want t
 
 .. attention::
 
-    The specific ``sub`` attribute is not defined here, but
-    in User attribute parameter (see below).
+    The specific ``sub`` attribute is not defined here, but in ``User attribute`` parameter (see below).
 
 
 .. _oidcextraclaims:
 
-Extra Claims
-^^^^^^^^^^^^
+Scope values content
+^^^^^^^^^^^^^^^^^^^^
 
+By default, the following scope-to-attributes are mapped by LL::NG:
 
-.. attention::
+.. csv-table::
+   :header: "Scope value", "Attribute list"
+   :delim: ;
+   :widths: auto
 
-    By default, only claims that are part of standard OpenID
-    Connect scopes will be sent to a client. If you want to send a claim
-    that is not in the OpenID Connect specification, you need to declare it
-    in the Extra Claims section
+   profile; name family_name given_name middle_name nickname preferred_username profile picture website gender birthdate zoneinfo locale updated_at
+   email; email email_verified
+   address; street_address locality region postal_code country
+   phone; phone_number phone_number_verified
 
-If you want to make custom claims visible to OpenID Connect clients, you
-need to declare them in a scope.
+If you want to expose custom attributes to OpenID Connect clients,
+you have to declare them in a new scope in this section.
 
-Add your additional scope as the **Key**, and a space-separated list of
-claims as the **Value**:
+Add your additional scope as **Key**, and a space-separated list of
+attributes as **Value**:
 
--  timelord => rebirth_count bloodline
+-  `employment_info` => `position company`
 
-In this example, an OpenID Client asking for the ``timelord`` scope will
-be able to read the ``rebirth_count`` and ``bloodline`` claims from the
-Userinfo endpoint.
+In this example, an OpenID Client requesting for the ``employment_info`` scope will
+be able to read the ``company`` and ``position`` attributes from the UserInfo endpoint.
+
+.. important::
+
+    Any attribute defined in this section must be mapped to a
+    LL::NG session variable in **Exported Attributes** section
+
+.. important::
+
+    Your custom attributes will only be visible if the application requests the
+    corresponding scope value
 
 
-.. danger::
 
-    Any Claim defined in this section must be mapped to a
-    LemonLDAP::NG session attribute in the **Exported Attributes**
-    section
 
 .. _oidcscoperules:
 
@@ -225,9 +231,9 @@ Scope Rules
 .. versionadded:: 2.0.12
 
 |beta| This feature may change in a future version in a way that breaks
-compatibility with existing configuration
+compatibility with existing configuration.
 
-By default, LemonLDAP::NG grants all scopes requested by the application, as
+By default, LL::NG grants all scopes requested by the application, as
 long as the user consents to them.
 
 This configuration screen allows you to change that behavior by attaching
@@ -260,11 +266,11 @@ Options
 -  **Basic**
 
    -  **Client ID**: Client ID for this RP
-   -  **Client secret**: Client secret for this RP (can be use for
+   -  **Client secret**: Client secret for this RP (can be used for
       symmetric signature)
-   -  **Public client** (since version ``2.0.4``): set this RP as public
-      client, so authentication is not needed on token endpoint
-   -  **Redirection addresses**: Space separated list of redirect
+   -  **Public client** (since version ``2.0.4``): Set this RP as public
+      client, so authentication is not needed on tokens endpoint
+   -  **Redirection addresses**: Space-separated list of redirect
       addresses allowed for this RP
 
 -  **Advanced**
@@ -273,39 +279,24 @@ Options
       sharing consent screen (consent will be accepted by default).
       Bypassing the consent is **not** compliant with OpenID Connect
       standard.
-   -  **User attribute**: session field that will be used as main
-      identifier (``sub``)
+   -  **User attribute**: Session field that will be used as main
+      identifier (``sub``). Default value is ``whatToTrace``.
    -  **Force claims to be returned in ID Token**: This options will
-      make user attributes from the requested scope appear as ID Token
-      claims.
+      make user attributes from the requested scope appear as ID Token claims
    -  **Use JWT format for Access Token** (since version ``2.0.12``): When
       using this option, Access Tokens will use the JWT format, which means they
       can be verified by external OAuth2.0 resource servers without using the
-      introspection or userinfo endpoint.
+      Introspection or UserInfo endpoint.
    -  **Release claims in Access Token** (since version ``2.0.12``): If Access
       Tokens are in JWT format, this option lets you release the claims defined
-      in the *Extra Claims* section inside the Access Token itself.
+      in the *Extra Claims* section inside the Access Token itself
    -  **Additional audiences** (since version ``2.0.8``): You can
-      specify a space-separate list of audiences that will be added the
-      audiences of the ID Token
+      specify a space-separated list of audiences that will be added to the
+      ID Token audiences
    -  **Use refresh tokens** (since version ``2.0.7``): If this option
-      is set, LemonLDAP::NG will issue a Refresh Token that can be used
+      is enabled, LL::NG will issue a Refresh Token that can be used
       to obtain new access tokens as long as the user session is still
-      valid.
-
--  **Timeouts**
-
-   -  **Authorization Code expiration**: Expiration time of
-      authorization code, when using the Authorization Code flow. The
-      default value is one minute.
-   -  **ID Token expiration**: Expiration time of ID Tokens. The default
-      value is one hour.
-   -  **Access token expiration**: Expiration time
-      of Access Tokens. The default value is one hour.
-   -  **Offline session expiration**: This sets the lifetime of the
-      refresh token obtained with the **offline_access** scope. The
-      default value is one month. This parameter only applies if offline
-      sessions are enabled.
+      valid
 
 -  **Security**
 
@@ -316,9 +307,8 @@ Options
    -  **Userinfo response format** (since version ``2.0.12``): By default,
       UserInfo is returned as a simple JSON object. You can also choose to
       return it as a JWT, using one of the available signature algorithms.
-   -  **Require PKCE** (since version ``2.0.4``): a code challenge is
-      required at token endpoint (see
-      `RFC7636 <https://tools.ietf.org/html/rfc7636>`__)
+   -  **Require PKCE** (since version ``2.0.4``): A code challenge is
+      required at Tokens endpoint (see :rfc:`7636`)
    -  **Allow offline access** (since version ``2.0.7``): After enabling
       this feature, an application may request the **offline_access**
       scope, and will obtain a Refresh Token that persists even after
@@ -326,17 +316,45 @@ Options
       https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
       for details. These offline sessions can be administered through
       the Session Browser.
-   - **Allow OAuth2.0 Password Grant** (since version ``2.0.8``): Allow the use of the :ref:`Resource Owner Password Credentials Grant <resource-owner-password-grant>` by this client. This feature only works if you have configured a form-based authentication module.
-   - **Allow OAuth2.0 Client Credentials Grant** (since version ``2.0.11``): Allow the use of the :ref:`Client Credentials Grant <client-credentials-grant>` by this client.
-   - **Authentication Level**: required authentication level to access this application
-   - **Access Rule**: lets you specify a :doc:`Perl rule<rules_examples>` to restrict access to this client
-
-- **Logout**
-
-   - **Allowed redirection addresses for logout**: A space separated list of URLs that this client can redirect the user to once the logout is done (through ``post_logout_redirect_uri``)
-   - **URL**: Specify the relying party's logout URL
-   - **Type**: Type of Logout to perform (only Front-Channel is implemented for now)
-   - **Session required**: Whether to send the Session ID in the logout request
+   -  **Allow OAuth2.0 Password Grant** (since version ``2.0.8``): Allow the use of
+      the :ref:`Resource Owner Password Credentials Grant <resource-owner-password-grant>` by this client.
+      This feature only works if you have configured a form-based authentication module.
+   -  **Allow OAuth2.0 Client Credentials Grant** (since version ``2.0.11``): Allow the use of the
+      :ref:`Client Credentials Grant <client-credentials-grant>` by this client.
+   -  **Authentication Level**: Required authentication level to access this application
+   -  **Access Rule**: Lets you specify a :doc:`Perl rule<rules_examples>` to restrict access to this client
+
+-  **Timeouts**
+
+   -  **Authorization Code expiration**: Expiration time of
+      authorization code, when using the Authorization Code flow. The
+      default value is one minute.
+   -  **ID Token expiration**: Expiration time of ID Tokens. The default
+      value is one hour.
+   -  **Access token expiration**: Expiration time
+      of Access Tokens. The default value is one hour.
+   -  **Offline session expiration**: This sets the lifetime of the
+      refresh token obtained with the **offline_access** scope. The
+      default value is one month. This parameter only applies if offline
+      sessions are enabled.
+
+-  **Logout**
+
+   -  **Allowed redirection addresses for logout**: A space-separated list of
+      URLs that this client can redirect the user to once the logout is done
+      (through ``post_logout_redirect_uri``)
+   -  **URL**: Specify the relying party's logout URL
+   -  **Type**: Type of logout to perform (only Front-Channel is implemented for now)
+   -  **Session required**: Whether to send the Session ID in the logout request
+
+Access Rule extra variables
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When writing your access rules, you can additionally use the following variables:
+
+* ``$_oidc_grant_type`` (since version ``2.0.14``): the grant type being used to
+  access this service. Possible values: ``authorizationcode``,
+  ``implicit``, ``hybrid``, ``clientcredentials``, ``password``
 
 .. _resource-owner-password-grant:
 
@@ -347,11 +365,11 @@ The Resource Owner Password Credentials
 
 .. versionchanged:: 2.0.12
 
-   when using the :doc:`Choice <authchoice>` authentication module, the *Choice used for password authentication* setting can be used to select which authentication choice is used by the Resource Owner Password Credentials Grant. Naturally, the selected choice must be a password-based authentication method (LDAP, DBI, REST, etc.)
+   When using the :doc:`Choice <authchoice>` authentication module, the *Choice used for password authentication* setting can be used for selecting which authentication choice is used by the Resource Owner Password Credentials Grant. Naturally, the selected choice must be a password-based authentication method (LDAP, DBI, REST, etc.).
 
 .. seealso::
 
-   `Specification for the Resource Owner Password Credentials Grant <https://tools.ietf.org/html/rfc6749#section-4.3>`__
+   Specification for the Resource Owner Password Credentials Grant: :rfc:`6749#section-4.3`
 
 .. _client-credentials-grant:
 
@@ -370,16 +388,16 @@ The following attributes are made availa
 * The ``_clientConfKey`` attribute is set to the LemonLDAP::NG configuration
   key for the client that obtained the access token.
 
-The Access Rule, if defined, will have access to those variables, as well as
+The **Access Rule**, if defined, will have access to those variables, as well as
 the `@ENV` array. You can use it to restrict the use of this grant to
 pre-determined scopes, a particular IP address, etc.
 
-These session attribute will be released on the UserInfo endpoint if they are
-mapped to Exported Attributes and Extra Claims
+These session attributes will be released on the UserInfo endpoint if they are
+mapped to **Exported Attributes** and **Extra Claims**.
 
 .. seealso::
 
-   `Specification for the Client Credentials Grant <https://tools.ietf.org/html/rfc6749#section-4.4>`__
+   Specification for the Client Credentials Grant: :rfc:`6749#section-4.4`
 
 Macros
 ^^^^^^
diff -pruN 2.0.13+ds-3/doc/sources/admin/idpsaml.rst 2.0.14+ds-1/doc/sources/admin/idpsaml.rst
--- 2.0.13+ds-3/doc/sources/admin/idpsaml.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/idpsaml.rst	2022-01-22 14:30:19.000000000 +0000
@@ -58,6 +58,8 @@ IDP related metadata.
 In both cases, the entityID of the LemonLDAP::NG server is
 http://auth.example.com/saml/metadata
 
+.. _samlidp-register-sp:
+
 Register partner Service Provider on LemonLDAP::NG
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/index_advanced.rst 2.0.14+ds-1/doc/sources/admin/index_advanced.rst
--- 2.0.13+ds-3/doc/sources/admin/index_advanced.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/index_advanced.rst	2022-02-07 19:06:14.000000000 +0000
@@ -4,20 +4,21 @@ Advanced features
 .. toctree::
    :maxdepth: 1
 
+   rbac
+   ssoaas
+   servertoserver
+   riskbased
    smtp
    notifications
    passwordstore
    cda
-   rbac
    customfunctions
    extendedfunctions
    resetpassword
    register
    logoutforward
    securetoken
-   handlerauthbasic
-   ssoaas
-   servertoserver
+   authbasichandler
    safejail
    loginhistory
    fastcgi
diff -pruN 2.0.13+ds-3/doc/sources/admin/index_authdb.rst 2.0.14+ds-1/doc/sources/admin/index_authdb.rst
--- 2.0.13+ds-3/doc/sources/admin/index_authdb.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/index_authdb.rst	2022-02-19 16:04:21.000000000 +0000
@@ -41,4 +41,5 @@ Authentication, users and password datab
    radius2f
    rest2f
    yubikey2f
+   webauthn2f
    sfextra
diff -pruN 2.0.13+ds-3/doc/sources/admin/index_beforeinstall.rst 2.0.14+ds-1/doc/sources/admin/index_beforeinstall.rst
--- 2.0.13+ds-3/doc/sources/admin/index_beforeinstall.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/index_beforeinstall.rst	2022-02-19 16:04:21.000000000 +0000
@@ -5,5 +5,5 @@ Before installation
    :maxdepth: 1
 
    prereq
-   download
+   Download <https://lemonldap-ng.org/download.html>
    upgrade
diff -pruN 2.0.13+ds-3/doc/sources/admin/index_exploitation.rst 2.0.14+ds-1/doc/sources/admin/index_exploitation.rst
--- 2.0.13+ds-3/doc/sources/admin/index_exploitation.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/index_exploitation.rst	2022-02-07 19:06:14.000000000 +0000
@@ -12,4 +12,5 @@ Exploitation
    monitoring
    logs
    error
+   error_codes
    highavailability
diff -pruN 2.0.13+ds-3/doc/sources/admin/index_handler.rst 2.0.14+ds-1/doc/sources/admin/index_handler.rst
--- 2.0.13+ds-3/doc/sources/admin/index_handler.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/index_handler.rst	2022-02-07 19:06:14.000000000 +0000
@@ -4,10 +4,9 @@ Handlers
 .. toctree::
    :maxdepth: 1
 
-   handlerauthbasic
+   authbasichandler
    cda
    ssoaas
-   servertoserver
    oauth2handler
    securetoken
    servertoserver
diff -pruN 2.0.13+ds-3/doc/sources/admin/index_protection.rst 2.0.14+ds-1/doc/sources/admin/index_protection.rst
--- 2.0.13+ds-3/doc/sources/admin/index_protection.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/index_protection.rst	2022-01-22 14:30:19.000000000 +0000
@@ -5,4 +5,5 @@ Attacks and Protection
    :maxdepth: 1
 
    bruteforceprotection
+   newlocationwarning
    safejail
diff -pruN 2.0.13+ds-3/doc/sources/admin/installdeb.rst 2.0.14+ds-1/doc/sources/admin/installdeb.rst
--- 2.0.13+ds-3/doc/sources/admin/installdeb.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/installdeb.rst	2022-02-19 16:04:21.000000000 +0000
@@ -68,7 +68,7 @@ repositories:
 
    apt install apt-transport-https
 
-You will need to trust the following GPG key : |image0|
+You will need to trust the `DEB signing key <https://lemonldap-ng.org/_media/rpm-gpg-key-ow2>`__ :
 
 ::
 
@@ -83,20 +83,19 @@ Then, add the official LL::NG repository
 ::
 
    # LemonLDAP::NG repository
-   deb     https://lemonldap-ng.org/deb stable main
-   deb-src https://lemonldap-ng.org/deb stable main
+   deb     https://lemonldap-ng.org/deb 2.0 main
 
 
 .. tip::
 
-
-
+    -  Use the ``stable`` repository to get packages from current major
+       version
     -  Use the ``oldstable`` repository to get packages from previous major
        version
     -  Use the ``testing`` repository to get packages from next major
        version
-    -  Use the ``2.0`` repository to avoid upgrade to next major version
-
+    -  Use the ``2.0`` repository to stay on this major version and avoid
+       upgrade to next major version
 
 
 Finally update your APT cache:
@@ -108,7 +107,7 @@ Finally update your APT cache:
 Manual download
 ~~~~~~~~~~~~~~~
 
-Packages are available on the :doc:`Download page</download>`.
+Packages are available on the `Download page <https://lemonldap-ng.org/download.html>`__.
 
 Install packages
 ----------------
@@ -189,7 +188,7 @@ File location
 Build your packages
 -------------------
 
-You can also get the :doc:`LemonLDAP::NG archive</download>` and make
+You can also get the `LemonLDAP::NG source <https://lemonldap-ng.org/download.html>`__ and make
 the package yourself:
 
 ::
@@ -197,6 +196,3 @@ the package yourself:
    tar xzf lemonldap-ng-*.tar.gz
    cd lemonldap-ng-*
    make debian-packages
-
-.. |image0| image:: /rpm-gpg-key-ow2
-
diff -pruN 2.0.13+ds-3/doc/sources/admin/installrpm.rst 2.0.14+ds-1/doc/sources/admin/installrpm.rst
--- 2.0.13+ds-3/doc/sources/admin/installrpm.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/installrpm.rst	2022-02-19 16:04:21.000000000 +0000
@@ -4,32 +4,32 @@ Installation on Red Hat/CentOS
 
 .. attention::
 
-    LL::NG requires at least Red Hat/CentOS 7
+    LemonLDAP::NG requires at least Red Hat/CentOS 7
 
-Organization
-------------
+List of pacakges
+----------------
 
 LemonLDAP::NG provides packages for Red Hat/Centos 7:
 
--  lemonldap-ng: metapackage, contains no file but dependencies on other
-   packages
--  lemonldap-ng-doc: contains HTML documentation and project docs
-   (README, etc.)
--  lemonldap-ng-conf: contains default configuration (DNS domain:
-   example.com)
--  lemonldap-ng-test: contains sample CGI test page
--  lemonldap-ng-handler: contains Apache Handler implementation (agent)
--  lemonldap-ng-manager: contains administration interface and session
-   explorer
--  lemonldap-ng-portal: contains authentication portal and menu
--  lemonldap-ng-fastcgi-server: FastCGI server needed to use Nginx
--  lemonldap-ng-nginx: contains Nginx configuration and dependencies
--  lemonldap-ng-uwsgi-app: contains Uwsgi application
--  lemonldap-ng-selinux: contains the SELinux policy for httpd
--  perl-Lemonldap-NG-Common: CPAN - Shared modules
--  perl-Lemonldap-NG-Handler: CPAN - Handler modules
--  perl-Lemonldap-NG-Manager: CPAN - Manager modules
--  perl-Lemonldap-NG-Portal: CPAN - Portal modules
+- ``lemonldap-ng``: metapackage, contains no file but dependencies on other
+  packages
+- ``lemonldap-ng-doc``: contains HTML documentation and project docs
+  (README, etc.)
+- ``lemonldap-ng-conf``: contains default configuration (DNS domain:
+  example.com)
+- ``lemonldap-ng-test``: contains sample CGI test page
+- ``lemonldap-ng-handler``: contains Apache Handler implementation (agent)
+- ``lemonldap-ng-manager``: contains administration interface and session
+  explorer
+- ``lemonldap-ng-portal``: contains authentication portal and menu
+- ``lemonldap-ng-fastcgi-server``: FastCGI server needed to use Nginx
+- ``lemonldap-ng-nginx``: contains Nginx configuration and dependencies
+- ``lemonldap-ng-uwsgi-app``: contains Uwsgi application
+- ``lemonldap-ng-selinux``: contains the SELinux policy for httpd
+- ``perl-Lemonldap-NG-Common``: CPAN - Shared modules
+- ``perl-Lemonldap-NG-Handler``: CPAN - Handler modules
+- ``perl-Lemonldap-NG-Manager``: CPAN - Manager modules
+- ``perl-Lemonldap-NG-Portal``: CPAN - Portal modules
 
 
 .. danger::
@@ -43,6 +43,14 @@ LemonLDAP::NG provides packages for Red
        rpm --nodeps -i lemonldap-ng-nginx*.rpm
 
 
+Prerequisites
+-------------
+
+LemonLDAP::NG has dependencies which are not in base RHEL repositories
+
+You need to enable `EPEL repositories <https://docs.fedoraproject.org/en-US/epel/#Quickstart>`__ before installing.
+
+On RHEL8 and derivatives, you also also need to enable the PowerTools repository in ``/etc/yum.repos.d``.
 
 Get the packages
 ----------------
@@ -91,23 +99,16 @@ Run this to update packages cache:
    yum update
 
 
-.. danger::
-
-    You must also install the EPEL repository for non-core
-    dependencies. See :ref:`prerequisites and dependencies<prereq-yum>`
-    chapter for more. 
-
 Manual download
 ~~~~~~~~~~~~~~~
 
-RPMs are available on the :doc:`Download page</download>`.
+RPMs are available on the `Download page <https://lemonldap-ng.org/download.html>`__.
 
 Package GPG signature
 ---------------------
 
-The GPG key can be downloaded here: |image0|
 
-Install it to trust RPMs:
+Get the `RPM signing key <https://lemonldap-ng.org/_media/rpm-gpg-key-ow2>`__ onto your LemonLDAP::NG server:
 
 ::
 
@@ -137,13 +138,11 @@ You can also use yum on local RPMs file:
 With RPM
 ~~~~~~~~
 
-Before installing the packages, install all :doc:`dependencies<prereq>`.
-
 You have then to install all the downloaded packages:
 
 ::
 
-   rpm -Uvh lemonldap-ng-* perl-Lemonldap-NG-*
+   yum install lemonldap-ng-* perl-Lemonldap-NG-*
 
 
 .. tip::
@@ -171,29 +170,30 @@ a sed command. For example, we change it
 Upgrade
 ~~~~~~~
 
-If you upgraded LL::NG, check all :doc:`upgrade notes<upgrade>`.
+If you upgraded LemonLDAP::NG, check all :doc:`upgrade notes<upgrade>`.
 
 DNS
 ~~~
 
 Configure your DNS server to resolve names with your server IP:
 
--  auth.<your domain>: main portal, must be public
--  manager.<your domain>: manager, only for adminsitrators
--  test1.<your domain>, test2.<your domain>: sample applications
+-  ``auth.<your domain>``: main portal, must be public
+-  ``manager.<your domain>``: manager, only for adminsitrators
+-  ``test1.<your domain>``, ``test2.<your domain>``: sample applications
 
 Follow the :ref:`next steps<start-configuration>`
 
 File location
 -------------
 
--  Configuration is in /etc/lemonldap-ng
+-  Configuration is in ``/etc/lemonldap-ng``
 -  LemonLDAP::NG configuration (edited by the Manager) is in
-   /var/lib/lemonldap-ng/conf/
--  All Perl modules are in the VENDOR perl directory
--  All Perl scripts/pages are in /var/lib/lemonldap-ng/
+   ``/var/lib/lemonldap-ng/conf/``
+-  All Perl modules are in the ``/usr/share/perl5/vendor_perl`` directory
+-  All Perl scripts/pages are in ``/var/lib/lemonldap-ng/``
 -  All static content (examples, CSS, images, etc.) is in
-   /usr/share/lemonldap-ng/
+   ``/usr/share/lemonldap-ng/``
+- Administration scripts are in ``/usr/libexec/lemonldap-ng/bin``
 
 Build your packages
 -------------------
@@ -202,9 +202,9 @@ If you need it, you can rebuild RPMs:
 
 -  Install rpm-build package
 -  Install all build dependencies (see BuildRequires in
-   lemonldap-ng.spec)
--  Put LemonLDAP::NG tarball in %_topdir/SOURCES
--  Edit ~/.rpmmacros and set your build parameters:
+   ``lemonldap-ng.spec`` )
+-  Put LemonLDAP::NG tarball in ``%_topdir/SOURCES``
+-  Edit ``~/.rpmmacros`` and set your build parameters:
 
 ::
 
@@ -212,12 +212,11 @@ If you need it, you can rebuild RPMs:
    %dist .el7
    %rhel 7
 
--  Go to %_topdir
+-  Go to ``%_topdir``
 -  Build:
 
 ::
 
    rpmbuild -ta SOURCES/lemonldap-ng-VERSION.tar.gz
 
-.. |image0| image:: /rpm-gpg-key-ow2
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/installsles.rst 2.0.14+ds-1/doc/sources/admin/installsles.rst
--- 2.0.13+ds-3/doc/sources/admin/installsles.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/installsles.rst	2022-02-19 16:04:21.000000000 +0000
@@ -89,17 +89,16 @@ or
 Manual download
 ~~~~~~~~~~~~~~~
 
-RPMs are available on the :doc:`Download page<download>`.
+RPMs are available on the `Download page <https://lemonldap-ng.org/download.html>`__.
 
 Package GPG signature
 ---------------------
 
-The GPG key can be downloaded here: |image0|
-
-Install it to trust RPMs:
+Install the `RPM signing key <https://lemonldap-ng.org/_media/rpm-gpg-key-ow2>`__ to trust RPMs:
 
 ::
 
+   wget https://lemonldap-ng.org/_media/rpm-gpg-key-ow2
    rpm --import rpm-gpg-key-ow2
 
 Install packages
@@ -237,12 +236,3 @@ If you need it, you can rebuild RPMs:
 ::
 
    rpmbuild -ba SPECS/lemonldap-ng.spec
-
-Alternatively, you can use the automatic script
-"create-lemonldap-packages.sh", available in rpm-sles directory in the
-:ref:`lemonldap svn repository<download-getting-sources-from-svn-repository>`.
-The automatic script can also generate intermediate dependencies. See
-README file in the same directory for more information.
-
-.. |image0| image:: /rpm-gpg-key-ow2
-
diff -pruN 2.0.13+ds-3/doc/sources/admin/installtarball.rst 2.0.14+ds-1/doc/sources/admin/installtarball.rst
--- 2.0.13+ds-3/doc/sources/admin/installtarball.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/installtarball.rst	2022-02-19 16:04:21.000000000 +0000
@@ -4,7 +4,7 @@ Installation from the tarball
 Get the tarball
 ---------------
 
-Get the tarball from :doc:`download page</download>`. You can also find
+Get the tarball from `download page <https://lemonldap-ng.org/download.html>`__. You can also find
 on this page the SVN tarball if you want to test latest features.
 
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/loginhistory.rst 2.0.14+ds-1/doc/sources/admin/loginhistory.rst
--- 2.0.13+ds-3/doc/sources/admin/loginhistory.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/loginhistory.rst	2022-01-22 14:30:19.000000000 +0000
@@ -26,12 +26,14 @@ not allowed to open a session. In other
 impossibility to authenticate user, to retrieve data or to create a
 session, nothing is stored.
 
-By default, login time and IP address are stored in history, and the
-error message prompted to the user for failed logins. It is possible to
-store any additional session data. For example to store authentication
-mode, you can set in ``Session data to store`` a new key ``_auth`` with
-value ``Authentication mode``. The value will be used to display the
-data.
+* **Max successful logins count**: How many successful logins should be remembered in the history
+* **Max failed logins count**: How many failed logins should be remembered in the history
+* **Session data to store**: additional session variables to store in the history. *Key* is the variable (or macro) name, *Value* is the title of the column used when displaying the field. Use ``__hidden__`` to store a variables without displaying it to the user.
+
+By default, login time and IP address are stored in history, and the error
+message prompted to the user for failed logins. It is possible to store any
+additional session data. For example to store authentication, add a new key
+``_auth`` with value ``Authentication mode``.
 
 To allow the Login History tab in Menu, configure it in
 ``General Parameters`` > ``Portal`` > ``Menu`` > ``Modules`` (see
Binary files 2.0.13+ds-3/doc/sources/admin/logos/favicon.ico and 2.0.14+ds-1/doc/sources/admin/logos/favicon.ico differ
Binary files 2.0.13+ds-3/doc/sources/admin/logos/lemonldap-ng-logo.png and 2.0.14+ds-1/doc/sources/admin/logos/lemonldap-ng-logo.png differ
Binary files 2.0.13+ds-3/doc/sources/admin/logos/logo_llng_600px.png and 2.0.14+ds-1/doc/sources/admin/logos/logo_llng_600px.png differ
diff -pruN 2.0.13+ds-3/doc/sources/admin/logs.rst 2.0.14+ds-1/doc/sources/admin/logs.rst
--- 2.0.13+ds-3/doc/sources/admin/logs.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/logs.rst	2022-02-19 16:04:21.000000000 +0000
@@ -6,10 +6,10 @@ Presentation
 
 Main settings:
 
--  **REMOTE_USER** : session attribute used for logging user access
--  **REMOTE_CUSTOM** : can be used for logging an another user attribute or a macro
+-  **REMOTE_USER**: session attribute used for logging user access
+-  **REMOTE_CUSTOM**: can be used for logging an another user attribute or a macro
    (optional)
--  **Hidden attributes** : session attributes never displayed or sent
+-  **Hidden attributes**: session attributes never displayed or sent
 
 LemonLDAP::NG provides 5 levels of error and has two kind of logs:
 
@@ -210,7 +210,30 @@ Default values:
 
    log4perlConfFile   = /etc/log4perl.conf
    log4perlLogger     = LLNG
-   log4perlUserLogger = LLNG.user
+   log4perlUserLogger = LLNGuser
+
+
+Sample ``log4perl.conf`` file
+
+.. code::
+
+    log4perl.logger.LLNG = DEBUG, Syslog
+    log4perl.logger.LLNGuser = INFO, Syslog
+    log4perl.appender.Syslog = Log::Dispatch::Syslog
+    log4perl.appender.Syslog.ident = LLNG
+    log4perl.appender.Syslog.layout = PatternLayout
+    log4perl.appender.Syslog.layout.ConversionPattern = [%p] %m
+
+For additional information, please read the `Log4Perl documentation <https://metacpan.org/pod/Log::Log4perl>`__
+
+.. versionadded:: 2.0.14
+
+    The following special formatters have been added to standard `PatternLayout placeholders <https://metacpan.org/pod/Log::Log4perl::Layout::PatternLayout>`__
+
+* ``%Q{address}``: IP address of the request
+* ``%Q{user}``: Username of the current user
+* ``%Q{id}``: Session ID of the current user
+* ``%E{ENV_VAR}``: content of the ``ENV_VAR`` variable
 
 Sentry
 ~~~~~~
diff -pruN 2.0.13+ds-3/doc/sources/admin/mail2f.rst 2.0.14+ds-1/doc/sources/admin/mail2f.rst
--- 2.0.13+ds-3/doc/sources/admin/mail2f.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/mail2f.rst	2022-01-22 14:30:19.000000000 +0000
@@ -48,6 +48,6 @@ Mail second factor".
 -  **Authentication level** (Optional): if you want to overwrite the
    value sent by your authentication module, you can define here the new
    authentication level. Example: 5
--  **Logo** (Optional): logo file *(in static/<skin> directory)*
 -  **Label** (Optional): label that should be displayed to the user on
    the choice screen
+-  **Logo** (Optional): logo file *(in static/<skin> directory)*
diff -pruN 2.0.13+ds-3/doc/sources/admin/newlocationwarning.rst 2.0.14+ds-1/doc/sources/admin/newlocationwarning.rst
--- 2.0.13+ds-3/doc/sources/admin/newlocationwarning.rst	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/newlocationwarning.rst	2022-01-22 14:30:19.000000000 +0000
@@ -0,0 +1,55 @@
+|image0|
+
+New Location Warning Plugin
+===========================
+
+Presentation
+------------
+
+This plugin allows LL::NG to send a warning message to the user's email
+address when their account connects from a new location.
+
+By default, the location is the IP address. Meaning that any connection from a
+different IP address will send a warning. If this is not what you want, you can
+change the way location is computed (see below).
+
+Following steps are performed when the user logs in
+
+#. Extract the location from session info (by default, the IP address is used)
+#. Compare the current location to the previous locations saved in history
+#. If it is a new location, send an email to warn the user
+#. On the next login, the location will no longer be considered as new
+
+The very first time a user logs in (empty login history), no email is sent.
+
+Configuration
+-------------
+
+Just enable it in the Manager (section ``General Parameters`` > ``Advanced parameters`` > ``Security`` > ``New location warning``:
+
+- **Activation**: Enable this plugin *(default: disabled)*
+- **Session attribute containing location**: Indicate the session attribute you are using to store the location. You can use `ipAddr`, or a custom macro.
+- **Session attribute to display**: By default, the raw value of the location session attribute is displayed in the warning email. If you want to use a different session attribute in the warning email, you can specify it here.
+- **Maximum number of locations to consider**: By default, all previous value of the location are checked
+- **Session mail attribute**: Session key containing mail address *(default: mail)*
+- **Warning mail subject**: Subject of the email containing the warning
+- **Warning mail content**: Content of the email containing the warning
+
+.. warning::
+    If you use a macro instead of ``ipAddr`` as the location value, be sure to add the name of this macro to
+
+    General Parameters » Plugins » Login History » Session data to store
+
+    Otherwise, the value of the macro will not be remembered across logins
+
+Email body variables
+~~~~~~~~~~~~~~~~~~~~
+
+Following variables are available in the Warning email body:
+
+* ``$location``: the location value, from **Session attribute to display**
+* ``$date``: the date of login
+* ``$ua``: the full user agent string
+
+.. |image0| image:: /documentation/beta.png
+   :width: 100px
diff -pruN 2.0.13+ds-3/doc/sources/admin/oauth2handler.rst 2.0.14+ds-1/doc/sources/admin/oauth2handler.rst
--- 2.0.13+ds-3/doc/sources/admin/oauth2handler.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/oauth2handler.rst	2022-01-22 14:30:19.000000000 +0000
@@ -48,6 +48,10 @@ The OAuth2 handler defines a few extra v
 * ``$_clientId``: client ID of the application which requested the Access Token
 * ``$_clientConfKey``: configuration key of the application which requested the
   Access Token
+* ``$_oidc_grant_type`` (since *2.0.14*): the grant type used to generate the Access Token. If
+  Refresh Tokens are used, this is the grant type of the first emitted Access
+  Token. Possible values: ``authorizationcode``, ``implicit``, ``hybrid``,
+  ``clientcredentials``, ``password``
 * ``$_scope``: list of space-separated scopes granted by the Access Token
 
 For example, to grant access to access tokens containing the ``write`` scope,
@@ -68,7 +72,7 @@ Define access rules and headers. Then in
 Reference
 ---------
 
-`RFC6750 <https://tools.ietf.org/html/rfc6750>`__
+:rfc:`6750`
 
 .. |image0| image:: /documentation/oauth-retina-preview.jpg
    :class: align-center
diff -pruN 2.0.13+ds-3/doc/sources/admin/openidconnectclaims.rst 2.0.14+ds-1/doc/sources/admin/openidconnectclaims.rst
--- 2.0.13+ds-3/doc/sources/admin/openidconnectclaims.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/openidconnectclaims.rst	1970-01-01 00:00:00.000000000 +0000
@@ -1,32 +0,0 @@
-OpenID Connect claims
-~~~~~~~~~~~~~~~~~~~~~
-
-===================== ================ ======= =======================================
-Claim name            Associated scope Type    Example of corresponding LDAP attribute
-===================== ================ ======= =======================================
-sub                   openid           string  uid
-name                  profile          string  cn
-given_name            profile          string  givenName
-family_name           profile          string  sn
-middle_name           profile          string
-nickname              profile          string
-preferred_username    profile          string  displayName
-profile               profile          string  labeledURI
-picture               profile          string
-website               profile          string
-email                 email            string  mail
-email_verified        email            boolean
-gender                profile          string
-birthdate             profile          string
-zoneinfo              profile          string
-locale                profile          string  preferredLanguage
-phone_number          phone            string  telephoneNumber
-phone_number_verified phone            boolean
-updated_at            profile          string
-formatted             address          string  registeredAddress
-street_address        address          string  street
-locality              address          string  l
-region                address          string  st
-postal_code           address          string  postalCode
-country               address          string  co
-===================== ================ ======= =======================================
diff -pruN 2.0.13+ds-3/doc/sources/admin/openidconnectservice.rst 2.0.14+ds-1/doc/sources/admin/openidconnectservice.rst
--- 2.0.13+ds-3/doc/sources/admin/openidconnectservice.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/openidconnectservice.rst	2022-02-19 16:04:21.000000000 +0000
@@ -13,14 +13,14 @@ Set the issuer identifier, which should
 
 For example: http://auth.example.com
 
-End points
+Endpoints
 ~~~~~~~~~~
 
 Name of different OpenID Connect endpoints. You can keep the default
 values unless you have a specific need to change them.
 
 -  **Authorization**
--  **Token**
+-  **Tokens**
 -  **User Info**
 -  **JWKS**
 -  **Registration**
@@ -30,57 +30,70 @@ values unless you have a specific need t
 
 .. tip::
 
-    The end points are published inside JSON metadata.
+    These endpoints are published inside JSON metadata.
 
 Authentication context
 ~~~~~~~~~~~~~~~~~~~~~~
 
-You can associate here an authentication context to an authentication
-level.
+You can associate here an authentication context to an authentication level.
 
 Security
 ~~~~~~~~
 
--  **Keys** : define public/private key pair to do asymmetric signature. A JWKS
-   ``kid`` (Key ID) is automatically derived when generating new keys.
--  **Dynamic Registration**: Set to 1 to allow clients to register
-   themselves. This may be a security risk as this will create a new
-   configuration in the backend per registration request. You can limit
-   this by protecting in the WebServer the registration end point with
-   an authentication module, and give the credentials to clients.
--  **Only allow declared scopes**: By default, LemonLDAP::NG will grant all requested scopes. When this option is in use, LemonLDAP will only grant:
+-  **Keys**: Define public/private key pair for asymmetric signature. A JWKS
+   ``kid`` (Key ID) is automatically derived when new keys are generated.
+-  **Authorization Code flow**: Set to 1 to allow Authorization Code flow
+-  **Implicit flow**: Set to 1 to allow Implicit flow
+-  **Hybrid flow**: Set to 1 to allow Hybrid flow
+-  **Only allow declared scopes**: By default, LL::NG will grant all requested scopes.
+   When this option is enabled, LL::NG will only grant:
 
    - Standard OIDC scopes (``openid`` ``profile`` ``email`` ``address`` ``phone``)
-   - Scopes declared in :ref:`Extra Claims <oidcextraclaims>`
+   - Scopes declared in :ref:`Scope values content <oidcextraclaims>`
    - Scopes declared in :ref:`Scope Rules <oidcscoperules>` (if they match the rule)
 
--  **Authorization Code flow**: Set to 1 to allow Authorization Code
-   flow
--  **Implicit flow**: Set to 1 to allow Implicit flow
--  **Hybrid flow**: Set to 1 to allow Hybrid flow
+Timeouts
+~~~~~~~~
+
+-  **Authorization Codes**: Expiration time of
+   authorization code. Default value is one minute.
+-  **ID Tokens**: Expiration time of ID Tokens.
+   Default value is one hour.
+-  **Access Tokens**: Expiration time of Access Tokens.
+   Default value is one hour.
+-  **Offline sessions**: This option sets lifetime of Refresh Tokens
+   retrieved with ``offline_access`` scope. Default value is one month.
+
 
 Sessions
 ~~~~~~~~
 
-It is recommended to use a separate sessions storage for OpenID Connect
-sessions, else they will stored in the main sessions storage.
+Best pratice is to use a separate sessions storage for OpenID Connect
+sessions, else they will be stored in main sessions storage.
 
 Dynamic Registration
 ~~~~~~~~~~~~~~~~~~~~
 
-If dynamic registration is enabled, you can configure the following
-options to define attributes and extra claims when a new relying party
-is registered through the ``/oauth2/register`` endpoint:
-
--  Exported vars for dynamic registration
--  Extra claims for dynamic registration
-
-Key rotation script
--------------------
-
-OpenID Connect specification let the possibility to rotate keys to
-improve security. LL::NG provide a script to do this, that should be put
-in a cronjob.
+-  **Activation**: Set to 1 to allow clients to register themselves
+
+If **Dynamic Registration** is enabled, you can configure the following
+options to define attributes and extra claims released when a new relying
+party is registered through ``/oauth2/register`` endpoint:
+
+-  **Exported vars**
+-  **Extra claims**
+
+.. warning::
+    Dynamic Registration can be a security risk because a new configuration
+    will be created in the backend for each registration request.
+    You can restrict this by protecting the WebServer registration endpoint
+    with an authentication module, and give credentials to clients.
+
+Keys rotation script
+--------------------
+
+OpenID Connect specifications allow to rotate keys to improve security.
+LL::NG provides a script to do this, that should be used in a cronjob.
 
 The script is ``/usr/share/lemonldap-ng/bin/rotateOidcKeys``. It can be
 run for example each week:
@@ -92,7 +105,7 @@ run for example each week:
 
 .. tip::
 
-    Set the correct Apache user, else generated configuration will
+    Set the correct WebServer user, else generated configuration will
     not be readable by LL::NG.
 
 Session management
@@ -101,11 +114,11 @@ Session management
 LL::NG implements the `OpenID Connect Change Notification specification <http://openid.net/specs/openid-connect-session-1_0.html#ChangeNotification>`__
 
 A ``changed`` state will be sent if the user is disconnected from LL::NG
-portal (or has destroyed its SSO cookie). Else the ``unchanged`` state
+portal (or has removed its SSO cookie). Else the ``unchanged`` state
 will be returned.
 
 
 .. tip::
 
-    To work, the LL::NG cookie must not be protected against
-    javascript (``httpOnly`` option should be set to ``0``).
+    This feature requires that the LL::NG cookie is exposed to 
+    javascript (``httpOnly`` option must be set to ``0``).
diff -pruN 2.0.13+ds-3/doc/sources/admin/parameterlist.rst 2.0.14+ds-1/doc/sources/admin/parameterlist.rst
--- 2.0.13+ds-3/doc/sources/admin/parameterlist.rst	2021-08-20 16:43:38.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/parameterlist.rst	2022-02-19 16:50:44.000000000 +0000
@@ -45,9 +45,9 @@ browsersDontStorePassword
 bruteForceProtection                                    Enable brute force attack protection                                                 ✔
 bruteForceProtectionIncrementalTempo                    Enable incremental lock time for brute force attack protection                       ✔
 bruteForceProtectionLockTimes                           Incremental lock time values for brute force attack protection                       ✔
-bruteForceProtectionMaxAge                              Max age between current and first failed login                                       ✔                      ✔
+bruteForceProtectionMaxAge                              Max age between current and first failed login                                       ✔
 bruteForceProtectionMaxFailed                           Max allowed failed login                                                             ✔
-bruteForceProtectionMaxLockTime                         Max lock time                                                                        ✔                      ✔
+bruteForceProtectionMaxLockTime                         Max lock time                                                                        ✔
 bruteForceProtectionTempo                               Lock time                                                                            ✔
 captcha_login_enabled                                   Captcha on login page                                                                ✔
 captcha_mail_enabled                                    Captcha on password reset page                                                       ✔
@@ -62,6 +62,7 @@ casSrvMetaDataOptions
 casStorage                                              Apache::Session module to store CAS user data                                        ✔
 casStorageOptions                                       Apache::Session module parameters                                                    ✔
 casStrictMatching                                       Disable host-based matching of CAS services                                          ✔
+casTicketExpiration                                     Expiration time of Service and Proxy tickets                                         ✔
 cda                                                     Enable Cross Domain Authentication                                                   ✔      ✔
 certificateResetByMailCeaAttribute                                                                                                           ✔
 certificateResetByMailCertificateAttribute                                                                                                   ✔
@@ -78,6 +79,8 @@ cfgLog
 cfgNum                                                  Enable Cross Domain Authentication                                                   ✔                      ✔
 cfgVersion                                              Version of LLNG which build configuration                                            ✔                      ✔
 checkDevOps                                             Enable check DevOps                                                                  ✔
+checkDevOpsCheckSessionAttributes                       Check if session attributes exist                                                    ✔
+checkDevOpsDisplayNormalizedHeaders                     Display normalized headers                                                           ✔
 checkDevOpsDownload                                     Enable check DevOps download field                                                   ✔
 checkState                                              Enable CheckState plugin                                                             ✔
 checkStateSecret                                        Secret token for CheckState plugin                                                   ✔
@@ -86,6 +89,8 @@ checkUser
 checkUserDisplayComputedSession                         Display empty headers rule                                                           ✔
 checkUserDisplayEmptyHeaders                            Display empty headers rule                                                           ✔
 checkUserDisplayEmptyValues                             Display session empty values rule                                                    ✔
+checkUserDisplayHiddenAttributes                        Display hidden attributes rule                                                       ✔
+checkUserDisplayHistory                                 Display history rule                                                                 ✔
 checkUserDisplayNormalizedHeaders                       Display normalized headers rule                                                      ✔
 checkUserDisplayPersistentInfo                          Display persistent session info rule                                                 ✔
 checkUserHiddenAttributes                               Attributes to hide in CheckUser plugin                                               ✔
@@ -313,6 +318,13 @@ max2FDevices
 max2FDevicesNameLength                                  Maximum 2F devices name length                                                       ✔                      ✔
 multiValuesSeparator                                    Separator for multiple values                                                        ✔      ✔       ✔
 mySessionAuthorizedRWKeys                               Alterable session keys by user itself                                                ✔                      ✔
+newLocationWarning                                      Enable New Location Warning                                                          ✔
+newLocationWarningLocationAttribute                     New location session attribute                                                       ✔
+newLocationWarningLocationDisplayAttribute              New location session attribute for user display                                      ✔
+newLocationWarningMailAttribute                         New location warning mail session attribute                                          ✔
+newLocationWarningMailBody                              Mail body for new location warning                                                   ✔
+newLocationWarningMailSubject                           Mail subject for new location warning                                                ✔
+newLocationWarningMaxValues                             How many previous locations should be compared                                       ✔
 nginxCustomHandlers                                     Custom Nginx handler (deprecated)                                                    ✔
 noAjaxHook                                              Avoid replacing 302 by 401 for Ajax responses                                        ✔
 notification                                            Notification activation                                                              ✔
@@ -428,7 +440,11 @@ portalStatus
 portalUserAttr                                          Session parameter to display connected user in portal                                ✔
 protection                                              Manager protection method                                                                   ✔       ✔       ✔
 proxyAuthService                                                                                                                             ✔
+proxyAuthServiceChoiceParam                                                                                                                  ✔
+proxyAuthServiceChoiceValue                                                                                                                  ✔
+proxyAuthServiceImpersonation                           Enable internal portal Impersonation                                                 ✔
 proxyAuthnLevel                                         Proxy authentication level                                                           ✔
+proxyCookieName                                         Name of the internal portal cookie                                                   ✔
 proxySessionService                                                                                                                          ✔
 proxyUseSoap                                            Use SOAP instead of REST                                                             ✔
 radius2fActivation                                      Radius second factor activation                                                      ✔
@@ -454,7 +470,7 @@ registerTimeout
 registerUrl                                             URL of register page                                                                 ✔
 reloadTimeout                                           Configuration reload timeout                                                                        ✔
 reloadUrls                                              URL to call on reload                                                                ✔
-remoteCookieName                                                                                                                             ✔
+remoteCookieName                                        Name of the remote portal cookie                                                     ✔
 remoteGlobalStorage                                     Remote session backend                                                               ✔
 remoteGlobalStorageOptions                              Apache::Session module parameters                                                    ✔
 remotePortal                                                                                                                                 ✔
@@ -573,7 +589,8 @@ sslByAjax
 sslHost                                                 URL for SSL Ajax request                                                             ✔
 staticPrefix                                            Prefix of static files for HTML templates                                            ✔                      ✔
 status                                                  Status daemon activation                                                                    ✔               ✔
-stayConnected                                           Enable StayConnected plugin                                                          ✔
+stayConnected                                           Stay connected activation rule                                                       ✔
+stayConnectedBypassFG                                   Disable fingerprint checkng                                                          ✔
 stayConnectedCookieName                                 Name of the stayConnected plugin cookie                                              ✔
 stayConnectedTimeout                                    StayConnected persistent connexion session timeout                                                  ✔
 storePassword                                           Store password in session                                                            ✔
@@ -586,6 +603,7 @@ tokenUseGlobalStorage
 totp2fActivation                                        TOTP activation                                                                      ✔
 totp2fAuthnLevel                                        Authentication level for users authentified by password+TOTP                         ✔
 totp2fDigits                                            Number of digits for TOTP code                                                       ✔
+totp2fEncryptSecret                                     Encrypt TOTP secrets in database                                                     ✔
 totp2fInterval                                          TOTP interval                                                                        ✔
 totp2fIssuer                                            TOTP Issuer                                                                          ✔
 totp2fLabel                                             Portal label for TOTP 2F                                                             ✔
@@ -627,6 +645,15 @@ viewerHiddenKeys
 webIDAuthnLevel                                         WebID authentication level                                                           ✔
 webIDExportedVars                                       WebID exported variables                                                             ✔
 webIDWhitelist                                                                                                                               ✔
+webauthn2fActivation                                    WebAuthn second factor activation                                                    ✔
+webauthn2fAuthnLevel                                    Authentication level for users authentified by WebAuthn second factor                ✔
+webauthn2fLabel                                         Portal label for WebAuthn second factor                                              ✔
+webauthn2fLogo                                          Custom logo for WebAuthn 2F                                                          ✔
+webauthn2fSelfRegistration                              WebAuthn self registration activation                                                ✔
+webauthn2fUserCanRemoveKey                              Authorize users to remove existing WebAuthn                                          ✔
+webauthn2fUserVerification                              Verify user during registration and login                                            ✔
+webauthnDisplayNameAttr                                 Session attribute containing user display name                                       ✔
+webauthnRpName                                          WebAuthn Relying Party display name                                                  ✔
 whatToTrace                                             Session parameter used to fill REMOTE_USER                                           ✔      ✔
 wsdlServer                                              Enable /portal.wsdl server                                                           ✔
 yubikey2fActivation                                     Yubikey second factor activation                                                     ✔
diff -pruN 2.0.13+ds-3/doc/sources/admin/plugincustom.rst 2.0.14+ds-1/doc/sources/admin/plugincustom.rst
--- 2.0.13+ds-3/doc/sources/admin/plugincustom.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/plugincustom.rst	2022-01-22 14:30:19.000000000 +0000
@@ -4,8 +4,22 @@ Write a custom plugin
 Presentation
 ------------
 
-Standard entry points
-~~~~~~~~~~~~~~~~~~~~~
+Portal plugins let you customize LemonLDAP::NG's behavior.
+
+Common use cases for plugins are:
+
+* Looking up session information in an additional backend
+* Implementing additional controls or steps during login
+* Adjusting the behavior of SAML, OIDC or CAS protocols to work around application bugs
+
+Creating a plugin can be as simple as writing a short Perl module file and
+declaring it in your configuration. See below for an example.
+
+Plugin API
+----------
+
+Authentication entry points
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 You can now write a custom portal plugin that will hook in the
 authentication process:
@@ -21,8 +35,8 @@ authentication process:
 -  ``forAuthUser``: method called for already authenticated users
 -  ``beforeLogout``: method called before logout
 
-Extended entry points
-~~~~~~~~~~~~~~~~~~~~~
+Generic entry points
+~~~~~~~~~~~~~~~~~~~~
 
 If you need to call a method just after any standard method in
 authentication process, then use ``afterSub``, for example:
@@ -75,51 +89,147 @@ The plugin can also define new routes an
 
 See also ``Lemonldap::NG::Portal::Main::Plugin`` man page.
 
+Configuration
+~~~~~~~~~~~~~
+
+The current LemonLDAP::NG configuration can be accessed in the ``$self->conf`` hash. This variable is only meant to be read. Don't try changing its content, or *Bad Things* may happen.
+
+You can set your own parameters in ``General Parameters`` » ``Plugins`` » ``Custom plugins`` » ``Additional parameters``
+and reach them through ``customPluginsParams``
+
+.. code-block:: perl
+
+    sub my_function {
+        my ($self, $req) = @_;
+
+        # Get a standard LLNG option
+        my $llng_logo = $self->conf->{portalMainLogo};
+
+        # Get your custom LLNG option
+        my $myvar = $self->conf->{customPluginsParams}->{myvar};
+        }
+
+Logs
+~~~~
+
+You can use the ``$self->logger`` and ``$self->userLogger`` objects to log information during your plugin execution. Use ``logger`` for technical logs and ``userLogger`` for accounting and tracability events.
+
+.. code-block:: perl
+
+    sub my_function {
+        my ($self, $req) = @_;
+
+        $self->logger->debug("Debug message");
+        if (my_custom_test($req->user)) {
+            $self->userLogger->debug("User ". $req->user .
+                " is not allowed because XXX");
+
+            return PE_ERROR;
+        }
+        return PE_OK;
+    }
+
+
+Remembering data
+~~~~~~~~~~~~~~~~
+
+In order to remember data between different steps, you can use the ``$req->data`` hash.
+
+Data will not be remembered in between requests, only in between methods that process the same HTTP request.
+
+History management
+~~~~~~~~~~~~~~~~~~
+
+Plugins may declare additional session fields to be stored in the :doc:`loginhistory`.
+
+.. code:: perl
+
+    sub init {
+        my ($self) = @_;
+
+        $self->addSessionDataToRemember({
+            # This field will be hidden from the user
+            _language => '__hidden__',
+
+            # This field will be displayed on the portal. The column name
+            # is treated like a message and can be internationalized
+            authenticationLevel => "Human friendly column name",
+        });
+        return 1;
+    }
+
+Column names can be translated by :ref:`overriding the corresponding message <intlmessages>`
+
 Example
 -------
 
 Plugin Perl module
 ~~~~~~~~~~~~~~~~~~
 
-Create for example the MyPlugin module:
+This example creates a ``Lemonldap::NG::Portal::MyPlugin`` plugin that
+showcases some features of the plugin system.
 
-::
+First, create a file to contain the plugin code ::
 
    vi /usr/share/perl5/Lemonldap/NG/Portal/MyPlugin.pm
 
-
 .. tip::
 
     If you do not want to mix files from the distribution with
     your own work, put your own code in
-    ``/usr/local/lib/site_perl/Lemonldap/NG/Portal/MyPlugin.pm``\
+    ``/usr/local/lib/site_perl/Lemonldap/NG/Portal/MyPlugin.pm``.
+    Or you can use your own namespace such as ``ACME::Corp::MyPlugin``.
 
 .. code-block:: perl
 
+   # The package name must match the file path
+   # This file must be in Lemonldap/NG/Portal/MyPlugin.pm
    package Lemonldap::NG::Portal::MyPlugin;
 
    use Mouse;
    use Lemonldap::NG::Portal::Main::Constants;
    extends 'Lemonldap::NG::Portal::Main::Plugin';
 
+   # Declare when LemonLDAP::NG must call your functions
    use constant beforeAuth => 'verifyIP';
+   use constant hook => { passwordAfterChange  => 'logPasswordChange' };
+
+   # This function will be called at the "beforeAuth" login step
+   sub verifyIP {
+             my ($self, $req) = @_;
+             return PE_ERROR if($req->address !~ /^10/);
+             return PE_OK;
+   }
+
+   # This function will be called when changing passwords
+   sub logPasswordChange {
+       my ( $self, $req, $user, $password, $old ) = @_;
+       $self->userLogger->info("Password changed for $user");
+       return PE_OK;
+   }
 
+   # You can define your custom initialization in the
+   # init method.
+   # Before LemonLDAP::NG 2.0.14, this function was mandatory
    sub init {
              my ($self) = @_;
+
+             # This is how you declare HTTP routes
              $self->addUnauthRoute( mypath => 'hello', [ 'GET', 'PUT' ] );
              $self->addAuthRoute( mypath => 'welcome', [ 'GET', 'PUT' ] );
+
+             # The function can return 0 to indicate failure
              return 1;
    }
-   sub verifyIP {
-             my ($self, $req) = @_;
-             return PE_ERROR if($req->address !~ /^10/);
-             return PE_OK;
-   }
+
+   # This method will be called to handle unauthenticated requests to /mypath
    sub hello {
              my ($self, $req) = @_;
              ...
              return $self->p->sendJSONresponse($req, { hello => 1 });
    }
+
+   # This method will be called to handle authenticated requests to /mypath
    sub welcome {
              my ($self, $req) = @_;
 
@@ -129,10 +239,13 @@ Create for example the MyPlugin module:
              ...
              return $self->p->sendHtml($req, 'template', params => { WELCOME => 1 });
    }
+
+   # Your file must return 1, or Perl will complain.
    1;
 
-Configuration
-~~~~~~~~~~~~~
+
+Enabling your plugin
+~~~~~~~~~~~~~~~~~~~~
 
 Declare the plugin in lemonldap-ng.ini:
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/portalcustom.rst 2.0.14+ds-1/doc/sources/admin/portalcustom.rst
--- 2.0.13+ds-3/doc/sources/admin/portalcustom.rst	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/portalcustom.rst	2022-01-22 14:30:19.000000000 +0000
@@ -130,7 +130,7 @@ Skin files
 
 A skin is composed of different files:
 
--  **.tpl**: Perl HTML::Template files, for HTML content
+-  **.tpl**: Perl `HTML::Template <https://metacpan.org/pod/HTML::Template>`__ files, for HTML content
 -  **.css**: CSS (styles)
 -  **.js**: Javascript
 -  images and other media files
@@ -212,6 +212,12 @@ lemonldap-ng-cli:
 
    /usr/share/lemonldap-ng/bin/lemonldap-ng-cli -yes 1 set portalSkin 'myskin' portalSkinBackground ''
 
+You can find additional documentation on the syntax of template files in the
+`official documentation of the HTML::Template module
+<https://metacpan.org/pod/HTML::Template>`__
+
+.. _intlmessages:
+
 Messages
 ~~~~~~~~
 
@@ -368,16 +374,16 @@ Other
 -  **Anti iframe protection**: Set ``X-Frame-Options`` and CSP
    ``frame-ancestors`` headers (see `Browser
    compatibility <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options#Browser_compatibility>`__)
--  **Ping interval**: Number of milliseconds between each ping (Ajax
+-  **Ping interval**: number of milliseconds between each ping (Ajax
    request) on the portal menu. Set to 0 to dismiss checks.
--  **Show error on expired session**: Display the error "Session
+-  **Show error on expired session**: display the error "Session
    expired", which stops the authentication process. This is enabled by
    default but can be disabled to prevent transparent authentication
    (like SSL or Kerberos) to be stopped.
--  **Show error on mail not found**: Display error if provided mail is
+-  **Show error on mail not found**: display error if provided mail is
    not found in password reset by mail process. Disabled by default to
    prevent mail enumeration from this page.
--  **Display rights refresh link**: Enable/disable link in Portal menu to allow users to refresh their rights
+-  **Display rights refresh link**: enable/disable link in Portal menu to allow users to refresh their rights
 
 .. |image0| image:: /documentation/manager-skin-background.png
    :class: align-center
diff -pruN 2.0.13+ds-3/doc/sources/admin/portal.rst 2.0.14+ds-1/doc/sources/admin/portal.rst
--- 2.0.13+ds-3/doc/sources/admin/portal.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/portal.rst	2022-02-19 16:04:21.000000000 +0000
@@ -1,7 +1,7 @@
-The portal
+The Portal
 ==========
 
-The portal is the main component of LL::NG. It provides many features:
+The Portal is the main component of LL::NG. It provides many features:
 
 -  **Authentication service** of course
 
@@ -79,16 +79,16 @@ Kinematics
 #. Check if URL asked is valid
 #. Check if user is already authenticated
 
-   -  If not authenticated (or authentication is forced) try to find it
-      (userDB module) and to authenticate it (auth module), create
-      session, ask for second factor if required, calculate groups and
-      macros and store them. In 1.3, LL::NG has got a captcha feature
-      which is used in this case.
+   -  If not authenticated (or authentication is forced), try to find
+      (userDB module) and authenticate him (auth module), collect user data,
+      compute groups and macros, ask for second factor if required,
+      create a session and store it. LL::NG affords a captcha feature
+      which can be enabled.
 
 #. Modify password if asked (password module)
-#. Provides identity if asked (IdP module)
+#. Provide identity if asked (IdP module)
 #. Build :doc:`cookie(s)<ssocookie>`
-#. Redirect user to the asked URL or display menu
+#. Redirect user to the asked URL or display dynamic menu
 
 
 .. note::
diff -pruN 2.0.13+ds-3/doc/sources/admin/portalservers.rst 2.0.14+ds-1/doc/sources/admin/portalservers.rst
--- 2.0.13+ds-3/doc/sources/admin/portalservers.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/portalservers.rst	2022-02-19 16:04:21.000000000 +0000
@@ -15,7 +15,7 @@ for several usage:
 Configuration
 -------------
 
--  **SOAP/REST exported attributes**: list session attributes shared
+-  **REST/SOAP exported attributes**: list session attributes shared
    through SOAP/REST
    
    -  Use ``+`` to append the default list of technical attributes,
diff -pruN 2.0.13+ds-3/doc/sources/admin/presentation.rst 2.0.14+ds-1/doc/sources/admin/presentation.rst
--- 2.0.13+ds-3/doc/sources/admin/presentation.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/presentation.rst	2022-02-09 19:10:40.000000000 +0000
@@ -36,7 +36,7 @@ Databases
 .. attention::
 
     We call "database" a backend where we can read or write a data.
-    This can be a file, an LDAP directory, etc.
+    This can be a file, an LDAP directory, etc...
 
 We split databases in two categories:
 
@@ -123,8 +123,7 @@ on protected applications, with differen
 -  **SSO and Application logout**: the request is forwarded to
    application and SSO session is closed
 
-After logout process, the user is redirected on portal, or on a
-configured URL.
+After logout process, the user is redirected on portal, or on a configured URL.
 
 Session expiration
 ~~~~~~~~~~~~~~~~~~
@@ -136,10 +135,8 @@ This duration can be set in the manager'
 
     -  Handlers have a session cache, with a default lifetime of 10 minutes.
        So for Handlers located on different physical servers than the Portal, a user
-       with an expired session can still be authorized until the cache
-       expires.
-    -  Sessions are deleted by a scheduled task. Don't forget to install
-       cron files !
+       with an expired session can still be authorized until the cache expires.
+    -  Sessions are deleted by a scheduled task. Don't forget to install cron files!
 
 
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/psgi.rst 2.0.14+ds-1/doc/sources/admin/psgi.rst
--- 2.0.13+ds-3/doc/sources/admin/psgi.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/psgi.rst	2022-02-07 19:06:14.000000000 +0000
@@ -1,7 +1,7 @@
 Advanced PSGI usage
 ===================
 
-LLNG is build on `Plack <http://plackperl.org/>`__, so it can be used
+LL::NG is built on `Plack <http://plackperl.org/>`__, so it can be used
 with any compatible server:
 
 -  `Starman <https://metacpan.org/pod/starman>`__
@@ -46,19 +46,18 @@ to replace exactly FastCGI server. You c
 
 .. attention::
 
-    Starman, Twiggy,... are HTTP servers, not FastCGI ones
-    !
+    Starman, Twiggy,... are HTTP servers, not FastCGI ones!
 
 You can also replace only a part of it to create a specialized FastCGI
 server (portal,...). Look at ``llng-server.psgi`` example and take the
 part you want to use.
 
-There are also some other psgi files in examples directory.
+There are also some other PSGI files in examples directory.
 
-LLNG FastCGI Server
-~~~~~~~~~~~~~~~~~~~
+LL::NG FastCGI Server
+~~~~~~~~~~~~~~~~~~~~~
 
-``llng-fastcgi-server`` can be launched with the following options:
+``llng-fastcgi-server`` can be started with the following options:
 
 ==================== ===================== ===================== ==========================================================================================
 Command-line options                       Environment variable  Explanation
@@ -98,16 +97,15 @@ FCGI::Engine::ProcManager
 Using uWSGI
 ~~~~~~~~~~~
 
-You must install uWSGI PSGI plugin. Then for example, launch
+You have to install uWSGI PSGI plugin. Then for example, start
 llng-server.psgi *(simple example)*:
 
-::
+.. code-block:: shell
 
    /usr/bin/uwsgi --plugins psgi --socket :5000 --uid www-data --gid www-data --psgi /usr/share/lemonldap-ng/llng-server/llng-server.psgi
 
-You will find in LLNG Nginx configuration files some comments that
-explain how to configure Nginx to use uWSGI instead of LLNG FastCGI
-server.
+You will find in LL::NG Nginx configuration files some comments that
+explain how to configure Nginx to use uWSGI instead of LL::NG FastCGI server.
 
 Using Debian lemonldap-ng-uwsgi-app package
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -148,7 +146,7 @@ and/or Nginx init files several options.
 
 .. note::
     Nginx natively includes support for upstream servers speaking the uwsgi protocol since version 0.8.40.
-    To improve performances, you can switch from a TCP socket to an Unix Domain Socket by editing
+    To improve performances, you can switch from a TCP socket to an UDS socket by editing
     ``llng-server.yaml``:
 
     .. code-block:: ini
@@ -161,7 +159,7 @@ and/or Nginx init files several options.
 
     .. code-block:: nginx
 
-      # OR TO USE uWSGI
+      # With uWSGI
       include /etc/nginx/uwsgi_params;
       uwsgi_pass unix:///tmp/uwsgi.sock;
       uwsgi_param LLTYPE psgi;
@@ -174,8 +172,8 @@ and/or Nginx init files several options.
 Protect a PSGI application
 --------------------------
 
-LLNG provides ``Plack::Middleware::Auth::LemonldapNG`` that can be used
-to protect any PSGI application: it acts exactly like a LLNG handler.
+LL::NG provides ``Plack::Middleware::Auth::LemonldapNG`` that can be used
+to protect any PSGI application: it works exactly like a LL::NG handler.
 Simple example:
 
 .. code-block:: perl
diff -pruN 2.0.13+ds-3/doc/sources/admin/radius2f.rst 2.0.14+ds-1/doc/sources/admin/radius2f.rst
--- 2.0.13+ds-3/doc/sources/admin/radius2f.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/radius2f.rst	2022-01-22 14:30:19.000000000 +0000
@@ -56,13 +56,13 @@ Mail second factor".
    code against the Radius server, use this attribute as the login and
    the OTP code as password. By default, the attribute designated as
    ``whatToTrace`` is used.
--  **Authentication timeout** (Optional) :
+-  **Authentication timeout** (Optional): Allowed time to perform authentication
 -  **Authentication level** (Optional): if you want to overwrite the
    value sent by your authentication module, you can define here the new
    authentication level. Example: 5
--  **Logo** (Optional): logo file *(in static/<skin> directory)*
 -  **Label** (Optional): label that should be displayed to the user on
    the choice screen
+-  **Logo** (Optional): logo file *(in static/<skin> directory)*
 
 Vendor specific
 ~~~~~~~~~~~~~~~
diff -pruN 2.0.13+ds-3/doc/sources/admin/renater.rst 2.0.14+ds-1/doc/sources/admin/renater.rst
--- 2.0.13+ds-3/doc/sources/admin/renater.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/renater.rst	2022-01-22 14:30:19.000000000 +0000
@@ -98,7 +98,7 @@ The script provide the following options
  * -h (--help): print this message
  * -m (--metadata): URL of metadata document
  * -s (--spconfprefix): Prefix used to set SP configuration key
- * --ignore-sp: ignore SP maching this entityID (can be specified multiple times)
+ * --ignore-sp: ignore SP matching this entityID (can be specified multiple times)
  * --ignore-idp: ignore IdP matching this entityID (can be specified multiple times)
  * -a (--nagios): output statistics in Nagios format
  * -n (--dry-run): print statistics but do not apply changes
diff -pruN 2.0.13+ds-3/doc/sources/admin/rest2f.rst 2.0.14+ds-1/doc/sources/admin/rest2f.rst
--- 2.0.13+ds-3/doc/sources/admin/rest2f.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/rest2f.rst	2022-01-22 14:30:19.000000000 +0000
@@ -20,9 +20,9 @@ Second Factors » REST 2nd Factor".
 -  **Authentication level** (Optional): if you want to overwrite the
    value sent by your authentication module, you can define here the new
    authentication level. Example: 5
--  **Logo** (Optional): logo file *(in static/<skin> directory)*
 -  **Label** (Optional): label that should be displayed to the user on
    the choice screen
+-  **Logo** (Optional): logo file *(in static/<skin> directory)*
 
 Arguments
 ---------
diff -pruN 2.0.13+ds-3/doc/sources/admin/restservices.rst 2.0.14+ds-1/doc/sources/admin/restservices.rst
--- 2.0.13+ds-3/doc/sources/admin/restservices.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/restservices.rst	2022-02-07 19:06:14.000000000 +0000
@@ -44,7 +44,7 @@ The JSON response fields are:
 -  ``result``: authentication result, ``0`` if it fails, ``1`` if it
    succeed
 -  ``error``: error code, the corresponding error can be found in
-   ``Lemonldap::NG::Portal::Main::Constants``
+   :doc:`Portal error codes<error_codes>`
 -  ``id``: if authentication succeed, the session id is returned in this
    field
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/restsessionbackend.rst 2.0.14+ds-1/doc/sources/admin/restsessionbackend.rst
--- 2.0.13+ds-3/doc/sources/admin/restsessionbackend.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/restsessionbackend.rst	2022-02-07 19:06:14.000000000 +0000
@@ -4,12 +4,11 @@ REST session backend
 Session <type> can be 'global' for SSO sessions or 'persistent' for
 persistent sessions.
 
-LL::NG portal provides REST end points for sessions management:
+LL::NG Portal provides REST end points for sessions management:
 
 -  GET /sessions/<type>/<session-id> : get session datas
 -  GET /sessions/<type>/<session-id>/<key> : get a session key value
--  GET /sessions/<type>/<session-id>/[k1,k2] : get some session key
-   value
+-  GET /sessions/<type>/<session-id>/[k1,k2] : get some keys value
 -  POST /sessions/<type> : create a session
 -  PUT /sessions/<type>/<session-id> : update some keys
 -  DELETE /sessions/<type>/<session-id> : delete a session
@@ -19,18 +18,23 @@ Sessions for connected users (used by :d
 -  GET /session/my/<type> : get session datas
 -  GET /session/my/<type>/key : get session key
 -  DELETE /session/my : ask for logout
+-  DELETE /sessions/my : ask for global logout (if GlobalLogout plugin is on)
 
-Authorizations for connected users (always enabled):
+Services for connected users (always enabled):
 
--  GET /mysession/?authorizationfor=<base64-encoded-url>: ask if url is
-   authorizated
+-  GET /mysession/?authorizationfor=<base64-encoded-url> : ask if an url
+   is authorized
+-  GET /mysession/?whoami       : get "my" uid
+-  PUT /mysession/<type>        : update some persistent data (restricted)
+-  DELETE /mysession/<type>/key : delete key in data (restricted)
+-  GET    /myapplications       : get "my" appplications list
 
 This session backend can be used to share sessions stored in a
 non-network backend (like
 :doc:`file session backend<filesessionbackend>`) or in a network backend
 protected with a firewall that only accepts HTTP flows.
 
-Most of the time, REST session backend is used by Handlers installed on
+Most of the time, REST session backend is used by Handlers deployed on
 external servers.
 
 To configure it, REST session backend will be set through Manager in
@@ -69,16 +73,16 @@ Name                Comment
 =================== ======================================== ==================================================
 
 `user` and `password` parameters are only used if the entry point `index.fcgi/sessions/global`
-is protected by a basic authentication. Thus, handlers will make requests to the portal
+is protected by a basic authentication. Thus, handlers will make requests to the Portal
 using these parameters.
 
 
 .. attention::
 
     By default, user password and other secret keys are
-    hidden by LLNG REST server. You can force REST server to export their
+    hidden by LL::NG REST server. You can force REST server to export their
     real values by selecting "Export secret attributes in REST" in the
-    manager. This less secure option is disabled by default.
+    Manager. This less secure option is disabled by default.
 
 Apache
 ~~~~~~
diff -pruN 2.0.13+ds-3/doc/sources/admin/riskbased.rst 2.0.14+ds-1/doc/sources/admin/riskbased.rst
--- 2.0.13+ds-3/doc/sources/admin/riskbased.rst	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/riskbased.rst	2022-02-07 19:06:14.000000000 +0000
@@ -0,0 +1,77 @@
+Risk-based Authentication
+=========================
+
+Our definition
+--------------
+
+Risk-based authentication is the ability to take into account the context of
+the authentication process, and react accordingly, by increasing the
+authentication challenge (second factor, email confirmation) or trigger out of
+band actions (email notifications, alerts..).
+
+.. warning::
+
+   All the features presented on this page are not natively supported by
+   LemonLDAP::NG but can be added through custom plugins or configuration
+
+The authentication context can include:
+
+* Source IP address
+* Access time
+* Previous authentications (history)
+* Using the same browser as previous logins
+
+Reactions can include:
+
+* Triggering or skipping the second factor
+* Sending an email to warn the user of a suspicious login
+* Denying attempt if the suspicion level is too high
+
+Implementation in LemonLDAP::NG
+-------------------------------
+
+LemonLDAP::NG uses the ``_riskLevel`` and ``_riskDetails`` session variables to
+keep track of the risk associated to the current authentication.
+
+Detection plugins will raise or lower the risk level, and store fine-grained
+details in the risk details object.
+
+Action plugins may use the risk level to trigger certain actions, and can
+translate the risk detail items into user-friendly messages.
+
+
+Compatible plugins
+------------------
+
+Detection
+~~~~~~~~~
+
+New location warning
+^^^^^^^^^^^^^^^^^^^^
+
+.. versionadded:: 2.0.14
+
+The :doc:`New Location warning <newlocationwarning>` plugin will increase the risk level by 1 when triggered, and will store the **Session attribute to display** in ``$_riskDetail->{newLocation}``.
+
+Action
+~~~~~~
+
+Forbidding/triggering second factors
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+You can use the following activation rule to trigger second factors if the risk level is high::
+
+    $_riskLevel > 0
+
+Or, if you use self registration::
+
+    has2f('TOTP') and $_riskLevel > 0
+
+Denying login
+^^^^^^^^^^^^^
+
+You can use :doc:`session opening conditions <grantsession>` to deny access if the risk level is too high with a rule like this ::
+
+    $_riskLevel < 2
+
+This will forbid sessions from being opened if the risk level is greater or equal to 2
diff -pruN 2.0.13+ds-3/doc/sources/admin/rpm-gpg-key-ow2 2.0.14+ds-1/doc/sources/admin/rpm-gpg-key-ow2
--- 2.0.13+ds-3/doc/sources/admin/rpm-gpg-key-ow2	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/rpm-gpg-key-ow2	1970-01-01 00:00:00.000000000 +0000
@@ -1,30 +0,0 @@
------BEGIN PGP PUBLIC KEY BLOCK-----
-Version: GnuPG v1.4.5 (GNU/Linux)
-
-mQGiBEpOEcERBACHzHP7ICtjmsG4YgwlstQw0ubp6154i57BN45siMoovioQ1nP5
-kXNR+fZjEW5BRqtJExQoWLdXTFL1gvsdW5V+zx7B7DIlP6H+oz1PFh8hGXUmnqb9
-pL1A0WUrhbye6nlzpxt9jhGn6ymbilAi8iIWSrFxC09GONGwBGCLwbbp5wCg/75n
-DHecwFtSwEt7o3YV5B6k9WcD/RcPtY3mwa3RfaC+rsGdaqmni/jy6P1OrgmQX59C
-Zm813j/JnXYoeV+xIdCs144xPvzrCH+k/czVFBjvcA3xr2F/kuW7Kn8F+u8Ma3lb
-EghlG6CdJpCeXwiou5lPfPURIM7n7TDi2zVktRxGUnIa3fyBC9Orar/HbWgDGSYR
-1R+vBACEcHOknp09FT8UB2YY/98cG4n5RaiBiUb6Znwd6MrEtdBC0x8PdR6PPrWf
-ujUZ1dgUlKUtTN2V7OC8Ql3fls8TlxLY3L2ql6PrjuF5/zhC/1lEl7QzS+tCHAzU
-FlDMbb3F5o+EZwxxK3Lrdf+SbmKiYq7gqv79+BJbPiLkQvLfTbQqQ2xlbWVudCBP
-VURPVCAoT1cyKSA8Y2xlbS5vdWRvdEBnbWFpbC5jb20+iGAEExECACAFAkpOEcEC
-GwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRBUixe/gfGOej2rAKD/mzoSicDh
-f2fhAEA3t+8qkJlwgACgvLUn30yj81bNjOo84p3NjEzpt6W5Ag0ESk4RwhAIAILk
-DF6M5GglCqysxF6gmO4RB24nkJELOmYAfknM0qmZPED3f//wgWFfYC3t2Hsic1HM
-9Dq1fQc9ziFfL7Ntt2oCu0YDoT4lrRL7eWwRn+H5sPmBisyfpTohZlObnNDOuGUZ
-jWZDP+7bIiNuj32TuR1Gl9q9hygm5rzjg/7d0eQfgMMSJ5D1x8FAcDRIgtF9dfQ0
-XLXF1SBuPqp6E7Q92rNxWlryifnGBIcOvVIYgayyxqgLf4+hkCOi47GDVlS+E4FQ
-Xc5DVHuhH8JJrMsBAd14m435c1uM9gTYhOtmpgDPocPUr5APSOd/zhV+b/8t+PDm
-ySa5qHVmShC/NFziyY8AAwUH/jBiZQ+qOyXaanAgIz2/uiqpJxO1MR+S6m+cazvk
-X4nXD9N8rsUYKnXxU6bNX731t6P2StG8kfkV84xkaPBTkssDBfQIFSwYFUuyBr/m
-6V8ulebig/6XHp7dVJ96DvQu8HHiLZ8YXeOVImCoEXp5fa8HgyhxVSLbVsAENYOd
-IEY7G4Lh/RAyrkRaLSGZuHnwXk3ioNQHCHB4m48q8tmQ2v4U8FJhXhxCmyKPKAru
-PPIKQ9kjPzX92NADmZc+n8RxzyBa9fppQ3z0v8mJ9SjoJ3qAO9ks+yQADLiZ8HsN
-jNS3Nf35jqQ5bKFF/uAygMLPzhi8iQtcBF1Q+3NDk/DRFfSISQQYEQIACQUCSk4R
-wgIbDAAKCRBUixe/gfGOekmNAKC4jduVjzzfeLDyH3Hnkz3G0MIFsACffY2Wv6ef
-bH9spStkLDt2jxvJ42Y=
-=6pG1
------END PGP PUBLIC KEY BLOCK-----
diff -pruN 2.0.13+ds-3/doc/sources/admin/samlservice.rst 2.0.14+ds-1/doc/sources/admin/samlservice.rst
--- 2.0.13+ds-3/doc/sources/admin/samlservice.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/samlservice.rst	2022-01-22 14:30:19.000000000 +0000
@@ -144,7 +144,7 @@ To define keys, you can:
 
 .. versionchanged:: 2.0.10
 
-   The signature method can now be overriden for a SP or IDP. This will only work
+   The signature method can now be overridden for a SP or IDP. This will only work
    if you are using a certificate for signature instead of a public key.
 
 
@@ -153,6 +153,9 @@ To define keys, you can:
    If you are running a version under 2.0.10, the choice of a signature
    algorithm will affect all SP and IDP.
 
+
+.. _samlservice-convert-certificate:
+
 Converting a RSA public key to a certificate
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/secondfactor.rst 2.0.14+ds-1/doc/sources/admin/secondfactor.rst
--- 2.0.13+ds-3/doc/sources/admin/secondfactor.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/secondfactor.rst	2022-02-19 16:04:21.000000000 +0000
@@ -23,28 +23,13 @@ complete authentication module with 2FA
 -  :doc:`External 2F<external2f>` *(to call an external command)*
 -  :doc:`REST<rest2f>` *(Remote REST app)*
 -  :doc:`RADIUS<radius2f>` *(Remote RADIUS server)*
+-  :doc:`WebAuthn<webauthn2f>` *(Web Authentication API)*
 
 The E-Mail, External and REST 2F modules
 :doc:`may be declared multiple times<sfextra>` with different sets of
 parameters.
 
 
-Registration on first use
--------------------------
-
-If you want to force a 2F registration on first login, you can use the *Force
-2FA registration at login* option.
-
-You can use a `rule<writingrulesand_headers>` to enable this behavior only for
-some users.
-
-Second factor expiration
-------------------------
-
-You can display a message if an expired second factor has been removed by
-enabling *Display a message if an expired SF is removed* option or setting a
-rule.
-
 Self-care on Portal
 -------------------
 
@@ -53,6 +38,15 @@ User may register second factors themsel
 The link will be displayed if at least one SFA module is enabled. You can set a
 rule to display or not the link.
 
+Registration on first use
+-------------------------
+
+If you want to force a 2F registration on first login, you can use the *Force
+2FA registration at login* option.
+
+You can use a :doc:`rule <writingrulesand_headers>` to enable this behavior only for
+some users.
+
 Session upgrade through 2FA
 ---------------------------
 
@@ -68,6 +62,20 @@ of doing a complete reauthentication.
 
 .. |beta| image:: /documentation/beta.png
 
+Registration timeout
+--------------------
+
+Allowed time to register a TOTP.
+
+Second factor expiration
+------------------------
+
+You can display a message if an expired second factor has been removed by
+enabling *Display a message if an expired SF is removed* option or setting a
+rule.
+SF name(s) or number of removed SF can be displayed in message BODY by using
+`_nameSF_` or `_removedSF_` respectively.
+
 Providing tokens from an external source
 ----------------------------------------
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/securetoken.rst 2.0.14+ds-1/doc/sources/admin/securetoken.rst
--- 2.0.13+ds-3/doc/sources/admin/securetoken.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/securetoken.rst	2022-01-22 14:30:19.000000000 +0000
@@ -44,15 +44,15 @@ Handler parameters
 SecureToken parameters are the following:
 
 -  **Memcached servers**: addresses of Memcached servers, separated with
-   spaces.
+   spaces
 -  **Token expiration**: time in seconds for token expiration (remove
-   from Memcached server).
--  **Attribute to store**: the session key that will be stored in
-   Memcached.
+   from Memcached server)
+-  **Attribute to store**: session key that will be stored in
+   Memcached
 -  **Protected URLs**: Regexp of URLs for which the secure token will be
    sent, separated by spaces
 -  **Header name**: name of the HTTP header carrying by the secure
-   token.
+   token
 -  **Allow requests in error**: allow a request that has generated an
    error in token generation to be forwarded to the protected
    application without secure token (default: yes)
diff -pruN 2.0.13+ds-3/doc/sources/admin/sessions.rst 2.0.14+ds-1/doc/sources/admin/sessions.rst
--- 2.0.13+ds-3/doc/sources/admin/sessions.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/sessions.rst	2022-01-22 14:30:19.000000000 +0000
@@ -9,13 +9,13 @@ To configure sessions, go in Manager, ``
 ``Sessions``:
 
 -  **Store user password in session data**: see
-   :doc:`password store documentation<passwordstore>`.
--  **Display session identifier**: Should the session ID be displayed in the manager's session explorer. The session ID is a sensitive information that should only be shown to highly trusted administrators.
--  **Sessions timeout**: Maximum lifetime of a session. Old sessions are
-   deleted by a cron script.
--  **Sessions activity timeout**: Maximum inactivity duration.
--  **Sessions update interval**: Minimum interval used to update session
-   when activity timeout is set.
+   :doc:`password store documentation<passwordstore>`
+-  **Display session identifier**: should the session ID be displayed in the manager's session explorer. The session ID is a sensitive information that should only be shown to highly trusted administrators
+-  **Sessions timeout**: maximum lifetime of a session. Old sessions are
+   deleted by a cron script
+-  **Sessions activity timeout**: maximum inactivity duration
+-  **Sessions update interval**: minimum interval used to update session
+   when activity timeout is set
 
 
 .. danger::
@@ -56,13 +56,13 @@ To configure sessions, go in Manager, ``
    disable persistent sessions storage to avoid too many database
    tuples.
 
-   -  **Disable storage**: Do not store user persitent sessions.
+   -  **Disable storage**: do not store user persitent sessions
 
 
 .. attention::
 
     Note that since HTTP protocol is not connected,
-    restrictions are not applied to the new session: the oldest are
+    restrictions are not applied to the new session. The oldest are
     destroyed.
 
 Command-line tools
diff -pruN 2.0.13+ds-3/doc/sources/admin/ssoaas.rst 2.0.14+ds-1/doc/sources/admin/ssoaas.rst
--- 2.0.13+ds-3/doc/sources/admin/ssoaas.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/ssoaas.rst	2022-02-07 19:06:14.000000000 +0000
@@ -1,4 +1,4 @@
-SSO as a service (SSOaaS)
+SSO as a Service (SSOaaS)
 =========================
 
 Our concept of SSOaaS
@@ -7,41 +7,52 @@ Our concept of SSOaaS
 Access management provides 3 services:
 
 -  Global Authentication: Single Sign-On
--  Authorization: to grant authentication is not enough. User rights
+-  Authorization: Grant authentication is not enough. User rights
    must be checked
 -  Accounting: SSO logs (access) + application logs *(transactions and
    results)*
 
 LL::NG affords all these services (except application logs of course,
-but headers are provided to permit this).
+but headers are provided to allow this).
 
 Headers setting is an another LL::NG service. LL::NG can provide any
-user attributes to an application (see
-:doc:`Rules and headers<writingrulesand_headers>`)
+user attributes to an application
+(see :doc:`Rules and headers<writingrulesand_headers>`)
 
 ``*aaS`` means that application can drive underlying layer (IaaS for
 infrastructure, PaaS for platform,…). So for us, ``SSOaaS`` must provide
-the ability for an app to manage authorizations and choose user
-attributes to set. Authentication can not be really ``*aaS``: app must
-just use it, not manage it.
-
-LL::NG affords some features that can be used to provide SSO as a
-service: a web application can manage its rules and headers. Docker or
-VM images (Nginx only) includes LL::NG Nginx configuration that aims to
-a global
-:ref:`LL::NG authorization server<platformsoverview-external-servers-for-nginx>`.
-By default, all authenticated users can access and one header is set:
-``Auth-User``. If application gives a RULES_URL parameter that refers to
+the ability for an application to manage authorizations and choose user
+attributes to set. Authentication can not be really ``*aaS``: application
+must just use it but not manage it.
+
+LL::NG affords some features that can be used for providing SSO as a
+service. So a web application can manage its rules and headers.
+Docker or VM images (Nginx only) includes LL::NG Nginx configuration that
+aims to a 
+:ref:`central LL::NG authorization server<platformsoverview-external-servers-for-nginx>`.
+By default, all authenticated users can access and just one header is set:
+``Auth-User``. If application defines a ``RULES_URL`` parameter that refers to
 a JSON file, authorization server will read it, apply specified rules
 and set required headers (see :doc:`DevOps Handler<devopshandler>`).
 
-There are two different architectures to do this:
+Two different kind of architecture are existing to do this:
 
--  Using a :doc:`global FastCGI (or uWSGI) server<psgi>`
--  Using front reverse-proxies *(some cloud installations use
+-  Using a :doc:`central FastCGI (or uWSGI) server<psgi>`
+-  Using front Reverse-Proxies *(some cloud or HA installations use
    reverse-proxies in front-end)*
 
-Example of a global FastCGI architecture:
+
+.. note::
+
+    Some requests can be dropped by the central FastCGI/uWSGI server.
+
+    Example below with an uWSGI server to prevent Load Balancer health check requests
+    being forwarded to the central DevOps Handler:
+
+    ```route-remote-addr = ^127\.0\.0\.25[34]$ break: 403 Forbidden for IP ${REMOTE_ADDR}```
+
+
+Example of a central FastCGI architecture:
 
 |image0|
 
@@ -50,23 +61,25 @@ In both case, Handler type must be set t
 Examples of webserver configuration for Docker/VM images
 --------------------------------------------------------
 
-Using a global FastCGI (or uWSGI) server
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Using a Central FastCGI (or uWSGI) Server
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Nginx
 ^^^^^
 
-In this example, web server templates (Nginx only) are configured to
-request authorization from a central FastCGI server:
+Examples below are web server templates customized for
+requesting authorization from a central FastCGI server.
+With a central uWSGI server (Nginx only), use 'uwsgi_param' directive:
 
-.. code::
+.. code-block:: nginx
 
    server {
+     listen <port>;
      server_name myapp.domain.com;
      location = /lmauth {
        internal;
        include /etc/nginx/fastcgi_params;
-       # Pass authorization requests to Central FastCGI server:
+       # Pass authorization requests to central FastCGI server
        fastcgi_pass 10.1.2.3:9090;
        fastcgi_param VHOSTTYPE DevOps;
        # Drop post datas
@@ -77,8 +90,12 @@ request authorization from a central Fas
        # Keep original request (LLNG server will received /lmauth)
        fastcgi_param X_ORIGINAL_URI  $original_uri;
 
-       # Set dynamically rules (LLNG will poll it every 10 mn)
-       fastcgi_param RULES_URL http://rulesserver/my.json
+       # Set redirection parameters
+       fastcgi_param HTTPS_REDIRECT "$https";
+       fastcgi_param PORT_REDIRECT $server_port;
+
+       # Set rules dynamically (LL::NG will poll it every 10 mn)
+       fastcgi_param RULES_URL http://rulesserver/my.json;
      }
      location /rules.json {
        auth_request off;
@@ -91,9 +108,9 @@ request authorization from a central Fas
        auth_request_set $lmremote_user $upstream_http_lm_remote_user;
        auth_request_set $lmlocation $upstream_http_location;
        error_page 401 $lmlocation;
-       include /etc/lemonldap-ng/nginx-lua-headers.conf;
+       include /etc/nginx/nginx-lua-headers.conf;
        # ...
-       # Example with php-fpm:
+       # Example with php-fpm
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
      }
@@ -105,23 +122,23 @@ request authorization from a central Fas
 Apache
 ^^^^^^
 
-There is an experimental FactCGI client in LLNG. You just have to
-install FCGI::Client and add this in the apache2.conf or your web
-applications or proxies.
+LL::NG provides an experimental FastCGI client. You have to
+install LemonLDAP::NG handler (LL::NG FastCGI client),
+FCGI::Client (Perl dependency) and Mod_Perl2 (Apache module)
+used for parsing HTTP headers.
+Then, add this in your apache2.conf web applications or reverse-proxies.
 
-The following configuration example assumes that you are in a "central
-FastCGI" configuration.
 
 .. code-block:: apache
 
-   <VirtualHost ...>
+   <VirtualHost port>
        ServerName app.tls
        PerlHeaderParserHandler Lemonldap::NG::Handler::ApacheMP2::FCGIClient
 
-       # This must point to the central FastCGI server
+       # The central FastCGI server socket
        PerlSetVar LLNG_SERVER 192.0.2.1:9090
 
-       # Declare this vhost as a DevOps vhost, so that we do not have
+       # Declare this vhost as a DevOps protected vhost. So you do not have
        # to declare it in the LemonLDAP::NG Manager
        PerlSetVar VHOSTTYPE DevOps
 
@@ -129,6 +146,8 @@ FastCGI" configuration.
        # used to make the authentication decision about this virtualhost
        # Make sure the central FastCGI server can reach it
        PerlSetVar RULES_URL http://app.tld/rules.json
+       PerlSetVar HTTPS_REDIRECT HTTPS
+       PerlSetVar PORT_REDIRECT SERVER_PORT
        ...
    </VirtualHost>
 
@@ -149,6 +168,8 @@ you can protect also an Express server.
      port: 9090,
      PARAMS: {
        RULES_URL: 'http://my-server/rules.json'
+       HTTPS_REDIRECT: 'ON',
+       PORT_REDIRECT: '443'
      }
    });
 
@@ -195,6 +216,8 @@ Simple example:
        port => '9090',
        fcgi_auth_params => {
          RULES_URL => 'https://my-server/my.json',
+         HTTPS_REDIRECT => 'ON',
+         PORT_REDIRECT  => 443
        },
        # Optional rejection subroutine
        #on_reject => \&on_reject;
@@ -202,15 +225,15 @@ Simple example:
      $app;
    };
 
-Using front reverse-proxies
+Using front Reverse-Proxies
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 This is a simple Nginx configuration file. It looks like a standard
-LL::NG nginx configuration file except for:
+LL::NG Nginx configuration file except for:
 
 -  VHOSTTYPE parameter forced to use DevOps handler
--  /rules.json must not be protected by LL::NG but by the web server
-   itself
+-  /rules.json do not have to be protected by LL::NG
+   but by the web server itself.
 
 This configuration handles ``*.dev.sso.my.domain`` URL and forwards
 authenticated requests to ``<vhost>.internal.domain``. Rules can be
@@ -220,6 +243,7 @@ directory.
 .. code-block:: nginx
 
    server {
+     listen <port>;
      server_name "~^(?<vhost>.+?)\.dev\.sso\.my\.domain$";
      location = /lmauth {
        internal;
@@ -232,8 +256,11 @@ directory.
        fastcgi_param CONTENT_LENGTH "";
        # Keep original hostname
        fastcgi_param HOST $http_host;
-       # Keep original request (LLNG server will received /lmauth)
+       # Keep original request (LL::NG server will received /lmauth)
        fastcgi_param X_ORIGINAL_URI  $original_uri;
+       # Set redirection params
+       fastcgi_param HTTPS_REDIRECT "$https";
+       fastcgi_param PORT_REDIRECT $server_port;
      }
      location /rules.json {
        auth_request off;
@@ -246,10 +273,9 @@ directory.
        auth_request_set $lmremote_user $upstream_http_lm_remote_user;
        auth_request_set $lmlocation $upstream_http_location;
        error_page 401 $lmlocation;
-       include /etc/lemonldap-ng/nginx-lua-headers.conf;
+       include /etc/nginx/nginx-lua-headers.conf;
        proxy_pass https://$vhost.internal.domain;
      }
    }
 
 .. |image0| image:: /documentation/devops.png
-
diff -pruN 2.0.13+ds-3/doc/sources/admin/start.rst 2.0.14+ds-1/doc/sources/admin/start.rst
--- 2.0.13+ds-3/doc/sources/admin/start.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/start.rst	2022-02-19 16:04:21.000000000 +0000
@@ -1,6 +1,11 @@
 Documentation for LemonLDAP::NG 2.0
 ===================================
 
+.. image:: logos/logo_llng_600px.png
+   :alt: LL::NG logo
+   :align: center
+   :target: https://www.lemonldap-ng.org
+
 .. toctree::
    
    Documentation index<documentation>
@@ -182,6 +187,7 @@ Second factor (:doc:`documentation<secon
 :doc:`Radius Second Factor<radius2f>` |new|  [3]_                    ✔
 :doc:`REST Second Factor<rest2f>` |new|                              ✔
 :doc:`Yubikey<yubikey2f>` |new|                                      ✔
+:doc:`WebAuthn<webauthn2f>` |new|                                    ✔
 :doc:`Additional second factors<sfextra>` |new|  [4]_                ✔
 ==================================================================== ==============
 
@@ -285,12 +291,13 @@ Name
 :doc:`Grant Sessions<grantsession>`                                  Rules to apply before allowing a user to open a session
 :doc:`Impersonation<impersonation>`  [11]_\ |new|                    Allow users to use another identity
 :doc:`Find user<finduser>`  [12]_\ |new|                             Search for user account
-:doc:`Notifications system<notifications>`                           DIsplay a message during log in process 
+:doc:`NewLocationWarning<newlocationwarning>`  [13]_\ |beta|         Send an email when user sign in from a new location 
+:doc:`Notifications system<notifications>`                           Display a message during log in process 
 :doc:`Portal Status<status>`                                         Experimental portal status page
 :doc:`Public pages<public_pages>`                                    Enable public pages system
-:doc:`Refresh session API<refreshsessionapi>`  [13]_                 Plugin that provides an API to refresh a user session
+:doc:`Refresh session API<refreshsessionapi>`  [14]_                 Plugin that provides an API to refresh a user session
 :doc:`Reset password by mail<resetpassword>`                         Send a mail to reset its password
-:doc:`Reset certificate by mail<resetcertificate>`  [14]_\ |new|     Allow users to reset their certificate
+:doc:`Reset certificate by mail<resetcertificate>`  [15]_\ |new|     Allow users to reset their certificate
 :doc:`REST services<restservices>` |new|                             REST server for :doc:`Proxy<authproxy>`
 :doc:`SOAP services<soapservices>` |deprecated|                      SOAP server for :doc:`Proxy<authproxy>`
 :doc:`Stay connected<stayconnected>` |new|                           Enable persistent connection on same browser
@@ -308,12 +315,12 @@ Handlers are software control agents to
 ==================================================================== ========== ============================================================= =========================================== ================================================================================== =============================================== ======================================================================================================================
 Handler type                                                         Apache     LLNG FastCGI/uWSGI server (Nginx, or :doc:`SSOaaS<ssoaas>`)   `Plack servers <https://plackperl.org>`__   Node.js  ( `express apps <http://expressjs.com/>`__\  or :doc:`SSOaaS<ssoaas>`)    :doc:`Self protected apps<selfmadeapplication>`   Comment
 ==================================================================== ========== ============================================================= =========================================== ================================================================================== =============================================== ======================================================================================================================
-Main *(default handler)*                                             ✔          ✔                                                             ✔                                           :doc:`Partial<nodehandler>` ** [15]_ **                                            ✔
-:doc:`AuthBasic<handlerauthbasic>`                                   ✔          ✔                                                             ✔                                                                                                                              ✔                                               Designed for some server-to-server applications
+Main *(default handler)*                                             ✔          ✔                                                             ✔                                           :doc:`Partial<nodehandler>` ** [16]_ **                                            ✔
+:doc:`AuthBasic<authbasichandler>`                                   ✔          ✔                                                             ✔                                                                                                                              ✔                                               Designed for some server-to-server applications
 :doc:`CDA<cda>`                                                      ✔          ✔                                                             ✔                                                                                                                              ✔                                               For Cross Domain Authentication
 :doc:`DevOps<devopshandler>`  (:doc:`SSOaaS<ssoaas>`)  |new|         ✔          ✔                                                             ✔                                           ✔                                                                                                                                  Allows application developers to define their own rules and headers inside their applications
 :doc:`DevOpsST<devopssthandler>`  (:doc:`SSOaaS<ssoaas>`)  |new|     ✔          ✔                                                             ✔                                           ✔                                                                                                                                  Enables both :doc:`DevOps<devopshandler>` and :doc:`Service Token<servertoserver>`
-:doc:`OAuth2<oauth2handler>`  [16]_\ |new|                           ✔          ✔                                                             ✔                                                                                                                              ✔                                               Uses OpenID Connect/OAuth2 access token to check authentication and authorization, can be used to protect Web Services
+:doc:`OAuth2<oauth2handler>`  [17]_\ |new|                           ✔          ✔                                                             ✔                                                                                                                              ✔                                               Uses OpenID Connect/OAuth2 access token to check authentication and authorization, can be used to protect Web Services
 :doc:`Secure Token<securetoken>`                                     ✔          ✔                                                             ✔                                                                                                                                                                              Designed to secure exchanges between a LLNG reverse-proxy and a remote app
 :doc:`Service Token<servertoserver>` |new| *(Server-to-Server)*      ✔          ✔                                                             ✔                                           ✔                                                                                  ✔                                               Designed to permit underlying requests *(API-Based Infrastructure)*
 :doc:`Zimbra PreAuth<applications/zimbra>`                           ✔          ✔                                                             ✔
@@ -458,7 +465,7 @@ Advanced features
 -  :doc:`Create an account (self service)<register>`
 -  :doc:`Forward logout to applications<logoutforward>`
 -  :doc:`Secure Token Handler<securetoken>`
--  :doc:`AuthBasic Handler<handlerauthbasic>`
+-  :doc:`AuthBasic Handler<authbasichandler>`
 -  :doc:`SSO as a Service<ssoaas>` *(SSOaaS)* |new|
 -  :doc:`Handling server webservice calls<servertoserver>` |new|
 -  `LemonLDAP::NG kubernetes
@@ -598,18 +605,22 @@ by your language code):
    2.0.11
 
 .. [13]
+   :doc:`NewLocationWarning<newlocationwarning>` is available
+   with LLNG ≥ 2.0.14
+
+.. [14]
    :doc:`Refresh session API plugin<refreshsessionapi>` is available
    with LLNG ≥ 2.0.7
 
-.. [14]
+.. [15]
    :doc:`Reset certificate by mail plugin<resetcertificate>` is
    available with LLNG ≥ 2.0.7
 
-.. [15]
+.. [16]
    :doc:`Node.js handler<nodehandler>` has not yet reached the same
    level of functionalities
 
-.. [16]
+.. [17]
    :doc:`OAuth2 Handler<oauth2handler>` is available with LLNG ≥ 2.0.4
 
 .. |image0| image:: /icons/kthememgr.png
diff -pruN 2.0.13+ds-3/doc/sources/admin/stayconnected.rst 2.0.14+ds-1/doc/sources/admin/stayconnected.rst
--- 2.0.13+ds-3/doc/sources/admin/stayconnected.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/stayconnected.rst	2022-02-07 19:06:14.000000000 +0000
@@ -11,6 +11,13 @@ Just enable it in the manager (section 
 
 -  **Parameters**:
 
-   -  **Activation**: Enable / Disable this plugin
+   -  **Activation**: Rule to enable/disable this plugin
+   -  **Do not check fingerprint**: Enable/Disable browser fingerprint checking 
    -  **Expiration time**: Persistent session connection and cookie timeout
-   -  **Cookie name**: Persistent connection cookie name
\ No newline at end of file
+   -  **Cookie name**: Persistent connection cookie name
+
+.. tip::
+
+    By example, you can allow users from 192.168.0.0/16 private network to register a fingerprinting:
+    
+    - Rule: ``$env->{REMOTE_ADDR} =~ /^192\.168\./``
\ No newline at end of file
diff -pruN 2.0.13+ds-3/doc/sources/admin/testopenidconnect.rst 2.0.14+ds-1/doc/sources/admin/testopenidconnect.rst
--- 2.0.13+ds-3/doc/sources/admin/testopenidconnect.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/testopenidconnect.rst	2022-01-22 14:30:19.000000000 +0000
@@ -8,9 +8,9 @@ We use in this example a public OIDC pro
 Authentication
 --------------
 
-The first step is to obtain a valid SSO session on the portal. Several solutions:
- * Use a web browser and log into the portal, then get the value of the SSO cookie
- * Use portal REST API, and adapt the `requireToken` configuration to get cookie value in JSON response (see :doc:`REST services<restservices>`)
+The first step is to obtain a valid SSO session on the portal. The standard solution  is to use a web browser and log into the portal, then get the value of the SSO cookie.
+
+In our case, to be able to use only command lines, we will use portal REST API (which requires to adapt the `requireToken` configuration to get cookie value in JSON response (see :doc:`REST services<restservices>`). This should not be what you will on a production service.
 
 Example of REST service usage, with credentials `dwho`/`dwho`:
 
@@ -130,3 +130,68 @@ JSON response:
     "preferred_username" : "dwho",
     "sub" : "dwho"
    }
+
+Introspection
+-------------
+
+You can the validity of the access token with the introspection endpoint.
+
+Parameters needed:
+ * Client ID and Client Secret, used as basic authorization
+ * Access token, sent as POST data
+
+.. code-block:: shell
+
+   curl -u private:tardis -X POST -d 'token=a88b8dde538719e55c3cb8fbd14d06ed77853c685a62abf6ecb88d86228a9c64' 'https://oidctest.wsweet.org/oauth2/introspect' | json_pp
+
+JSON response:
+
+.. code-block:: javascript
+
+   {
+    "active" : true,
+    "client_id" : "private",
+    "exp" : 1630684115,
+    "iss" : "https://oidctest.wsweet.org/",
+    "scope" : "openid profile email",
+    "sub" : "dwho"
+   }
+
+Refresh an access token
+-----------------------
+
+If the access token has expired, you can get a new one with the refresh token.
+
+Parameters needed:
+ * Grant type: we use here `refresh_token`, sent as POST data
+ * Refresh token, sent as POST data
+ * Client ID and Client Secret, used as basic authorization
+
+.. code-block:: shell
+
+   curl -X POST -d grant_type=refresh_token -d refresh_token=19434440ed4da2803e8ba9d91cb2eabd5b8bd12af2609429bda03ed487e6ef57 -u 'private:tardis' 'https://oidctest.wsweet.org/oauth2/token' | json_pp
+
+JSON response:
+
+.. code-block:: javascript
+
+   {
+   "access_token" : "78929118546b1a11a2e3b607f607d0ccb73d72bbd95c59d0b03ae69ffa17f41a",
+   "expires_in" : 3600,
+    "id_token" : "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9pZGN0ZXN0IiwidHlwIjoiSldUIn0.eyJhdXRoX3RpbWUiOjE2MTQxNjAwMDYsImlhdCI6MTYxNDE2MzIxOCwiaXNzIjoiaHR0cHM6Ly9vaWRjdGVzdC53c3dlZXQub3JnLyIsImF0X2hhc2giOiJIVGswOVNjSjRObEFua3k5SGFFX2VRIiwiYWNyIjoibG9hLTIiLCJleHAiOjE2MTQxNjY4MTgsInN1YiI6ImR3aG8iLCJhenAiOiJwcml2YXRlIiwiYXVkIjpbInByaXZhdGUiXX0.N3TNufjKLzKM3qiIitA7JHUei4L572XjF6AcVl7UAFB6efdGUCiAL7amlUl0FgjZfzW9bzvulBVDidoYSicIaysIdI4KkjmjpVN0Z3gOSu0ecuk5p8fD1KbX6-tmA3txeR18nzfhdckq-S-6Lx7wrWpPNyrzGx-FImbOaUPN2yeVhKPXhdyHJbzI0RqJETxnBkyW-CLEzAJyq3rCUVX-D8kHADvg6a42QQyPdxvBuGrdBfyDDDb_Py13H1qhn40NnuFknR1wSahsY6U97uUooyk-0_U4J3XJAHySjCtivtSeP0fM_5eblMuh6WdVjrfnUF0xnCTbCa2gYRlTS38BkqcsWY26PXoRAOo31a1cmB5sMSZyPtRF9UZcmGiNBIymMMdFgVAJONb6uliiTS5j9-nkmHOqVC-XJ6tuiU3ZSBQ8nCRyNW2LaCzpJ5c3ytP9yYQtyT8HmhN0VnXob3K1uJEA_Xcu4sADjtrm-LbrGiwaVMkfu-C6YIrbuC9riOW6TneV2gAzAjXPOW_UZeXrCrx66GHIJPsJIq29UfbTN5Pxo9SH2yKw6PSfxevkZhBIhEXCOMaIUHrlWz2jDBBzPIWeiSRbK_MRtejQmdRUs8nqdq-McVwnFiUMDt1KZXxqScTtMDF_Lo9oK2RaCijEJ7MSPEscr_YOyp3KIq2FLVg",
+   "token_type" : "Bearer"
+   }
+
+Logout
+------
+
+To kill SSO session, call the OIDC logout endpoint. By default a confirmation is requested, but you can bypass it by adding `confirm=1` to URL.
+
+Parameters needed:
+ * SSO session id (will be passed in `lemonldap` cookie)
+
+.. code-block:: shell
+
+   curl -s -D - -o /dev/null -b lemonldap=0640f95827111f00ba7ad5863ba819fe46cfbcecdb18ce525836369fb4c8350b 'https://oidctest.wsweet.org/oauth2/logout?confirm=1'
+
+The session is deleted on server side and the cookie is destroyed in the browser. You can use the introspection endpoint to verify that the access token is no longer valid.
diff -pruN 2.0.13+ds-3/doc/sources/admin/totp2f.rst 2.0.14+ds-1/doc/sources/admin/totp2f.rst
--- 2.0.13+ds-3/doc/sources/admin/totp2f.rst	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/totp2f.rst	2022-01-22 14:30:25.000000000 +0000
@@ -43,25 +43,30 @@ In the manager (advanced parameters), yo
 -  **Activation**: set it to "on"
 -  **Self registration**: set it to "on" if users are authorized to
    generate themselves a TOTP secret
+-  **Allow users to remove TOTP**: If enabled, users can unregister
+   TOTP
+-  **Issuer name** (Optional): default to portal hostname
+-  **Interval**: interval for TOTP algorithm (default: 30)
+-  **Range of attempts**: number of additional intervals to test (default: 1)
+-  **Number of digits**: number of digit by codes (default: 6)
 -  **Authentication level**: you can overwrite here auth level for TOTP
    registered users. Leave it blank keeps auth level provided by first
    authentication module *(default: 2 for user/password based modules)*.
    **It is recommended to set an higher value here if you want to give
-   access to some apps only to users enrolled**
--  **Issuer**: default to portal hostname
--  **Interval**: interval for TOTP algorithm (default: 30)
--  **Range**: number of additional intervals to test (default: 1)
--  **Digits**: number of digit by codes (default: 6)
--  **Allow users to remove TOTP**: If enabled, users can unregister
-   TOTP.
--  **Lifetime**: Unlimited by default. Set a Time To Live in seconds.
+   access to some apps only for enrolled users**
+-  **Label** (Optional): label that should be displayed to the user on
+   the choice screen
+-  **Logo** (Optional): logo file *(in static/<skin> directory)*
+-  **Lifetime** (Optional): Unlimited by default. Set a Time To Live in seconds.
    TTL is checked at each login process if set. If TTL is expired,
    relative TOTP is removed.
+-  **Encrypt TOTP secrets**: By default, the TOTP secret key is stored in the
+   persistent session database in cleartext. Set this option to encrypt all
+   newly-generated secrets. More details :ref:`below<totp-encryption>`
 -  **Logo** (Optional): logo file *(in static/<skin> directory)*
 -  **Label** (Optional): label that should be displayed to the user on
    the choice screen
 
-
 .. attention::
 
     If you want to use a custom rule for "activation" and
@@ -94,6 +99,45 @@ module.// // To enable manager Second Fa
    [portal]
    enabledModules = conf, sessions, notifications, 2ndFA
 
+.. _totp-encryption:
+
+Encryption of TOTP secrets
+--------------------------
+
+During registration of a TOTP device, a secret key is exchanged between the
+mobile device and the server, generally through the use of a QR-Code. Once the
+exchange is done, secret keys must never leave the device or the server.
+
+Administrators may want to protect TOTP secrets by encrypting them in the
+persistent session database, in order to prevent them from leaking through
+backups or unauthorized database access.
+
+Setting the *Encrypt TOTP secrets* option will automatically encrypt newly
+generated secrets.
+
+The *Encrypt TOTP secrets* options only affects *NEW* secrets, meaning that:
+
+* A cleartext TOTP secret will be accepted even if the option is on
+* An already encrypted TOTP secret will be accepted even if the option if off
+
+The ``encryptTotpSecrets`` script can be used to encrypt previously registered TOTP
+secrets so that they can be protected as well.
+
+Encryption key
+~~~~~~~~~~~~~~
+
+By default, the key used for encryption is the global one, set in
+
+*General Parameters* » *Advanced Parameters* » *Security* » *Key*
+
+However, if you store your configuration and persistent sessions in the same
+database, this defeats the point of encryption entirely.
+
+It is recommended to set the TOTP encryption key in ``/etc/lemonldap-ng/lemonldap-ng.ini`` instead::
+
+    [all]
+    totp2fKey=changeme
+
 Developer corner
 ----------------
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/u2f.rst 2.0.14+ds-1/doc/sources/admin/u2f.rst
--- 2.0.13+ds-3/doc/sources/admin/u2f.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/u2f.rst	2022-01-22 14:30:19.000000000 +0000
@@ -43,20 +43,19 @@ In the manager (second factors), you jus
 -  **Activation**: set it to "on"
 -  **Self registration**: set it to "on" if users are authorized to
    register their keys
+-  **Allow users to remove U2F key**: If enabled, users can unregister
+   enrolled U2F device
 -  **Authentication level**: you can overwrite here auth level for U2F
    registered users. Leave it blank keeps auth level provided by first
    authentication module *(default: 2 for user/password based modules)*.
    **It is recommended to set an higher value here if you want to give
    access to some apps only for enrolled users**
--  **Allow users to remove U2F key**: If enabled, users can unregister
-   enrolled U2F device.
--  **Lifetime**: Unlimited by default. Set a Time To Live in seconds.
-   TTL is checked at each login process if set. If TTL is expired,
-   relative 2F device is removed.
--  **Logo** (Optional): logo file *(in static/<skin> directory)*
 -  **Label** (Optional): label that should be displayed to the user on
    the choice screen
-
+-  **Logo** (Optional): logo file *(in static/<skin> directory)*
+-  **Lifetime** (Optional): Unlimited by default. Set a Time To Live in seconds.
+   TTL is checked at each login process if set. If TTL is expired,
+   relative 2F device is removed.
 
 .. attention::
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/upgrade_2_0.rst 2.0.14+ds-1/doc/sources/admin/upgrade_2_0.rst
--- 2.0.13+ds-3/doc/sources/admin/upgrade_2_0.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/upgrade_2_0.rst	2022-02-07 19:06:14.000000000 +0000
@@ -170,7 +170,7 @@ Handlers
 -  :doc:`CDA<cda>`,
    :doc:`ZimbraPreAuth<applications/zimbra>`,
    :doc:`SecureToken<securetoken>` and
-   :doc:`AuthBasic<handlerauthbasic>` are now
+   :doc:`AuthBasic<authbasichandler>` are now
    :doc:`Handler Types<handlerarch>`. So there is no
    more special file to load: you just have to choose "VirtualHost type"
    in the manager/VirtualHosts.
@@ -222,7 +222,7 @@ SOAP/REST services
 
 .. attention::
 
-    \ :doc:`AuthBasic Handler<handlerauthbasic>` uses now
+    \ :doc:`AuthBasic Handler<authbasichandler>` uses now
     REST services instead of SOAP.
 
 CAS
diff -pruN 2.0.13+ds-3/doc/sources/admin/upgrade_2_0_x.rst 2.0.14+ds-1/doc/sources/admin/upgrade_2_0_x.rst
--- 2.0.13+ds-3/doc/sources/admin/upgrade_2_0_x.rst	2021-08-20 15:20:31.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/upgrade_2_0_x.rst	2022-02-19 16:04:21.000000000 +0000
@@ -26,6 +26,158 @@ Known regressions in the latest released
 
 None
 
+2.0.14
+------
+
+Security
+~~~~~~~~
+
+* **CVE-2021-40874**: RESTServer pwdConfirm always returns true with Combination + Kerberos (see `issue 2612 <https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/2612>`__)
+
+
+U2F deprecation in Chrome 98
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Chrome 98 `removed U2F support by default <https://developer.chrome.com/blog/deps-rems-95/#deprecate-u2f-api-cryptotoken>`__. You can enable them back temporarily in ``chrome://flags`` by setting *Enable the U2F Security Key API* to *Enabled* and *Enable a permission prompt for the U2F Security Key API* to *Disabled*
+
+LemonLDAP::NG provides a newer alternative: :doc:`webauthn2f`, which is compatible with U2F security keys. Please read :ref:`migrateu2ftowebauthn` for instructions on how to convert U2F secrets to WebAuthn.
+
+After migration, you will need to disable U2F from the configuration and enable WebAuthn instead, in *General Parameters* » *Second Factors* » *WebAuthn*
+
+Weak encryption used for password-protected SAML keys
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Previous versions of LemonLDAP::NG used a weak encryption algorithm to protect
+SAML keys when a password was set during certificate generation.
+
+Run the following command to check if this is your case::
+
+    lemonldap-ng-cli get samlServicePrivateKeySig
+    lemonldap-ng-cli get samlServicePrivateKeyEnc
+
+If the output of either command starts with ``BEGIN ENCRYPTED PRIVATE KEY``,
+then it probably means you generated your keys using the vulnerable manager
+code.
+
+In this case, you can convert your existing keys to a stronger encryption using
+the following command ::
+
+    # Extract your existing keys. If samlServicePrivateKeyEnc is empty, you can
+    # skip it entirely
+    lemonldap-ng-cli get samlServicePrivateKeySig | \
+        sed 's/samlServicePrivateKeySig = //' > saml-sig.pem
+    lemonldap-ng-cli get samlServicePrivateKeyEnc | \
+        sed 's/samlServicePrivateKeyEnc = //' > saml-enc.pem
+
+    # Re-encrypt the private key, using the same passphrase
+    openssl pkey -in saml-sig.pem -aes256 -out saml-sig-aes.pem
+    openssl pkey -in saml-enc.pem -aes256 -out saml-enc-aes.pem
+
+    #Or, if you are using OpenSSL 3+
+    openssl pkey -provider legacy -provider default -in saml-sig.pem \
+        -aes256 -out saml-sig-aes.pem
+    openssl pkey -provider legacy -provider default -in saml-enc.pem \
+        -aes256 -out saml-enc-aes.pem
+
+Then, simply reimport your keys ::
+
+    lemonldap-ng-cli set samlServicePrivateKeySig "$(cat saml-sig-aes.pem)"
+    lemonldap-ng-cli set samlServicePrivateKeyEnc "$(cat saml-enc-aes.pem)"
+
+If is recommended to keep the same password as before, if not, adjust the
+``samlServicePrivateKeySigPwd`` and ``samlServicePrivateKeyEncPwd`` variables as well.
+
+This operation is transparent and does not require any change to your existing
+SAML configuration or SAML applications
+
+LemonLDAP::NG version is returned by the CheckState plugin
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you use the `/checkstate` URL to monitor LemonLDAP::NG, you may notice a slight change in the output format:
+
+*2.0.13* :
+
+```
+{"result":1}
+```
+
+*2.0.14* :
+
+```
+{"result":1,"version":"2.0.14"}
+```
+
+Depending on your load balancer or monitoring configuration, this can cause false negatives.
+
+This plugin is disabled by default, and you may use a shared secret to hide this information to regular users and bots, please check the :doc:`checkstate` documentation for more information.
+
+Empty scopes now rejected in OAuth2.0 grants
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Previously, it was possible to be granted an empty scope, or an automatic
+``openid`` scope when doing :ref:`OAuth2.0 Password Grant
+<resource-owner-password-grant>` or :ref:`Client Credentials Grant
+<client-credentials-grant>`.
+
+Starting with *2.0.14*, empty scopes are no longer allowed (:rfc:`6749#section-3.3`).
+You need to either add a `scope` parameter to your request, or define a default
+scope in your Relying Party's :ref:`Scope Rules <oidcscoperules>`.
+
+
+Portal templates changes
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you defined the "Register page URL" or the password "Reset page URL" to an external application, you need to fix the ``standardform.tpl`` template by applying the following patch:
+
+.. code:: diff
+
+    diff --git a/lemonldap-ng-portal/site/templates/bootstrap/standardform.tpl b/lemonldap-ng-portal/site/templates/bootstrap/standardform.tpl
+    index 3a6256e59..d5192f0ce 100644
+    --- a/lemonldap-ng-portal/site/templates/bootstrap/standardform.tpl
+    +++ b/lemonldap-ng-portal/site/templates/bootstrap/standardform.tpl
+    @@ -48,14 +48,14 @@
+
+     <div class="actions">
+       <TMPL_IF NAME="DISPLAY_RESETPASSWORD">
+    -  <a class="btn btn-secondary" href="<TMPL_VAR NAME="MAIL_URL">?skin=<TMPL_VAR NAME="SKIN"><TMPL_IF NAME="key">&<TMPL_VAR NAME="CHOICE_PARAM">=<TMPL_VAR NAME="key"></TMPL_IF><TMPL_IF NAME="AUTH_URL">&url=<TMPL_VAR NAME="AUTH_URL"></TMPL_IF>">
+    +  <a class="btn btn-secondary" href="<TMPL_VAR NAME="MAIL_URL"><TMPL_UNLESS NAME="MAIL_URL_EXTERNAL">?skin=<TMPL_VAR NAME="SKIN"><TMPL_IF NAME="key">&<TMPL_VAR NAME="CHOICE_PARAM">=<TMPL_VAR NAME="key"></TMPL_IF><TMPL_IF NAME="AUTH_URL">&url=<TMPL_VAR NAME="AUTH_URL"></TMPL_IF></TMPL_UNLESS>">
+         <span class="fa fa-info-circle"></span>
+         <span trspan="resetPwd">Reset my password</span>
+       </a>
+       </TMPL_IF>
+
+       <TMPL_IF NAME="DISPLAY_UPDATECERTIF">
+    -     <a class="btn btn-secondary" href="<TMPL_VAR NAME="MAILCERTIF_URL">?skin=<TMPL_VAR NAME="SKIN"><TMPL_IF NAME="key">&<TMPL_VAR NAME="CHOICE_PARAM">=<TMPL_VAR NAME="key"></TMPL_IF><TMPL_IF NAME="AUTH_URL">&url=<TMPL_VAR NAME="AUTH_URL"></TMPL_IF>">
+    +     <a class="btn btn-secondary" href="<TMPL_VAR NAME="MAILCERTIF_URL"><TMPL_UNLESS NAME="MAILCERTIF_URL_EXTERNAL">?skin=<TMPL_VAR NAME="SKIN"><TMPL_IF NAME="key">&<TMPL_VAR NAME="CHOICE_PARAM">=<TMPL_VAR NAME="key"></TMPL_IF><TMPL_IF NAME="AUTH_URL">&url=<TMPL_VAR NAME="AUTH_URL"></TMPL_IF></TMPL_UNLESS>">
+             <span class="fa fa-refresh"></span>
+             <span trspan="certificateReset">Reset my certificate</span>
+          </a>
+    @@ -69,7 +69,7 @@
+       </TMPL_IF>
+
+       <TMPL_IF NAME="DISPLAY_REGISTER">
+    -    <a class="btn btn-secondary" href="<TMPL_VAR NAME="REGISTER_URL">?skin=<TMPL_VAR NAME="SKIN"><TMPL_IF NAME="key">&<TMPL_VAR NAME="CHOICE_PARAM">=<TMPL_VAR NAME="key"></TMPL_IF><TMPL_IF NAME="AUTH_URL">&url=<TMPL_VAR NAME="AUTH_URL"></TMPL_IF>">
+    +    <a class="btn btn-secondary" href="<TMPL_VAR NAME="REGISTER_URL"><TMPL_UNLESS NAME="REGISTER_URL_EXTERNAL">?skin=<TMPL_VAR NAME="SKIN"><TMPL_IF NAME="key">&<TMPL_VAR NAME="CHOICE_PARAM">=<TMPL_VAR NAME="key"></TMPL_IF><TMPL_IF NAME="AUTH_URL">&url=<TMPL_VAR NAME="AUTH_URL"></TMPL_IF></TMPL_UNLESS>">
+           <span class="fa fa-plus-circle"></span>
+           <span trspan="createAccount">Create an account</span>
+         </a>
+
+
+
+Manager API
+~~~~~~~~~~~
+
+The service parameter set in a request to create or update a CAS application must now be an array, and no more a string.
+
+Changes impacting plugin developpers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* If you are using Custom authentication modules, userDB modules or password
+  modules, ``$portal->loadedPlugins`` no longer contains a key with the name of
+  your module. You should use ``$portal->_authentication``, ``$portal->_userDB``,
+  or ``$portal->_passwordDB`` instead to get your module instance.
+
+
 2.0.13
 ------
 
@@ -365,7 +517,7 @@ Please note that it is HIGHLY recommende
 -  OAuth2.0 Handler: a VHost protected by the OAuth2.0 handler will now
    return a 401 when called without an Access Token, instead of
    redirecting to the portal, as specified by
-   `RFC6750 <https://tools.ietf.org/html/rfc6750>`__
+   :rfc:`6750#section-3`.
 
 -  If you encounter the following issue:
 
diff -pruN 2.0.13+ds-3/doc/sources/admin/utotp2f.rst 2.0.14+ds-1/doc/sources/admin/utotp2f.rst
--- 2.0.13+ds-3/doc/sources/admin/utotp2f.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/utotp2f.rst	2022-01-22 14:30:19.000000000 +0000
@@ -21,9 +21,9 @@ In the manager (second factors), you jus
    authentication module (By default: 2 for user/password based
    modules). It is recommended to set an higher value here if you want
    to give access to apps just for enrolled users.
--  **Logo** (Optional): logo file *(in static/<skin> directory)*
 -  **Label** (Optional): label that should be displayed to the user on
    the choice screen
+-  **Logo** (Optional): logo file *(in static/<skin> directory)*
 
 
 .. tip::
diff -pruN 2.0.13+ds-3/doc/sources/admin/webauthn2f.rst 2.0.14+ds-1/doc/sources/admin/webauthn2f.rst
--- 2.0.13+ds-3/doc/sources/admin/webauthn2f.rst	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/webauthn2f.rst	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,73 @@
+WebAuthn as a second factor
+===========================
+
+`Web Authentication <https://www.w3.org/TR/webauthn/>`__ , shortened as WebAuthn, is a standard method by which a web browser can authenticate to an application (*Relying Party*, in our case, this is LemonLDAP::NG) through the use of an *Authenticator*, which can be a hardware token (USB, NFC...) or provided by the user's device itself (TPM).
+
+
+.. versionadded:: 2.0.14
+   Currently, we only implement WebAuthn as a second factor. Passwordless,
+   first-factor authentication will be added in a later release.
+
+Implementation status
+~~~~~~~~~~~~~~~~~~~~~
+
+Currently, we implement:
+
+* Device registration without attestation validation (attestation type: *None*)
+* Authentication as a second factor with the registered device
+
+Requirements
+~~~~~~~~~~~~
+
+You need to install the `Authen::WebAuthn` CPAN module for WebAuthn to work on
+your LemonLDAP::NG installation. If there is no package for it in your
+distribution, you can install it with::
+
+    cpanm Authen::WebAuthn
+
+Configuration
+~~~~~~~~~~~~~
+
+- **Activation**: set it to "on"
+- **User verification**: Whether or not LemonLDAP::NG requires the user to
+  authenticate to their second factor device. Usually by entering a PIN code.
+  *Warning*: The *Required* option is not supported by older U2F security keys.
+- **Self registration**: set it to "on" if users are authorized to
+  register their keys
+- **Relying Party display name**: How the LemonLDAP::NG server will appear in
+  the web browser messages displayed to the user
+- **Allow users to remove WebAuthn**: If enabled, users can unregister their WebAuthn device.
+- **Authentication level**: you can overwrite here auth level for
+  WebAuthn registered users. Leave it blank keeps auth level provided by
+  first authentication module *(default: 2 for user/password based
+  modules)*. **It is recommended to set an higher value here if you
+  want to give access to some apps only for enrolled users**
+- **Label** (Optional): label that should be displayed to the user on
+  the choice screen
+- **Logo** (Optional): logo file *(in static/<skin> directory)*
+
+
+.. _migrateu2ftowebauthn:
+
+Migrating existing U2F devices
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+WebAuthn is compatible with both FIDO and FIDO2 standards. Which means this
+module lets you use any U2F-compatible device you already own.
+
+You can use the ``lemonldap-ng-sessions`` tool to migrate existing U2F devices to the WebAuthn plugin ::
+
+    # For one user
+    lemonldap-ng-sessions secondfactors migrateu2f dwho
+
+    # For all users
+    lemonldap-ng-sessions secondfactors migrateu2f --all
+
+Once you are satisfied with WebAuthn, you can remove existing U2F devices and
+disable the U2F second factor module ::
+
+    # For one user
+    lemonldap-ng-sessions secondfactors delType dwho U2F
+
+    # For all users
+    lemonldap-ng-sessions secondfactors delType --all U2F
diff -pruN 2.0.13+ds-3/doc/sources/admin/webserviceprotection.rst 2.0.14+ds-1/doc/sources/admin/webserviceprotection.rst
--- 2.0.13+ds-3/doc/sources/admin/webserviceprotection.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/webserviceprotection.rst	2022-02-19 16:04:21.000000000 +0000
@@ -35,8 +35,8 @@ OAuth2 endpoints.
 UserInfo
 ~~~~~~~~
 
-You can use the UserInfo endpoint, which requires the access token to
-deliver user attributes.
+You can use the UserInfo endpoint, which requires the Access Token to
+provide user attributes.
 
 For example:
 
@@ -49,19 +49,18 @@ For example:
 .. code-block:: javascript
 
    {
-     "family_name" : "OUDOT",
-     "name" : "Clément OUDOT",
-     "email" : "clement@example.com",
-     "sub" : "coudot"
+      "family_name" : "OUDOT",
+      "name" : "Clément OUDOT",
+      "email" : "clement@example.com",
+      "sub" : "coudot"
    }
 
 Introspection
 ~~~~~~~~~~~~~
 
-Introspection endpoint is defined in `RFC
-7662 <https://tools.ietf.org/html/rfc7662>`__. It requires an
-authentication (same as the authentication for the token endpoint) and
-takes to access token as parameter.
+Introspection endpoint is defined in :rfc:`7662`. It requires an authentication
+(same as the authentication for the tokens endpoint) and consumes an Access Token
+as parameter.
 
 For example:
 
Binary files 2.0.13+ds-3/doc/sources/admin/wiki/logo.png and 2.0.14+ds-1/doc/sources/admin/wiki/logo.png differ
diff -pruN 2.0.13+ds-3/doc/sources/admin/yubikey2f.rst 2.0.14+ds-1/doc/sources/admin/yubikey2f.rst
--- 2.0.13+ds-3/doc/sources/admin/yubikey2f.rst	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/admin/yubikey2f.rst	2022-01-22 14:30:19.000000000 +0000
@@ -23,27 +23,27 @@ In the manager (second factors), you jus
 -  **Activation**: set it to "on"
 -  **Self registration**: set it to "on" if users are authorized to
    register their keys
--  **Authentication level**: you can overwrite here auth level for
-   Yubikey registered users. Leave it blank keeps auth level provided by
-   first authentication module *(default: 2 for user/password based
-   modules)*. **It is recommended to set an higher value here if you
-   want to give access to some apps only to enrolled users**
--  **Client ID**: given by Yubico or another service
+-  **Allow users to remove Yubikey**: If enabled, users can unregister
+   Yubikey device.
+-  **API client ID**: given by Yubico or another service
 -  **API secret key**: given by Yubico or another service
--  **Nonce (optional)**: if any
--  **URL**: Url of service (leave blank to use Yubico cloud services)
+-  **Nonce** (optional): if any
+-  **Service URL**: service URL (leave it blank to use Yubico cloud services)
 -  **OTP public ID part size**: leave it to default (12) unless you know
    what you are doing
--  **Allow users to remove Yubikey**: If enabled, users can unregister
-   Yubikey device.
--  **Get Yubikey ID from session attribute**: If non-empty, the Yubikey ID will
+-  **Get Yubikey ID from session attribute**: if non-empty, the Yubikey ID will
    be read from this session attribute. This allows external provisionning of Yubikeys.
--  **Lifetime**: Unlimited by default. Set a Time To Live in seconds.
-   TTL is checked at each login process if set. If TTL is expired,
-   relative Yubikey is removed.
--  **Logo** (Optional): logo file *(in static/<skin> directory)*
+-  **Authentication level**: you can overwrite here auth level for
+   Yubikey registered users. Leave it blank keeps auth level provided by
+   first authentication module *(default: 2 for user/password based
+   modules)*. **It is recommended to set an higher value here if you
+   want to give access to some apps only for enrolled users**
 -  **Label** (Optional): label that should be displayed to the user on
    the choice screen
+-  **Logo** (Optional): logo file *(in static/<skin> directory)*
+-  **Lifetime** (Optional): Unlimited by default. Set a Time To Live in seconds.
+   TTL is checked at each login process if set. If TTL is expired,
+   relative Yubikey is removed.
 
 
 .. attention::
diff -pruN 2.0.13+ds-3/doc/sources/manager-api/openapi-spec.yaml 2.0.14+ds-1/doc/sources/manager-api/openapi-spec.yaml
--- 2.0.13+ds-3/doc/sources/manager-api/openapi-spec.yaml	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/doc/sources/manager-api/openapi-spec.yaml	2022-02-19 16:04:21.000000000 +0000
@@ -1397,7 +1397,7 @@ components:
       type: object
       properties:
         service:
-          type: string
+          type: array
         userAttribute:
           type: string
           default: none
@@ -1417,7 +1417,7 @@ components:
         type:
           type: string
           description: "The type of token in use"
-          example: "TOTP, U2F, UBK (Yubikey)"
+          example: "TOTP, U2F, UBK (Yubikey), WebAuthn"
         name:
           type: string
           description: "A user-set description of the token"
diff -pruN 2.0.13+ds-3/e2e-tests/lemonldap-ng.ini 2.0.14+ds-1/e2e-tests/lemonldap-ng.ini
--- 2.0.13+ds-3/e2e-tests/lemonldap-ng.ini	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/e2e-tests/lemonldap-ng.ini	2022-01-22 14:30:19.000000000 +0000
@@ -22,7 +22,7 @@ dirName=__pwd__/e2e-tests/conf
 checkXSS = 1
 portalSkin = bootstrap
 staticPrefix = /static
-languages    = fr, en, vi, it, ar, de, zh, nl, es, pt, ro, tr, zh_TW
+languages    = fr, en, vi, it, ar, de, zh, nl, es, pt, ro, tr, zh_TW, pt_BR, he
 templateDir  = __pwd__/lemonldap-ng-portal/site/templates
 portalStatus = 1
 totp2fActivation = 1
diff -pruN 2.0.13+ds-3/e2e-tests/lemonldap-ng-ldap.ini 2.0.14+ds-1/e2e-tests/lemonldap-ng-ldap.ini
--- 2.0.13+ds-3/e2e-tests/lemonldap-ng-ldap.ini	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/e2e-tests/lemonldap-ng-ldap.ini	2022-01-22 14:30:19.000000000 +0000
@@ -16,7 +16,7 @@ ldapBindPassword = admin
 checkXSS = 0
 portalSkin = bootstrap
 staticPrefix = /static
-languages    = fr, en, vi, it, ar, tr
+languages    = fr, en, vi, it, ar, de, zh, nl, es, pt, ro, tr, zh_TW, pt_BR, he
 templateDir  = __pwd__/lemonldap-ng-portal/site/templates
 portalStatus = 1
 ;totp2fActivation = 1
diff -pruN 2.0.13+ds-3/e2e-tests/lemonldap-ng-sql.ini 2.0.14+ds-1/e2e-tests/lemonldap-ng-sql.ini
--- 2.0.13+ds-3/e2e-tests/lemonldap-ng-sql.ini	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/e2e-tests/lemonldap-ng-sql.ini	2022-01-22 14:30:19.000000000 +0000
@@ -13,7 +13,7 @@ dbiChain = dbi:SQLite:dbname=__pwd__/e2e
 checkXSS = 0
 portalSkin = bootstrap
 staticPrefix = /static
-languages    = fr, en, vi, it, ar, tr
+languages    = fr, en, vi, it, ar, de, zh, nl, es, pt, ro, tr, zh_TW, pt_BR, he
 templateDir  = __pwd__/lemonldap-ng-portal/site/templates
 portalStatus = 1
 ;totp2fActivation = 1
diff -pruN 2.0.13+ds-3/e2e-tests/lmConf-1.json 2.0.14+ds-1/e2e-tests/lmConf-1.json
--- 2.0.13+ds-3/e2e-tests/lmConf-1.json	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/e2e-tests/lmConf-1.json	2022-01-22 14:30:19.000000000 +0000
@@ -143,6 +143,7 @@
   "locationRules": {
     "auth.example.com" : {
       "(?#checkUser)^/checkuser": "$uid eq \"dwho\"",
+      "(?#checkDevOps)^/checkdevops": "$uid eq \"dwho\"",
       "(?#errors)^/lmerror/": "accept",
       "default" : "accept"
     },
diff -pruN 2.0.13+ds-3/fastcgi-server/default/llng-fastcgi-server 2.0.14+ds-1/fastcgi-server/default/llng-fastcgi-server
--- 2.0.13+ds-3/fastcgi-server/default/llng-fastcgi-server	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/fastcgi-server/default/llng-fastcgi-server	2022-01-22 14:30:19.000000000 +0000
@@ -1,7 +1,7 @@
 # Number of process (default: 7)
 #NPROC = 7
 
-# Unix socket to listen to
+# Socket to listen to (UDS or TCP)
 SOCKET=__FASTCGISOCKDIR__/llng-fastcgi.sock
 
 # Pid file
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lemonldap-ng.ini 2.0.14+ds-1/lemonldap-ng-common/lemonldap-ng.ini
--- 2.0.13+ds-3/lemonldap-ng-common/lemonldap-ng.ini	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lemonldap-ng.ini	2022-02-19 16:04:21.000000000 +0000
@@ -80,12 +80,12 @@ logLevel     = warn
 ;logger             = Lemonldap::NG::Common::Logger::Log4perl
 ;log4perlConfFile   = /etc/log4perl.conf
 ;log4perlLogger     = LLNG
-;log4perlUserLogger = LLNG.user
+;log4perlUserLogger = LLNGuser
 ;
 ;   Here, Log4perl configuration is read from /etc/log4perl.conf. The "LLNG"
 ;   value points to the logger class. Example:
 ;     log4perl.logger.LLNG      = WARN, File1
-;     log4perl.logger.LLNG.user = INFO, File2
+;     log4perl.logger.LLNGuser  = INFO, File2
 ;     ...
 
 ; CONFIGURATION CHECK
@@ -196,7 +196,7 @@ staticPrefix = __PORTALSTATICDIR__
 templateDir  = __PORTALTEMPLATESDIR__
 
 ; languages: available languages for portal interface
-languages    = en, fr, vi, it, ar, de, fi, tr, pl, zh_TW, es
+languages    = en, fr, vi, it, ar, de, fi, tr, pl, zh_TW, es, pt_BR, he
 
 ; II - Optional parameters (overwrite configuration)
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Cli.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Cli.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Cli.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Cli.pm	2022-02-19 16:04:21.000000000 +0000
@@ -63,10 +63,11 @@ sub testEmail {
     eval {
         Lemonldap::NG::Common::EmailTransport::sendTestMail( $conf, $dest );
     };
-    my $error   = $@;
+    my $error = $@;
     if ($error) {
         die $error;
-    } else {
+    }
+    else {
         print STDERR "Test email successfully sent to $dest\n";
     }
 }
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/CliSessions.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/CliSessions.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/CliSessions.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/CliSessions.pm	2022-02-19 16:04:21.000000000 +0000
@@ -84,7 +84,6 @@ sub _search {
     }
 
     return $res;
-
 }
 
 sub search {
@@ -264,12 +263,86 @@ sub _del_psession_special {
     if ($deleted) {
         $data->{$specialKeyName} = to_json( [@new] );
     }
+
+    # TODO should this be in the if???
     $psession->update($data);
 }
 
+sub _migrateu2f_device {
+    my ( $self, $device ) = @_;
+
+    my $credential_id = $device->{_keyHandle};
+    my $_userKey      = $device->{_userKey};
+
+    eval { require Authen::WebAuthn };
+    if ($@) {
+        die "Missing Authen::WebAuthn dependency: $@";
+    }
+
+    my $credential_pubkey =
+      Authen::WebAuthn::convert_raw_ecc_to_cose($_userKey);
+
+    return {
+        type                 => "WebAuthn",
+        name                 => "$device->{name}",
+        _credentialId        => "$credential_id",
+        _credentialPublicKey => "$credential_pubkey",
+        _signCount           => 0,
+        epoch                => "$device->{epoch}",
+    };
+}
+
+sub _migrateu2f {
+    my $self   = shift;
+    my $target = shift;
+
+    my $psession = $self->_get_psession($target);
+    my $data     = $psession->data;
+    my $migrated = 0;
+
+    my $_2fDevices = $data->{_2fDevices} || "[]";
+    $_2fDevices = from_json($_2fDevices);
+
+    die "Expecting JSON array in _2fDevices"
+      unless ref($_2fDevices) eq "ARRAY";
+
+    my @new_2fDevices = @{$_2fDevices};
+    my @u2f_devices   = grep { $_->{type} eq "U2F" } @{$_2fDevices};
+
+    my %migrated_devices;
+    for my $u2f_device (@u2f_devices) {
+        my $migrated_device = $self->_migrateu2f_device($u2f_device);
+        $migrated_devices{ $migrated_device->{_credentialId} } =
+          $migrated_device;
+    }
+
+    for my $migrated_device ( keys %migrated_devices ) {
+
+        # If credentialId is not already present
+        unless (
+            grep {
+                      $_->{type} eq "WebAuthn"
+                  and $_->{_credentialId} eq $migrated_device
+            } @new_2fDevices
+          )
+        {
+            push @new_2fDevices, $migrated_devices{$migrated_device};
+            $migrated = 1;
+        }
+    }
+
+    if ($migrated) {
+        $data->{_2fDevices} = to_json( [@new_2fDevices] );
+        $psession->update($data);
+    }
+
+}
+
 sub consents_get {
-    my $self     = shift;
-    my $target   = shift;
+    my $self   = shift;
+    my $target = shift;
+    return 0 unless $target;
+
     my $o        = $self->stdout;
     my $consents = $self->_get_psession_special( $target, '_oidcConsents',
         sub { $_[0]->{rp} } );
@@ -278,8 +351,10 @@ sub consents_get {
 }
 
 sub secondfactors_get {
-    my $self     = shift;
-    my $target   = shift;
+    my $self   = shift;
+    my $target = shift;
+    return 0 unless $target;
+
     my $o        = $self->stdout;
     my $consents = $self->_get_psession_special( $target, '_2fDevices',
         sub { genId2F( $_[0] ) } );
@@ -290,8 +365,11 @@ sub secondfactors_get {
 sub consents_delete {
     my $self   = shift;
     my $target = shift;
-    my @ids    = @_;
-    return unless @ids;
+    return 0 unless $target;
+
+    my @ids = @_;
+    return 0 unless @ids;
+
     $self->_del_psession_special( $target, '_oidcConsents',
         sub { $_[0]->{rp} }, @ids );
     return 0;
@@ -300,23 +378,66 @@ sub consents_delete {
 sub secondfactors_delete {
     my $self   = shift;
     my $target = shift;
-    my @ids    = @_;
-    return unless @ids;
+    return 0 unless $target;
+
+    my @ids = @_;
+    return 0 unless @ids;
     $self->_del_psession_special( $target, '_2fDevices',
         sub { genId2F( $_[0] ) }, @ids );
     return 0;
 }
 
+sub _get_psession_targets {
+    my ( $self, @args ) = @_;
+
+    if ( $self->opts->{where} or $self->opts->{all} ) {
+        $self->opts->{persistent} = 1;
+
+        if ( $self->opts->{all} ) {
+            delete $self->opts->{where};
+        }
+
+        my $res = $self->_search();
+        return ( map { $res->{$_}->{_session_uid} } keys %{$res} );
+    }
+    else {
+        return @args;
+    }
+}
+
 sub secondfactors_delType {
-    my $self   = shift;
-    my $target = shift;
-    my @types  = @_;
-    return unless @types;
-    $self->_del_psession_special( $target, '_2fDevices', sub { $_[0]->{type} },
-        @types );
+    my $self = shift;
+
+    my $target;
+    unless ( $self->opts->{where} or $self->opts->{all} ) {
+        $target = shift;
+    }
+
+    my @types = @_;
+    return 0 unless @types;
+
+    my @targets = $self->_get_psession_targets($target);
+    for my $target (@targets) {
+        $self->_del_psession_special( $target, '_2fDevices',
+            sub { $_[0]->{type} }, @types );
+    }
+
     return 0;
 }
 
+sub secondfactors_migrateu2f {
+    my ( $self, @ids ) = @_;
+    my $result   = 0;
+    my @sessions = $self->_get_psession_targets(@ids);
+
+    for my $id (@sessions) {
+        if ( !$self->_migrateu2f($id) ) {
+            $result = 1;
+        }
+    }
+    return $result;
+}
+
 sub setKey {
     my $self = shift;
     my $id   = shift;
@@ -380,8 +501,8 @@ sub run {
     # Subcommands and target
     elsif ( $action =~ /^(?:secondfactors|consents)$/ ) {
         my $subcommand = shift;
-        unless ( $subcommand and @_ ) {
-            die "Missing subcommand and target for $action";
+        unless ($subcommand) {
+            die "Missing subcommand $action";
         }
         my $func = "${action}_${subcommand}";
         if ( $self->can($func) ) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Combination/Parser.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Combination/Parser.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Combination/Parser.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Combination/Parser.pm	2022-02-19 16:04:21.000000000 +0000
@@ -5,7 +5,7 @@ use Mouse;
 use Safe;
 use constant PE_OK => 0;
 
-our $VERSION = '2.0.6';
+our $VERSION = '2.0.14';
 
 # Handle "if then else" (used during init)
 # return a sub that can be called with ($req) to get a [array] of combination
@@ -88,14 +88,16 @@ sub parseAnd {
                     my ( $r, $name ) = $obj->(@_);
 
                     # Case "string" (form type)
-                    if ( $r & ~$r ) {
+                    if ( $r && $r & ~$r ) {
                         $str{$r}++;
                     }
                     else {
-                        return ( $r, $name ) unless ( $r == PE_OK );
+                        return ( wantarray ? ( $r, $name ) : $r )
+                          unless ( !$r || $r == PE_OK );
                     }
                 }
-                return ( ( %str ? join( ',', keys %str ) : PE_OK ), $expr );
+                my $res = %str ? join( ',', keys %str ) : PE_OK;
+                return wantarray ? ( $res, $expr ) : $res;
             };
         }
         return \@res;
@@ -135,7 +137,8 @@ sub parseMod {
         my ($m) = @mods;
         return sub {
             my $sub = shift;
-            return ( $m->$sub(@_), $expr );
+            my $res = $m->$sub(@_);
+            return wantarray ? ( $res, $expr ) : $res;
         };
     }
     return sub {
@@ -149,10 +152,12 @@ sub parseMod {
                 $str{$res}++;
             }
             else {
-                return ( $res, $list[$i] ) unless ( $res == PE_OK );
+                return ( wantarray ? ( $res, $list[$i] ) : $res )
+                  unless ( $res == PE_OK );
             }
         }
-        return ( ( %str ? join( ',', keys %str ) : PE_OK ), $expr );
+        my $res = %str ? join( ',', keys %str ) : PE_OK;
+        return wantarray ? ( $res, $expr ) : $res;
     };
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/AccessLib.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/AccessLib.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/AccessLib.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/AccessLib.pm	2022-02-19 16:04:21.000000000 +0000
@@ -7,9 +7,9 @@ use Mouse;
 use Lemonldap::NG::Common::Conf;
 
 has '_confAcc'      => ( is => 'rw', isa => 'Lemonldap::NG::Common::Conf' );
-has 'configStorage' => ( is => 'rw', isa => 'HashRef', default => sub { {} } );
-has 'currentConf' => ( is => 'rw', required => 1, default => sub { {} } );
-has 'protection'  => ( is => 'rw', isa      => 'Str', default => 'manager' );
+has 'configStorage' => ( is => 'rw', isa => 'HashRef',  default => sub { {} } );
+has 'currentConf'   => ( is => 'rw', required => 1,     default => sub { {} } );
+has 'protection'    => ( is => 'rw', isa      => 'Str', default => 'manager' );
 
 our $VERSION = '2.0.11';
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/_DBI.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/_DBI.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/_DBI.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/_DBI.pm	2022-02-19 16:04:21.000000000 +0000
@@ -36,7 +36,8 @@ sub available {
     my $sth =
       $self->_dbh->prepare( "SELECT DISTINCT cfgNum from "
           . $self->{dbiTable}
-          . " order by cfgNum" ) or $self->logError;
+          . " order by cfgNum" )
+      or $self->logError;
     $sth->execute() or $self->logError;
     my @conf;
     while ( my @row = $sth->fetchrow_array ) {
@@ -105,8 +106,8 @@ sub unlock {
 sub delete {
     my ( $self, $cfgNum ) = @_;
     my $req =
-      $self->_dbh->prepare("DELETE FROM $self->{dbiTable} WHERE cfgNum=?")
-        or $self->logError;
+         $self->_dbh->prepare("DELETE FROM $self->{dbiTable} WHERE cfgNum=?")
+      or $self->logError;
     my $res = $req->execute($cfgNum) or $self->logError;
     $Lemonldap::NG::Common::Conf::msg .=
       "Unable to find conf $cfgNum (" . $self->_dbh->errstr . ")"
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/LDAP.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/LDAP.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/LDAP.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/LDAP.pm	2022-02-19 16:04:21.000000000 +0000
@@ -194,8 +194,8 @@ sub store {
         $operation = $self->ldap->add(
             $confDN,
             attrs => [
-                objectClass => [ 'top', $self->{ldapObjectClass} ],
-                $self->{ldapAttributeId}      => $confName,
+                objectClass              => [ 'top', $self->{ldapObjectClass} ],
+                $self->{ldapAttributeId} => $confName,
                 $self->{ldapAttributeContent} => \@confValues,
             ]
         );
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/Local.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/Local.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/Local.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/Local.pm	2022-02-19 16:04:21.000000000 +0000
@@ -3,7 +3,7 @@ package Lemonldap::NG::Common::Conf::Bac
 use strict;
 use Lemonldap::NG::Common::Conf::Constants;
 
-our $VERSION = '2.0.0';
+our $VERSION = '2.0.14';
 
 sub prereq {
     return 1;
@@ -26,21 +26,22 @@ sub unlock {
 }
 
 sub store {
-    $Lemonldap::NG::Common::Conf::msg = 'Read-only backend !';
+    $Lemonldap::NG::Common::Conf::msg = 'Read-only backend!';
     return DATABASE_LOCKED;
 }
 
 sub load {
     return {
         cfgNum    => 1,
+        cfgDate   => time,
         cfgAuthor => 'LLNG Team',
-        cfgLog =>
-q"Don't edit this configuration, Null backend uses only lemonldap-ng.ini values",
+        cfgLog    =>
+q"Do not edit this configuration, Null backend uses lemonldap-ng.ini values only",
     };
 }
 
 sub delete {
-    $Lemonldap::NG::Common::Conf::msg = 'Read-only backend !';
+    $Lemonldap::NG::Common::Conf::msg = 'Read-only backend!';
     return 0;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/RDBI.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/RDBI.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/RDBI.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Backends/RDBI.pm	2022-02-19 16:04:21.000000000 +0000
@@ -19,7 +19,7 @@ sub store {
     $req = $self->_dbh->prepare(
         "INSERT INTO $self->{dbiTable} (cfgNum,field,value) VALUES (?,?,?)");
 
-    _delete($self,$cfgNum) if $lastCfg == $cfgNum;
+    _delete( $self, $cfgNum ) if $lastCfg == $cfgNum;
     unless ($req) {
         $self->logError;
         return UNKNOWN_ERROR;
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/RESTServer.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/RESTServer.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/RESTServer.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/RESTServer.pm	2022-02-19 16:04:21.000000000 +0000
@@ -394,16 +394,17 @@ sub _oidcMetaDataNodes {
     my ( $id, $resp ) = ( 1, [] );
 
     # Handle RP Attributes
-    if ($query eq "oidcRPMetaDataExportedVars") {
+    if ( $query eq "oidcRPMetaDataExportedVars" ) {
         my $pk = eval { $self->getConfKey( $req, $query )->{$partner} } // {};
         return $self->sendError( $req, undef, 400 ) if ( $req->error );
         foreach my $h ( sort keys %$pk ) {
+
             # Set default values for type and array
             my $data = [ split /;/, $pk->{$h} ];
-            unless ( $data->[1]) {
+            unless ( $data->[1] ) {
                 $data->[1] = "string";
             }
-            unless ( $data->[2]) {
+            unless ( $data->[2] ) {
                 $data->[2] = "auto";
             }
             push @$resp,
@@ -416,6 +417,7 @@ sub _oidcMetaDataNodes {
         }
         return $self->sendJSONresponse( $req, $resp );
     }
+
     # Return all exported attributes if asked
     elsif ( $query =~
 /^(?:oidc${type}MetaDataExportedVars|oidcRPMetaDataOptionsExtraClaims|oidcRPMetaDataMacros|oidcRPMetaDataScopeRules)$/
@@ -733,9 +735,9 @@ sub combModules {
     my $res = [];
     foreach my $mod ( keys %$val ) {
         my $tmp;
-        $tmp->{title} = $mod;
-        $tmp->{id}    = "combModules/$mod";
-        $tmp->{type}  = 'cmbModule';
+        $tmp->{title}      = $mod;
+        $tmp->{id}         = "combModules/$mod";
+        $tmp->{type}       = 'cmbModule';
         $tmp->{data}->{$_} = $val->{$mod}->{$_} foreach (qw(type for));
         my $over = $val->{$mod}->{over} // {};
         $tmp->{data}->{over} = [ map { [ $_, $over->{$_} ] } keys %$over ];
@@ -809,8 +811,8 @@ sub metadata {
         }
 
         # Find next and previous conf
-        my @a  = $self->confAcc->available;
-        my $id = -1;
+        my @a     = $self->confAcc->available;
+        my $id    = -1;
         my ($ind) = map { $id++; $_ == $res->{cfgNum} ? ($id) : () } @a;
         if ($ind) { $res->{prev} = $a[ $ind - 1 ]; }
         if ( defined $ind and $ind < $#a ) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/SAML/Metadata.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/SAML/Metadata.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/SAML/Metadata.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/SAML/Metadata.pm	2022-02-19 16:04:21.000000000 +0000
@@ -14,7 +14,7 @@ use MIME::Base64;
 use Safe;
 use Encode;
 
-our $VERSION = '2.0.9';
+our $VERSION = '2.0.14';
 
 my $dataStart = tell(DATA);
 
@@ -26,7 +26,7 @@ sub serviceToXML {
     my ( $self, $conf, $type ) = @_;
 
     seek DATA, $dataStart, 0;
-    my $s        = join '', <DATA>;
+    my $s = join '', <DATA>;
     my $template = HTML::Template->new(
         scalarref         => \$s,
         die_on_bad_params => 0,
@@ -164,12 +164,22 @@ sub serviceToXML {
       samlIDPSSODescriptorArtifactResolutionServiceArtifact
     );
 
+    my %indexed_endpoints;
     foreach (@param_assertion) {
         my @_tab = split( /;/, $self->getValue( $_, $conf ) );
-        $template->param( $_ . 'Default', $_tab[0] ? 'true' : 'false' );
-        $template->param( $_ . 'Index',   $_tab[1] );
-        $template->param( $_ . 'Binding', $_tab[2] );
-        $template->param( $_ . 'Location', $_tab[3] );
+        $indexed_endpoints{ $_ . 'Default' }  = ( $_tab[0] ? 'true' : 'false' );
+        $indexed_endpoints{ $_ . 'Index' }    = $_tab[1];
+        $indexed_endpoints{ $_ . 'Binding' }  = $_tab[2];
+        $indexed_endpoints{ $_ . 'Location' } = $_tab[3];
+    }
+    $template->param(%indexed_endpoints);
+
+    if (
+        $indexed_endpoints{samlSPSSODescriptorAssertionConsumerServiceHTTPArtifactDefault}
+        eq 'true'
+      )
+    {
+        $template->param( "ACSArtifactDefault" => 1 );
     }
 
     # Return the XML metadata.
@@ -310,6 +320,7 @@ __DATA__
     <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos</NameIDFormat>
     <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:entity</NameIDFormat>
     <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+    <TMPL_IF ACSArtifactDefault>
     <AssertionConsumerService
       isDefault="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPArtifactDefault">"
       index="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPArtifactIndex">"
@@ -320,6 +331,18 @@ __DATA__
       index="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPPostIndex">"
       Binding="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPPostBinding">"
       Location="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPPostLocation">" />
+    <TMPL_ELSE>
+    <AssertionConsumerService
+      isDefault="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPPostDefault">"
+      index="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPPostIndex">"
+      Binding="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPPostBinding">"
+      Location="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPPostLocation">" />
+    <AssertionConsumerService
+      isDefault="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPArtifactDefault">"
+      index="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPArtifactIndex">"
+      Binding="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPArtifactBinding">"
+      Location="<TMPL_VAR NAME="samlSPSSODescriptorAssertionConsumerServiceHTTPArtifactLocation">" />
+    </TMPL_IF>
   </SPSSODescriptor>
   </TMPL_UNLESS>
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf.pm	2021-08-20 09:36:39.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf.pm	2022-02-19 16:04:21.000000000 +0000
@@ -27,7 +27,7 @@ use Config::IniFiles;
 #inherits Lemonldap::NG::Common::Conf::Backends::SOAP
 #inherits Lemonldap::NG::Common::Conf::Backends::LDAP
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 our $msg     = '';
 our $iniObj;
 
@@ -107,6 +107,7 @@ sub new {
               $self->{localStorage}->new( $self->{localStorageOptions} );
         }
     }
+
     return $self;
 }
 
@@ -189,6 +190,7 @@ sub getConf {
             eval { $r = $self->{refLocalStorage}->get('conf') }
               if ( $> and not $args->{noCache} );
             $msg .= "Warn: $@" if ($@);
+
             if (    ref($r)
                 and $r->{cfgNum}
                 and $args->{cfgNum}
@@ -240,7 +242,11 @@ sub getConf {
     return $res;
 }
 
-# Set default values
+## @method hashRef setDefault(hashRef conf, hashRef localPrm)
+# Set default params
+# @param $conf Lemonldap::NG configuration hashRef
+# @param $localPrm Local parameters
+# @return conf
 sub setDefault {
     my ( $self, $conf, $localPrm ) = @_;
     if ( defined $localPrm ) {
@@ -414,7 +420,7 @@ sub _launch {
         alarm 0;
         die $@ if $@;
     };
-    if($@) {
+    if ($@) {
         $msg .= $@;
         print STDERR "MSG $msg\n";
         return undef;
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/EmailTransport.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/EmailTransport.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/EmailTransport.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/EmailTransport.pm	2022-02-19 16:04:21.000000000 +0000
@@ -95,7 +95,7 @@ sub configTest {
 }
 
 sub sendTestMail {
-    my ($conf, $dest) = @_;
+    my ( $conf, $dest ) = @_;
     my $transport = Lemonldap::NG::Common::EmailTransport->new($conf);
     my $message   = MIME::Entity->build(
         From    => $conf->{mailFrom},
@@ -110,7 +110,7 @@ sub sendTestMail {
     # Send the mail
     eval { sendmail( $message->stringify, { transport => $transport } ); };
     if ($@) {
-        my $error   = ( $@->isa('Throwable::Error') ? $@->message : $@ );
+        my $error = ( $@->isa('Throwable::Error') ? $@->message : $@ );
         die $error;
     }
 }
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/IPv6.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/IPv6.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/IPv6.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/IPv6.pm	2022-02-19 16:04:21.000000000 +0000
@@ -4,7 +4,7 @@ use strict;
 use base 'Exporter';
 
 our $VERSION = '2.0.10';
-our @EXPORT = qw(&isIPv6 &net6 &expand6);
+our @EXPORT  = qw(&isIPv6 &net6 &expand6);
 
 sub isIPv6 {
     my ($ip) = @_;
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Dispatch.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Dispatch.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Dispatch.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Dispatch.pm	2022-02-19 16:04:21.000000000 +0000
@@ -2,7 +2,7 @@ package Lemonldap::NG::Common::Logger::D
 
 use strict;
 
-our $VERSION = '2.0.0';
+our $VERSION = '2.0.14';
 
 sub new {
     no warnings 'redefine';
@@ -35,7 +35,7 @@ sub new {
         $show = 0 if ( $conf->{logLevel} eq $l );
 
     }
-    die "unknown level $conf->{logLevel}" if ($show);
+    die "Unknown logLevel $conf->{logLevel}" if ($show);
     return $self;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Log4perl.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Log4perl.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Log4perl.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Log4perl.pm	2022-02-19 16:04:21.000000000 +0000
@@ -2,6 +2,7 @@ package Lemonldap::NG::Common::Logger::L
 
 use strict;
 use Log::Log4perl;
+use Log::Log4perl::MDC;
 
 our $VERSION = '2.0.0';
 
@@ -10,8 +11,41 @@ our $init = 0;
 sub new {
     my ( $class, $conf, %args ) = @_;
     my $self = bless {}, $class;
+
     unless ($init) {
         my $file = $conf->{log4perlConfFile} || '/etc/log4perl.conf';
+
+        # Fix reporting of code location
+        Log::Log4perl->wrapper_register(
+            "Lemonldap::NG::Common::Logger::_Duplicate");
+        Log::Log4perl->wrapper_register(__PACKAGE__);
+
+        # map %E to the stored $req->env
+        Log::Log4perl::Layout::PatternLayout::add_global_cspec(
+            'E',
+            sub {
+                my $layout = shift;
+                my $subvar = $layout->{curlies};
+                my $req    = Log::Log4perl::MDC->get("req");
+                return defined($req) ? $req->env->{$subvar} : undef;
+            }
+        );
+
+        # map %Q to the stored $req
+        Log::Log4perl::Layout::PatternLayout::add_global_cspec(
+            'Q',
+            sub {
+                my $layout = shift;
+                my $subvar = $layout->{curlies};
+                my $req    = Log::Log4perl::MDC->get("req");
+                if ( ref($req) and $req->can($subvar) ) {
+                    return $req->$subvar;
+                }
+                else {
+                    return undef;
+                }
+            }
+        );
         Log::Log4perl->init($file);
         $init++;
     }
@@ -23,6 +57,16 @@ sub new {
     return $self;
 }
 
+sub setRequestObj {
+    my ( $self, $req ) = @_;
+    Log::Log4perl::MDC->put( "req", $req );
+}
+
+sub clearRequestObj {
+    my ( $self, $req ) = @_;
+    my $text = Log::Log4perl::MDC->remove();
+}
+
 sub AUTOLOAD {
     my $self = shift;
     no strict;
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Sentry.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Sentry.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Sentry.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Sentry.pm	2022-02-19 16:04:21.000000000 +0000
@@ -10,12 +10,12 @@ package Lemonldap::NG::Common::Logger::S
 use strict;
 use Sentry::Raven;
 
-our $VERSION = '2.0.0';
+our $VERSION = '2.0.14';
 
 sub new {
-    my $self = bless {}, shift;
+    my $self   = bless {}, shift;
     my ($conf) = @_;
-    my $show = 1;
+    my $show   = 1;
     $self->{raven} = Sentry::Raven->new( sentry_dsn => $conf->{sentryDsn} );
     foreach (qw(error warn notice info debug)) {
         my $rl = $_;
@@ -31,7 +31,7 @@ qq'sub $_ {\$_[0]->{raven}->capture_mess
         }
         $show = 0 if ( $conf->{logLevel} eq $_ );
     }
-    die "unknown level $conf->{logLevel}" if ($show);
+    die "Unknown logLevel $conf->{logLevel}" if ($show);
     return $self;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Std.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Std.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Std.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Std.pm	2022-02-19 16:04:21.000000000 +0000
@@ -2,7 +2,7 @@ package Lemonldap::NG::Common::Logger::S
 
 use strict;
 
-our $VERSION = '2.0.5';
+our $VERSION = '2.0.14';
 
 sub new {
     no warnings 'redefine';
@@ -18,7 +18,7 @@ qq'sub $_ {print STDERR "[".localtime."]
         }
         $show = 0 if ( $level eq $_ );
     }
-    die "unknown level $level" if ($show);
+    die "Unknown logLevel $level" if ($show);
     return bless {}, shift;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Syslog.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Syslog.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Syslog.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Logger/Syslog.pm	2022-02-19 16:04:21.000000000 +0000
@@ -3,7 +3,7 @@ package Lemonldap::NG::Common::Logger::S
 use strict;
 use Sys::Syslog qw(:standard);
 
-our $VERSION = '2.0.9';
+our $VERSION = '2.0.14';
 
 sub new {
     my ( $class, $conf, %args ) = @_;
@@ -34,7 +34,7 @@ sub new {
         }
         $show = 0 if ( $level eq $_ );
     }
-    die "unknown level $level" if ($show);
+    die "Unknown logLevel $level" if ($show);
     return $self;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Cli/Lib.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Cli/Lib.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Cli/Lib.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Cli/Lib.pm	2022-02-19 16:04:21.000000000 +0000
@@ -27,7 +27,7 @@ sub _get {
             'REQUEST_URI'          => $path . ( $query ? "?$query" : '' ),
             'SERVER_PORT'          => '8002',
             'SERVER_PROTOCOL'      => 'HTTP/1.1',
-            'HTTP_USER_AGENT' =>
+            'HTTP_USER_AGENT'      =>
               'Mozilla/5.0 (VAX-4000; rv:36.0) Gecko/20350101 Firefox',
             'REMOTE_ADDR' => '127.0.0.1',
             'HTTP_HOST'   => '127.0.0.1:8002'
@@ -52,7 +52,7 @@ sub _post {
             'REQUEST_URI'          => $path . ( $query ? "?$query" : '' ),
             'SERVER_PORT'          => '8002',
             'SERVER_PROTOCOL'      => 'HTTP/1.1',
-            'HTTP_USER_AGENT' =>
+            'HTTP_USER_AGENT'      =>
               'Mozilla/5.0 (VAX-4000; rv:36.0) Gecko/20350101 Firefox',
             'REMOTE_ADDR'          => '127.0.0.1',
             'HTTP_HOST'            => '127.0.0.1:8002',
@@ -81,7 +81,7 @@ sub _put {
             'REQUEST_URI'          => $path . ( $query ? "?$query" : '' ),
             'SERVER_PORT'          => '8002',
             'SERVER_PROTOCOL'      => 'HTTP/1.1',
-            'HTTP_USER_AGENT' =>
+            'HTTP_USER_AGENT'      =>
               'Mozilla/5.0 (VAX-4000; rv:36.0) Gecko/20350101 Firefox',
             'REMOTE_ADDR'          => '127.0.0.1',
             'HTTP_HOST'            => '127.0.0.1:8002',
@@ -110,7 +110,7 @@ sub _patch {
             'REQUEST_URI'          => $path . ( $query ? "?$query" : '' ),
             'SERVER_PORT'          => '8002',
             'SERVER_PROTOCOL'      => 'HTTP/1.1',
-            'HTTP_USER_AGENT' =>
+            'HTTP_USER_AGENT'      =>
               'Mozilla/5.0 (VAX-4000; rv:36.0) Gecko/20350101 Firefox',
             'REMOTE_ADDR'          => '127.0.0.1',
             'HTTP_HOST'            => '127.0.0.1:8002',
@@ -137,7 +137,7 @@ sub _del {
             'REQUEST_URI'          => $path . ( $query ? "?$query" : '' ),
             'SERVER_PORT'          => '8002',
             'SERVER_PROTOCOL'      => 'HTTP/1.1',
-            'HTTP_USER_AGENT' =>
+            'HTTP_USER_AGENT'      =>
               'Mozilla/5.0 (VAX-4000; rv:36.0) Gecko/20350101 Firefox',
             'REMOTE_ADDR' => '127.0.0.1',
             'HTTP_HOST'   => '127.0.0.1:8002',
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Request.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Request.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Request.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Request.pm	2022-02-19 16:04:21.000000000 +0000
@@ -48,8 +48,7 @@ sub userData {
     return $self->{userData}
       || {
         ( $Lemonldap::NG::Handler::Main::tsv->{whatToTrace}
-              || '_whatToTrace' ) => $self->{user},
-      };
+              || '_whatToTrace' ) => $self->{user}, };
 }
 
 sub respHeaders {
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Router.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Router.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Router.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Router.pm	2022-02-19 16:04:21.000000000 +0000
@@ -11,8 +11,8 @@ extends 'Lemonldap::NG::Common::PSGI';
 
 # Properties
 has 'routes' => (
-    is  => 'rw',
-    isa => 'HashRef',
+    is      => 'rw',
+    isa     => 'HashRef',
     default =>
       sub { { GET => {}, POST => {}, PUT => {}, PATCH => {}, DELETE => {} } }
 );
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI.pm	2022-02-19 16:04:21.000000000 +0000
@@ -21,8 +21,8 @@ has instanceName => ( is => 'rw', isa
 has templateDir  => ( is => 'rw', isa     => 'Str|ArrayRef' );
 has links        => ( is => 'rw', isa     => 'ArrayRef' );
 has menuLinks    => ( is => 'rw', isa     => 'ArrayRef' );
-has logger     => ( is => 'rw' );
-has userLogger => ( is => 'rw' );
+has logger       => ( is => 'rw' );
+has userLogger   => ( is => 'rw' );
 
 # INITIALIZATION
 
@@ -43,7 +43,17 @@ sub init {
         unless ( ref $self->logger ) {
             eval "require $logger";
             die $@ if ($@);
+            my $err;
+            unless ( $self->{logLevel} =~ /^(?:debug|info|notice|warn|error)$/ )
+            {
+                $err =
+                    'Bad logLevel value \''
+                  . $self->{logLevel}
+                  . "', switching to 'info'";
+                $self->{logLevel} = 'info';
+            }
             $self->logger( $logger->new($self) );
+            $self->logger->error($err) if $err;
         }
         unless ( ref $self->userLogger ) {
             $logger = $ENV{LLNG_USERLOGGER} || $args->{userLogger} || $logger;
@@ -334,10 +344,39 @@ sub run {
 sub _run {
     my $self = shift;
     return sub {
-        $self->handler( Lemonldap::NG::Common::PSGI::Request->new( $_[0] ) );
+        $self->_logAndHandle(
+            Lemonldap::NG::Common::PSGI::Request->new( $_[0] ) );
     };
 }
 
+sub _logAndHandle {
+    my ( $self, $req ) = @_;
+
+    # register the request object to the logging system
+    if ( ref( $self->logger ) and $self->logger->can('setRequestObj') ) {
+        $self->logger->setRequestObj($req);
+    }
+    if ( ref( $self->userLogger ) and $self->userLogger->can('setRequestObj') )
+    {
+        $self->userLogger->setRequestObj($req);
+    }
+
+    # Call the handler
+    my $res = $self->handler($req);
+
+    # Clear the logging system before the next request
+    if ( ref( $self->logger ) and $self->logger->can('clearRequestObj') ) {
+        $self->logger->clearRequestObj($req);
+    }
+    if ( ref( $self->userLogger )
+        and $self->userLogger->can('clearRequestObj') )
+    {
+        $self->userLogger->clearRequestObj($req);
+    }
+
+    return $res;
+}
+
 1;
 __END__
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Safelib.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Safelib.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Safelib.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Safelib.pm	2022-02-19 16:04:21.000000000 +0000
@@ -18,7 +18,7 @@ our $VERSION = '2.0.12';
 # Not that only functions, not methods, can be written here
 our $functions =
   [
-    qw(&checkLogonHours &date &dateToTime &checkDate &basic &unicode2iso &iso2unicode &groupMatch &isInNet6 &varIsInUri &has2f)
+    qw(&checkLogonHours &date &dateToTime &checkDate &basic &unicode2iso &iso2unicode &groupMatch &isInNet6 &varIsInUri &has2f_internal)
   ];
 
 ## @function boolean checkLogonHours(string logon_hours, string syntax, string time_correction, boolean default_access)
@@ -121,20 +121,29 @@ sub date {
     return $year . $mon . $mday . $hour . $min . $sec;
 }
 
-
 ## @function integer dateToTime(string date)
 # Converts a LDAP date into epoch time or returns undef upon failure.
 # @param $date string Date in YYYYMMDDHHMMSS[+/-0000] format. It may contain a differential timezone, otherwise default TZ is GMT
 # @return Date converted to time
 sub dateToTime {
     my $date = shift;
-    return undef unless ( $date );
+    return undef unless ($date);
 
     # Parse date
-    my ( $year, $month, $day, $hour, $min, $sec, $zone ) = ( $date =~ /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})([-+\w]*)/ );
+    my ( $year, $month, $day, $hour, $min, $sec, $zone ) =
+      ( $date =~ /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})([-+\w]*)/ );
 
-    # Convert date to epoch time with GMT as default timezone if date contains none
-    return str2time( $year . "-" . $month . "-" . $day . "T" . $hour . ":" . $min . ":" . $sec . $zone, "GMT" );
+ # Convert date to epoch time with GMT as default timezone if date contains none
+    return str2time(
+        $year . "-"
+          . $month . "-"
+          . $day . "T"
+          . $hour . ":"
+          . $min . ":"
+          . $sec
+          . $zone,
+        "GMT"
+    );
 }
 
 ## @function boolean checkDate(string start, string end, boolean default_access)
@@ -158,10 +167,10 @@ sub checkDate {
 
     # Convert dates to epoch time
     my $starttime = &dateToTime($start);
-    my $endtime = &dateToTime($end);
+    my $endtime   = &dateToTime($end);
 
     # Convert current GMT date to epoch time
-    my $datetime = &dateToTime(&date(1));
+    my $datetime = &dateToTime( &date(1) );
 
     return 1 if ( ( $datetime >= $starttime ) and ( $datetime <= $endtime ) );
     return 0;
@@ -244,7 +253,7 @@ sub varIsInUri {
 
 my $json = JSON::XS->new;
 
-sub has2f {
+sub has2f_internal {
     my ( $session, $type ) = @_;
     return 0 unless $session->{_2fDevices};
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session/REST.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session/REST.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session/REST.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session/REST.pm	2022-02-19 16:04:21.000000000 +0000
@@ -5,7 +5,7 @@ use Mouse;
 use Lemonldap::NG::Common::Conf::Constants;
 use JSON qw(from_json to_json);
 
-our $VERSION = '2.0.9';
+our $VERSION = '2.0.14';
 
 has sessionTypes => ( is => 'rw' );
 
@@ -258,13 +258,13 @@ sub getApacheSession {
     my $apacheSession = Lemonldap::NG::Common::Session->new( {
             storageModule        => $mod->{module},
             storageModuleOptions => $mod->{options},
-            cacheModule =>
+            cacheModule          =>
               Lemonldap::NG::Handler::PSGI::Main->tsv->{sessionCacheModule},
             cacheModuleOptions =>
               Lemonldap::NG::Handler::PSGI::Main->tsv->{sessionCacheOptions},
             id    => $id,
             force => $force,
-            ( $id ? () : ( kind => $mod->{kind} ) ),
+            ( $id   ? ()                : ( kind => $mod->{kind} ) ),
             ( $info ? ( info => $info ) : () ),
         }
     );
@@ -293,4 +293,9 @@ sub getMod {
     return $m;
 }
 
+sub getGlobal {
+    my ($self) = @_;
+    return $self->sessionTypes->{global};
+}
+
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session.pm	2022-02-19 16:04:21.000000000 +0000
@@ -126,8 +126,8 @@ sub BUILD {
 
     if ( $self->{info} ) {
         foreach ( keys %{ $self->{info} } ) {
-            next if ( $_ eq "_session_id" and $data->{_session_id} );
-            next if ( $_ eq "_session_kind" and $data->{_session_kind});
+            next if ( $_ eq "_session_id"   and $data->{_session_id} );
+            next if ( $_ eq "_session_kind" and $data->{_session_kind} );
             if ( defined $self->{info}->{$_} ) {
                 $data->{$_} = $self->{info}->{$_};
             }
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/TOTP.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/TOTP.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/TOTP.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/TOTP.pm	2022-02-07 19:06:14.000000000 +0000
@@ -8,12 +8,80 @@ use Mouse;
 use Convert::Base32 qw(decode_base32 encode_base32);
 use Crypt::URandom;
 use Digest::HMAC_SHA1 'hmac_sha1_hex';
+use Lemonldap::NG::Common::Crypto;
 
-our $VERSION = '2.0.10';
+our $VERSION = '2.0.14';
+
+has 'key' => (
+    is      => 'ro',
+    lazy    => 1,
+    default => sub {
+        my ($self) = @_;
+        return $self->conf->{totp2fKey} || $self->conf->{key};
+    }
+);
+
+has encryptSecret => (
+    is      => 'ro',
+    lazy    => 1,
+    default => sub {
+        my ($self) = @_;
+        return $self->conf->{totp2fEncryptSecret};
+    }
+);
+
+has 'crypto' => (
+    is      => 'ro',
+    lazy    => 1,
+    default => sub {
+        my ($self) = @_;
+        Lemonldap::NG::Common::Crypto->new( $self->key );
+    }
+);
+
+use constant PREFIX => "{llngcrypt}";
+
+sub is_encrypted {
+    my ( $self, $secret ) = @_;
+    return ( substr( $secret, 0, length(PREFIX) ) eq PREFIX );
+}
+
+sub get_ciphertext {
+    my ( $self, $secret ) = @_;
+    return substr( $secret, length(PREFIX) );
+}
+
+# This returns the TOTP secret from its stored form
+sub get_cleartext_secret {
+    my ( $self, $secret ) = @_;
+    my $cleartext_secret = $secret;
+    if ( $self->is_encrypted($secret) ) {
+        $cleartext_secret =
+          $self->crypto->decrypt( $self->get_ciphertext($secret) );
+    }
+    return $cleartext_secret;
+}
+
+# This returns the cleartext or encrypted code for storage
+sub get_storable_secret {
+    my ( $self, $secret ) = @_;
+    my $storable_secret = $secret;
+    if ( $self->encryptSecret ) {
+        $storable_secret = PREFIX . $self->crypto->encrypt($secret);
+    }
+    return $storable_secret;
+}
 
 # Verify that TOTP $code matches with $secret
 sub verifyCode {
-    my ( $self, $interval, $range, $digits, $secret, $code ) = @_;
+    my ( $self, $interval, $range, $digits, $stored_secret, $code ) = @_;
+
+    my $secret = $self->get_cleartext_secret($stored_secret);
+    if ( !$secret ) {
+        $self->logger->error('Unable to decrypt TOTP secret');
+        return -1;
+    }
+
     my $s = eval { decode_base32($secret) };
     if ($@) {
         $self->logger->error('Bad characters in TOTP secret');
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Util.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Util.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common/Util.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common/Util.pm	2022-01-22 14:30:19.000000000 +0000
@@ -3,9 +3,9 @@ require Exporter;
 
 use strict;
 use Digest::MD5;
-use MIME::Base64 qw/encode_base64/;
+use MIME::Base64 qw(encode_base64);
 
-our $VERSION   = '2.0.10';
+our $VERSION   = '2.0.14';
 our @ISA       = qw(Exporter);
 our @EXPORT_OK = qw(getSameSite getPSessionID genId2F);
 
@@ -15,36 +15,27 @@ sub getPSessionID {
 }
 
 sub genId2F {
-    my ( $device ) = @_;
+    my ($device) = @_;
     return encode_base64( "$device->{epoch}::$device->{type}::$device->{name}",
         "" );
 }
 
-
 sub getSameSite {
     my ($conf) = @_;
 
     # Initialize cookie SameSite value
-    unless ( $conf->{sameSite} ) {
+    return $conf->{sameSite} if $conf->{sameSite};
 
-        # SAML requires SameSite=None for POST bindings
-        if ( $conf->{issuerDBSAMLActivation}
-            or keys %{ $conf->{samlIDPMetaDataXML} } )
-        {
-            return "None";
-        }
-        else {
-            return "Lax";
-        }
-
-        # if CDA, OIDC, CAS: Lax
-        # TODO: try to detect when we can use 'Strict'?
-        # Any scenario that uses pdata to save state during login,
-        # Issuers, and CDA all require at least Lax
-    }
-    else {
-        return $conf->{sameSite};
-    }
+    # SAML requires SameSite=None for POST bindings
+    return (
+        $conf->{issuerDBSAMLActivation}
+          or keys %{ $conf->{samlIDPMetaDataXML} }
+    ) ? 'None' : 'Lax';
+
+    # if CDA, OIDC, CAS: Lax
+    # TODO: try to detect when we can use 'Strict'?
+    # Any scenario that uses pdata to save state during login,
+    # Issuers, and CDA all require at least Lax
 }
 
 1;
@@ -74,7 +65,7 @@ This method computes the unique ID of ea
 =head3 getSameSite($conf)
 
 Try to find a sensible value for the SameSite cookie attribute.
-If the user has overriden it, return the forced value
+If the user has overridden it, return the forced value
 
 =head1 AUTHORS
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common.pm 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common.pm
--- 2.0.13+ds-3/lemonldap-ng-common/lib/Lemonldap/NG/Common.pm	2021-08-20 16:29:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/lib/Lemonldap/NG/Common.pm	2022-02-19 16:43:01.000000000 +0000
@@ -1,6 +1,6 @@
 package Lemonldap::NG::Common;
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 
 1;
 __END__
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/Makefile.PL 2.0.14+ds-1/lemonldap-ng-common/Makefile.PL
--- 2.0.13+ds-3/lemonldap-ng-common/Makefile.PL	2021-08-20 16:29:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/Makefile.PL	2022-02-21 12:04:41.000000000 +0000
@@ -62,7 +62,7 @@ WriteMakefile(
             },
             MailingList => 'mailto:lemonldap-ng-dev@ow2.org',
             license     => 'http://opensource.org/licenses/GPL-2.0',
-            homepage    => 'http://lemonldap-ng.org/',
+            homepage    => 'https://lemonldap-ng.org/',
             bugtracker =>
               'https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues',
             x_twitter => 'https://twitter.com/lemonldapng',
@@ -94,7 +94,7 @@ WriteMakefile(
             ABSTRACT_FROM =>
               'lib/Lemonldap/NG/Common.pm',    # retrieve abstract from module
             AUTHOR =>
-'Xavier Guimard <x.guimard@free.fr>, Clément Oudot <clement@oodo.net>'
+'Xavier Guimard <x.guimard@free.fr>, Clement Oudot <clement@oodo.net>, Christophe Maudoux <chrmdx@gmail.com>, Maxime Besson <maxime.besson@worteks.com>'
           )
         : ()
     ),
@@ -104,6 +104,7 @@ WriteMakefile(
     MAN1PODS => {
         'scripts/convertConfig'         => 'blib/man1/convertConfig.1p',
         'scripts/convertSessions'       => 'blib/man1/convertSessions.1p',
+        'scripts/encryptTotpSecrets'    => 'blib/man1/encryptTotpSecrets.1p',
         'scripts/lemonldap-ng-cli'      => 'blib/man1/lemonldap-ng-cli.1p',
         'scripts/lemonldap-ng-sessions' => 'blib/man1/lemonldap-ng-sessions.1p',
         'scripts/importMetadata'        => 'blib/man1/importMetadata.1p',
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/MANIFEST 2.0.14+ds-1/lemonldap-ng-common/MANIFEST
--- 2.0.13+ds-3/lemonldap-ng-common/MANIFEST	2021-07-22 17:21:57.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/MANIFEST	2022-02-19 16:04:21.000000000 +0000
@@ -76,6 +76,7 @@ META.yml
 README
 scripts/convertConfig
 scripts/convertSessions
+scripts/encryptTotpSecrets
 scripts/importMetadata
 scripts/lemonldap-ng-cli
 scripts/lemonldap-ng-sessions
@@ -89,9 +90,11 @@ t/05-Common-Conf-LDAP.t
 t/30-Common-Safelib.t
 t/35-Common-Crypto.t
 t/36-Common-Regexp.t
+t/37-Common-TOTP.pm
 t/40-Common-Session.t
 t/50-Combination-Parser.t
 t/60-Session-Cli.t
+t/60-U2F-Migrate.t
 t/99-pod.t
 tools/apache-session-mysql.sql
 tools/lmConfig.CDBI.mysql
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/META.json 2.0.14+ds-1/lemonldap-ng-common/META.json
--- 2.0.13+ds-3/lemonldap-ng-common/META.json	2021-08-20 16:29:36.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/META.json	2022-02-21 12:04:41.000000000 +0000
@@ -1,7 +1,7 @@
 {
    "abstract" : "Common files for Lemonldap::NG infrastructure",
    "author" : [
-      "Xavier Guimard <x.guimard@free.fr>, ClÃ©ment Oudot <clement@oodo.net>"
+      "Xavier Guimard <x.guimard@free.fr>, Clement Oudot <clement@oodo.net>, Christophe Maudoux <chrmdx@gmail.com>, Maxime Besson <maxime.besson@worteks.com>"
    ],
    "dynamic_config" : 1,
    "generated_by" : "ExtUtils::MakeMaker version 7.34, CPAN::Meta::Converter version 2.150010",
@@ -72,12 +72,12 @@
       "bugtracker" : {
          "web" : "https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues"
       },
-      "homepage" : "http://lemonldap-ng.org/",
+      "homepage" : "https://lemonldap-ng.org/",
       "license" : [
          "http://opensource.org/licenses/GPL-2.0"
       ],
       "x_MailingList" : "mailto:lemonldap-ng-dev@ow2.org"
    },
-   "version" : "v2.0.13",
+   "version" : "v2.0.14",
    "x_serialization_backend" : "JSON::PP version 4.04"
 }
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/META.yml 2.0.14+ds-1/lemonldap-ng-common/META.yml
--- 2.0.13+ds-3/lemonldap-ng-common/META.yml	2021-08-20 16:29:36.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/META.yml	2022-02-21 12:04:41.000000000 +0000
@@ -1,7 +1,7 @@
 ---
 abstract: 'Common files for Lemonldap::NG infrastructure'
 author:
-  - 'Xavier Guimard <x.guimard@free.fr>, ClÃ©ment Oudot <clement@oodo.net>'
+  - 'Xavier Guimard <x.guimard@free.fr>, Clement Oudot <clement@oodo.net>, Christophe Maudoux <chrmdx@gmail.com>, Maxime Besson <maxime.besson@worteks.com>'
 build_requires:
   IO::String: '0'
   Net::LDAP: '0'
@@ -52,7 +52,7 @@ resources:
   MailingList: mailto:lemonldap-ng-dev@ow2.org
   X_twitter: https://twitter.com/lemonldapng
   bugtracker: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues
-  homepage: http://lemonldap-ng.org/
+  homepage: https://lemonldap-ng.org/
   license: http://opensource.org/licenses/GPL-2.0
-version: v2.0.13
+version: v2.0.14
 x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/README 2.0.14+ds-1/lemonldap-ng-common/README
--- 2.0.13+ds-3/lemonldap-ng-common/README	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/README	2022-02-21 12:04:41.000000000 +0000
@@ -3,7 +3,7 @@ LemonLDAP::NG
 
 LemonLDAP::NG is a modular Web-SSO based on Apache::Session modules.
 This is the common part of it. You can find documentation here:
- * for administrators: http://lemonldap-ng.org/
+ * for administrators: https://lemonldap-ng.org/
  * for developers: see embedded perldoc
 
 LemonLDAP::NG is a free software; you can redistribute it and/or modify
@@ -20,7 +20,9 @@ You should have received a copy of the G
 along with this program.  If not, see L<http://www.gnu.org/licenses/>.
 
 Copyright:
- * 2005-2015 by Xavier Guimard and Clément Oudot
+ * 2005-2022 by Xavier Guimard and Clément Oudot
+ * 2018-2022 by Christophe Maudoux
+ * 2019-2022 by Maxime Besson
  * 2008-2011 by Thomas Chemineau
  * 2012-2015 by François-Xavier Deltombe and Sandro Cazzaniga
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/scripts/encryptTotpSecrets 2.0.14+ds-1/lemonldap-ng-common/scripts/encryptTotpSecrets
--- 2.0.13+ds-3/lemonldap-ng-common/scripts/encryptTotpSecrets	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/scripts/encryptTotpSecrets	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,308 @@
+#!/usr/bin/perl
+#
+use warnings;
+use strict;
+use POSIX;
+use Lemonldap::NG::Common::Conf;
+use Lemonldap::NG::Common::Apache::Session;
+use Lemonldap::NG::Common::Session;
+use Lemonldap::NG::Common::TOTP;
+use JSON qw(from_json to_json);
+use Getopt::Long qw(:config auto_help);
+use Pod::Usage;
+
+my ( $dryrun, $verbose, $force, $update, $oldkey, $newkey );
+GetOptions(
+    "n|dry-run"   => \$dryrun,
+    "v|verbose"   => \$verbose,
+    "f|force"     => \$force,
+    "u|update"    => \$update,
+    "o|old-key=s" => \$oldkey,
+    "k|new-key=s" => \$newkey,
+);
+
+eval {
+    POSIX::setgid( scalar( getgrnam('www-data') ) );
+    POSIX::setuid( scalar( getpwnam('www-data') ) );
+};
+
+sub verbose {
+    print STDERR @_, "\n" if $verbose;
+}
+
+sub info {
+    print STDERR @_, "\n";
+}
+
+# Get config
+my $res = Lemonldap::NG::Common::Conf->new();
+die $Lemonldap::NG::Common::Conf::msg unless ($res);
+my $conf = $res->getConf();
+
+my $localconf = $res->getLocalConf()
+  or die "Unable to get local configuration ($!)";
+
+if ($localconf) {
+    $conf->{$_} = $localconf->{$_} foreach ( keys %$localconf );
+}
+
+if ( !$conf->{totp2fEncryptSecret} ) {
+    if ( !$force ) {
+        die "Encryption of TOTP secrets is not enabled in configuration."
+          . " Use --force to ignore this error";
+    }
+}
+
+# Create TOTP object
+
+my $decrypt_totp = Lemonldap::NG::Common::TOTP->new(
+    key           => ( $oldkey || $conf->{totp2fKey} || $conf->{key} ),
+    encryptSecret => 0,
+);
+
+my $encrypt_totp = Lemonldap::NG::Common::TOTP->new(
+    key           => ( $newkey || $conf->{totp2fKey} || $conf->{key} ),
+    encryptSecret => ( ( $newkey || "" ) eq "DECRYPT" ? 0 : 1 ),
+);
+
+# Search psessions
+my $args;
+if ( $conf->{"persistentStorage"} ) {
+    $args = $conf->{"persistentStorageOptions"};
+    $args->{backend} = $conf->{"persistentStorage"};
+}
+else {
+    $args = $conf->{"globalStorageOptions"};
+    $args->{backend} = $conf->{"globalStorage"};
+}
+
+verbose "Searching for persistent sessions";
+$res = Lemonldap::NG::Common::Apache::Session->searchOn( $args, '_session_kind',
+    'Persistent', '_2fDevices', '_session_uid' );
+
+if ( ref($res) eq "HASH" ) {
+    verbose "Found " . scalar( keys %{$res} ) . " persistent sessions";
+
+    # For each found psession
+    for my $k ( keys %{$res} ) {
+        my $_2fDevices = $res->{$k}->{_2fDevices};
+        my $uid        = $res->{$k}->{_session_uid};
+        verbose "Processing psession $k for user $uid";
+        encrypt_session( $k, $uid, $_2fDevices );
+    }
+}
+else {
+    die "Could not find any persistent sessions";
+}
+
+sub encrypt_session {
+    my ( $k, $uid, $_2fDevices ) = @_;
+
+    eval {
+        # parse _2fDevices if found
+        if ($_2fDevices) {
+            $_2fDevices = from_json($_2fDevices);
+
+            # If the session has 2f devices
+            if ( ref($_2fDevices) eq "ARRAY" and @{$_2fDevices} > 0 ) {
+                my $changed = convert_keys_for_user( $uid, $_2fDevices );
+                if ( $changed and !$dryrun ) {
+                    eval { update2fArray( $k, $_2fDevices ); };
+                    if ($@) {
+                        info "Error updating session for $uid: $@";
+                    }
+                }
+            }
+            else {
+                verbose "User $uid does not have a TOTP";
+            }
+        }
+        else {
+            verbose "User $uid does not have a TOTP";
+        }
+
+    };
+    if ($@) {
+        verbose "Error on psession $k: $@";
+    }
+}
+
+sub update2fArray {
+    my ( $id, $_2fDevices ) = @_;
+
+    my $session = Lemonldap::NG::Common::Session->new(
+        storageModule        => $args->{backend},
+        storageModuleOptions => $args,
+        id                   => $id,
+    );
+
+    unless ( $session->data ) {
+        die "Error while opening session $id";
+    }
+
+    unless ( $session->update( { _2fDevices => to_json($_2fDevices) } ) ) {
+        die "Error while updating session $id";
+    }
+}
+
+sub convert_device_for_user {
+    my ( $uid, $device ) = @_;
+    my $changed = 0;
+
+    # In update mode, decrypt then encrypt
+    if ($update) {
+        my $cleartext_secret =
+          $decrypt_totp->get_cleartext_secret( $device->{_secret} );
+        if ($cleartext_secret) {
+            my $newsecret =
+              $encrypt_totp->get_storable_secret($cleartext_secret);
+            $device->{_secret} = $newsecret;
+            $changed = 1;
+            verbose 'Updated secret for ' . $uid;
+        }
+        else {
+            info 'Unable to decrypt TOTP secret for ' . $uid;
+        }
+
+        # In normal mode, only encrypt non-encrypted secrets
+    }
+    else {
+        if ( !$encrypt_totp->is_encrypted( $device->{_secret} ) ) {
+            $device->{_secret} =
+              $encrypt_totp->get_storable_secret( $device->{_secret} );
+            $changed = 1;
+            info 'Encrypted TOTP secret for ' . $uid;
+        }
+        else {
+            verbose 'Secret is already encrypted';
+        }
+    }
+    return $changed;
+}
+
+sub convert_keys_for_user {
+    my ( $uid, $devices ) = @_;
+    my $has_totp = 0;
+    my $changed  = 0;
+    for my $device ( @{$devices} ) {
+        if ( $device->{type} eq "TOTP" ) {
+            $has_totp = 1;
+            my $epoch = $device->{epoch};
+            verbose "Processing device with epoch $epoch for user $uid";
+            my $changed_current = convert_device_for_user( $uid, $device );
+            $changed = 1 if $changed_current;
+        }
+    }
+
+    if ( !$has_totp ) {
+        verbose "User $uid does not have a TOTP";
+    }
+    return ( $has_totp, $changed );
+}
+
+__END__
+
+=encoding utf8
+
+=head1 NAME
+
+encryptTotpSecret - A tool to encrypt existing TOTP secrets
+
+=head1 SYNOPSIS
+
+  encryptTotpSecret [options]
+
+=head1 DESCRIPTION
+
+This script is a migration tool that you can use after enabling TOTP secret
+encryption in the Manager. It will make sure that existing secrets are
+encrypted, and not just newly registered secrets.
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<--help>, B<-h>
+
+Print a brief help message and exit.
+
+=item B<--dry-run>, B<-n>
+
+Prevent the script from saving modifications to the session database
+
+=item B<--update>, B<-u>
+
+By default, secrets that are already in encrypted form are skipped by the
+script. Use this option to force already encrypted secrets to be decrypted,
+then re-encrypted using a different key (or decrypted)
+
+=item B<--old-key>, B<-o>
+
+The key used to decrypt secrets in B<--update> mode.
+
+By default, the B<totp2fKey> or B<key> LemonLDAP::NG configuration parameters
+are used.
+
+=item B<--new-key>, B<-k>
+
+The key used to encrypt secrets. Use B<-u -k DECRYPT> to decrypt secrets instead.
+
+By default, the B<totp2fKey> or B<key> LemonLDAP::NG configuration parameters
+are used.
+
+=item B<--force>, B<-f>
+
+Encrypt existing TOTP secrets even if encryption is disabled in the configuration
+
+=item B<--verbose>, B<-v>
+
+Increase the level of details provided by the script
+
+=back
+
+=head1 SEE ALSO
+
+L<http://lemonldap-ng.org/>
+
+=head1 AUTHORS
+
+=over
+
+=item Maxime Besson, E<lt>maxime.besson@worteks.comE<gt>
+
+=back
+
+=head1 BUG REPORT
+
+Use OW2 system to report bug or ask for features:
+L<https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues>
+
+=head1 DOWNLOAD
+
+Lemonldap::NG is available at
+L<https://lemonldap-ng.org/download>
+
+=head1 COPYRIGHT AND LICENSE
+
+=over
+
+=item Copyright (C) 2008-2016 by Xavier Guimard, E<lt>x.guimard@free.frE<gt>
+
+=item Copyright (C) 2008-2016 by Clément Oudot, E<lt>clem.oudot@gmail.comE<gt>
+
+=back
+
+This library is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2, or (at your option)
+any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see L<http://www.gnu.org/licenses/>.
+
+=cut
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/scripts/importMetadata 2.0.14+ds-1/lemonldap-ng-common/scripts/importMetadata
--- 2.0.13+ds-3/lemonldap-ng-common/scripts/importMetadata	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/scripts/importMetadata	2022-01-22 14:30:19.000000000 +0000
@@ -32,7 +32,8 @@ my $result = GetOptions(
 );
 
 pod2usage(1) if $opts{help};
-pod2usage( -message => "Missing metadata URL (-m)", -exitval => 2) if !$opts{metadata};
+pod2usage( -message => "Missing metadata URL (-m)", -exitval => 2 )
+  if !$opts{metadata};
 
 #==============================================================================
 # Default values
@@ -338,22 +339,20 @@ foreach
             }
             else {
                 # Check if entityID already in configuration
+                my $confKey;
                 if ( defined $spList->{$entityID} ) {
+                    $confKey = $spList->{$entityID};
 
                     # Update metadata
-                    $lastConf->{samlSPMetaDataXML}->{ $spList->{$entityID} }
+                    $lastConf->{samlSPMetaDataXML}->{$confKey}
                       ->{samlSPMetaDataXML} = $partner_metadata;
 
                     # Update attributes
                     $lastConf->{samlSPMetaDataExportedAttributes}
                       ->{ $spList->{$entityID} } = $requestedAttributes;
 
-# Update options
-#                  $lastConf->{samlSPMetaDataOptions}->{ $spList->{$entityID} } =
-#                    $spOptions;
-# FIX AGA
-                    $lastConf->{samlSPMetaDataOptions}->{ $spList->{$entityID} }
-                      = { %{$spOptions} };
+                    $lastConf->{samlSPMetaDataOptions}->{$confKey} =
+                      { %{$spOptions} };
 
                     if ( $opts{verbose} ) {
                         print "Update SP $entityID in configuration\n";
@@ -362,7 +361,7 @@ foreach
                 }
                 else {
                     # Create a new partner
-                    my $confKey = toEntityIDkey( $spConfKeyPrefix, $entityID );
+                    $confKey = toEntityIDkey( $spConfKeyPrefix, $entityID );
 
                     # Metadata
                     $lastConf->{samlSPMetaDataXML}->{$confKey}
@@ -372,10 +371,6 @@ foreach
                     $lastConf->{samlSPMetaDataExportedAttributes}->{$confKey} =
                       $requestedAttributes;
 
-                  # Options
-                  # $lastConf->{samlSPMetaDataOptions}->{$confKey} = $spOptions;
-
-                    # FIX AGA
                     $lastConf->{samlSPMetaDataOptions}->{$confKey} =
                       { %{$spOptions} };
 
@@ -387,9 +382,12 @@ foreach
                 }
 
                 # handle eduPersonTargetedID
-                if ( $requestedAttributes->{eduPersonTargetedID} ) {
-                    delete $requestedAttributes->{eduPersonTargetedID};
-                    $lastConf->{samlSPMetaDataOptions}->{ $spList->{$entityID} }
+                if ( $lastConf->{samlSPMetaDataExportedAttributes}->{$confKey}
+                    ->{eduPersonTargetedID} )
+                {
+                    delete $lastConf->{samlSPMetaDataExportedAttributes}
+                      ->{$confKey}->{eduPersonTargetedID};
+                    $lastConf->{samlSPMetaDataOptions}->{$confKey}
                       ->{samlSPMetaDataOptionsNameIDFormat} = 'persistent';
                 }
 
@@ -556,7 +554,7 @@ Options:
     -m, --metadata          URL of metadata document
     -i, --idpconfprefix     Prefix used to set IDP configuration key
     -s, --spconfprefix      Prefix used to set SP configuration key
-    --ignore-sp             ignore SP maching this entityID (can be specified multiple times)
+    --ignore-sp             ignore SP matching this entityID (can be specified multiple times)
     --ignore-idp            ignore IdP matching this entityID (can be specified multiple times)
     -a, --nagios            output statistics in Nagios format
     -r, --remove            remove provider from LemonLDAP::NG if it does not appear in metadata
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/scripts/lemonldap-ng-sessions 2.0.14+ds-1/lemonldap-ng-common/scripts/lemonldap-ng-sessions
--- 2.0.13+ds-3/lemonldap-ng-common/scripts/lemonldap-ng-sessions	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/scripts/lemonldap-ng-sessions	2022-02-19 16:04:21.000000000 +0000
@@ -22,6 +22,7 @@ GetOptions(
     'help|h'       => \$help,
     'select|s=s@'  => \$opts->{select},
     'where|w=s'    => \$opts->{where},
+    'all|a'        => \$opts->{all},
     'backend|b=s'  => \$opts->{backend},
     'persistent|p' => \$opts->{persistent},
     'id-only|i'    => \$opts->{idonly},
@@ -80,7 +81,7 @@ if ( $action eq "setKey" ) {
 }
 
 if ( $action eq "secondfactors" ) {
-    unless ( @ARGV >= 2 ) {
+    unless ( @ARGV >= 1 ) {
         pod2usage(
             -exitval  => 1,
             -verbose  => 99,
@@ -238,8 +239,10 @@ Commands:
     delete <user> <id> [<id> ...]
         delete second factors for a user. The ID must match one of the
         IDs returned by the "show" command.
-    delType <user> <type> [<type> ...]
+    delType [<user>|--all] <type> [<type> ...]
         delete all second factors of a given type for a user
+    migrateu2f [<user>|--all]
+        migrate U2F device registrations to WebAuthn device registrations
 
 =head2 Consents
 
@@ -311,7 +314,7 @@ is the same as
 This option replaces the standard JSON output format with a simpler format of
 one session ID per line.
 
-This allows some intersting combos using xargs. For example, if you want to
+This allows some interesting combos using xargs. For example, if you want to
 remove all sessions started by "dwho"
 
 	lemonldap-ng-sessions search --where uid=dwho --id-only | \
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/t/37-Common-TOTP.pm 2.0.14+ds-1/lemonldap-ng-common/t/37-Common-TOTP.pm
--- 2.0.13+ds-3/lemonldap-ng-common/t/37-Common-TOTP.pm	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/t/37-Common-TOTP.pm	2022-01-22 14:30:25.000000000 +0000
@@ -0,0 +1,116 @@
+# Before `make install' is performed this script should be runnable with
+# `make test'. After `make install' it should work as `perl Lemonldap-NG-Common.t'
+#########################
+use Time::Fake;
+
+# Must subclass TOTP because it uses $self->logger etc.
+package TestableTotp;
+use Moose;
+use Test::More;
+use Lemonldap::NG::Common::TOTP;
+use Lemonldap::NG::Common::Logger::Null;
+extends 'Lemonldap::NG::Common::TOTP';
+has logger     => ( is => "ro", lazy => 1, builder => '_null_logger' );
+has userLogger => ( is => "ro", lazy => 1, builder => '_null_logger' );
+
+sub _null_logger {
+    return Lemonldap::NG::Common::Logger::Null->new;
+}
+
+package main;
+use Test::More tests => 16;
+
+BEGIN {
+    use_ok('Lemonldap::NG::Common::TOTP');
+}
+use strict;
+
+### WARNING FOR DEVELOPPERS ###
+# These constants are not to be messed with. If this unit test breaks, do NOT
+# modify them, fix the code instead.
+#
+# In particular, if the $stored_secret no longer decrypts to $cleartext_secret,
+# it means that users will lose their encrypted TOTP secrets on the next
+# upgrade.
+# If you need to change the cryptographic algorithm, make sure you remain
+# compatible with existing stored values
+
+my $timestamp          = 1633009395;
+my $totp_for_timestamp = 766039;
+my $cleartext_secret   = "ggtoch5x6naorymli6nh72ku4khwd4jr";
+my $key                = "azert";
+my $encrypted_secret =
+"{llngcrypt}TdEcd2vkmn4j0D8+str3v2D8zt0Dbm3sZ8TwlzdOKcang+qUmLraTQBztSrESRHDpAh+pQCKvDozuz9va7GxhHIkaKI3EZxOCWJ0rQCun/I=";
+
+#########################
+
+# Insert your test code below, the Test::More module is used here so read
+# its man page ( perldoc Test::More ) for help writing this test script.
+
+my $t = TestableTotp->new( key => $key, encryptSecret => 0 );
+
+# Verification with no offset
+Time::Fake->offset($timestamp);
+is( $t->verifyCode( 30, 0, 6, $cleartext_secret, $totp_for_timestamp ),
+    1, "TOTP code is valid" );
+
+Time::Fake->offset( $timestamp + 30 );
+is( $t->verifyCode( 30, 0, 6, $cleartext_secret, $totp_for_timestamp ),
+    0, "TOTP code is no longer valid" );
+
+Time::Fake->offset( $timestamp - 30 );
+is( $t->verifyCode( 30, 0, 6, $cleartext_secret, $totp_for_timestamp ),
+    0, "TOTP code is not valid yet" );
+
+# Verification with offset 2 allows +1m and -1m
+Time::Fake->offset( $timestamp + 45 );
+is( $t->verifyCode( 30, 2, 6, $cleartext_secret, $totp_for_timestamp ),
+    1, "TOTP code is valid" );
+
+Time::Fake->offset( $timestamp - 45 );
+is( $t->verifyCode( 30, 2, 6, $cleartext_secret, $totp_for_timestamp ),
+    1, "TOTP code is valid" );
+
+Time::Fake->offset( $timestamp + 95 );
+is( $t->verifyCode( 30, 2, 6, $cleartext_secret, $totp_for_timestamp ),
+    0, "TOTP code is no longer valid" );
+
+Time::Fake->offset( $timestamp - 95 );
+is( $t->verifyCode( 30, 2, 6, $cleartext_secret, $totp_for_timestamp ),
+    0, "TOTP code is not valid yet" );
+
+# TOTP encryption tests
+
+$t = TestableTotp->new( key => $key, encryptSecret => 0 );
+Time::Fake->offset($timestamp);
+is( $t->verifyCode( 30, 0, 6, $encrypted_secret, $totp_for_timestamp ),
+    1, "TOTP is valid with encrypted secret and encryption disabled" );
+
+$t = TestableTotp->new( key => $key, encryptSecret => 1 );
+Time::Fake->offset($timestamp);
+is( $t->verifyCode( 30, 0, 6, $encrypted_secret, $totp_for_timestamp ),
+    1, "TOTP is valid with encrypted secret and encryption enabled" );
+Time::Fake->offset($timestamp);
+is( $t->verifyCode( 30, 0, 6, $cleartext_secret, $totp_for_timestamp ),
+    1, "TOTP is valid with cleartext secret and encryption enabled" );
+
+# Encryption of TOTP secret, wrong key
+$t = TestableTotp->new( key => "idunno", encryptSecret => 0 );
+is( $t->verifyCode( 30, 0, 6, $encrypted_secret, $totp_for_timestamp ),
+    -1, "TOTP code fails to verify" );
+
+# Do not encrypt new secrets unless we configured it
+$t = TestableTotp->new( key => $key, encryptSecret => 0 );
+is( $t->get_storable_secret($cleartext_secret),
+    $cleartext_secret,
+    "TOTP secret is stored as-is when encryption is disabled" );
+
+# Encrypt new secrets if we configured it
+$t = TestableTotp->new( key => $key, encryptSecret => 1 );
+my $new = $t->get_storable_secret($cleartext_secret);
+like( $new, qr/^{llngcrypt}/, "Secret looks encrypted" );
+unlike( $new, qr/$cleartext_secret/, "Secret looks encrypted" );
+
+Time::Fake->offset($timestamp);
+is( $t->verifyCode( 30, 0, 6, $new, $totp_for_timestamp ),
+    1, "get_storable_secret produces working secret" );
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/t/50-Combination-Parser.t 2.0.14+ds-1/lemonldap-ng-common/t/50-Combination-Parser.t
--- 2.0.13+ds-3/lemonldap-ng-common/t/50-Combination-Parser.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/t/50-Combination-Parser.t	2022-02-19 16:04:21.000000000 +0000
@@ -55,8 +55,11 @@ ok(
 
 # Test "and"
 
-@tests = ( '[A and B, A]', '[A,B] and [B,C]',
-    'if(0) then [A,B] else [A,B] and [B,C]' );
+@tests = (
+    '[A and B, A]',
+    '[A,B] and [B,C]',
+    'if(0) then [A,B] else [A,B] and [B,C]'
+);
 
 while ( my $expr = shift @tests ) {
     ok( [ getok($expr) ]->[0] == 0, qq{"$expr" returns PE_OK as auth result} )
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/t/60-Session-Cli.t 2.0.14+ds-1/lemonldap-ng-common/t/60-Session-Cli.t
--- 2.0.13+ds-3/lemonldap-ng-common/t/60-Session-Cli.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/t/60-Session-Cli.t	2022-02-19 16:04:21.000000000 +0000
@@ -62,97 +62,115 @@ my @psessionsOpts = (
     force => 1,
 );
 
-Lemonldap::NG::Common::Session->new( {
-        @sessionsOpts,
-        id   => "1b3231655cebb7a1f783eddf27d254ca",
-        info => {
-            "uid" => "rtyler",
-        }
-    }
-);
-Lemonldap::NG::Common::Session->new( {
-        @sessionsOpts,
-        id   => "9684dd2a6489bf2be2fbdd799a8028e3",
-        info => {
-            "uid" => "dwho",
-        }
-    }
-);
-Lemonldap::NG::Common::Session->new( {
-        @sessionsOpts,
-        id   => "f90f597566f5cce47d9641377776c0c2",
-        info => {
-            "uid"      => "dwho",
-            "deleteme" => 1,
-        }
-    }
-);
-Lemonldap::NG::Common::Session->new( {
-        @sessionsOpts,
-        id   => "1234",
-        info => {
-            "uid" => "foo",
-        }
-    }
-);
-Lemonldap::NG::Common::Session->new( {
-        @sessionsOpts,
-        id   => "1235",
-        info => {
-            "uid" => "foo",
-        }
-    }
-);
-
-Lemonldap::NG::Common::Session->new( {
-        @psessionsOpts,
-        id    => "5efe8af397fc3577e05b483aca964f1b",
-        force => 1,
-        info  => {
-            "_2fDevices" => to_json( [ {
-                        'type'     => 'UBK',
-                        'epoch'    => 1588691690,
-                        '_yubikey' => 'cccccceijfnf',
-                        'name'     => 'Imported automatically'
-                    },
-                    {
-                        'name'  => 'MyU2F',
-                        'type'  => 'U2F',
-                        'epoch' => 1588691728
-                    },
-                    {
-                        '_secret' => 'mnxkiirpswuojr47kkrty7ax34fy2ix7',
-                        'name'    => 'MyTOTP',
-                        'type'    => 'TOTP',
-                        'epoch'   => 1588691728
-                    }
-                ]
-            ),
-            "_oidcConsents" => to_json( [ {
-                        'scope' => 'openid email',
-                        'rp'    => 'rp-example',
-                        'epoch' => 1589288341
-                    },
-                    {
-                        'scope' => 'openid email',
-                        'epoch' => 1589291482,
-                        'rp'    => 'rp-example2'
-                    }
-                ]
-            ),
-            "_session_uid" => "dwho",
-        }
-    }
-);
-Lemonldap::NG::Common::Session->new( {
-        @psessionsOpts,
-        id    => "8d3bc3b0e14ea2a155f275aa7c07ebee",
-        force => 1,
-        info  => {
-            "_session_uid" => "rtyler",
+sub resetSessions {
+    Lemonldap::NG::Common::Session->new( {
+            @sessionsOpts,
+            id   => "1b3231655cebb7a1f783eddf27d254ca",
+            info => {
+                "uid" => "rtyler",
+            }
         }
-    }
-);
+    );
+    Lemonldap::NG::Common::Session->new( {
+            @sessionsOpts,
+            id   => "9684dd2a6489bf2be2fbdd799a8028e3",
+            info => {
+                "uid" => "dwho",
+            }
+        }
+    );
+    Lemonldap::NG::Common::Session->new( {
+            @sessionsOpts,
+            id   => "f90f597566f5cce47d9641377776c0c2",
+            info => {
+                "uid"      => "dwho",
+                "deleteme" => 1,
+            }
+        }
+    );
+    Lemonldap::NG::Common::Session->new( {
+            @sessionsOpts,
+            id   => "1234",
+            info => {
+                "uid" => "foo",
+            }
+        }
+    );
+    Lemonldap::NG::Common::Session->new( {
+            @sessionsOpts,
+            id   => "1235",
+            info => {
+                "uid" => "foo",
+            }
+        }
+    );
+
+    Lemonldap::NG::Common::Session->new( {
+            @psessionsOpts,
+            id    => "5efe8af397fc3577e05b483aca964f1b",
+            force => 1,
+            info  => {
+                "_2fDevices" => to_json( [ {
+                            'type'     => 'UBK',
+                            'epoch'    => 1588691690,
+                            '_yubikey' => 'cccccceijfnf',
+                            'name'     => 'Imported automatically'
+                        },
+                        {
+                            'name'  => 'MyU2F',
+                            'type'  => 'U2F',
+                            'epoch' => 1588691728
+                        },
+                        {
+                            '_secret' => 'mnxkiirpswuojr47kkrty7ax34fy2ix7',
+                            'name'    => 'MyTOTP',
+                            'type'    => 'TOTP',
+                            'epoch'   => 1588691728
+                        }
+                    ]
+                ),
+                "_oidcConsents" => to_json( [ {
+                            'scope' => 'openid email',
+                            'rp'    => 'rp-example',
+                            'epoch' => 1589288341
+                        },
+                        {
+                            'scope' => 'openid email',
+                            'epoch' => 1589291482,
+                            'rp'    => 'rp-example2'
+                        }
+                    ]
+                ),
+                "_session_uid" => "dwho",
+            }
+        }
+    );
+    Lemonldap::NG::Common::Session->new( {
+            @psessionsOpts,
+            id    => "8d3bc3b0e14ea2a155f275aa7c07ebee",
+            force => 1,
+            info  => {
+                "_session_uid" => "rtyler",
+                "_2fDevices"   => to_json( [ {
+                            'type'     => 'UBK',
+                            'epoch'    => 1588691690,
+                            '_yubikey' => 'cccccceijfnf',
+                            'name'     => 'Imported automatically'
+                        },
+                        {
+                            '_secret' => 'mnxkiirpswuojr47kkrty7ax34fy2ix7',
+                            'name'    => 'MyTOTP',
+                            'type'    => 'TOTP',
+                            'epoch'   => 1588691728
+                        }
+                    ]
+                ),
+            }
+        }
+    );
+}
+
+resetSessions;
 
 sub getJson {
     my @args = @_;
@@ -222,7 +240,7 @@ is( @{$res}, 2, "Found 2 psessions" );
 
 # Test search with where
 $res = getJson( "search", { where => "uid=dwho" } );
-is( @{$res}, 2, "Found 2 sessions" );
+is( @{$res},                                  2, "Found 2 sessions" );
 is( ( grep { $_->{uid} eq "dwho" } @{$res} ), 2, "Both sessions are dwho" );
 
 # Test search with where and field selection
@@ -241,7 +259,7 @@ is(
 );
 
 # Delete session
-$cli->run( 'delete', {}, "9684dd2a6489bf2be2fbdd799a8028e3" );
+$cli->run( 'delete', {},                  "9684dd2a6489bf2be2fbdd799a8028e3" );
 $cli->run( 'delete', { persistent => 1 }, "rtyler" );
 
 $res = getJson( "get", {}, "9684dd2a6489bf2be2fbdd799a8028e3" );
@@ -301,6 +319,27 @@ is( ( keys %{$res} ), 1, "Found one seco
 is( ( grep { $_->{type} eq "U2F" } values %{$res} ),  0, "U2F was removed" );
 is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 1, "TOTP survived" );
 
+# Delete 2FA by type (with search)
+resetSessions;
+$cli->run( "secondfactors", { where => "_session_uid=dwho" }, "delType",
+    "U2F" );
+$res = getJson( "secondfactors", {}, "get", "dwho" );
+is( ( keys %{$res} ), 2, "Found one second factors" );
+is( ( grep { $_->{type} eq "U2F" } values %{$res} ),  0, "U2F was removed" );
+is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 1, "TOTP survived" );
+
+# Delete 2FA by type (with all)
+resetSessions;
+$cli->run( "secondfactors", { all => 1 }, "delType", "TOTP" );
+$res = getJson( "secondfactors", {}, "get", "dwho" );
+is( ( keys %{$res} ), 2, "Found two second factors for dwho" );
+is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 0, "TOTP was removed" );
+is( ( grep { $_->{type} eq "UBK" } values %{$res} ),  1, "UBK survived" );
+$res = getJson( "secondfactors", {}, "get", "rtyler" );
+is( ( keys %{$res} ), 1, "Found one second factors for rtyler" );
+is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 0, "TOTP was removed" );
+is( ( grep { $_->{type} eq "UBK" } values %{$res} ),  1, "UBK survived" );
+
 # Show consents
 $res = getJson( "consents", {}, "get", "dwho" );
 is( ( keys %{$res} ), 2, "Found two consents" );
@@ -309,7 +348,7 @@ is( ( keys %{$res} ), 2, "Found two cons
 
 $cli->run( "consents", {}, "delete", "dwho", "rp-example" );
 $res = getJson( "consents", {}, "get", "dwho" );
-is( ( keys %{$res} ), 1, "Found one consent" );
+is( ( keys %{$res} ),     1,     "Found one consent" );
 is( $res->{'rp-example'}, undef, "Consent for test-rp removed" );
 ok( $res->{'rp-example2'}, "Consent for test-rp2 still present" );
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-common/t/60-U2F-Migrate.t 2.0.14+ds-1/lemonldap-ng-common/t/60-U2F-Migrate.t
--- 2.0.13+ds-3/lemonldap-ng-common/t/60-U2F-Migrate.t	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-common/t/60-U2F-Migrate.t	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,228 @@
+# Before `make install' is performed this script should be runnable with
+# `make test'. After `make install' it should work as `perl Lemonldap-NG-Manager.t'
+
+#########################
+
+# change 'tests => 1' to 'tests => last_test_to_print';
+
+use Test::More;
+use Test::Output;
+use File::Path;
+use JSON;
+
+BEGIN {
+    use_ok('Lemonldap::NG::Common::Session');
+    use_ok('Lemonldap::NG::Common::CliSessions');
+}
+
+#########################
+
+SKIP: {
+    eval "use Authen::WebAuthn::Test; use Authen::WebAuthn;";
+    if ($@) {
+        skip 'Authen::WebAuthn not found';
+    }
+    my $dir;
+    my $cli;
+
+    sub setup_sessions {
+        use File::Temp;
+        $dir = File::Temp::tempdir();
+        my $sessionsdir  = "$dir/sessions";
+        my $psessionsdir = "$dir/psessions";
+        mkdir $sessionsdir;
+        mkdir $psessionsdir;
+
+        $cli = Lemonldap::NG::Common::CliSessions->new(
+            conf => {
+                globalStorage        => "Apache::Session::File",
+                globalStorageOptions => {
+                    Directory     => $sessionsdir,
+                    LockDirectory => $sessionsdir,
+                },
+                persistentStorage        => "Apache::Session::File",
+                persistentStorageOptions => {
+                    Directory     => $psessionsdir,
+                    LockDirectory => $psessionsdir,
+                },
+            }
+        );
+
+        # Provision test sessions
+        my @psessionsOpts = (
+            storageModule        => "Apache::Session::File",
+            storageModuleOptions => {
+                Directory     => $psessionsdir,
+                LockDirectory => $psessionsdir,
+            },
+            kind  => 'Persistent',
+            force => 1,
+        );
+
+        #dwho
+        Lemonldap::NG::Common::Session->new( {
+                @psessionsOpts,
+                id    => "5efe8af397fc3577e05b483aca964f1b",
+                force => 1,
+                info  => {
+                    "_2fDevices" => to_json( [ {
+                                'type'     => 'UBK',
+                                'epoch'    => 1588691690,
+                                '_yubikey' => 'cccccceijfnf',
+                                'name'     => 'Imported automatically'
+                            },
+                            {
+                                'name'       => 'U2F-1',
+                                'type'       => 'U2F',
+                                'epoch'      => 1588691728,
+                                '_keyHandle' =>
+'4aS6vXlFQpG5XZSoad6auM9fFu7Q1wazQYwfPtPKN_Hll6Up_ceeWkOgqxm49swWq4Vvcg5UlX0sQQhuRe8heA',
+                                '_userKey' =>
+'BMgMqKPL2PhsjCNW78UEQyNF8zlJtrAAPtWMUDBp9VfDRF5oL2xkwFuyXRMPtRZ7lNfGijDrMc06bDNfp478sQQ',
+                            },
+                            {
+                                'name'       => 'U2F-2',
+                                'type'       => 'U2F',
+                                'epoch'      => 1588691730,
+                                '_keyHandle' =>
+'F1Kk9V_O7KDPIx-mqp6CIjbz7ljA-ihWVWyoP1xYBe_HPLHR74aTLanmn0b4vI8DumiBWO1DAle3k6N55cXreg',
+                                '_userKey' =>
+'BAE_svIcxLfm2Knart7DI1ScfBnCt-OFKDWugp3YMO14tamwuc_wN0vSh1D_0DV4Ao3S5GNQZXxtjtUADHTwXHA',
+                            },
+                            {
+                                _credentialId => 'noconflict',
+                                'name'        => 'Existing WebAuthn',
+                                'type'        => 'WebAuthn',
+                                'epoch'       => 1588691798
+                            }
+                        ]
+                    ),
+                    "_oidcConsents" => to_json( [ {
+                                'scope' => 'openid email',
+                                'rp'    => 'rp-example',
+                                'epoch' => 1589288341
+                            },
+                            {
+                                'scope' => 'openid email',
+                                'epoch' => 1589291482,
+                                'rp'    => 'rp-example2'
+                            }
+                        ]
+                    ),
+                    "_session_uid" => "dwho",
+                }
+            }
+        );
+
+        # rtyler
+        Lemonldap::NG::Common::Session->new( {
+                @psessionsOpts,
+                id    => "8d3bc3b0e14ea2a155f275aa7c07ebee",
+                force => 1,
+                info  => {
+                    "_session_uid" => "rtyler",
+                    "_2fDevices"   => to_json( [ {
+                                'type'     => 'UBK',
+                                'epoch'    => 1588691690,
+                                '_yubikey' => 'cccccceijfnf',
+                                'name'     => 'Imported automatically'
+                            },
+                            {
+                                'name'       => 'U2F-3',
+                                'type'       => 'U2F',
+                                'epoch'      => 1588691734,
+                                '_keyHandle' =>
+'4suXv5Cf10vbJEP72mVkLpBjhSqy5niOgfc0X_MjdxZ_g2e-V8biC6WyCTpF_kGV1FCa06YlcryPCtWUuUST_g',
+                                '_userKey' =>
+'BIXrgc12iGGOYIGyWKd8WeOGCKyTkFA7jXkjlLS0i1MA3vy8gDocfqYCngXMzBAmtGI7FfMlbkG6DJeSubdxAVc',
+                            },
+                        ]
+                    ),
+                }
+            }
+        );
+    }
+
+    sub getJson {
+        my @args = @_;
+        my ($str) = Test::Output::output_from( sub { $cli->run(@args); } );
+        return from_json($str);
+    }
+
+    sub getLines {
+        my @args = @_;
+        my ($str) = Test::Output::output_from( sub { $cli->run(@args); } );
+        return [ split /\n/, $str ];
+    }
+
+    setup_sessions();
+    my $res;
+
+    # Migrate U2F
+    $cli->run( "secondfactors", {}, "migrateu2f", "dwho" );
+
+    $res = getJson( "secondfactors", {}, "get", "rtyler" );
+    is( values %{$res}, 2, "Still 2 devices" );
+    is( ( grep { $_->{type} eq "WebAuthn" } values %{$res} ),
+        0, "No WebAuthn sessions created" );
+
+    $res = getJson( "secondfactors", {}, "get", "dwho" );
+    is( values %{$res}, 6, "Expect 6 devices after migration" );
+    is( ( grep { $_->{type} eq "U2F" } values %{$res} ),
+        2, "U2F still present" );
+    is( ( grep { $_->{type} eq "UBK" } values %{$res} ),
+        1, "UBK still in place" );
+    is( ( grep { $_->{type} eq "WebAuthn" } values %{$res} ),
+        3, "New WebAuthn device" );
+    my $migratedid = "MTU4ODY5MTcyODo6V2ViQXV0aG46OlUyRi0x";
+    is( $res->{$migratedid}->{_signCount}, 0, "migrated signcount" );
+    is(
+        $res->{$migratedid}->{_credentialId},
+'4aS6vXlFQpG5XZSoad6auM9fFu7Q1wazQYwfPtPKN_Hll6Up_ceeWkOgqxm49swWq4Vvcg5UlX0sQQhuRe8heA',
+        "migrated credential ID"
+    );
+    is(
+        $res->{$migratedid}->{_credentialPublicKey},
+'pQECAyYgASFYIMgMqKPL2PhsjCNW78UEQyNF8zlJtrAAPtWMUDBp9VfDIlggRF5oL2xkwFuyXRMPtRZ7lNfGijDrMc06bDNfp478sQQ',
+        "migrated credential key"
+    );
+    is( $res->{$migratedid}->{epoch}, '1588691728', "migrated epoch" );
+    is( $res->{$migratedid}->{name},  "U2F-1",      "migrated name" );
+
+    # Check idempotence
+    $cli->run( "secondfactors", {}, "migrateu2f", "dwho" );
+    $res = getJson( "secondfactors", {}, "get", "dwho" );
+    is( values %{$res}, 6, "Expect still 6 devices after rerunning migration" );
+    is( ( grep { $_->{type} eq "U2F" } values %{$res} ),
+        2, "U2F still in place" );
+    is( ( grep { $_->{type} eq "UBK" } values %{$res} ),
+        1, "UBK still in place" );
+    is( ( grep { $_->{type} eq "WebAuthn" } values %{$res} ),
+        3, "Same WebAuthn devices" );
+
+    rmtree $dir;
+    setup_sessions();
+
+    # Migrate all
+    $cli->run( "secondfactors", { all => 1 }, "migrateu2f" );
+    $res = getJson( "secondfactors", {}, "get", "dwho" );
+    is( values %{$res}, 6, "Expect 6 devices after migration" );
+    is( ( grep { $_->{type} eq "U2F" } values %{$res} ),
+        2, "U2F still in place" );
+    is( ( grep { $_->{type} eq "UBK" } values %{$res} ),
+        1, "UBK still in place" );
+    is( ( grep { $_->{type} eq "WebAuthn" } values %{$res} ),
+        3, "New WebAuthn device" );
+
+    $res = getJson( "secondfactors", {}, "get", "rtyler" );
+    is( values %{$res}, 3, "Expect 3 devices after migration" );
+    is( ( grep { $_->{type} eq "U2F" } values %{$res} ),
+        1, "U2F still in place" );
+    is( ( grep { $_->{type} eq "UBK" } values %{$res} ),
+        1, "UBK still in place" );
+    is( ( grep { $_->{type} eq "WebAuthn" } values %{$res} ),
+        1, "New WebAuthn device" );
+
+    rmtree $dir;
+}
+done_testing();
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/ApacheMP2/FCGIClient.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/ApacheMP2/FCGIClient.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/ApacheMP2/FCGIClient.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/ApacheMP2/FCGIClient.pm	2022-02-07 19:06:14.000000000 +0000
@@ -21,7 +21,7 @@ use constant REDIRECT          => Apache
 use constant DECLINED          => Apache2::Const::DECLINED;
 use constant SERVER_ERROR      => Apache2::Const::SERVER_ERROR;
 
-our $VERSION = '2.0.6';
+our $VERSION = '2.0.14';
 
 sub handler {
     my ( $class, $r ) = @_;
@@ -43,7 +43,7 @@ sub handler {
         SERVER_PORT    => $r->get_server_port,
         REQUEST_METHOD => $r->method,
     };
-    foreach (qw(VHOSTTYPE RULES_URL)) {
+    foreach (qw(VHOSTTYPE RULES_URL HTTPS_REDIRECT PORT_REDIRECT)) {
         if ( my $t = $r->dir_config($_) ) {
             $env->{$_} = $t;
         }
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/ApacheMP2.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/ApacheMP2.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/ApacheMP2.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/ApacheMP2.pm	2022-02-19 16:04:21.000000000 +0000
@@ -43,7 +43,28 @@ sub launch {
     my $class = "Lemonldap::NG::Handler::ApacheMP2::$type";
     eval "require $class";
     die $@ if ($@);
+
+    # register the request object to the logging system
+    if ( ref( $class->logger ) and $class->logger->can('setRequestObj') ) {
+        $class->logger->setRequestObj($req);
+    }
+    if ( ref( $class->userLogger )
+        and $class->userLogger->can('setRequestObj') )
+    {
+        $class->userLogger->setRequestObj($req);
+    }
+
     my ($res) = $class->$sub($req);
+
+    # Clear the logging system before the next request
+    if ( ref( $class->logger ) and $class->logger->can('clearRequestObj') ) {
+        $class->logger->clearRequestObj($req);
+    }
+    if ( ref( $class->userLogger )
+        and $class->userLogger->can('clearRequestObj') )
+    {
+        $class->userLogger->clearRequestObj($req);
+    }
     return $res;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/DevOps.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/DevOps.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/DevOps.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/DevOps.pm	2022-02-19 16:04:21.000000000 +0000
@@ -4,10 +4,9 @@ use strict;
 use Lemonldap::NG::Common::UserAgent;
 use JSON qw(from_json);
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 our $_ua;
 
-
 sub ua {
     return $_ua if ($_ua);
     return $_ua = Lemonldap::NG::Common::UserAgent->new( $_[0]->localConfig );
@@ -30,42 +29,60 @@ sub checkMaintenanceMode {
 
 sub _loadVhostConfig {
     my ( $class, $req, $vhost ) = @_;
-    my $json;
+    my ( $json, $rUrl, $rVhost );
     if ( $class->tsv->{useSafeJail} ) {
-        my $rUrl = $req->{env}->{RULES_URL}
-          || ( (
-                $class->localConfig->{loopBackUrl}
-                || "http://127.0.0.1:" . $req->{env}->{SERVER_PORT}
-            )
-            . '/rules.json'
-          );
+        if ( $req->env->{RULES_URL} || $class->tsv->{devOpsRulesUrl}->{$vhost} )
+        {
+            $rUrl = $req->{env}->{RULES_URL}
+              || $class->tsv->{devOpsRulesUrl}->{$vhost};
+            $rVhost = ( $rUrl =~ m#^https?://([^/]*).*# )[0];
+            $rVhost =~ s/:\d+$//;
+        }
+        else {
+            $rUrl =
+              ( $class->localConfig->{loopBackUrl}
+                  || "http://127.0.0.1:" . $req->{env}->{SERVER_PORT} )
+              . '/rules.json';
+            $rVhost = $vhost;
+        }
+
+        $class->logger->debug("Try to retrieve rules file from $rUrl");
         my $get = HTTP::Request->new( GET => $rUrl );
-        $get->header( Host => $vhost );
+        $class->logger->debug("Set Host header with $rVhost");
+        $get->header( Host => $rVhost );
         my $resp = $class->ua->request($get);
         if ( $resp->is_success ) {
-            eval {
-                $json = from_json( $resp->content, { allow_nonref => 1 } ); };
+            $class->logger->debug('Response is success');
+            eval { $json = from_json( $resp->content, { allow_nonref => 1 } ); };
             if ($@) {
+                $class->logger->debug('Bad json file received');
                 $class->logger->error(
-                    "Bad rules.json for $vhost, skipping ($@)");
+"Bad rules file retrieved from $rUrl for $vhost, skipping ($@)"
+                );
             }
             else {
-                $class->logger->info("Compiling rules.json for $vhost");
+                $class->logger->debug('Good json file received');
+                $class->logger->info(
+                    "Compiling rules retrieved from $rUrl for $vhost");
             }
         }
+        else {
+            $class->logger->error(
+                "Unable to retrieve rules file from $rUrl -> "
+                  . $resp->status_line );
+            $class->logger->info("Default rule and header are employed");
+        }
     }
     else {
         $class->logger->error(
-q"I refuse to compile rules.json when useSafeJail isn't activated! Yes I know, I'm a coward..."
+q"I refuse to compile 'rules.json' when useSafeJail isn't activated! Yes I know, I'm a coward..."
         );
     }
     $json->{rules} ||= { default => 1 };
     $json->{headers} //= { 'Auth-User' => '$uid' };
 
-    # Removed forbidden session attributes
-    foreach
-      my $v ( split /\s+/, $class->tsv->{hiddenAttributes} )
-    {
+    # Removed hidden session attributes
+    foreach my $v ( split /[,\s]+/, $class->tsv->{hiddenAttributes} ) {
         foreach ( keys %{ $json->{headers} } ) {
             delete $json->{headers}->{$_}
               if $json->{headers}->{$_} eq '$' . $v;
@@ -74,8 +91,12 @@ q"I refuse to compile rules.json when us
 
     $class->logger->debug("DevOps handler called by $vhost");
     $class->locationRulesInit( undef, { $vhost => $json->{rules} } );
-    $class->headersInit( undef,       { $vhost => $json->{headers} } );
+    $class->headersInit( undef, { $vhost => $json->{headers} } );
     $class->tsv->{lastVhostUpdate}->{$vhost} = time;
+    $class->tsv->{https}->{$vhost} = uc $req->env->{HTTPS_REDIRECT} eq 'ON'
+      if exists $req->env->{HTTPS_REDIRECT};
+    $class->tsv->{port}->{$vhost} = $req->env->{PORT_REDIRECT}
+      if exists $req->env->{PORT_REDIRECT};
 
     return;
 }
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/OAuth2.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/OAuth2.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/OAuth2.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/OAuth2.pm	2022-02-07 19:06:14.000000000 +0000
@@ -3,7 +3,7 @@ use Lemonldap::NG::Common::JWT qw(getAcc
 
 use strict;
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 sub retrieveSession {
     my ( $class, $req, $id ) = @_;
@@ -102,8 +102,9 @@ sub fetchId {
     # Store scope and rpid for future session attributes
     if ( $infos->{rp} ) {
         my $rp = $infos->{rp};
-        $req->data->{_scope}         = $infos->{scope};
-        $req->data->{_clientConfKey} = $rp;
+        $req->data->{_scope}           = $infos->{scope};
+        $req->data->{_oidc_grant_type} = $infos->{grant_type};
+        $req->data->{_clientConfKey}   = $rp;
         if (    $class->tsv->{oauth2Options}->{$rp}
             and $class->tsv->{oauth2Options}->{$rp}->{clientId} )
         {
@@ -193,7 +194,7 @@ sub goToPortal {
 sub _getTokenAttributes {
     my ( $class, $req ) = @_;
     my %res;
-    for my $attr (qw/_scope _clientConfKey _clientId/) {
+    for my $attr (qw/_scope _clientConfKey _clientId _oidc_grant_type/) {
         if ( $req->data->{$attr} ) {
             $res{$attr} = $req->data->{$attr};
         }
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/PSGI.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/PSGI.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/PSGI.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/PSGI.pm	2022-02-19 16:04:21.000000000 +0000
@@ -5,7 +5,7 @@ use Mouse;
 
 #use Lemonldap::NG::Handler::Main qw(:jailSharedVars);
 
-our $VERSION = '2.0.11';
+our $VERSION = '2.0.14';
 
 has protection => ( is => 'rw', isa => 'Str' );
 has rule       => ( is => 'rw', isa => 'Str' );
@@ -16,12 +16,12 @@ has api        => ( is => 'rw', isa => '
 sub init {
     my ( $self, $args ) = @_;
     eval { $self->api->init($args) };
-    if ( $@ and not( $self->{protection} and $self->{protection} eq 'none' ) ) {
+    if ( $@ and not( $args->{protection} and $args->{protection} eq 'none' ) ) {
         $self->error($@);
         return 0;
     }
     unless ( $self->api->checkConf($self)
-        or ( $self->{protection} and $self->{protection} eq 'none' ) )
+        or ( $args->{protection} and $args->{protection} eq 'none' ) )
     {
         $self->error(
             "Unable to protect this server ($Lemonldap::NG::Common::Conf::msg)"
@@ -30,7 +30,7 @@ sub init {
     }
     eval { $self->portal( $self->api->tsv->{portal}->() ) };
     my $rule =
-      $self->{protection} || $self->api->localConfig->{protection} || '';
+      $args->{protection} || $self->api->localConfig->{protection} || '';
     $self->rule(
         $rule eq 'authenticate' ? 1 : $rule eq 'manager' ? '' : $rule );
     return 1;
@@ -38,7 +38,7 @@ sub init {
 
 ## @methodi void _run()
 # Check if protecton is activated then return a code ref that will launch
-# _authAndTrace() if protection in on or handler() else
+# _logAuthTrace() if protection in on or handler() else
 #@return code-ref
 sub _run {
     my $self = shift;
@@ -50,7 +50,7 @@ sub _run {
         # Handle requests
         # Developers, be careful: Only this part is executed at each request
         return sub {
-            return $self->_authAndTrace(
+            return $self->_logAuthTrace(
                 Lemonldap::NG::Common::PSGI::Request->new( $_[0] ) );
         };
     }
@@ -68,7 +68,7 @@ sub _run {
         # Handle unprotected requests
         return sub {
             my $req = Lemonldap::NG::Common::PSGI::Request->new( $_[0] );
-            my $res = $self->handler($req);
+            my $res = $self->_logAndHandle($req);
             push @{ $res->[1] }, $req->spliceHdrs;
             return $res;
         };
@@ -111,6 +111,34 @@ sub reload {
     };
 }
 
+sub _logAuthTrace {
+    my ( $self, $req, $noCall ) = @_;
+
+    # register the request object to the logging system
+    if ( ref( $self->logger ) and $self->logger->can('setRequestObj') ) {
+        $self->logger->setRequestObj($req);
+    }
+    if ( ref( $self->userLogger ) and $self->userLogger->can('setRequestObj') )
+    {
+        $self->userLogger->setRequestObj($req);
+    }
+
+    # Call the handler
+    my $res = $self->_authAndTrace( $req, $noCall );
+
+    # Clear the logging system before the next request
+    if ( ref( $self->logger ) and $self->logger->can('clearRequestObj') ) {
+        $self->logger->clearRequestObj($req);
+    }
+    if ( ref( $self->userLogger )
+        and $self->userLogger->can('clearRequestObj') )
+    {
+        $self->userLogger->clearRequestObj($req);
+    }
+
+    return $res;
+}
+
 ## @method private PSGI-Response _authAndTrace($req)
 # Launch $self->api::run() and then handler() if
 # response is 200.
@@ -128,7 +156,7 @@ sub _authAndTrace {
     eval "require $type";
     die $@ if ($@);
     my ( $res, $session ) = $type->run( $req, $self->{rule} );
-    eval { $self->portal( $type->tsv->{portal}->() ) };
+    eval { $self->portal( $type->tsv->{portal}->() ) } unless $self->portal;
     $self->logger->warn($@)  if $@;
     $req->userData($session) if ($session);
 
@@ -138,7 +166,7 @@ sub _authAndTrace {
         }
         else {
             $self->logger->debug('User authenticated, calling handler()');
-            $res = $self->handler($req);
+            $res = $self->_logAndHandle($req);
             push @{ $res->[1] }, $req->spliceHdrs;
             return $res;
         }
@@ -201,7 +229,7 @@ sub userId {
     my $userId =
       $req->userData->{ $Lemonldap::NG::Handler::Main::tsv->{whatToTrace}
           || '_whatToTrace' }
-      || $req->userData->{'_user'} # Fix 2377
+      || $req->userData->{'_user'}    # Fix 2377
       || 'anonymous';
 
     $self->logger->debug("Returned userId: $userId");
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/ServiceToken.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/ServiceToken.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/ServiceToken.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/ServiceToken.pm	2022-02-19 16:04:21.000000000 +0000
@@ -7,7 +7,8 @@ our $VERSION = '2.0.9';
 sub fetchId {
     my ( $class, $req ) = @_;
     my $token = $req->{env}->{HTTP_X_LLNG_TOKEN};
-    return $class->Lemonldap::NG::Handler::Main::fetchId($req) unless ($token =~ /\w+/);
+    return $class->Lemonldap::NG::Handler::Main::fetchId($req)
+      unless ( $token =~ /\w+/ );
     $class->logger->debug("Found token: $token");
 
     # Decrypt token
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/StatusConstants.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/StatusConstants.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/StatusConstants.pm	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/StatusConstants.pm	2022-01-22 14:30:19.000000000 +0000
@@ -4,7 +4,7 @@ package Lemonldap::NG::Handler::Lib::Sta
 use strict;
 use Exporter 'import';
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 
 sub portalConsts {
     return {
@@ -22,6 +22,7 @@ sub portalConsts {
         '103' => 'PE_NO_SECOND_FACTORS',
         '104' => 'PE_BAD_DEVOPS_FILE',
         '105' => 'PE_FILENOTFOUND',
+        '106' => 'PE_OIDC_AUTH_ERROR',
         '2'   => 'PE_FORMEMPTY',
         '20'  => 'PE_NO_PASSWORD_BE',
         '21'  => 'PE_PP_ACCOUNT_LOCKED',
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/Status.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/Status.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/Status.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/Status.pm	2022-02-07 19:06:14.000000000 +0000
@@ -63,7 +63,7 @@ sub run {
 
             # Activity collect
             if (
-/^(\S+)\s+=>\s+(\S+)\s+(OK|REJECT|REDIRECT|LOGOUT|UNPROTECT|\-?\d+)$/
+/^(\S+)\s+=>\s+(\S+)\s+(OK|REJECT|REDIRECT|LOGOUT|UNPROTECT|SKIP|EXPIRED|\-?\d+)$/
               )
             {
                 my ( $user, $uri, $code ) = ( $1, $2, $3 );
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/ZimbraPreAuth.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/ZimbraPreAuth.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/ZimbraPreAuth.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Lib/ZimbraPreAuth.pm	2022-02-19 16:04:21.000000000 +0000
@@ -29,10 +29,10 @@ sub run {
     my $localConfig      = $class->localConfig;
     my $zimbraPreAuthKey = $localConfig->{zimbraPreAuthKey};
     my $zimbraAccountKey = $localConfig->{zimbraAccountKey} || 'uid';
-    my $zimbraBy         = $localConfig->{zimbraBy} || 'id';
-    my $zimbraUrl        = $localConfig->{zimbraUrl} || '/service/preauth';
+    my $zimbraBy         = $localConfig->{zimbraBy}         || 'id';
+    my $zimbraUrl        = $localConfig->{zimbraUrl}    || '/service/preauth';
     my $zimbraSsoUrl     = $localConfig->{zimbraSsoUrl} || '^/zimbrasso$';
-    my $timeout          = $localConfig->{'timeout'} || '0';
+    my $timeout          = $localConfig->{'timeout'}    || '0';
 
     # Remove trailing white-spaces
     $zimbraAccountKey =~ s/\s+$//;
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Init.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Init.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Init.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Init.pm	2022-02-19 16:04:21.000000000 +0000
@@ -50,18 +50,21 @@ sub init($$) {
 # Set log level for Lemonldap::NG logs
 sub logLevelInit {
     my ($class) = @_;
-    my $logger = $class->localConfig->{logger} ||= $class->defaultLogger;
+    my $logger  = $class->localConfig->{logger} ||= $class->defaultLogger;
     eval "require $logger";
     die $@ if ($@);
-    unless (
-        $class->localConfig->{logLevel} =~ /^(debug|info|notice|warn|error)$/ )
+    my $err;
+    unless ( $class->localConfig->{logLevel} =~
+        /^(?:debug|info|notice|warn|error)$/ )
     {
-        print STDERR 'Bad logLevel value \''
+        $err =
+            'Bad logLevel value \''
           . $class->localConfig->{logLevel}
           . "', switching to 'info'\n";
         $class->localConfig->{logLevel} = 'info';
     }
     $class->logger( $logger->new( $class->localConfig ) );
+    $class->logger->error($err) if $err;
     $class->logger->debug("Logger $logger loaded");
     $logger = $class->localConfig->{userLogger} || $logger;
     eval "require $logger";
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Jail.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Jail.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Jail.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Jail.pm	2022-02-19 16:04:21.000000000 +0000
@@ -26,7 +26,7 @@ has multiValuesSeparator => ( is => 'rw'
 has jail                 => ( is => 'rw' );
 has error                => ( is => 'rw' );
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 our @builtCustomFunctions;
 
 ## @imethod protected build_jail()
@@ -45,7 +45,7 @@ sub build_jail {
     $self->useSafeJail(1) unless defined $self->useSafeJail;
 
     if ($require) {
-        foreach my $f ( split /[, ]+/, $require ) {
+        foreach my $f ( split /[,\s]+/, $require ) {
             if ( $f =~ /^[\w\:]+$/ ) {
                 eval "require $f";
             }
@@ -63,7 +63,9 @@ sub build_jail {
 
     if ($build) {
         @builtCustomFunctions =
-          $self->customFunctions ? split( /\s+/, $self->customFunctions ) : ();
+          $self->customFunctions
+          ? split( /[,\s]+/, $self->customFunctions )
+          : ();
         foreach (@builtCustomFunctions) {
             no warnings 'redefine';
             $api->logger->debug("Custom function: $_");
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Reload.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Reload.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Reload.pm	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Reload.pm	2022-02-19 16:04:21.000000000 +0000
@@ -1,6 +1,6 @@
 package Lemonldap::NG::Handler::Main::Reload;
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 package Lemonldap::NG::Handler::Main;
 
@@ -65,7 +65,8 @@ sub checkConf {
         or $class->cfgNum != $conf->{cfgNum}
         or $class->cfgDate != $conf->{cfgDate} )
     {
-        $class->logger->debug("Get configuration $conf->{cfgNum}");
+        $class->logger->debug(
+            "Get configuration $conf->{cfgNum} aged $conf->{cfgDate}");
         unless ( $class->cfgNum( $conf->{cfgNum} )
             && $class->cfgDate( $conf->{cfgDate} ) )
         {
@@ -248,6 +249,8 @@ sub defaultValuesInit {
               $conf->{vhostOptions}->{$vhost}->{vhostServiceTokenTTL};
             $class->tsv->{accessToTrace}->{$vhost} =
               $conf->{vhostOptions}->{$vhost}->{vhostAccessToTrace};
+            $class->tsv->{devOpsRulesUrl}->{$vhost} =
+              $conf->{vhostOptions}->{$vhost}->{vhostDevOpsRulesUrl};
         }
     }
     return 1;
@@ -639,7 +642,7 @@ sub oauth2Init {
 
 sub substitute {
     my ( $class, $expr ) = @_;
-    $expr ||= '';
+    $expr //= '';
 
     # substitute special vars, just for retro-compatibility
     $expr =~ s/\$date\b/&date/sg;
@@ -656,7 +659,7 @@ sub substitute {
     $expr =~ s/\binGroup\(([^)]*)\)/listMatch(\$s->{'hGroups'},$1,1)/g;
 
     # handle has2f
-    $expr =~ s/\bhas2f\(([^),]*)\)/has2f(\$s,$1)/g;
+    $expr =~ s/\bhas2f\(([^),]*)\)/has2f_internal(\$s,$1)/g;
 
     return $expr;
 }
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Run.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Run.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Run.pm	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Run.pm	2022-02-19 16:04:21.000000000 +0000
@@ -1,7 +1,7 @@
 # Main running methods file
 package Lemonldap::NG::Handler::Main::Run;
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 package Lemonldap::NG::Handler::Main;
 
@@ -504,7 +504,7 @@ sub fetchId {
       if $class->_isHttps( $req, $vhost );
     my $lookForHttpCookie = ( $class->tsv->{securedCookie} =~ /^(2|3)$/
           and not $class->_isHttps( $req, $vhost ) );
-    my $cn = $class->tsv->{cookieName};
+    my $cn    = $class->tsv->{cookieName};
     my $value = $lookForHttpCookie    # Avoid prefix and bad cookie name (#2417)
       ? ( $t =~ /(?<![-.~])\b${cn}http=([^,; ]+)/o ? $1 : 0 )
       : ( $t =~ /(?<![-.~])\b$cn=([^,; ]+)/o       ? $1 : 0 );
@@ -536,8 +536,8 @@ sub retrieveSession {
     #     (15 seconds)
     if (    defined $class->data->{_session_id}
         and $id eq $class->data->{_session_id}
-        and
-        ( $now - $class->dataUpdate < $class->tsv->{handlerInternalCache} ) )
+        and ( $now - $class->dataUpdate < $class->tsv->{handlerInternalCache} )
+      )
     {
         $class->logger->debug("Get session $id from Handler internal cache");
         return $class->data;
@@ -734,10 +734,10 @@ sub sendHeaders {
           $class->tsv->{forgeHeaders}->{$vhost}->( $req, $session );
         foreach my $h ( sort keys %headers ) {
             if ( defined( my $v = $headers{$h} ) ) {
-                $class->logger->debug("Send header $h with value $v");
+                $class->logger->debug("Send header '$h' with value '$v'");
             }
             else {
-                $class->logger->debug("Send header $h with empty value");
+                $class->logger->debug("Send header '$h' with an empty value");
             }
         }
         $class->set_header_in( $req, %headers );
@@ -898,7 +898,9 @@ sub postJavascript {
     my $filler;
     foreach my $name ( keys %$data ) {
         use bytes;
-        my $value = "x" x bytes::length( $data->{$name} );
+        my @characterSet = ( '0' .. '9', 'A' .. 'Z', 'a' .. 'z' );
+        my $value        = join '' => map $characterSet[ rand @characterSet ],
+          1 .. bytes::length( $data->{$name} );
         $filler .=
 "form.find('input[name=\"$name\"], select[name=\"$name\"], textarea[name=\"$name\"]').val('$value')\n";
     }
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/PSGI/Try.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/PSGI/Try.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/PSGI/Try.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/PSGI/Try.pm	2022-02-19 16:04:21.000000000 +0000
@@ -3,22 +3,38 @@ package Lemonldap::NG::Handler::PSGI::Tr
 use strict;
 use Mouse;
 
-our $VERSION = '2.0.6';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Handler::PSGI::Router';
 
 has 'authRoutes' => (
-    is  => 'rw',
-    isa => 'HashRef',
-    default =>
-      sub { { GET => {}, POST => {}, PUT => {}, DELETE => {}, OPTIONS => {} } }
+    is      => 'rw',
+    isa     => 'HashRef',
+    default => sub {
+        {
+            GET     => {},
+            POST    => {},
+            PUT     => {},
+            PATCH   => {},
+            DELETE  => {},
+            OPTIONS => {}
+        }
+    }
 );
 
 has 'unAuthRoutes' => (
-    is  => 'rw',
-    isa => 'HashRef',
-    default =>
-      sub { { GET => {}, POST => {}, PUT => {}, DELETE => {}, OPTIONS => {} } }
+    is      => 'rw',
+    isa     => 'HashRef',
+    default => sub {
+        {
+            GET     => {},
+            POST    => {},
+            PUT     => {},
+            PATCH   => {},
+            DELETE  => {},
+            OPTIONS => {}
+        }
+    }
 );
 
 sub addRoute {
@@ -69,7 +85,7 @@ sub _run {
 
     return sub {
         my $req = Lemonldap::NG::Common::PSGI::Request->new( $_[0] );
-        my $res = $self->_authAndTrace( $req, 1 );
+        my $res = $self->_logAuthTrace( $req, 1 );
         if ( $res->[0] < 300 ) {
             $self->routes( $self->authRoutes );
             $req->userData( $self->api->data );
@@ -87,10 +103,11 @@ sub _run {
         else {
             return $res;
         }
-        $res = $self->handler($req);
+        $res = $self->_logAndHandle($req);
         push @{ $res->[1] }, $req->spliceHdrs;
         return $res;
     };
+
 }
 
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/PSGI.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/PSGI.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/PSGI.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/PSGI.pm	2022-02-07 19:06:14.000000000 +0000
@@ -9,14 +9,14 @@ use Lemonldap::NG::Handler::PSGI::Main;
 
 extends 'Lemonldap::NG::Handler::Lib::PSGI', 'Lemonldap::NG::Common::PSGI';
 
-our $VERSION = '2.0.10';
+our $VERSION = '2.0.14';
 
 sub init {
     my ( $self, $args ) = @_;
     $self->api('Lemonldap::NG::Handler::PSGI::Main') unless ( $self->api );
-    my $tmp = ( $self->Lemonldap::NG::Common::PSGI::init($args)
-          and $self->Lemonldap::NG::Handler::Lib::PSGI::init($args) );
-    return $tmp;
+    return 0 unless $self->Lemonldap::NG::Handler::Lib::PSGI::init($args);
+
+    return $self->Lemonldap::NG::Common::PSGI::init( $self->api->localConfig );
 }
 
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Server/Nginx.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Server/Nginx.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Server/Nginx.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Server/Nginx.pm	2022-02-19 16:04:21.000000000 +0000
@@ -17,7 +17,7 @@ sub init {
 }
 
 ## @method void _run()
-# Return a subroutine that call _authAndTrace() and tranform redirection
+# Return a subroutine that call _logAuthTrace() and tranform redirection
 # response code from 302 to 401 (not authenticated) ones. This is required
 # because Nginx "auth_request" parameter does not accept it. The Nginx
 # configuration file should transform them back to 302 using:
@@ -31,7 +31,7 @@ sub _run {
     return sub {
         my $req = $_[0];
         $self->logger->debug('New request');
-        my $res = $self->_authAndTrace(
+        my $res = $self->_logAuthTrace(
             Lemonldap::NG::Common::PSGI::Request->new($req) );
 
         # Transform 302 responses in 401 since Nginx refuse it
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Server.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Server.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Server.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Server.pm	2022-02-19 16:04:21.000000000 +0000
@@ -25,7 +25,7 @@ sub _run {
     my ($self) = @_;
     return sub {
         my $req = Lemonldap::NG::Common::PSGI::Request->new( $_[0] );
-        my $res = $self->_authAndTrace($req);
+        my $res = $self->_logAuthTrace($req);
         push @{ $res->[1] }, $req->spliceHdrs,
           Cookie => ( $req->{Cookie} // '' );
         return $res;
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler.pm 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/lib/Lemonldap/NG/Handler.pm	2021-08-20 16:29:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/lib/Lemonldap/NG/Handler.pm	2022-02-19 16:43:01.000000000 +0000
@@ -3,7 +3,7 @@ package Lemonldap::NG::Handler;
 # Use the appropriate handler
 # For Apache, use Lemonldap::NG::Handler::ApacheMP2
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 
 1;
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/Makefile.PL 2.0.14+ds-1/lemonldap-ng-handler/Makefile.PL
--- 2.0.13+ds-3/lemonldap-ng-handler/Makefile.PL	2021-08-20 16:29:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/Makefile.PL	2022-02-21 12:04:41.000000000 +0000
@@ -32,14 +32,14 @@ WriteMakefile(
             },
             MailingList => 'mailto:lemonldap-ng-dev@ow2.org',
             license     => 'http://opensource.org/licenses/GPL-2.0',
-            homepage    => 'http://lemonldap-ng.org/',
+            homepage    => 'https://lemonldap-ng.org/',
             bugtracker =>
               'https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues',
             x_twitter => 'https://twitter.com/lemonldapng',
         },
     },
     PREREQ_PM => {
-        'Lemonldap::NG::Common' => '2.0.13',
+        'Lemonldap::NG::Common' => '2.0.14',
         'LWP::UserAgent'        => 0,
         'Mouse'                 => 0,
         'URI'                   => 0,
@@ -51,7 +51,7 @@ WriteMakefile(
             ABSTRACT_FROM =>
               'lib/Lemonldap/NG/Handler.pm',    # retrieve abstract from module
             AUTHOR =>
-'Xavier Guimard <x.guimard@free.fr>, Clément Oudot <clement@oodo.net>'
+'Xavier Guimard <x.guimard@free.fr>, Clement Oudot <clement@oodo.net>, Christophe Maudoux <chrmdx@gmail.com>, Maxime Besson <maxime.besson@worteks.com>'
           )
         : ()
     ),
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/MANIFEST 2.0.14+ds-1/lemonldap-ng-handler/MANIFEST
--- 2.0.13+ds-3/lemonldap-ng-handler/MANIFEST	2021-07-22 17:21:57.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/MANIFEST	2022-01-22 15:57:14.000000000 +0000
@@ -67,18 +67,21 @@ t/01-Lemonldap-NG-Handler-Main.t
 t/05-Lemonldap-NG-Handler-Reload.t
 t/12-Lemonldap-NG-Handler-Jail.t
 t/13-Lemonldap-NG-Handler-Fake-Safe.t
+t/14-Lemonldap-NG-Handler-Rule-Building.t
 t/50-Lemonldap-NG-Handler-SecureToken.t
 t/51-Lemonldap-NG-Handler-Zimbra.t
 t/60-Lemonldap-NG-Handler-PSGI.t
 t/61-Lemonldap-NG-Handler-PSGI-Server.t
 t/62-Lemonldap-NG-Handler-Nginx.t
 t/63-Lemonldap-NG-Handler-PSGI-Try.t
+t/64-Lemonldap-NG-Handler-PSGI-DevOps-vhostOptions.t
+t/64-Lemonldap-NG-Handler-PSGI-DevOps-with-param.t
 t/64-Lemonldap-NG-Handler-PSGI-DevOps.t
 t/65-Lemonldap-NG-Handler-Nginx-ServiceToken.t
 t/65-Lemonldap-NG-Handler-PSGI-ServiceToken.t
 t/66-Lemonldap-NG-Handler-PSGI-wildcard.t
-t/67-Lemonldap-NG-Handler-PSGI-vhostoptions-with-reload.t
-t/67-Lemonldap-NG-Handler-PSGI-vhostoptions.t
+t/67-Lemonldap-NG-Handler-PSGI-vhostOptions-with-reload.t
+t/67-Lemonldap-NG-Handler-PSGI-vhostOptions.t
 t/68-Lemonldap-NG-Handler-PSGI-Zimbra.t
 t/69-Lemonldap-NG-Handler-PSGI-SecureToken.t
 t/70-Lemonldap-NG-Handler-PSGI-AuthBasic.t
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/META.json 2.0.14+ds-1/lemonldap-ng-handler/META.json
--- 2.0.13+ds-3/lemonldap-ng-handler/META.json	2021-08-20 16:29:36.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/META.json	2022-02-21 12:04:41.000000000 +0000
@@ -1,7 +1,7 @@
 {
    "abstract" : "The Apache protection module part of Lemonldap::NG Web-SSO system.",
    "author" : [
-      "Xavier Guimard <x.guimard@free.fr>, ClÃ©ment Oudot <clement@oodo.net>"
+      "Xavier Guimard <x.guimard@free.fr>, Clement Oudot <clement@oodo.net>, Christophe Maudoux <chrmdx@gmail.com>, Maxime Besson <maxime.besson@worteks.com>"
    ],
    "dynamic_config" : 1,
    "generated_by" : "ExtUtils::MakeMaker version 7.34, CPAN::Meta::Converter version 2.150010",
@@ -45,7 +45,7 @@
          },
          "requires" : {
             "LWP::UserAgent" : "0",
-            "Lemonldap::NG::Common" : "v2.0.13",
+            "Lemonldap::NG::Common" : "v2.0.14",
             "Mouse" : "0",
             "URI" : "0"
          }
@@ -57,12 +57,12 @@
       "bugtracker" : {
          "web" : "https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues"
       },
-      "homepage" : "http://lemonldap-ng.org/",
+      "homepage" : "https://lemonldap-ng.org/",
       "license" : [
          "http://opensource.org/licenses/GPL-2.0"
       ],
       "x_MailingList" : "mailto:lemonldap-ng-dev@ow2.org"
    },
-   "version" : "v2.0.13",
+   "version" : "v2.0.14",
    "x_serialization_backend" : "JSON::PP version 4.04"
 }
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/META.yml 2.0.14+ds-1/lemonldap-ng-handler/META.yml
--- 2.0.13+ds-3/lemonldap-ng-handler/META.yml	2021-08-20 16:29:36.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/META.yml	2022-02-21 12:04:41.000000000 +0000
@@ -1,7 +1,7 @@
 ---
 abstract: 'The Apache protection module part of Lemonldap::NG Web-SSO system.'
 author:
-  - 'Xavier Guimard <x.guimard@free.fr>, ClÃ©ment Oudot <clement@oodo.net>'
+  - 'Xavier Guimard <x.guimard@free.fr>, Clement Oudot <clement@oodo.net>, Christophe Maudoux <chrmdx@gmail.com>, Maxime Besson <maxime.besson@worteks.com>'
 build_requires:
   Cwd: '0'
   Digest::HMAC_SHA1: '0'
@@ -30,14 +30,14 @@ recommends:
   SOAP::Lite: '0'
 requires:
   LWP::UserAgent: '0'
-  Lemonldap::NG::Common: v2.0.13
+  Lemonldap::NG::Common: v2.0.14
   Mouse: '0'
   URI: '0'
 resources:
   MailingList: mailto:lemonldap-ng-dev@ow2.org
   X_twitter: https://twitter.com/lemonldapng
   bugtracker: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues
-  homepage: http://lemonldap-ng.org/
+  homepage: https://lemonldap-ng.org/
   license: http://opensource.org/licenses/GPL-2.0
-version: v2.0.13
+version: v2.0.14
 x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/README 2.0.14+ds-1/lemonldap-ng-handler/README
--- 2.0.13+ds-3/lemonldap-ng-handler/README	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/README	2022-02-21 12:04:41.000000000 +0000
@@ -3,7 +3,7 @@ LemonLDAP::NG
 
 LemonLDAP::NG is a modular Web-SSO based on Apache::Session modules.
 This is the handler part of it. You can find documentation here:
- * for administrators: http://lemonldap-ng.org/
+ * for administrators: https://lemonldap-ng.org/
  * for developers: see embedded perldoc
 
 LemonLDAP::NG is a free software; you can redistribute it and/or modify
@@ -20,7 +20,9 @@ You should have received a copy of the G
 along with this program.  If not, see L<http://www.gnu.org/licenses/>.
 
 Copyright:
- * 2005-2015 by Xavier Guimard and Clément Oudot
+ * 2005-2022 by Xavier Guimard and Clément Oudot
+ * 2018-2022 by Christophe Maudoux
+ * 2019-2022 by Maxime Besson
  * 2008-2011 by Thomas Chemineau
  * 2012-2015 by François-Xavier Deltombe and Sandro Cazzaniga
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/05-Lemonldap-NG-Handler-Reload.t 2.0.14+ds-1/lemonldap-ng-handler/t/05-Lemonldap-NG-Handler-Reload.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/05-Lemonldap-NG-Handler-Reload.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/05-Lemonldap-NG-Handler-Reload.t	2022-02-19 16:04:21.000000000 +0000
@@ -26,7 +26,7 @@ sub Lemonldap::NG::Handler::Main::defaul
     'Lemonldap::NG::Common::Logger::Std';
 }
 
-eval { Lemonldap::NG::Handler::Main->logLevelInit('error') };
+eval { Lemonldap::NG::Handler::Main->logLevelInit() };
 ok( !$@, 'logLevelInit' );
 
 ok(
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/12-Lemonldap-NG-Handler-Jail.t 2.0.14+ds-1/lemonldap-ng-handler/t/12-Lemonldap-NG-Handler-Jail.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/12-Lemonldap-NG-Handler-Jail.t	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/12-Lemonldap-NG-Handler-Jail.t	2022-02-19 16:04:21.000000000 +0000
@@ -36,7 +36,7 @@ ok(
     ( defined($code) and ref($code) eq 'CODE' ),
     'encode_base64 function is defined'
 );
-ok( $res = &$code, "Function works" );
+ok( $res = &$code,      "Function works" );
 ok( $res eq 'dGVzdA==', 'Get good result' );
 
 $sub  = "sub { return ( listMatch('ABC; DEF; GHI','abc',1) ) }";
@@ -58,7 +58,7 @@ ok(
     'checkDate extended function is defined'
 );
 ok( $res = &$code, "Function works" );
-ok( $res == 1, 'Get good result' );
+ok( $res == 1,     'Get good result' );
 
 $sub = "sub { return(checkDate('20000101000000+0100','21000101000000+0100')) }";
 $code = $jail->jail_reval($sub);
@@ -67,9 +67,9 @@ ok(
     'checkDate extended function is defined'
 );
 ok( $res = &$code, "Function works" );
-ok( $res == 1, 'Get good result' );
+ok( $res == 1,     'Get good result' );
 
-$sub  = "sub { return(has2f(\$_[0],\$_[1])) }";
+$sub  = "sub { return(has2f_internal(\$_[0],\$_[1])) }";
 $code = $jail->jail_reval($sub);
 ok(
     ( defined($code) and ref($code) eq 'CODE' ),
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/13-Lemonldap-NG-Handler-Fake-Safe.t 2.0.14+ds-1/lemonldap-ng-handler/t/13-Lemonldap-NG-Handler-Fake-Safe.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/13-Lemonldap-NG-Handler-Fake-Safe.t	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/13-Lemonldap-NG-Handler-Fake-Safe.t	2022-02-07 19:06:14.000000000 +0000
@@ -62,7 +62,7 @@ ok( ( defined($listMatch) and ref($listM
 ok( &$listMatch eq '0', 'Get good result' );
 
 # Test has2f method
-my $sub7  = "sub { return(has2f(\$_[0],\$_[1])) }";
+my $sub7  = "sub { return(has2f_internal(\$_[0],\$_[1])) }";
 my $has2f = $jail->jail_reval($sub7);
 ok(
     ( defined($has2f) and ref($has2f) eq 'CODE' ),
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/14-Lemonldap-NG-Handler-Rule-Building.t 2.0.14+ds-1/lemonldap-ng-handler/t/14-Lemonldap-NG-Handler-Rule-Building.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/14-Lemonldap-NG-Handler-Rule-Building.t	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/14-Lemonldap-NG-Handler-Rule-Building.t	2022-01-22 14:30:19.000000000 +0000
@@ -0,0 +1,104 @@
+# Before `make install' is performed this script should be runnable with
+# `make test'. After `make install' it should work as `perl Lemonldap-NG-Handler-SharedConf.t'
+
+#########################
+
+# change 'tests => 1' to 'tests => last_test_to_print';
+
+package main;
+use strict;
+use warnings;
+require 't/test.pm';
+
+use Test::More tests => 17;
+BEGIN { use_ok('Lemonldap::NG::Handler::Main') }
+
+# get a standard basic configuration in $args hashref
+use Cwd 'abs_path';
+use File::Basename;
+use lib dirname( abs_path $0 );
+
+#########################
+
+# Insert your test code below, the Test::More module is used here so read
+# its man page (perldoc Test::More) for help writing this test script.
+my $h;
+$h = 'Lemonldap::NG::Handler::Test';
+$ENV{SERVER_NAME} = "test1.example.com";
+
+#open STDERR, '>/dev/null';
+
+my $conf = {
+    cfgNum        => 1,
+    logLevel      => 'error',
+    portal        => 'http://auth.example.com/',
+    globalStorage => 'Apache::Session::File',
+    post          => {},
+    key           => 1,
+    locationRules => {
+        'test1.example.com' => {
+
+            # Basic rules
+            'default' => 'accept',
+            '^/no'    => 'deny',
+            'test'    => '$groups =~ /\badmin\b/',
+
+            # Bad ordered rules
+            '^/a/a' => 'deny',
+            '^/a'   => 'accept',
+
+            # Good ordered rules
+            '(?#1 first)^/b/a' => 'deny',
+            '(?#2 second)^/b'  => 'accept',
+        },
+    },
+};
+
+eval { $h->localConfig($conf); $h->logLevelInit() };
+ok( !$@,                     'init' );
+ok( $h->configReload($conf), 'Load conf' );
+
+#########################
+# Make sure that the rule building idiom (substitute/buildRule) yields
+# consistent results in edge cases see #2595
+
+sub compileRule {
+    my $rule = shift;
+    return $h->buildSub( $h->substitute($rule) );
+}
+
+# Undef expression yields a sub that returns undef
+my $r = compileRule(undef);
+is( ref($r), "CODE", "Returned code ref" );
+is( $r->(),  undef,  "Returned undef" );
+
+# empty expression yields a sub that returns undef
+$r = compileRule("");
+is( ref($r), "CODE", "Returned code ref" );
+is( $r->(),  undef,  "Returned undef" );
+
+# empty string yields a sub that returns undef
+$r = compileRule("\"\"");
+is( ref($r), "CODE", "Returned code ref" );
+is( $r->(),  "",     "Returned empty string" );
+
+# 0 expression returns a sub that returns 0
+$r = compileRule("0");
+is( ref($r), "CODE", "Returned code ref" );
+is( $r->(),  0,      "Returned 0" );
+
+# string expression returns a sub that returns the string
+#
+$r = compileRule("\"abc def\"");
+is( ref($r), "CODE",    "Returned code ref" );
+is( $r->(),  "abc def", "Returned abc def" );
+
+# Access sessionInfo
+$r = compileRule('$foo');
+is( ref($r),                      "CODE", "Returned code ref" );
+is( $r->( {}, { foo => "bar" } ), "bar",  "Returned bar" );
+
+# Access request
+$r = compileRule('$ENV{foo}');
+is( ref($r),                                 "CODE", "Returned code ref" );
+is( $r->( { env => { foo => "bar" } }, {} ), "bar",  "Returned bar" );
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/60-Lemonldap-NG-Handler-PSGI.t 2.0.14+ds-1/lemonldap-ng-handler/t/60-Lemonldap-NG-Handler-PSGI.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/60-Lemonldap-NG-Handler-PSGI.t	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/60-Lemonldap-NG-Handler-PSGI.t	2022-02-19 16:04:21.000000000 +0000
@@ -15,7 +15,7 @@ my $SKIPUSER = 0;
 # --------------------
 ok( $res = $client->_get('/'), 'Unauthentified query' );
 ok( ref($res) eq 'ARRAY', 'Response is an array' ) or explain( $res, 'array' );
-ok( $res->[0] == 302, ' Code is 302' ) or explain( $res->[0], 302 );
+ok( $res->[0] == 302,     ' Code is 302' )         or explain( $res->[0], 302 );
 my %h = @{ $res->[1] };
 ok(
     $h{Location} eq 'http://auth.example.com/?url='
@@ -224,8 +224,13 @@ ok( $res->[0] == 200, ' Code is 200' ) o
 count(2);
 
 # Forged headers
-ok( $res = $client->_get( '/skipif/zz', undef, 'test1.example.com', undef, HTTP_AUTH_USER => 'rtyler' ),
-    'Test skip() with forged header' );
+ok(
+    $res = $client->_get(
+        '/skipif/zz', undef, 'test1.example.com', undef,
+        HTTP_AUTH_USER => 'rtyler'
+    ),
+    'Test skip() with forged header'
+);
 ok( $res->[0] == 200, ' Code is 200' ) or explain( $res, 200 );
 count(2);
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/61-Lemonldap-NG-Handler-PSGI-Server.t 2.0.14+ds-1/lemonldap-ng-handler/t/61-Lemonldap-NG-Handler-PSGI-Server.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/61-Lemonldap-NG-Handler-PSGI-Server.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/61-Lemonldap-NG-Handler-PSGI-Server.t	2022-02-19 16:04:21.000000000 +0000
@@ -13,7 +13,7 @@ my $res;
 # --------------------
 ok( $res = $client->_get('/'), 'Unauthentified query' );
 ok( ref($res) eq 'ARRAY', 'Response is an array' ) or explain( $res, 'array' );
-ok( $res->[0] == 302, 'Code is 302' ) or explain( $res->[0], 302 );
+ok( $res->[0] == 302,     'Code is 302' )          or explain( $res->[0], 302 );
 my %h = @{ $res->[1] };
 ok(
     $h{Location} eq 'http://auth.example.com/?url='
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/62-Lemonldap-NG-Handler-Nginx.t 2.0.14+ds-1/lemonldap-ng-handler/t/62-Lemonldap-NG-Handler-Nginx.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/62-Lemonldap-NG-Handler-Nginx.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/62-Lemonldap-NG-Handler-Nginx.t	2022-02-19 16:04:21.000000000 +0000
@@ -16,7 +16,7 @@ my $res;
 # Unauthentified query
 ok( $res = $client->_get('/'), 'Unauthentified query' );
 ok( ref($res) eq 'ARRAY', 'Response is an array' ) or explain( $res, 'array' );
-ok( $res->[0] == 401, 'Code is 401' ) or explain( $res->[0], 401 );
+ok( $res->[0] == 401,     'Code is 401' )          or explain( $res->[0], 401 );
 my %h = @{ $res->[1] };
 ok(
     $h{Location} eq 'http://auth.example.com/?url='
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/63-Lemonldap-NG-Handler-PSGI-Try.t 2.0.14+ds-1/lemonldap-ng-handler/t/63-Lemonldap-NG-Handler-PSGI-Try.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/63-Lemonldap-NG-Handler-PSGI-Try.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/63-Lemonldap-NG-Handler-PSGI-Try.t	2022-02-19 16:04:21.000000000 +0000
@@ -39,7 +39,7 @@ my $res;
 
 # Unauth tests
 ok( $res = $client->_get('/test'), 'Get response' );
-ok( $res->[0] == 200, 'Response code is 200' )
+ok( $res->[0] == 200,              'Response code is 200' )
   or print "Expect 200, got $res->[0]\n";
 ok( $res->[2]->[0] eq 'Unauth', 'Get unauth result' )
   or print "Expect Unauth, got $res->[2]->[0]\n";
@@ -64,7 +64,7 @@ count(3);
 # Bad path test
 
 ok( $res = $client->_get('/[]/test'), 'Try a bad path' );
-ok( $res->[0] == 400, 'Response is 400' );
+ok( $res->[0] == 400,                 'Response is 400' );
 count(2);
 
 clean();
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/64-Lemonldap-NG-Handler-PSGI-DevOps.t 2.0.14+ds-1/lemonldap-ng-handler/t/64-Lemonldap-NG-Handler-PSGI-DevOps.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/64-Lemonldap-NG-Handler-PSGI-DevOps.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/64-Lemonldap-NG-Handler-PSGI-DevOps.t	2022-02-19 16:04:21.000000000 +0000
@@ -2,7 +2,6 @@ use Test::More;
 use JSON;
 use MIME::Base64;
 use LWP::UserAgent;
-use Data::Dumper;
 
 BEGIN {
     require 't/test-psgi-lib.pm';
@@ -17,7 +16,7 @@ ok(
     $res = $client->_get(
         '/',                 undef,
         'test3.example.com', "lemonldap=$sessionId",
-        VHOSTTYPE => 'DevOps'
+        VHOSTTYPE => 'DevOps',
     ),
     'Authorized query'
 );
@@ -35,7 +34,7 @@ ok(
     $res = $client->_get(
         '/testyes',          undef,
         'test3.example.com', "lemonldap=$sessionId",
-        VHOSTTYPE => 'DevOps'
+        VHOSTTYPE => 'DevOps',
     ),
     'Authorized query'
 );
@@ -47,7 +46,7 @@ ok(
     $res = $client->_get(
         '/deny',             undef,
         'test3.example.com', "lemonldap=$sessionId",
-        VHOSTTYPE => 'DevOps'
+        VHOSTTYPE => 'DevOps',
     ),
     'Denied query'
 );
@@ -58,7 +57,7 @@ ok(
     $res = $client->_get(
         '/testno',           undef,
         'test3.example.com', "lemonldap=$sessionId",
-        VHOSTTYPE => 'DevOps'
+        VHOSTTYPE => 'DevOps',
     ),
     'Denied query'
 );
@@ -74,6 +73,12 @@ no warnings 'redefine';
 
 sub LWP::UserAgent::request {
     my ( $self, $req ) = @_;
+    ok( $req->header('host') eq 'test3.example.com', 'Host header found' )
+      or explain( $req->headers(), 'test3.example.com' );
+    ok( $req->as_string() =~ m#http://127.0.0.1:80/rules.json#,
+        'Rules file URL found' )
+      or explain( $req->as_string(), 'GET http://127.0.0.1:80/rules.json' );
+    count(2);
     my $httpResp;
     my $s = '{
   "rules": {
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/64-Lemonldap-NG-Handler-PSGI-DevOps-vhostOptions.t 2.0.14+ds-1/lemonldap-ng-handler/t/64-Lemonldap-NG-Handler-PSGI-DevOps-vhostOptions.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/64-Lemonldap-NG-Handler-PSGI-DevOps-vhostOptions.t	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/64-Lemonldap-NG-Handler-PSGI-DevOps-vhostOptions.t	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,111 @@
+use Test::More;
+use JSON;
+use MIME::Base64;
+use LWP::UserAgent;
+
+BEGIN {
+    require 't/test-psgi-lib.pm';
+}
+
+init(
+    'Lemonldap::NG::Handler::Server',
+    {
+        vhostOptions => {
+            'test3.example.com' => {
+                vhostDevOpsRulesUrl => 'http://devops.example.com/myfile.json',
+            },
+        },
+    }
+);
+
+my $res;
+
+# Authorized queries
+ok(
+    $res = $client->_get(
+        '/',                 undef,
+        'test3.example.com', "lemonldap=$sessionId",
+        VHOSTTYPE => 'DevOps',
+    ),
+    'Authorized query'
+);
+ok( $res->[0] == 200, 'Code is 200' ) or explain( $res->[0], 200 );
+my %headers = @{ $res->[1] };
+ok( $headers{User} eq 'dwho', "'User' => 'dwho'" )
+  or explain( \%headers, 'dwho' );
+ok( $headers{Name} eq '', "'Name' => ''" ) or explain( \%headers, 'No Name' );
+ok( $headers{Mail} eq '', "'Mail' => ''" ) or explain( \%headers, 'No Mail' );
+ok( keys %headers == 7,   "Seven headers sent" )
+  or explain( \%headers, 'Seven headers' );
+count(6);
+
+ok(
+    $res = $client->_get(
+        '/testyes',          undef,
+        'test3.example.com', "lemonldap=$sessionId",
+        VHOSTTYPE => 'DevOps',
+    ),
+    'Authorized query'
+);
+ok( $res->[0] == 200, 'Code is 200' ) or explain( $res->[0], 200 );
+count(2);
+
+# Denied queries
+ok(
+    $res = $client->_get(
+        '/deny',             undef,
+        'test3.example.com', "lemonldap=$sessionId",
+        VHOSTTYPE => 'DevOps',
+    ),
+    'Denied query'
+);
+ok( $res->[0] == 403, 'Code is 403' ) or explain( $res->[0], 403 );
+count(2);
+
+ok(
+    $res = $client->_get(
+        '/testno',           undef,
+        'test3.example.com', "lemonldap=$sessionId",
+        VHOSTTYPE => 'DevOps',
+    ),
+    'Denied query'
+);
+ok( $res->[0] == 403, 'Code is 403' ) or explain( $res->[0], 403 );
+count(2);
+
+done_testing( count() );
+
+clean();
+
+# Redefine LWP methods for tests
+no warnings 'redefine';
+
+sub LWP::UserAgent::request {
+    my ( $self, $req ) = @_;
+    ok( $req->header('host') eq 'devops.example.com', 'Host header found' )
+      or explain( $req->headers(), 'devops.example.com' );
+    ok( $req->as_string() =~ m#http://devops.example.com/myfile.json#,
+        'Rules file URL found' )
+      or
+      explain( $req->as_string(), 'GET http://devops.example.com/myfile.json' );
+    count(2);
+    my $httpResp;
+    my $s = '{
+  "rules": {
+    "^/deny": "deny",
+    "^/testno": "$uid ne qq{dwho}",
+    "^/testyes": "$uid eq qq{dwho}",
+    "default": "accept"
+  },
+  "headers": {
+    "User": "$uid",
+    "Mail": "$mail",
+    "Name": "$cn"
+  }
+}';
+    $httpResp = HTTP::Response->new( 200, 'OK' );
+    $httpResp->header( 'Content-Type',   'application/json' );
+    $httpResp->header( 'Content-Length', length($s) );
+    $httpResp->content($s);
+    return $httpResp;
+}
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/64-Lemonldap-NG-Handler-PSGI-DevOps-with-param.t 2.0.14+ds-1/lemonldap-ng-handler/t/64-Lemonldap-NG-Handler-PSGI-DevOps-with-param.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/64-Lemonldap-NG-Handler-PSGI-DevOps-with-param.t	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/64-Lemonldap-NG-Handler-PSGI-DevOps-with-param.t	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,160 @@
+use Test::More;
+use JSON;
+use MIME::Base64;
+use LWP::UserAgent;
+
+BEGIN {
+    require 't/test-psgi-lib.pm';
+}
+
+### vhostOptions are overridden by fastcgi_param
+init(
+    'Lemonldap::NG::Handler::Server',
+    {
+        #logLevel     => 'debug',
+        vhostOptions => {
+            'test3.example.com' => {
+                vhostHttps          => 0,
+                vhostPort           => 80,
+                vhostDevOpsRulesUrl =>
+                  'http://donotuse.example.com/myfile.json',
+            },
+        },
+    }
+);
+
+my $res;
+
+# Unauthorized queries
+ok(
+    $res = $client->_get(
+        '/',                 undef,
+        'test3.example.com', undef,
+        VHOSTTYPE => 'DevOps',
+        RULES_URL => 'http://devops.example.com/file.json'
+    ),
+    'Unauthorized query'
+);
+ok( $res->[0] == 302, 'Code is 302' ) or explain( $res->[0], 302 );
+${ $res->[1] }[1] =~ m#http://auth\.example\.com/\?url=(.+?)%#;
+ok( decode_base64 $1 eq 'http://test3.example.com/', 'Redirect URL found' )
+  or explain( decode_base64 $1, 'http://test3.example.com/' );
+count(3);
+
+Time::Fake->offset("+700s");
+
+ok(
+    $res = $client->_get(
+        '/',                 undef,
+        'test3.example.com', undef,
+        HTTPS_REDIRECT => 'on',
+        PORT_REDIRECT  => 8443,
+        VHOSTTYPE      => 'DevOps',
+        RULES_URL      => 'http://devops.example.com/file.json'
+    ),
+    'Unauthorized query 2'
+);
+ok( $res->[0] == 302, 'Code is 302' ) or explain( $res->[0], 302 );
+${ $res->[1] }[1] =~ m#http://auth\.example\.com/\?url=(.+?)%#;
+ok( decode_base64 $1 eq 'https://test3.example.com:8443/',
+    'Redirect URL found' )
+  or explain( decode_base64 $1, 'https://test3.example.com:8443/' );
+count(3);
+
+# Authorized queries
+ok(
+    $res = $client->_get(
+        '/',                 undef,
+        'test3.example.com', "lemonldap=$sessionId",
+        VHOSTTYPE => 'DevOps',
+        RULES_URL => 'http://devops.example.com/file.json'
+    ),
+    'Authorized query'
+);
+ok( $res->[0] == 200, 'Code is 200' ) or explain( $res->[0], 200 );
+my %headers = @{ $res->[1] };
+ok( $headers{User} eq 'dwho', "'User' => 'dwho'" )
+  or explain( \%headers, 'dwho' );
+ok( $headers{Name} eq '', "'Name' => ''" ) or explain( \%headers, 'No Name' );
+ok( $headers{Mail} eq '', "'Mail' => ''" ) or explain( \%headers, 'No Mail' );
+ok( keys %headers == 7,   "Seven headers sent" )
+  or explain( \%headers, 'Seven headers' );
+count(6);
+
+ok(
+    $res = $client->_get(
+        '/testyes',          undef,
+        'test3.example.com', "lemonldap=$sessionId",
+        VHOSTTYPE => 'DevOps',
+        RULES_URL => 'http://devops.example.com/file.json'
+    ),
+    'Authorized query'
+);
+ok( $res->[0] == 200, 'Code is 200' ) or explain( $res->[0], 200 );
+count(2);
+
+Time::Fake->offset("+100s");
+
+# Denied queries
+ok(
+    $res = $client->_get(
+        '/deny',             undef,
+        'test3.example.com', "lemonldap=$sessionId",
+        VHOSTTYPE => 'DevOps',
+        RULES_URL => 'http://devops.example.com/file.json'
+    ),
+    'Denied query'
+);
+ok( $res->[0] == 403, 'Code is 403' ) or explain( $res->[0], 403 );
+count(2);
+
+Time::Fake->offset("+600s");
+
+ok(
+    $res = $client->_get(
+        '/testno',           undef,
+        'test3.example.com', "lemonldap=$sessionId",
+        VHOSTTYPE => 'DevOps',
+        RULES_URL => 'http://devops.example.com/file.json'
+    ),
+    'Denied query'
+);
+ok( $res->[0] == 403, 'Code is 403' ) or explain( $res->[0], 403 );
+count(2);
+
+done_testing( count() );
+
+clean();
+
+# Redefine LWP methods for tests
+no warnings 'redefine';
+
+sub LWP::UserAgent::request {
+    my ( $self, $req ) = @_;
+    ok( $req->header('host') eq 'devops.example.com', 'Host header found' )
+      or explain( $req->headers(), 'devops.example.com' );
+    ok( $req->as_string() =~ m#http://devops.example.com/file.json#,
+        'Rules file URL found' )
+      or
+      explain( $req->as_string(), 'GET http://devops.example.com/file.json' );
+    count(2);
+    my $httpResp;
+    my $s = '{
+  "rules": {
+    "^/deny": "deny",
+    "^/testno": "$uid ne qq{dwho}",
+    "^/testyes": "$uid eq qq{dwho}",
+    "default": "accept"
+  },
+  "headers": {
+    "User": "$uid",
+    "Mail": "$mail",
+    "Name": "$cn"
+  }
+}';
+    $httpResp = HTTP::Response->new( 200, 'OK' );
+    $httpResp->header( 'Content-Type',   'application/json' );
+    $httpResp->header( 'Content-Length', length($s) );
+    $httpResp->content($s);
+    return $httpResp;
+}
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/66-Lemonldap-NG-Handler-PSGI-wildcard.t 2.0.14+ds-1/lemonldap-ng-handler/t/66-Lemonldap-NG-Handler-PSGI-wildcard.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/66-Lemonldap-NG-Handler-PSGI-wildcard.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/66-Lemonldap-NG-Handler-PSGI-wildcard.t	2022-02-19 16:04:21.000000000 +0000
@@ -15,7 +15,7 @@ my $res;
 ok( $res = $client->_get( '/', undef, 'test.example.org' ),
     'Unauthentified query' );
 ok( ref($res) eq 'ARRAY', 'Response is an array' ) or explain( $res, 'array' );
-ok( $res->[0] == 302, 'Code is 302' ) or explain( $res->[0], 302 );
+ok( $res->[0] == 302,     'Code is 302' )          or explain( $res->[0], 302 );
 my %h = @{ $res->[1] };
 ok(
     $h{Location} eq 'http://auth.example.com/?url='
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostoptions.t 2.0.14+ds-1/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostoptions.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostoptions.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostoptions.t	1970-01-01 00:00:00.000000000 +0000
@@ -1,45 +0,0 @@
-use Test::More;
-use JSON;
-use MIME::Base64;
-use Data::Dumper;
-use URI::Escape;
-
-require 't/test-psgi-lib.pm';
-
-init(
-    'Lemonldap::NG::Handler::PSGI',
-    {
-        vhostOptions => {
-            'test1.example.com' => {
-                vhostHttps => 1,
-                vhostPort  => 443,
-            },
-        },
-        locationRules   => {},
-        exportedHeaders => {},
-        https           => undef,
-        port            => undef,
-        maintenance     => undef,
-    }
-);
-
-my $res;
-
-ok( $res = $client->_get('/'), 'Unauthentified query' );
-ok( ref($res) eq 'ARRAY', 'Response is an array' ) or explain( $res, 'array' );
-ok( $res->[0] == 302, 'Code is 302' ) or explain( $res->[0], 302 );
-my %h = @{ $res->[1] };
-ok(
-    $h{Location} eq 'http://auth.example.com/?url='
-      . uri_escape( encode_base64( 'https://test1.example.com/', '' ) ),
-    'Redirection points to portal and site is https'
-  )
-  or explain(
-    \%h,
-    'Location => http://auth.example.com/?url='
-      . uri_escape( encode_base64( 'https://test1.example.com/', '' ) )
-  );
-
-count(4);
-done_testing( count() );
-clean();
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostOptions.t 2.0.14+ds-1/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostOptions.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostOptions.t	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostOptions.t	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,45 @@
+use Test::More;
+use JSON;
+use MIME::Base64;
+use Data::Dumper;
+use URI::Escape;
+
+require 't/test-psgi-lib.pm';
+
+init(
+    'Lemonldap::NG::Handler::PSGI',
+    {
+        vhostOptions => {
+            'test1.example.com' => {
+                vhostHttps => 1,
+                vhostPort  => 443,
+            },
+        },
+        locationRules   => {},
+        exportedHeaders => {},
+        https           => undef,
+        port            => undef,
+        maintenance     => undef,
+    }
+);
+
+my $res;
+
+ok( $res = $client->_get('/'), 'Unauthentified query' );
+ok( ref($res) eq 'ARRAY', 'Response is an array' ) or explain( $res, 'array' );
+ok( $res->[0] == 302,     'Code is 302' )          or explain( $res->[0], 302 );
+my %h = @{ $res->[1] };
+ok(
+    $h{Location} eq 'http://auth.example.com/?url='
+      . uri_escape( encode_base64( 'https://test1.example.com/', '' ) ),
+    'Redirection points to portal and site is https'
+  )
+  or explain(
+    \%h,
+    'Location => http://auth.example.com/?url='
+      . uri_escape( encode_base64( 'https://test1.example.com/', '' ) )
+  );
+
+count(4);
+done_testing( count() );
+clean();
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostoptions-with-reload.t 2.0.14+ds-1/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostoptions-with-reload.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostoptions-with-reload.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostoptions-with-reload.t	1970-01-01 00:00:00.000000000 +0000
@@ -1,59 +0,0 @@
-use Test::More;
-use JSON;
-use MIME::Base64;
-use Data::Dumper;
-use URI::Escape;
-
-require 't/test-psgi-lib.pm';
-
-init(
-    'Lemonldap::NG::Handler::PSGI',
-    {
-        locationRules   => {},
-        exportedHeaders => {},
-        https           => undef,
-        port            => undef,
-        maintenance     => undef,
-    }
-);
-
-my $res;
-
-ok( $res = $client->_get('/'), 'Unauthentified query' );
-ok( ref($res) eq 'ARRAY', 'Response is an array' ) or explain( $res, 'array' );
-ok( $res->[0] == 302, 'Code is 302' ) or explain( $res->[0], 302 );
-
-my $conf;
-eval {
-    local $/ = undef;
-    open my $file, 't/lmConf-1.json' or die $!;
-    $conf = JSON::from_json(<$file>);
-    close $file;
-};
-fail $@ if $@;
-$conf->{vhostOptions} = {
-    'test1.example.com' => {
-        vhostHttps => 1,
-        vhostPort  => 443,
-    },
-};
-Lemonldap::NG::Handler::Main->configReload($conf);
-fail $@ if $@;
-ok( $res = $client->_get('/'), 'Unauthentified query' );
-ok( ref($res) eq 'ARRAY', 'Response is an array' ) or explain( $res, 'array' );
-ok( $res->[0] == 302, 'Code is 302' ) or explain( $res->[0], 302 );
-my %h = @{ $res->[1] };
-ok(
-    $h{Location} eq 'http://auth.example.com/?url='
-      . uri_escape( encode_base64( 'https://test1.example.com/', '' ) ),
-    'Redirection points to portal and site is https'
-  )
-  or explain(
-    \%h,
-    'Location => http://auth.example.com/?url='
-      . uri_escape( encode_base64( 'https://test1.example.com/', '' ) )
-  );
-
-count(7);
-done_testing( count() );
-clean();
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostOptions-with-reload.t 2.0.14+ds-1/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostOptions-with-reload.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostOptions-with-reload.t	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/67-Lemonldap-NG-Handler-PSGI-vhostOptions-with-reload.t	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,59 @@
+use Test::More;
+use JSON;
+use MIME::Base64;
+use Data::Dumper;
+use URI::Escape;
+
+require 't/test-psgi-lib.pm';
+
+init(
+    'Lemonldap::NG::Handler::PSGI',
+    {
+        locationRules   => {},
+        exportedHeaders => {},
+        https           => undef,
+        port            => undef,
+        maintenance     => undef,
+    }
+);
+
+my $res;
+
+ok( $res = $client->_get('/'), 'Unauthentified query' );
+ok( ref($res) eq 'ARRAY', 'Response is an array' ) or explain( $res, 'array' );
+ok( $res->[0] == 302,     'Code is 302' )          or explain( $res->[0], 302 );
+
+my $conf;
+eval {
+    local $/ = undef;
+    open my $file, 't/lmConf-1.json' or die $!;
+    $conf = JSON::from_json(<$file>);
+    close $file;
+};
+fail $@ if $@;
+$conf->{vhostOptions} = {
+    'test1.example.com' => {
+        vhostHttps => 1,
+        vhostPort  => 443,
+    },
+};
+Lemonldap::NG::Handler::Main->configReload($conf);
+fail $@ if $@;
+ok( $res = $client->_get('/'), 'Unauthentified query' );
+ok( ref($res) eq 'ARRAY', 'Response is an array' ) or explain( $res, 'array' );
+ok( $res->[0] == 302,     'Code is 302' )          or explain( $res->[0], 302 );
+my %h = @{ $res->[1] };
+ok(
+    $h{Location} eq 'http://auth.example.com/?url='
+      . uri_escape( encode_base64( 'https://test1.example.com/', '' ) ),
+    'Redirection points to portal and site is https'
+  )
+  or explain(
+    \%h,
+    'Location => http://auth.example.com/?url='
+      . uri_escape( encode_base64( 'https://test1.example.com/', '' ) )
+  );
+
+count(7);
+done_testing( count() );
+clean();
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/71-Lemonldap-NG-Handler-PSGI-OAuth2.t 2.0.14+ds-1/lemonldap-ng-handler/t/71-Lemonldap-NG-Handler-PSGI-OAuth2.t
--- 2.0.13+ds-3/lemonldap-ng-handler/t/71-Lemonldap-NG-Handler-PSGI-OAuth2.t	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/71-Lemonldap-NG-Handler-PSGI-OAuth2.t	2022-02-19 16:04:21.000000000 +0000
@@ -50,7 +50,7 @@ init(
 Lemonldap::NG::Common::Session->new( {
         storageModule        => 'Apache::Session::File',
         storageModuleOptions => { Directory => 't/sessions' },
-        id =>
+        id                   =>
           'f0fd4e85000ce35d062f97f5b466fc00abc2fad0406e03e086605f929ec4a249',
         force => 1,
         kind  => 'OIDCI',
@@ -144,7 +144,7 @@ ok(
     $res = $client->_get(
         '/read',             undef,
         'test1.example.com', '',
-        VHOSTTYPE => 'OAuth2',
+        VHOSTTYPE          => 'OAuth2',
         HTTP_AUTHORIZATION =>
 'Bearer f0fd4e85000ce35d062f97f5b466fc00abc2fad0406e03e086605f929ec4a249',
     ),
@@ -165,7 +165,7 @@ ok(
     $res = $client->_get(
         '/write',            undef,
         'test1.example.com', '',
-        VHOSTTYPE => 'OAuth2',
+        VHOSTTYPE          => 'OAuth2',
         HTTP_AUTHORIZATION =>
 'Bearer f0fd4e85000ce35d062f97f5b466fc00abc2fad0406e03e086605f929ec4a249',
     ),
@@ -178,7 +178,7 @@ ok(
     $res = $client->_get(
         '/test',             undef,
         'test1.example.com', '',
-        VHOSTTYPE => 'OAuth2',
+        VHOSTTYPE          => 'OAuth2',
         HTTP_AUTHORIZATION =>
 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwianRpIjoiZjBmZDRlODUwMDBjZTM1ZDA2MmY5N2Y1YjQ2NmZjMDBhYmMyZmFkMDQwNmUwM2UwODY2MDVmOTI5ZWM0YTI0OSJ9.h0RDBLo5Vy8lqbltEP2L496KOzJLhLCIRZZmEqcPuN8',
     ),
diff -pruN 2.0.13+ds-3/lemonldap-ng-handler/t/test-psgi-lib.pm 2.0.14+ds-1/lemonldap-ng-handler/t/test-psgi-lib.pm
--- 2.0.13+ds-3/lemonldap-ng-handler/t/test-psgi-lib.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-handler/t/test-psgi-lib.pm	2022-02-19 16:04:21.000000000 +0000
@@ -75,7 +75,7 @@ sub init {
         '_utime'              => $now,
         '_passwordDB'         => 'Demo',
         '_auth'               => 'Demo',
-        'UA' =>
+        'UA'                  =>
 'Mozilla/5.0 (X11; VAX4000; rv:43.0) Gecko/20100101 Firefox/143.0 Iceweasel/143.0.1'
     };
 
@@ -152,7 +152,7 @@ sub _get {
             'X_ORIGINAL_URI'       => $path . ( $query ? "?$query" : '' ),
             'SERVER_PORT'          => '80',
             'SERVER_PROTOCOL'      => 'HTTP/1.1',
-            'HTTP_USER_AGENT' =>
+            'HTTP_USER_AGENT'      =>
               'Mozilla/5.0 (VAX-4000; rv:36.0) Gecko/20350101 Firefox',
             'REMOTE_ADDR' => '127.0.0.1',
             'HTTP_HOST'   => $host,
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/2ndFA.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/2ndFA.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/2ndFA.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/2ndFA.pm	2022-02-19 16:04:21.000000000 +0000
@@ -15,6 +15,7 @@ extends qw(
   Lemonldap::NG::Common::Conf::AccessLib
 );
 
+use constant _2FTYPES => [ "UBK", "U2F", "TOTP", "WebAuthn" ];
 our $VERSION = '2.0.10';
 
 #############################
@@ -46,7 +47,8 @@ sub init {
     $self->{hiddenAttributes} //= "_password";
     $self->{hiddenAttributes} .= ' _session_id'
       unless $conf->{displaySessionId};
-    $self->{TOTPCheck} = $self->{U2FCheck} = $self->{UBKCheck} = '1';
+    $self->{TOTPCheck} = $self->{U2FCheck} = $self->{UBKCheck} =
+      $self->{WebAuthnCheck} = '1';
     return 1;
 }
 
@@ -67,7 +69,7 @@ sub del2F {
     my $epoch = $params->{epoch}
       or return $self->sendError( $req, 'Missing "epoch" parameter', 400 );
 
-    if ( $type =~ /\b(?:U2F|TOTP|UBK)\b/ ) {
+    if ( grep { $_ eq $type } @{ _2FTYPES() } ) {
         $self->logger->debug(
             "Call procedure delete2F with type=$type and epoch=$epoch");
         return $self->delete2F( $req, $session, $skey );
@@ -117,7 +119,7 @@ sub sfa {
     $moduleOptions->{backend} = $mod->{module};
 
     # Select 2FA sessions to display
-    foreach (qw(U2F TOTP UBK)) {
+    foreach ( @{ _2FTYPES() } ) {
         $self->{ $_ . 'Check' } = delete $params->{ $_ . 'Check' }
           if ( defined $params->{ $_ . 'Check' } );
     }
@@ -188,17 +190,18 @@ sub sfa {
     # Remove sessions without at least one 2F device(s)
     $self->logger->debug(
         "Removing sessions without at least one 2F device(s)...");
+    my $_2f_types_re = join( '|', @{ _2FTYPES() } );
     foreach my $session ( keys %$res ) {
         delete $res->{$session}
           unless ( defined $res->{$session}->{_2fDevices}
             and $res->{$session}->{_2fDevices} =~
-            /"type":\s*"(?:U2F|TOTP|UBK)"/s );
+            /"type":\s*"(?:$_2f_types_re)"/s );
     }
 
     # Filter 2FA sessions if needed
     $self->logger->debug("Filtering 2F sessions...");
     my $all = ( keys %$res );
-    foreach (qw(U2F TOTP UBK)) {
+    foreach ( @{ _2FTYPES() } ) {
         if ( $self->{ $_ . 'Check' } eq '2' ) {
             foreach my $session ( keys %$res ) {
                 delete $res->{$session}
@@ -266,7 +269,7 @@ qq{Use of an uninitialized attribute "$g
     #   { session => <sessionId>, userId => <_session_uid> }
     else {
         $res = [
-            sort { $a->{date} <=> $b->{date} }
+            sort  { $a->{date} <=> $b->{date} }
               map { { session => $_, userId => $res->{$_}->{_session_uid} } }
               keys %$res
         ];
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/2F.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/2F.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/2F.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/2F.pm	2022-02-19 16:04:21.000000000 +0000
@@ -159,7 +159,7 @@ sub _get2F {
                 type => $device->{type},
                 name => $device->{name}
               }
-              unless ( ( defined $type and $type ne $device->{type} )
+              unless ( ( defined $type and uc($type) ne uc( $device->{type} ) )
                 or ( defined $id and $id ne genId2F($device) ) );
         }
     }
@@ -224,8 +224,12 @@ sub _delete2FFromSessions {
                 my $element = shift @$devices;
                 if (
                     ( defined $type or defined $id )
-                    and (  ( defined $type and $type ne $element->{type} )
-                        or ( defined $id and $id ne genId2F($element) ) )
+                    and ( (
+                            defined $type
+                            and uc($type) ne uc( $element->{type} )
+                        )
+                        or ( defined $id and $id ne genId2F($element) )
+                    )
                   )
                 {
                     push @keep, $element;
@@ -333,10 +337,10 @@ sub _checkType {
     return {
         res  => "ko",
         code => 400,
-        msg =>
-"Invalid input: Type \"$type\" does not exist. Allowed values for type are: \"U2F\", \"TOTP\" or \"UBK\""
+        msg  =>
+"Invalid input: Type \"$type\" does not exist. Allowed values for type are: \"U2F\", \"TOTP\", \"WebAuthn\" or \"UBK\""
       }
-      unless ( $type =~ /\b(?:U2F|TOTP|UBK)\b/ );
+      unless ( $type =~ /\b(?:U2F|TOTP|UBK|WebAuthn)\b/i );
 
     return { res => "ok" };
 }
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Common.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Common.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Common.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Common.pm	2022-01-24 21:26:10.000000000 +0000
@@ -1,6 +1,6 @@
 package Lemonldap::NG::Manager::Api::Common;
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 package Lemonldap::NG::Manager::Api;
 
@@ -110,6 +110,9 @@ sub _translateValueConfToApi {
     elsif ( $optionName eq "oidcRPMetaDataOptionsAdditionalAudiences" ) {
         return [ split( /\s+/, $optionValue, ) ];
     }
+    elsif ( $optionName eq "casAppMetaDataOptionsService" ) {
+        return [ split( /\s+/, $optionValue, ) ];
+    }
     else {
         return $optionValue;
     }
@@ -134,7 +137,14 @@ sub _translateValueApiToConf {
 
     # additionalAudiences is handled as an array
     elsif ( $optionName eq 'additionalAudiences' ) {
-        die "postLogoutRedirectUris is not an array\n"
+        die "additionalAudiences is not an array\n"
+          unless ( ref($optionValue) eq "ARRAY" );
+        return join( ' ', @{$optionValue} );
+    }
+
+    # service is handled as an array
+    elsif ( $optionName eq 'service' ) {
+        die "service is not an array\n"
           unless ( ref($optionValue) eq "ARRAY" );
         return join( ' ', @{$optionValue} );
     }
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Providers/CasApp.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Providers/CasApp.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Providers/CasApp.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Providers/CasApp.pm	2022-02-19 16:04:21.000000000 +0000
@@ -1,6 +1,6 @@
 package Lemonldap::NG::Manager::Api::Providers::CasApp;
 
-our $VERSION = '2.0.10';
+our $VERSION = '2.0.14';
 
 package Lemonldap::NG::Manager::Api;
 
@@ -109,9 +109,9 @@ sub addCasApp {
     return $self->sendError( $req, 'Invalid input: service is missing', 400 )
       unless ( defined $add->{options}->{service} );
 
-    return $self->sendError( $req, 'Invalid input: service is not a string',
+    return $self->sendError( $req, 'Invalid input: service must be an array',
         400 )
-      if ( ref $add->{options}->{service} );
+      unless ( ref $add->{options}->{service} eq "ARRAY" );
 
     $self->logger->debug(
         "[API] Add CAS App with confKey $add->{confKey} requested");
@@ -125,18 +125,18 @@ sub addCasApp {
         409
     ) if ( defined $self->_getCasAppByConfKey( $conf, $add->{confKey} ) );
 
-    my $res =
-      $self->_getCasAppByServiceUrl( $conf, $add->{options}->{service} );
-    if ( defined $res ) {
-        my $conflict = $res->{options}->{service};
-        return $self->sendError(
-            $req,
-"Invalid input: A CAS application with service URL $conflict already exists",
-            409
-        );
+    for my $serviceUrl ( @{ $add->{options}->{service} } ) {
+        my $res = $self->_getCasAppByServiceUrl( $conf, $serviceUrl );
+        if ( defined $res ) {
+            return $self->sendError(
+                $req,
+"Invalid input: A CAS application with service URL $serviceUrl already exists",
+                409
+            );
+        }
     }
 
-    $res = $self->_pushCasApp( $conf, $add->{confKey}, $add, 1 );
+    my $res = $self->_pushCasApp( $conf, $add->{confKey}, $add, 1 );
 
     return $self->sendError( $req, $res->{msg}, 400 )
       unless ( $res->{res} eq 'ok' );
@@ -285,12 +285,19 @@ sub _getCasAppByServiceUrl {
 
     my ($serviceHost) = $serviceUrl =~ m#^(https?://[^/]+)(?:/.*)?$#;
     return undef unless $serviceHost;
-    foreach ( keys %{ $conf->{casAppMetaDataOptions} } ) {
-        my $url =
-          $conf->{casAppMetaDataOptions}->{$_}->{casAppMetaDataOptionsService};
-        my ($curHost) = $url =~ m#^(https?://[^/]+)(?:/.*)?$#;
-        if ( $serviceHost eq $curHost ) {
-            return $self->_getCasAppByConfKey( $conf, $_ );
+    for my $confKey ( keys %{ $conf->{casAppMetaDataOptions} } ) {
+        for my $url (
+            split(
+                /\s+/,
+                $conf->{casAppMetaDataOptions}->{$confKey}
+                  ->{casAppMetaDataOptionsService}
+            )
+          )
+        {
+            my ($curHost) = $url =~ m#^(https?://[^/]+)(?:/.*)?$#;
+            if ( $serviceHost eq $curHost ) {
+                return $self->_getCasAppByConfKey( $conf, $confKey );
+            }
         }
     }
 
@@ -301,14 +308,29 @@ sub _isNewCasAppServiceUrlUnique {
     my ( $self, $conf, $confKey, $casApp ) = @_;
     my $curServiceUrl =
       $self->_getCasAppByConfKey( $conf, $confKey )->{options}->{service};
-    my $newServiceUrl = $casApp->{options}->{service} || "";
-    if ( $newServiceUrl ne '' && $newServiceUrl ne $curServiceUrl ) {
+
+    # Check service paramater
+    unless ( ref $casApp->{options}->{service} eq "ARRAY" ) {
         return {
             res => 'ko',
-            msg =>
+            msg => "The parameter 'service' must be an array",
+        };
+    }
+
+    my $newService = $casApp->{options}->{service} || [];
+    for my $newServiceUrl (@$newService) {
+        if ( $newServiceUrl ne ''
+            && !grep( /^$newServiceUrl$/, @$curServiceUrl ) )
+        {
+            return {
+                res => 'ko',
+                msg =>
 "A CAS application with service URL '$newServiceUrl' already exists"
-          }
-          if ( defined $self->_getCasAppByServiceUrl( $conf, $newServiceUrl ) );
+              }
+              if (
+                defined $self->_getCasAppByServiceUrl( $conf, $newServiceUrl )
+              );
+        }
     }
 
     return { res => 'ok' };
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Providers/OidcRp.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Providers/OidcRp.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Providers/OidcRp.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Api/Providers/OidcRp.pm	2022-02-19 16:04:21.000000000 +0000
@@ -140,7 +140,7 @@ sub addOidcRp {
         409
     ) if ( defined $self->_getOidcRpByClientId( $conf, $add->{clientId} ) );
 
-    $add->{options} = {} unless ( defined $add->{options} );
+    $add->{options}                 = {} unless ( defined $add->{options} );
     $add->{options}->{clientId}     = $add->{clientId};
     $add->{options}->{redirectUris} = $add->{redirectUris};
 
@@ -246,8 +246,8 @@ sub replaceOidcRp {
     return $self->sendError( $req, $res->{msg}, 409 )
       unless ( $res->{res} eq 'ok' );
 
-    $replace->{options} = {} unless ( defined $replace->{options} );
-    $replace->{options}->{clientId}     = $replace->{clientId};
+    $replace->{options}             = {} unless ( defined $replace->{options} );
+    $replace->{options}->{clientId} = $replace->{clientId};
     $replace->{options}->{redirectUris} = $replace->{redirectUris};
 
     $res = $self->_pushOidcRp( $conf, $confKey, $replace, 1 );
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm	2022-02-19 16:04:21.000000000 +0000
@@ -6,7 +6,7 @@
 
 package Lemonldap::NG::Manager::Build::Attributes;
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 use strict;
 use Regexp::Common qw/URI/;
 
@@ -239,7 +239,7 @@ sub attributes {
 
         # Other
         checkTime => {
-            type => 'int',
+            type          => 'int',
             documentation =>
               'Timeout to check new configuration in local cache',
             default => 600,
@@ -248,7 +248,7 @@ sub attributes {
         mySessionAuthorizedRWKeys => {
             type          => 'array',
             documentation => 'Alterable session keys by user itself',
-            default =>
+            default       =>
               [ '_appsListOrder', '_oidcConnectedRP', '_oidcConsents' ],
         },
         configStorage => {
@@ -297,7 +297,7 @@ sub attributes {
             flags         => 'h',
         },
         confirmFormMethod => {
-            type => "select",
+            type   => "select",
             select =>
               [ { k => 'get', v => 'GET' }, { k => 'post', v => 'POST' }, ],
             default       => 'post',
@@ -318,7 +318,7 @@ sub attributes {
             flags         => 'h',
         },
         infoFormMethod => {
-            type => "select",
+            type   => "select",
             select =>
               [ { k => 'get', v => 'GET' }, { k => 'post', v => 'POST' }, ],
             default       => 'get',
@@ -379,13 +379,13 @@ sub attributes {
             documentation => 'Enable portal status',
         },
         portalUserAttr => {
-            type    => 'text',
-            default => '_user',
+            type          => 'text',
+            default       => '_user',
             documentation =>
               'Session parameter to display connected user in portal',
         },
         redirectFormMethod => {
-            type => "select",
+            type   => "select",
             select =>
               [ { k => 'get', v => 'GET' }, { k => 'post', v => 'POST' }, ],
             default       => 'get',
@@ -436,13 +436,18 @@ sub attributes {
             flags         => 'hmp',
         },
         stayConnected => {
+            type          => 'boolOrExpr',
+            default       => 0,
+            documentation => 'Stay connected activation rule',
+        },
+        stayConnectedBypassFG => {
             type          => 'bool',
             default       => 0,
-            documentation => 'Enable StayConnected plugin',
+            documentation => 'Disable fingerprint checkng',
         },
         stayConnectedTimeout => {
-            type    => 'int',
-            default => 2592000,
+            type          => 'int',
+            default       => 2592000,
             documentation =>
               'StayConnected persistent connexion session timeout',
             flags => 'm',
@@ -476,6 +481,18 @@ sub attributes {
             documentation => 'Enable check DevOps download field',
             flags         => 'p',
         },
+        checkDevOpsDisplayNormalizedHeaders => {
+            default       => 1,
+            type          => 'bool',
+            documentation => 'Display normalized headers',
+            flags         => 'p',
+        },
+        checkDevOpsCheckSessionAttributes => {
+            default       => 1,
+            type          => 'bool',
+            documentation => 'Check if session attributes exist',
+            flags         => 'p',
+        },
         checkUser => {
             default       => 0,
             type          => 'bool',
@@ -496,12 +513,12 @@ sub attributes {
         },
         checkUserHiddenAttributes => {
             type          => 'text',
-            default       => '_loginHistory _session_id hGroups',
+            default       => '_loginHistory, _session_id, hGroups',
             documentation => 'Attributes to hide in CheckUser plugin',
             flags         => 'p',
         },
         checkUserSearchAttributes => {
-            type => 'text',
+            type          => 'text',
             documentation =>
               'Attributes used for retrieving sessions in user DataBase',
             flags => 'p',
@@ -536,6 +553,18 @@ sub attributes {
             documentation => 'Display empty headers rule',
             flags         => 'p',
         },
+        checkUserDisplayHistory => {
+            default       => 0,
+            type          => 'boolOrExpr',
+            documentation => 'Display history rule',
+            flags         => 'p',
+        },
+        checkUserDisplayHiddenAttributes => {
+            default       => 0,
+            type          => 'boolOrExpr',
+            documentation => 'Display hidden attributes rule',
+            flags         => 'p',
+        },
         checkUserHiddenHeaders => {
             type       => 'keyTextContainer',
             keyTest    => qr/^\S+$/,
@@ -573,6 +602,38 @@ sub attributes {
             default       => '^[*\w]+$',
             documentation => 'Regular expression to validate parameters',
         },
+        newLocationWarning => {
+            default       => 0,
+            type          => 'bool',
+            documentation => 'Enable New Location Warning',
+        },
+        newLocationWarningLocationAttribute => {
+            type          => 'text',
+            default       => 'ipAddr',
+            documentation => 'New location session attribute',
+        },
+        newLocationWarningLocationDisplayAttribute => {
+            type          => 'text',
+            default       => '',
+            documentation => 'New location session attribute for user display',
+        },
+        newLocationWarningMaxValues => {
+            type          => 'int',
+            default       => '0',
+            documentation => 'How many previous locations should be compared',
+        },
+        newLocationWarningMailAttribute => {
+            type          => 'text',
+            documentation => 'New location warning mail session attribute',
+        },
+        newLocationWarningMailBody => {
+            type          => 'longtext',
+            documentation => 'Mail body for new location warning',
+        },
+        newLocationWarningMailSubject => {
+            type          => 'text',
+            documentation => 'Mail subject for new location warning',
+        },
         globalLogoutRule => {
             type          => 'boolOrExpr',
             default       => 0,
@@ -623,7 +684,7 @@ sub attributes {
         },
         impersonationHiddenAttributes => {
             type          => 'text',
-            default       => '_2fDevices _loginHistory',
+            default       => '_2fDevices, _loginHistory',
             documentation => 'Attributes to skip',
             flags         => 'p',
         },
@@ -684,14 +745,14 @@ sub attributes {
             flags         => 'p',
         },
         skipRenewConfirmation => {
-            type    => 'bool',
-            default => 0,
+            type          => 'bool',
+            default       => 0,
             documentation =>
               'Avoid asking confirmation when an Issuer asks to renew auth',
         },
         skipUpgradeConfirmation => {
-            type    => 'bool',
-            default => 0,
+            type          => 'bool',
+            default       => 0,
             documentation =>
               'Avoid asking confirmation during a session upgrade',
         },
@@ -700,7 +761,7 @@ sub attributes {
             documentation => 'Refresh sessions plugin',
         },
         forceGlobalStorageIssuerOTT => {
-            type => 'bool',
+            type          => 'bool',
             documentation =>
               'Force Issuer tokens to be stored into Global Storage',
         },
@@ -784,8 +845,8 @@ sub attributes {
             documentation => 'Show error if session is expired',
         },
         portalErrorOnMailNotFound => {
-            type    => 'bool',
-            default => 0,
+            type          => 'bool',
+            default       => 0,
             documentation =>
               'Show error if mail is not found in password reset process',
         },
@@ -877,15 +938,15 @@ sub attributes {
             documentation => 'Check XSS',
         },
         portalForceAuthn => {
-            default => 0,
-            help    => 'forcereauthn.html',
-            type    => 'bool',
+            default       => 0,
+            help          => 'forcereauthn.html',
+            type          => 'bool',
             documentation =>
               'Enable force to authenticate when displaying portal',
         },
         portalForceAuthnInterval => {
-            default => 5,
-            type    => 'int',
+            default       => 5,
+            type          => 'int',
             documentation =>
 'Maximum interval in seconds since last authentication to force reauthentication',
         },
@@ -916,15 +977,15 @@ sub attributes {
             documentation => 'Max lock time',
         },
         bruteForceProtectionIncrementalTempo => {
-            default => 0,
-            help    => 'bruteforceprotection.html',
-            type    => 'bool',
+            default       => 0,
+            help          => 'bruteforceprotection.html',
+            type          => 'bool',
             documentation =>
               'Enable incremental lock time for brute force attack protection',
         },
         bruteForceProtectionLockTimes => {
-            type    => 'text',
-            default => '15, 30, 60, 300, 600',
+            type          => 'text',
+            default       => '15, 30, 60, 300, 600',
             documentation =>
               'Incremental lock time values for brute force attack protection',
         },
@@ -937,7 +998,7 @@ sub attributes {
         },
         hiddenAttributes => {
             type          => 'text',
-            default       => '_password _2fDevices',
+            default       => '_password, _2fDevices',
             documentation => 'Name of attributes to hide in logs',
         },
         displaySessionId => {
@@ -960,38 +1021,38 @@ sub attributes {
             documentation => 'Enable Cross-Origin Resource Sharing',
         },
         corsAllow_Credentials => {
-            type    => 'text',
-            default => 'true',
+            type          => 'text',
+            default       => 'true',
             documentation =>
               'Allow credentials for Cross-Origin Resource Sharing',
         },
         corsAllow_Headers => {
-            type    => 'text',
-            default => '*',
+            type          => 'text',
+            default       => '*',
             documentation =>
               'Allowed headers for Cross-Origin Resource Sharing',
         },
         corsAllow_Methods => {
-            type    => 'text',
-            default => 'POST,GET',
+            type          => 'text',
+            default       => 'POST,GET',
             documentation =>
               'Allowed methods for Cross-Origin Resource Sharing',
         },
         corsAllow_Origin => {
-            type    => 'text',
-            default => '*',
+            type          => 'text',
+            default       => '*',
             documentation =>
               'Allowed origine for Cross-Origin Resource Sharing',
         },
         corsExpose_Headers => {
-            type    => 'text',
-            default => '*',
+            type          => 'text',
+            default       => '*',
             documentation =>
               'Exposed headers for Cross-Origin Resource Sharing',
         },
         corsMax_Age => {
-            type    => 'text',
-            default => '86400',    # 24 hours
+            type          => 'text',
+            default       => '86400',    # 24 hours
             documentation => 'MAx-age for Cross-Origin Resource Sharing',
         },
         cspDefault => {
@@ -1000,8 +1061,8 @@ sub attributes {
             documentation => 'Default value for Content-Security-Policy',
         },
         cspFormAction => {
-            type    => 'text',
-            default => "*",
+            type          => 'text',
+            default       => "*",
             documentation =>
               'Form action destination for Content-Security-Policy',
         },
@@ -1021,8 +1082,8 @@ sub attributes {
             documentation => 'Style source for Content-Security-Policy',
         },
         cspConnect => {
-            type    => 'text',
-            default => "'self'",
+            type          => 'text',
+            default       => "'self'",
             documentation =>
               'Authorized Ajax destination for Content-Security-Policy',
         },
@@ -1193,8 +1254,8 @@ sub attributes {
             documentation => 'Display logout tab in portal',
         },
         portalDisplayCertificateResetByMail => {
-            type    => 'bool',
-            default => 0,
+            type          => 'bool',
+            default       => 0,
             documentation =>
               'Display certificate reset by mail button in portal',
         },
@@ -1219,8 +1280,8 @@ sub attributes {
             documentation => 'Display OIDC consent tab in portal',
         },
         portalDisplayGeneratePassword => {
-            default => 1,
-            type    => 'bool',
+            default       => 1,
+            type          => 'bool',
             documentation =>
               'Display password generate box in reset password form',
         },
@@ -1299,7 +1360,7 @@ sub attributes {
         # Viewer
         viewerHiddenKeys => {
             type          => 'text',
-            default       => 'samlIDPMetaDataNodes samlSPMetaDataNodes',
+            default       => 'samlIDPMetaDataNodes, samlSPMetaDataNodes',
             documentation => 'Hidden Conf keys',
             flags         => 'm',
         },
@@ -1370,8 +1431,8 @@ sub attributes {
             documentation => 'Notification server activation',
         },
         notificationServerSentAttributes => {
-            type    => 'text',
-            default => 'uid reference date title subtitle text check',
+            type          => 'text',
+            default       => 'uid reference date title subtitle text check',
             documentation =>
               'Prameters to send with notification server GET method',
             flags => 'p',
@@ -1449,8 +1510,8 @@ sub attributes {
         globalStorageOptions => {
             type    => 'keyTextContainer',
             default => {
-                'Directory'     => '/var/lib/lemonldap-ng/sessions/',
-                'LockDirectory' => '/var/lib/lemonldap-ng/sessions/lock/',
+                'Directory'      => '/var/lib/lemonldap-ng/sessions/',
+                'LockDirectory'  => '/var/lib/lemonldap-ng/sessions/lock/',
                 'generateModule' =>
                   'Lemonldap::NG::Common::Apache::Session::Generate::SHA256',
             },
@@ -1576,8 +1637,8 @@ sub attributes {
             documentation => 'Send a mail when password is changed',
         },
         portalRequireOldPassword => {
-            default => 1,
-            type    => 'boolOrExpr',
+            default       => 1,
+            type          => 'boolOrExpr',
             documentation =>
               'Rule to require old password to change the password',
         },
@@ -1792,7 +1853,7 @@ sub attributes {
             documentation => 'Upgrade session activation',
         },
         forceGlobalStorageUpgradeOTT => {
-            type => 'bool',
+            type          => 'bool',
             documentation =>
               'Force Upgrade tokens be stored into Global Storage',
         },
@@ -1821,7 +1882,7 @@ sub attributes {
             documentation => 'U2F self registration activation',
         },
         u2fAuthnLevel => {
-            type => 'int',
+            type          => 'int',
             documentation =>
               'Authentication level for users authentified by password+U2F'
         },
@@ -1855,7 +1916,7 @@ sub attributes {
             documentation => 'TOTP self registration activation',
         },
         totp2fAuthnLevel => {
-            type => 'int',
+            type          => 'int',
             documentation =>
               'Authentication level for users authentified by password+TOTP'
         },
@@ -1895,6 +1956,11 @@ sub attributes {
             type          => 'int',
             documentation => 'TOTP device time to live ',
         },
+        totp2fEncryptSecret => {
+            type          => 'bool',
+            default       => 0,
+            documentation => 'Encrypt TOTP secrets in database',
+        },
 
         # UTOTP 2F
         utotp2fActivation => {
@@ -1903,7 +1969,7 @@ sub attributes {
             documentation => 'UTOTP activation (mixed U2F/TOTP module)',
         },
         utotp2fAuthnLevel => {
-            type => 'int',
+            type          => 'int',
             documentation =>
 'Authentication level for users authentified by password+(U2F or TOTP)'
         },
@@ -1940,7 +2006,7 @@ sub attributes {
             documentation => 'Second factor code timeout',
         },
         mail2fAuthnLevel => {
-            type => 'int',
+            type          => 'int',
             documentation =>
 'Authentication level for users authenticated by Mail second factor'
         },
@@ -1977,7 +2043,7 @@ sub attributes {
             documentation => 'Validation command of External second factor',
         },
         ext2fAuthnLevel => {
-            type => 'int',
+            type          => 'int',
             documentation =>
 'Authentication level for users authentified by External second factor'
         },
@@ -2008,7 +2074,7 @@ sub attributes {
             documentation => 'Radius 2f verification timeout',
         },
         radius2fAuthnLevel => {
-            type => 'int',
+            type          => 'int',
             documentation =>
 'Authentication level for users authenticated by Radius second factor'
         },
@@ -2052,7 +2118,7 @@ sub attributes {
             documentation => 'Args for REST 2F init',
         },
         rest2fAuthnLevel => {
-            type => 'int',
+            type          => 'int',
             documentation =>
 'Authentication level for users authentified by REST second factor'
         },
@@ -2077,7 +2143,7 @@ sub attributes {
             documentation => 'Yubikey self registration activation',
         },
         yubikey2fAuthnLevel => {
-            type => 'int',
+            type          => 'int',
             documentation =>
 'Authentication level for users authentified by Yubikey second factor'
         },
@@ -2116,7 +2182,7 @@ sub attributes {
             documentation => 'Authorize users to remove existing Yubikey',
         },
         yubikey2fFromSessionAttribute => {
-            type => 'text',
+            type          => 'text',
             documentation =>
               'Provision yubikey from the given session variable',
         },
@@ -2125,6 +2191,54 @@ sub attributes {
             documentation => 'Yubikey device time to live',
         },
 
+        # WebAuthn 2FA
+        webauthn2fActivation => {
+            type          => 'boolOrExpr',
+            default       => 0,
+            documentation => 'WebAuthn second factor activation',
+        },
+        webauthn2fSelfRegistration => {
+            type          => 'boolOrExpr',
+            default       => 0,
+            documentation => 'WebAuthn self registration activation',
+        },
+        webauthn2fAuthnLevel => {
+            type          => 'int',
+            documentation =>
+'Authentication level for users authentified by WebAuthn second factor'
+        },
+        webauthn2fLabel => {
+            type          => 'text',
+            documentation => 'Portal label for WebAuthn second factor'
+        },
+        webauthn2fLogo => {
+            type          => 'text',
+            documentation => 'Custom logo for WebAuthn 2F',
+        },
+        webauthn2fUserVerification => {
+            type   => 'select',
+            select => [
+                { k => 'discouraged', v => 'Discouraged' },
+                { k => 'preferred',   v => 'Preferred' },
+                { k => 'required',    v => 'Required' },
+            ],
+            default       => 'preferred',
+            documentation => 'Verify user during registration and login',
+        },
+        webauthn2fUserCanRemoveKey => {
+            type          => 'bool',
+            default       => 1,
+            documentation => 'Authorize users to remove existing WebAuthn',
+        },
+        webauthnDisplayNameAttr => {
+            type          => 'text',
+            documentation => 'Session attribute containing user display name',
+        },
+        webauthnRpName => {
+            type          => 'text',
+            documentation => 'WebAuthn Relying Party display name',
+        },
+
         # Single session
         notifyDeleted => {
             default       => 1,
@@ -2169,14 +2283,14 @@ sub attributes {
             documentation => 'Enable REST password reset server',
         },
         restExportSecretKeys => {
-            default => 0,
-            type    => 'bool',
+            default       => 0,
+            type          => 'bool',
             documentation =>
               'Allow to export secret keys in REST session server',
         },
         restClockTolerance => {
-            default => 15,
-            type    => 'int',
+            default       => 15,
+            type          => 'int',
             documentation =>
               'How tolerant the REST session server will be to clock dift',
         },
@@ -2205,7 +2319,7 @@ sub attributes {
             documentation => 'Enable SOAP config server',
         },
         exportedAttr => {
-            type => 'text',
+            type          => 'text',
             documentation =>
               'List of attributes to export by SOAP or REST servers',
         },
@@ -2236,6 +2350,7 @@ sub attributes {
                 return $@ ? 0 : 1;
             },
             keyMsgFail    => '__badRegexp__',
+            help          => "adaptativeauthenticationlevel.html",
             documentation => 'Adaptative authentication level rules',
             flags         => 'p',
         },
@@ -2335,7 +2450,8 @@ sub attributes {
             default       => 'Main',
             documentation => 'Handler type',
         },
-        vhostAuthnLevel => { type => 'int' },
+        vhostAuthnLevel     => { type => 'int' },
+        vhostDevOpsRulesUrl => { type => 'url' },
 
         # SecureToken parameters
         secureTokenAllowOnError => {
@@ -2426,6 +2542,11 @@ sub attributes {
             type          => 'bool',
             documentation => 'Disable host-based matching of CAS services',
         },
+        casTicketExpiration => {
+            default       => 0,
+            type          => 'int',
+            documentation => 'Expiration time of Service and Proxy tickets',
+        },
         issuerDBCASActivation => {
             default       => 0,
             type          => 'bool',
@@ -2453,7 +2574,7 @@ sub attributes {
             documentation => 'CAS exported variables',
         },
         casAppMetaDataOptionsService => {
-            type          => 'url',
+            type          => 'text',
             documentation => 'CAS App service',
         },
         casAppMetaDataOptionsUserAttribute => {
@@ -2461,7 +2582,7 @@ sub attributes {
             documentation => 'CAS User attribute',
         },
         casAppMetaDataOptionsAuthnLevel => {
-            type => 'int',
+            type          => 'int',
             documentation =>
               'Authentication level requires to access to this CAS application',
         },
@@ -2623,8 +2744,8 @@ sub attributes {
             default => 'RSA_SHA256',
         },
         samlServiceUseCertificateInResponse => {
-            type    => 'bool',
-            default => 0,
+            type          => 'bool',
+            default       => 0,
             documentation =>
               'Use certificate instead of public key in SAML responses',
         },
@@ -2647,8 +2768,8 @@ sub attributes {
             documentation => 'SAML authn context password level',
         },
         samlAuthnContextMapPasswordProtectedTransport => {
-            type    => 'int',
-            default => 3,
+            type          => 'int',
+            default       => 3,
             documentation =>
               'SAML authn context password protected transport level',
         },
@@ -3009,15 +3130,15 @@ sub attributes {
             documentation => 'SAML SP SLO SOAP',
         },
         samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact => {
-            type => 'samlAssertion',
+            type    => 'samlAssertion',
             default =>
-              '1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;'
+              '0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;'
               . '#PORTAL#/saml/proxySingleSignOnArtifact',
             documentation => 'SAML SP ACS HTTP artifact',
         },
         samlSPSSODescriptorAssertionConsumerServiceHTTPPost => {
             type    => 'samlAssertion',
-            default => '0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;'
+            default => '1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;'
               . '#PORTAL#/saml/proxySingleSignOnPost',
             documentation => 'SAML SP ACS HTTP POST',
         },
@@ -3101,7 +3222,7 @@ sub attributes {
             default => 1,
         },
         samlSPMetaDataOptionsAuthnLevel => {
-            type => 'int',
+            type          => 'int',
             documentation =>
               'Authentication level requires to access to this SP',
         },
@@ -3213,9 +3334,9 @@ sub attributes {
             documentation => 'Rule to display second factor Manager link',
         },
         sfRemovedMsgRule => {
-            type    => 'boolOrExpr',
-            default => 0,
-            help    => 'secondfactor.html',
+            type          => 'boolOrExpr',
+            default       => 0,
+            help          => 'secondfactor.html',
             documentation =>
               'Display a message if at leat one expired SF has been removed',
         },
@@ -3237,7 +3358,7 @@ sub attributes {
             documentation => 'Notification title',
         },
         sfRemovedNotifMsg => {
-            type => 'text',
+            type    => 'text',
             default =>
 '_removedSF_ expired second factor(s) has/have been removed (_nameSF_)!',
             help          => 'secondfactor.html',
@@ -3248,13 +3369,14 @@ sub attributes {
             documentation => 'Timeout for 2F registration process',
         },
         available2F => {
-            type          => 'text',
-            default       => 'UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,Yubikey,Radius',
+            type    => 'text',
+            default =>
+              'UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,WebAuthn,Yubikey,Radius',
             documentation => 'Available second factor modules',
         },
         available2FSelfRegistration => {
-            type    => 'text',
-            default => 'TOTP,U2F,Yubikey',
+            type          => 'text',
+            default       => 'TOTP,U2F,WebAuthn,Yubikey',
             documentation =>
               'Available self-registration modules for second factor',
         },
@@ -3418,8 +3540,8 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
             documentation => 'LDAP attribute name for member in groups',
         },
         ldapGroupAttributeNameUser => {
-            type    => 'text',
-            default => 'dn',
+            type          => 'text',
+            default       => 'dn',
             documentation =>
 'LDAP attribute name in user entry referenced as member in groups',
         },
@@ -3429,8 +3551,8 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
             documentation => 'LDAP attributes to search in groups',
         },
         ldapGroupAttributeNameGroup => {
-            type    => 'text',
-            default => 'dn',
+            type          => 'text',
+            default       => 'dn',
             documentation =>
 'LDAP attribute name in group entry referenced as member in groups',
         },
@@ -3472,12 +3594,12 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
             default => 'require',
         },
         ldapCAFile => {
-            type => 'text',
+            type          => 'text',
             documentation =>
               'Location of the certificate file for LDAP connections',
         },
         ldapCAPath => {
-            type => 'text',
+            type          => 'text',
             documentation =>
               'Location of the CA directory for LDAP connections',
         },
@@ -3585,8 +3707,8 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
             default       => 3,
             documentation => 'Radius authentication level',
         },
-        radiusSecret => { type => 'text', },
-        radiusServer => { type => 'text', },
+        radiusSecret => { type => 'text' },
+        radiusServer => { type => 'text' },
 
         # REST
         restAuthUrl       => { type => 'url' },
@@ -3597,7 +3719,14 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
         restPwdModifyUrl  => { type => 'url' },
 
         # Remote
-        remotePortal        => { type => 'text', },
+        remoteCookieName => {
+            type          => 'text',
+            test          => qr/^[a-zA-Z][a-zA-Z0-9_-]*$/,
+            msgFail       => '__badCookieName__',
+            documentation => 'Name of the remote portal cookie',
+            flags         => 'p',
+        },
+        remotePortal        => { type => 'text' },
         remoteGlobalStorage => {
             type          => 'PerlModule',
             default       => 'Lemonldap::NG::Common::Apache::Session::SOAP',
@@ -3607,17 +3736,33 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
             type    => 'keyTextContainer',
             default => {
                 proxy => 'http://auth.example.com/sessions',
-                ns =>
+                ns    =>
 'http://auth.example.com/Lemonldap/NG/Common/PSGI/SOAPService',
             },
             documentation => 'Apache::Session module parameters',
         },
 
         # Proxy
-        proxyAuthService    => { type => 'text', },
-        proxySessionService => { type => 'text', },
-        remoteCookieName    => { type => 'text', },
-        proxyUseSoap        => {
+        proxyAuthService            => { type => 'text' },
+        proxySessionService         => { type => 'text' },
+        proxyAuthServiceChoiceValue => { type => 'text' },
+        proxyAuthServiceChoiceParam => {
+            type    => 'text',
+            default => 'lmAuth'
+        },
+        proxyAuthServiceImpersonation => {
+            type          => 'bool',
+            default       => 0,
+            documentation => 'Enable internal portal Impersonation',
+        },
+        proxyCookieName => {
+            type          => 'text',
+            test          => qr/^[a-zA-Z][a-zA-Z0-9_-]*$/,
+            msgFail       => '__badCookieName__',
+            documentation => 'Name of the internal portal cookie',
+            flags         => 'p',
+        },
+        proxyUseSoap => {
             type          => 'bool',
             default       => 0,
             documentation => 'Use SOAP instead of REST',
@@ -3689,7 +3834,7 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
             default => 'id,first-name,last-name,email-address'
         },
         linkedInUserField => { type => 'text', default => 'emailAddress' },
-        linkedInScope =>
+        linkedInScope     =>
           { type => 'text', default => 'r_liteprofile r_emailaddress' },
 
         # GitHub
@@ -3736,10 +3881,10 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
         dbiUserTable    => { type => 'text', },
 
         # TODO: add dbiMailCol
-        dbiAuthLoginCol    => { type => 'text', },
-        dbiAuthPasswordCol => { type => 'text', },
-        dbiPasswordMailCol => { type => 'text', },
-        userPivot          => { type => 'text', },
+        dbiAuthLoginCol     => { type => 'text', },
+        dbiAuthPasswordCol  => { type => 'text', },
+        dbiPasswordMailCol  => { type => 'text', },
+        userPivot           => { type => 'text', },
         dbiAuthPasswordHash =>
           { type => 'text', help => 'authdbi.html#password', },
         dbiDynamicHashEnabled =>
@@ -4124,13 +4269,13 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
             documentation => 'OpenID Connect global access token TTL',
         },
         oidcServiceDynamicRegistrationExportedVars => {
-            type => 'keyTextContainer',
+            type          => 'keyTextContainer',
             documentation =>
               'OpenID Connect exported variables for dynamic registration',
         },
         oidcServiceDynamicRegistrationExtraClaims => {
-            type    => 'keyTextContainer',
-            keyTest => qr/^[\x21\x23-\x5B\x5D-\x7E]+$/,
+            type          => 'keyTextContainer',
+            keyTest       => qr/^[\x21\x23-\x5B\x5D-\x7E]+$/,
             documentation =>
               'OpenID Connect extra claims for dynamic registration',
         },
@@ -4189,7 +4334,7 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
         oidcOPMetaDataOptionsJWKSTimeout  => { type => 'int', default => 0 },
         oidcOPMetaDataOptionsClientID     => { type => 'text', },
         oidcOPMetaDataOptionsClientSecret => { type => 'password', },
-        oidcOPMetaDataOptionsScope =>
+        oidcOPMetaDataOptionsScope        =>
           { type => 'text', default => 'openid profile' },
         oidcOPMetaDataOptionsDisplay => {
             type   => 'select',
@@ -4218,13 +4363,14 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
           { type => 'bool', default => 1 },
         oidcOPMetaDataOptionsIDTokenMaxAge => { type => 'int',  default => 30 },
         oidcOPMetaDataOptionsUseNonce      => { type => 'bool', default => 1 },
-        oidcOPMetaDataOptionsDisplayName  => { type => 'text', },
-        oidcOPMetaDataOptionsIcon         => { type => 'text', },
-        oidcOPMetaDataOptionsStoreIDToken => { type => 'bool', default => 0 },
-        oidcOPMetaDataOptionsSortNumber   => { type => 'int', },
+        oidcOPMetaDataOptionsDisplayName   => { type => 'text', },
+        oidcOPMetaDataOptionsIcon          => { type => 'text', },
+        oidcOPMetaDataOptionsStoreIDToken  => { type => 'bool', default => 0 },
+        oidcOPMetaDataOptionsSortNumber    => { type => 'int', },
 
         # OpenID Connect relying parties
         oidcRPMetaDataExportedVars => {
+            help    => 'idpopenidconnect.html#oidcexportedattr',
             type    => 'oidcAttributeContainer',
             keyTest => qr/\w/,
             test    => qr/\w/,
@@ -4252,7 +4398,7 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
             ],
             default => 'HS512',
         },
-        oidcRPMetaDataOptionsIDTokenExpiration => { type => 'int' },
+        oidcRPMetaDataOptionsIDTokenExpiration  => { type => 'int' },
         oidcRPMetaDataOptionsIDTokenForceClaims =>
           { type => 'bool', default => 0 },
         oidcRPMetaDataOptionsAccessTokenSignAlg => {
@@ -4289,7 +4435,8 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
         oidcRPMetaDataOptionsExtraClaims                 => {
             type    => 'keyTextContainer',
             keyTest => qr/^[\x21\x23-\x5B\x5D-\x7E]+$/,
-            default => {}
+            default => {},
+            help    => 'idpopenidconnect.html#oidcextraclaims'
         },
         oidcRPMetaDataOptionsBypassConsent => {
             type    => 'bool',
@@ -4333,8 +4480,8 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
             documentation => 'Allow offline access',
         },
         oidcRPMetaDataOptionsAllowPasswordGrant => {
-            type    => 'bool',
-            default => 0,
+            type          => 'bool',
+            default       => 0,
             documentation =>
               'Allow OAuth2 Resource Owner Password Credentials Grant',
         },
@@ -4349,7 +4496,7 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{
             documentation => 'Issue refresh tokens',
         },
         oidcRPMetaDataOptionsAuthnLevel => {
-            type => 'int',
+            type          => 'int',
             documentation =>
               'Authentication level requires to access to this RP',
         },
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/CTrees.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/CTrees.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/CTrees.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/CTrees.pm	2022-01-22 14:30:19.000000000 +0000
@@ -14,7 +14,7 @@
 
 package Lemonldap::NG::Manager::Build::CTrees;
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 sub cTrees {
     return {
@@ -30,7 +30,8 @@ sub cTrees {
                     'vhostPort',          'vhostHttps',
                     'vhostMaintenance',   'vhostAliases',
                     'vhostAccessToTrace', 'vhostType',
-                    'vhostAuthnLevel',    'vhostServiceTokenTTL'
+                    'vhostAuthnLevel',    'vhostDevOpsRulesUrl',
+                    'vhostServiceTokenTTL'
                 ],
             },
         ],
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/PortalConstants.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/PortalConstants.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/PortalConstants.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/PortalConstants.pm	2022-01-22 14:30:19.000000000 +0000
@@ -113,7 +113,8 @@ sub portalConstants {
         PE_UPGRADESESSION                    => 102,
         PE_NO_SECOND_FACTORS                 => 103,
         PE_BAD_DEVOPS_FILE                   => 104,
-        PE_FILENOTFOUND                      => 105
+        PE_FILENOTFOUND                      => 105,
+        PE_OIDC_AUTH_ERROR                   => 106,
     };
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Tree.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Tree.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Tree.pm	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Tree.pm	2022-02-19 16:04:21.000000000 +0000
@@ -17,7 +17,7 @@
 
 package Lemonldap::NG::Manager::Build::Tree;
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 
 # TODO: Missing:
 #  * activeTimer
@@ -367,11 +367,21 @@ sub tree {
                         {
                             title => 'proxyParams',
                             help  => 'authproxy.html',
-                            form  => 'simpleInputContainer',
                             nodes => [
-                                'proxyAuthnLevel',     'proxyAuthService',
-                                'proxySessionService', 'remoteCookieName',
-                                'proxyUseSoap'
+                                'proxyAuthnLevel',
+                                'proxyUseSoap',
+                                {
+                                    title => 'proxyInternalPortal',
+                                    form  => 'simpleInputContainer',
+                                    nodes => [
+                                        'proxyAuthService',
+                                        'proxySessionService',
+                                        'proxyAuthServiceChoiceParam',
+                                        'proxyAuthServiceChoiceValue',
+                                        'proxyCookieName',
+                                        'proxyAuthServiceImpersonation',
+                                    ]
+                                }
                             ]
                         },
                         {
@@ -615,6 +625,7 @@ sub tree {
                             form  => 'simpleInputContainer',
                             nodes => [
                                 'stayConnected',
+                                'stayConnectedBypassFG',
                                 'stayConnectedTimeout',
                                 'stayConnectedCookieName'
                             ],
@@ -798,6 +809,8 @@ sub tree {
                                         'checkUserDisplayNormalizedHeaders',
                                         'checkUserDisplayEmptyHeaders',
                                         'checkUserDisplayEmptyValues',
+                                        'checkUserDisplayHiddenAttributes',
+                                        'checkUserDisplayHistory'
                                     ]
                                 },
                             ]
@@ -806,7 +819,12 @@ sub tree {
                             title => 'devOpsCheck',
                             help  => 'checkdevops.html',
                             form  => 'simpleInputContainer',
-                            nodes => [ 'checkDevOps', 'checkDevOpsDownload' ],
+                            nodes => [
+                                'checkDevOps',
+                                'checkDevOpsDownload',
+                                'checkDevOpsDisplayNormalizedHeaders',
+                                'checkDevOpsCheckSessionAttributes'
+                            ],
                         },
                         {
                             title => 'impersonation',
@@ -866,6 +884,7 @@ sub tree {
                         'sfManagerRule',
                         'sfRequired',
                         'sfOnlyUpgrade',
+                        'sfRegisterTimeout',
                         {
                             title => 'utotp2f',
                             help  => 'utotp2f.html',
@@ -887,10 +906,11 @@ sub tree {
                                 'totp2fInterval',
                                 'totp2fRange',
                                 'totp2fDigits',
-                                'totp2fTTL',
+                                'totp2fEncryptSecret',
                                 'totp2fAuthnLevel',
                                 'totp2fLabel',
                                 'totp2fLogo',
+                                'totp2fTTL'
                             ]
                         },
                         {
@@ -899,9 +919,9 @@ sub tree {
                             form  => 'simpleInputContainer',
                             nodes => [
                                 'u2fActivation',       'u2fSelfRegistration',
-                                'u2fUserCanRemoveKey', 'u2fTTL',
-                                'u2fAuthnLevel',       'u2fLabel',
-                                'u2fLogo',
+                                'u2fUserCanRemoveKey', 'u2fAuthnLevel',
+                                'u2fLabel',            'u2fLogo',
+                                'u2fTTL'
                             ]
                         },
                         {
@@ -918,10 +938,10 @@ sub tree {
                                 'yubikey2fUrl',
                                 'yubikey2fPublicIDSize',
                                 'yubikey2fFromSessionAttribute',
-                                'yubikey2fTTL',
                                 'yubikey2fAuthnLevel',
                                 'yubikey2fLabel',
                                 'yubikey2fLogo',
+                                'yubikey2fTTL'
                             ],
                         },
                         {
@@ -931,9 +951,9 @@ sub tree {
                             nodes => [
                                 'mail2fActivation', 'mail2fCodeRegex',
                                 'mail2fTimeout',    'mail2fSubject',
-                                'mail2fBody',       'mail2fAuthnLevel',
-                                'mail2fLabel',      'mail2fLogo',
-                                'mail2fSessionKey',
+                                'mail2fBody',       'mail2fSessionKey',
+                                'mail2fAuthnLevel', 'mail2fLabel',
+                                'mail2fLogo'
                             ]
                         },
                         {
@@ -944,7 +964,7 @@ sub tree {
                                 'ext2fActivation',  'ext2fCodeActivation',
                                 'ext2FSendCommand', 'ext2FValidateCommand',
                                 'ext2fAuthnLevel',  'ext2fLabel',
-                                'ext2fLogo',
+                                'ext2fLogo'
                             ]
                         },
                         {
@@ -958,18 +978,35 @@ sub tree {
                                 'radius2fUsernameSessionKey',
                                 'radius2fTimeout',
                                 'radius2fAuthnLevel',
-                                'radius2fLogo',
                                 'radius2fLabel',
+                                'radius2fLogo'
                             ]
                         },
                         {
                             title => 'rest2f',
                             help  => 'rest2f.html',
+                            form  => 'simpleInputContainer',
                             nodes => [
                                 'rest2fActivation', 'rest2fInitUrl',
                                 'rest2fInitArgs',   'rest2fVerifyUrl',
                                 'rest2fVerifyArgs', 'rest2fAuthnLevel',
-                                'rest2fLabel',      'rest2fLogo',
+                                'rest2fLabel',      'rest2fLogo'
+                            ]
+                        },
+                        {
+                            title => 'webauthn2f',
+                            help  => 'webauthn2f.html',
+                            form  => 'simpleInputContainer',
+                            nodes => [
+                                'webauthn2fActivation',
+                                'webauthn2fSelfRegistration',
+                                'webauthn2fUserVerification',
+                                'webauthn2fUserCanRemoveKey',
+                                'webauthnRpName',
+                                'webauthnDisplayNameAttr',
+                                'webauthn2fAuthnLevel',
+                                'webauthn2fLabel',
+                                'webauthn2fLogo',
                             ]
                         },
                         'sfExtra',
@@ -980,10 +1017,9 @@ sub tree {
                             nodes => [
                                 'sfRemovedMsgRule',  'sfRemovedUseNotif',
                                 'sfRemovedNotifRef', 'sfRemovedNotifTitle',
-                                'sfRemovedNotifMsg',
+                                'sfRemovedNotifMsg'
                             ],
-                        },
-                        'sfRegisterTimeout',
+                        }
                     ]
                 },
                 {
@@ -1034,12 +1070,27 @@ sub tree {
                                 {
                                     title => 'CrowdSecPlugin',
                                     help  => 'crowdsec.html',
+                                    form  => 'simpleInputContainer',
                                     nodes => [
                                         'crowdsec',    'crowdsecAction',
                                         'crowdsecUrl', 'crowdsecKey',
                                     ],
                                 },
                                 {
+                                    title => 'newLocationWarnings',
+                                    help  => 'newlocationwarning.html',
+                                    form  => 'simpleInputContainer',
+                                    nodes => [
+                                        'newLocationWarning',
+                                        'newLocationWarningLocationAttribute',
+'newLocationWarningLocationDisplayAttribute',
+                                        'newLocationWarningMaxValues',
+                                        'newLocationWarningMailAttribute',
+                                        'newLocationWarningMailSubject',
+                                        'newLocationWarningMailBody'
+                                    ]
+                                },
+                                {
                                     title => 'bruteForceAttackProtection',
                                     help  => 'bruteforceprotection.html',
                                     form  => 'simpleInputContainer',
@@ -1049,6 +1100,8 @@ sub tree {
                                         'bruteForceProtectionMaxFailed',
                                         'bruteForceProtectionIncrementalTempo',
                                         'bruteForceProtectionLockTimes',
+                                        'bruteForceProtectionMaxLockTime',
+                                        'bruteForceProtectionMaxAge'
                                     ]
                                 },
                                 'lwpOpts',
@@ -1317,6 +1370,14 @@ sub tree {
                 },
                 'oidcServiceMetaDataAuthnContext',
                 {
+                    title => "oidcServiceDynamicRegistration",
+                    nodes => [
+                        'oidcServiceAllowDynamicRegistration',
+                        'oidcServiceDynamicRegistrationExportedVars',
+                        'oidcServiceDynamicRegistrationExtraClaims',
+                    ],
+                },
+                {
                     title => 'oidcServiceMetaDataSecurity',
                     nodes => [ {
                             title => 'oidcServiceMetaDataKeys',
@@ -1327,23 +1388,25 @@ sub tree {
                                 'oidcServiceKeyIdSig',
                             ],
                         },
-                        'oidcServiceAllowDynamicRegistration',
-                        'oidcServiceAllowOnlyDeclaredScopes',
                         'oidcServiceAllowAuthorizationCodeFlow',
                         'oidcServiceAllowImplicitFlow',
                         'oidcServiceAllowHybridFlow',
+                        'oidcServiceAllowOnlyDeclaredScopes',
+                    ],
+                },
+                {
+                    title => 'oidcServiceMetaDataTimeouts',
+                    nodes => [
                         'oidcServiceAuthorizationCodeExpiration',
-                        'oidcServiceAccessTokenExpiration',
                         'oidcServiceIDTokenExpiration',
+                        'oidcServiceAccessTokenExpiration',
                         'oidcServiceOfflineSessionExpiration',
-                    ],
+                    ]
                 },
                 {
                     title => "oidcServiceMetaDataSessions",
-                    nodes => [ 'oidcStorage', 'oidcStorageOptions', ],
+                    nodes => [ 'oidcStorage', 'oidcStorageOptions' ],
                 },
-                'oidcServiceDynamicRegistrationExportedVars',
-                'oidcServiceDynamicRegistrationExtraClaims',
             ]
         },
         'oidcOPMetaDataNodes',
@@ -1354,11 +1417,11 @@ sub tree {
             nodes => [
                 'casAttr',
                 'casAccessControlPolicy',
+                'casStrictMatching',
+                'casTicketExpiration',
                 'casStorage',
                 'casStorageOptions',
                 'casAttributes',
-                'casStrictMatching',
-
             ]
         },
         'casSrvMetaDataNodes',
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build.pm	2022-02-19 16:04:21.000000000 +0000
@@ -188,7 +188,7 @@ our \$specialNodeHash = {
     samlIDPMetaDataNodes => [qw(samlIDPMetaDataXML samlIDPMetaDataExportedAttributes samlIDPMetaDataOptions)],
     samlSPMetaDataNodes  => [qw(samlSPMetaDataXML samlSPMetaDataExportedAttributes samlSPMetaDataOptions samlSPMetaDataMacros)],
     oidcOPMetaDataNodes  => [qw(oidcOPMetaDataJSON oidcOPMetaDataJWKS oidcOPMetaDataOptions oidcOPMetaDataExportedVars)],
-    oidcRPMetaDataNodes  => [qw(oidcRPMetaDataOptions oidcRPMetaDataExportedVars oidcRPMetaDataOptionsExtraClaims oidcRPMetaDataMacros)],
+    oidcRPMetaDataNodes  => [qw(oidcRPMetaDataOptions oidcRPMetaDataExportedVars oidcRPMetaDataOptionsExtraClaims oidcRPMetaDataMacros oidcRPMetaDataScopeRules)],
     casSrvMetaDataNodes  => [qw(casSrvMetaDataOptions casSrvMetaDataExportedVars)],
     casAppMetaDataNodes  => [qw(casAppMetaDataOptions casAppMetaDataExportedVars casAppMetaDataMacros)],
 };
@@ -207,7 +207,8 @@ EOF
     foreach (@simpleHashKeys) {
         $ra->add($_);
     }
-    print F "our \$simpleHashKeys = '" . $ra->as_string . "';\n"
+    print F "our \$simpleHashKeys = '"
+      . $ra->as_string . "';\n"
       . "our \$specialNodeKeys = '${ignoreKeys}s';\n";
     foreach ( sort keys %cnodesRe ) {
         print F "our \$${_}Keys = '$cnodesRe{$_}';\n";
@@ -467,7 +468,7 @@ sub buildPortalConstants() {
 
     printf STDERR $format, $self->portalConstantsFile;
     open( F, '>', $self->portalConstantsFile ) or die($!);
-    my $urire = $RE{URI}{HTTP}{ -scheme=>qr/https?/ }{-keep};
+    my $urire = $RE{URI}{HTTP}{ -scheme => qr/https?/ }{-keep};
     $urire =~ s/([\$\@])/\\$1/g;
     my $content = <<EOF;
 # This file is generated by $module. Don't modify it by hand
@@ -596,7 +597,7 @@ sub scanTree {
         # Subnode
         elsif ( ref($leaf) ) {
             $jleaf->{title} = $jleaf->{id} = $leaf->{title};
-            $jleaf->{type} = $leaf->{form} if ( $leaf->{form} );
+            $jleaf->{type}  = $leaf->{form} if ( $leaf->{form} );
             if ( $leaf->{title} =~ /^((?:oidc|saml|cas)Service)MetaData$/ ) {
                 no strict 'refs';
                 my @tmp = $self->scanLeaf( $leaf->{nodes} );
@@ -677,6 +678,7 @@ sub scanTree {
                     my $type = $attr->{type};
                     $type =~ s/Container//;
                     foreach my $k ( sort keys( %{ $attr->{default} } ) ) {
+
                         # Special handling for oidcAttribute
                         my $default = $attr->{default}->{$k};
                         if ( $attr->{type} eq 'oidcAttributeContainer' ) {
@@ -703,9 +705,9 @@ sub scanTree {
                 push @cnodesKeys, $leaf;
             }
 
-            # issue 2439
-            # FIXME: in future versions, oidcOPMetaDataJSON and samlIDPMetaDataXML shoud
-            # behave the same
+    # issue 2439
+    # FIXME: in future versions, oidcOPMetaDataJSON and samlIDPMetaDataXML shoud
+    # behave the same
             if ( $leaf =~ /^oidcOPMetaData(?:JSON|JWKS)$/ ) {
                 push @simpleHashKeys, $leaf;
             }
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Cli.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Cli.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Cli.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Cli.pm	2022-02-19 16:04:21.000000000 +0000
@@ -24,8 +24,8 @@ has cfgNum => (
 
 has log    => ( is => 'rw' );
 has req    => ( is => 'ro' );
-has sep    => ( is => 'rw', isa => 'Str', default => '/' );
-has format => ( is => 'rw', isa => 'Str', default => "%-25s | %-25s | %-25s" );
+has sep    => ( is => 'rw', isa => 'Str',  default => '/' );
+has format => ( is => 'rw', isa => 'Str',  default => "%-25s | %-25s | %-25s" );
 has yes    => ( is => 'rw', isa => 'Bool', default => 0 );
 has safe   => ( is => 'rw', isa => 'Bool', default => 0 );
 has force  => ( is => 'rw', isa => 'Bool', default => 0 );
@@ -317,8 +317,8 @@ sub lastCfg {
 
 sub save {
     my ($self) = @_;
-    my $conf = $self->jsonResponse( '/confs/' . $self->cfgNum, 'full=1' );
-    my $json = JSON->new->indent->canonical;
+    my $conf   = $self->jsonResponse( '/confs/' . $self->cfgNum, 'full=1' );
+    my $json   = JSON->new->indent->canonical;
     print $json->encode($conf);
 }
 
@@ -404,9 +404,9 @@ sub _getKey {
 
 sub _setKey {
     my ( $self, $conf, $key, $value ) = @_;
-    my $sep = $self->sep;
+    my $sep    = $self->sep;
     my (@path) = split $sep, $key;
-    my $last = pop @path;
+    my $last   = pop @path;
     while ( my $next = shift @path ) {
         $conf = $conf->{$next};
     }
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Diff.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Diff.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Diff.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Diff.pm	2022-02-19 16:04:21.000000000 +0000
@@ -20,9 +20,10 @@ sub diff {
                 $res[$i]->{$key} = $tmp[$i] if ( $tmp[$i] );
             }
         }
-        elsif ( $key =~ $hashParameters
-            or
-            ( ref( $conf[0]->{$key} ) and ref( $conf[0]->{$key} ) eq 'HASH' ) )
+        elsif (
+            $key =~ $hashParameters
+            or ( ref( $conf[0]->{$key} ) and ref( $conf[0]->{$key} ) eq 'HASH' )
+          )
         {
             if ( ref $conf[1]->{$key} ) {
                 my @tmp =
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Parser.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Parser.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Parser.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Parser.pm	2022-02-19 16:04:21.000000000 +0000
@@ -438,8 +438,8 @@ sub _scanNodes {
                         $self->_scanNodes($subNodes);
                     }
                 }
-                elsif (
-                    $target =~ /^oidc(?:O|R)PMetaData(?:ExportedVars|Macros|ScopeRules)$/ )
+                elsif ( $target =~
+                    /^oidc(?:O|R)PMetaData(?:ExportedVars|Macros|ScopeRules)$/ )
                 {
                     hdebug("  $target");
                     if ( $leaf->{cnodes} ) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Tests.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Tests.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Tests.pm	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Tests.pm	2022-02-19 16:04:21.000000000 +0000
@@ -7,7 +7,7 @@ use Lemonldap::NG::Common::Regexp;
 use Lemonldap::NG::Handler::Main;
 use Lemonldap::NG::Common::Util qw(getSameSite);
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 ## @method hashref tests(hashref conf)
 # Return a hash ref where keys are the names of the tests and values
@@ -44,10 +44,18 @@ sub tests {
 
         # Check if portal URL is well formated
         portalURL => sub {
+            my $url = $conf->{portal};
 
-            # Append or remove trailing ending slashes
+            # Append or remove trailing slashes
             $conf->{portal} =~ s%/*$%/%;
-            return 1;
+            return (
+                1,
+                (
+                    ( $url =~ m%/$% )
+                    ? ''
+                    : "Portal URL should end with a /"
+                )
+            );
         },
 
         # Check if virtual hosts are in the domain
@@ -622,7 +630,7 @@ sub tests {
 
             my $msg = '';
             my $ok  = 0;
-            foreach (qw(u totp yubikey)) {
+            foreach (qw(u totp yubikey webauthn)) {
                 $ok ||= $conf->{ $_ . '2fActivation' }
                   && $conf->{ $_ . '2fSelfRegistration' };
                 last if ($ok);
@@ -738,8 +746,8 @@ sub tests {
             return ( 1,
                 "Impersonation and ContextSwitching are simultaneously enabled"
               )
-              if ( $conf->{impersonationRule}
-                && $conf->{contextSwitchingRule} );
+              if (  $conf->{impersonationRule}
+                and $conf->{contextSwitchingRule} );
             return 1;
         },
 
@@ -883,25 +891,32 @@ sub tests {
             my %casUrl;
             foreach my $casConfKey ( keys %{ $conf->{casAppMetaDataOptions} } )
             {
-                my $appUrl =
-                  $conf->{casAppMetaDataOptions}->{$casConfKey}
-                  ->{casAppMetaDataOptionsService}
-                  || "";
-                $appUrl =~ m#^(https?://[^/]+)(/.*)?$#;
-                my $appHost = $1;
-                unless ($appHost) {
-                    push @msg, "$casConfKey CAS Application has no Service URL";
-                    $res = 0;
-                    next;
-                }
+                for my $appUrl (
+                    split(
+                        /\s+/,
+                        $conf->{casAppMetaDataOptions}->{$casConfKey}
+                          ->{casAppMetaDataOptionsService}
+                    )
+                  )
+                {
+                    $appUrl ||= "";
+                    $appUrl =~ m#^(https?://[^/]+)(/.*)?$#;
+                    my $appHost = $1;
+                    unless ($appHost) {
+                        push @msg,
+                          "$casConfKey CAS Application has no Service URL";
+                        $res = 0;
+                        next;
+                    }
 
-                if ( defined $casUrl{$appUrl} ) {
-                    push @msg,
+                    if ( defined $casUrl{$appUrl} ) {
+                        push @msg,
 "$casConfKey and $casUrl{$appUrl} have the same Service URL";
-                    $res = 0;
-                    next;
+                        $res = 0;
+                        next;
+                    }
+                    $casUrl{$appUrl} = $casConfKey;
                 }
-                $casUrl{$appUrl} = $casConfKey;
             }
             return ( $res, join( ', ', @msg ) );
         },
@@ -911,7 +926,9 @@ sub tests {
             return 1 unless ( $conf->{sfRemovedMsgRule} );
             return ( 1,
 'Notification system must be enabled to display a notification if a SF is removed'
-            ) if ( $conf->{sfRemovedUseNotif} and not $conf->{notification} );
+              )
+              if ( $conf->{sfRemovedUseNotif}
+                and not $conf->{notification} );
             return 1;
         },
 
@@ -928,7 +945,8 @@ sub tests {
         # Same with SameSite=(auto) and SAML issuer in use
         SameSiteNoneWithSecure => sub {
             return ( -1, 'SameSite value = None requires the secured flag' )
-              if ( getSameSite($conf) eq 'None' and !$conf->{securedCookie} );
+              if ( getSameSite($conf) eq 'None'
+                and !$conf->{securedCookie} );
             return 1;
         },
 
@@ -1024,6 +1042,33 @@ sub tests {
               if ( $conf->{authentication} eq 'Choice'
                 and scalar keys %{ $conf->{authChoiceModules} } == 0 );
             return 1;
+        },
+
+        # Internal portal URL must be defined with Proxy authentication
+        authProxy => sub {
+            return ( 0,
+                'Proxy authentication enabled without internal portal URL' )
+              if ( $conf->{authentication} eq 'Proxy'
+                and !$conf->{proxyAuthService} );
+            return 1;
+        },
+
+# Warn if Impersonation and proxyAuthServiceImpersonation are simultaneously enabled
+        impersonationProxy => sub {
+            return ( -1,
+'Impersonation and internal portal Impersonation are simultaneously enabled'
+              )
+              if (  $conf->{impersonationRule}
+                and $conf->{proxyAuthServiceImpersonation} );
+            return 1;
+        },
+
+        # CheckDevOps requires Safe jail
+        checkDevOpsWithSafeJail => sub {
+            return ( 0, 'Safe jail must be enabled with CheckDevOps plugin' )
+              if ( $conf->{checkDevOps}
+                and !$conf->{useSafeJail} );
+            return 1;
         }
     };
 }
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Zero.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Zero.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Zero.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Zero.pm	2022-02-19 16:04:21.000000000 +0000
@@ -106,7 +106,7 @@ sub zeroConf {
         },
         'cfgNum'               => 0,
         'globalStorageOptions' => {
-            'Directory' => $sessionDir,
+            'Directory'      => $sessionDir,
             'generateModule' =>
               'Lemonldap::NG::Common::Apache::Session::Generate::SHA256',
             'LockDirectory' => "$sessionDir/lock"
@@ -177,14 +177,14 @@ sub zeroConf {
                   'inGroup("timelords") or $uid eq "rtyler"',
             }
         },
-        'whatToTrace'   => '_whatToTrace',
-        'securedCookie' => 0,
-        'cookieName'    => 'lemonldap',
-        'cfgAuthor'     => 'The LemonLDAP::NG team',
-        'cfgDate'       => '1627287638',
-        'cfgVersion'    => $VERSION,
-        'exportedVars'  => {},
-        'portalSkin'    => 'bootstrap',
+        'whatToTrace'          => '_whatToTrace',
+        'securedCookie'        => 0,
+        'cookieName'           => 'lemonldap',
+        'cfgAuthor'            => 'The LemonLDAP::NG team',
+        'cfgDate'              => '1627287638',
+        'cfgVersion'           => $VERSION,
+        'exportedVars'         => {},
+        'portalSkin'           => 'bootstrap',
         'portalSkinBackground' =>
           '1280px-Cedar_Breaks_National_Monument_partially.jpg',
         'mailUrl'                    => "http://auth.$domain/resetpwd",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf.pm	2022-02-07 19:06:14.000000000 +0000
@@ -24,7 +24,7 @@ extends qw(
   Lemonldap::NG::Common::Conf::RESTServer
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 #############################
 # I. INITIALIZATION METHODS #
@@ -212,7 +212,10 @@ sub _generateX509 {
     my $strCert = Net::SSLeay::PEM_get_string_X509($cert);
     my $strPrivate;
     if ($password) {
-        $strPrivate = Net::SSLeay::PEM_get_string_PrivateKey( $key, $password );
+        my $alg = Net::SSLeay::EVP_get_cipherbyname("AES-256-CBC")
+          || Net::SSLeay::EVP_get_cipherbyname("DES-EDE3-CBC");
+        $strPrivate =
+          Net::SSLeay::PEM_get_string_PrivateKey( $key, $password, $alg );
     }
     else {
         $strPrivate = Net::SSLeay::PEM_get_string_PrivateKey($key);
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Sessions.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Sessions.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Sessions.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Sessions.pm	2022-02-19 16:04:21.000000000 +0000
@@ -248,7 +248,8 @@ sub sessions {
                 value    => $uid,
                 count    => scalar( @{ $r->{$uid} } ),
                 sessions => [
-                    map { {
+                    map {
+                        {
                             session =>
                               $self->_maybeEncryptSessionId( $_->{_sessionId} ),
                             date => $_->{_utime}
@@ -399,7 +400,8 @@ qq{Use of an uninitialized attribute "$g
     else {
         $res = [
             sort { $a->{date} <=> $b->{date} }
-              map { {
+              map {
+                {
                     session => $self->_maybeEncryptSessionId($_),
                     date    => $res->{$_}->{_utime}
                 }
@@ -459,8 +461,8 @@ sub delSession {
 }
 
 sub cmpIPv4 {
-    my @a = split /\./, $_[0];
-    my @b = split /\./, $_[1];
+    my @a   = split /\./, $_[0];
+    my @b   = split /\./, $_[1];
     my $cmp = 0;
   F: for ( my $i = 0 ; $i < 4 ; $i++ ) {
         if ( $a[$i] != $b[$i] ) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Viewer.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Viewer.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Viewer.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Viewer.pm	2022-01-22 14:30:19.000000000 +0000
@@ -12,7 +12,7 @@ extends 'Lemonldap::NG::Manager::Conf';
 has diffRule => ( is => 'rw', default => sub { 0 } );
 has brwRule  => ( is => 'rw', default => sub { 0 } );
 
-our $VERSION = '2.0.10';
+our $VERSION = '2.0.14';
 
 #############################
 # I. INITIALIZATION METHODS #
@@ -67,7 +67,7 @@ sub init {
     }
 
     # Forbid hidden keys
-    foreach ( split /\s+/, $hiddenKeys ) {
+    foreach ( split /[,\s]+/, $hiddenKeys ) {
         $self->addRoute(
             view => { ':cfgNum' => { $_ => 'rejectKey' } },
             ['GET']
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager.pm 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager.pm
--- 2.0.13+ds-3/lemonldap-ng-manager/lib/Lemonldap/NG/Manager.pm	2021-08-20 16:29:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/lib/Lemonldap/NG/Manager.pm	2022-02-19 16:43:01.000000000 +0000
@@ -17,14 +17,14 @@ use JSON;
 use Lemonldap::NG::Common::Conf::Constants;
 use Lemonldap::NG::Common::PSGI::Constants;
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Handler::PSGI::Router
   Lemonldap::NG::Common::Conf::AccessLib
 );
 
-has csp => ( is => 'rw' );
+has csp            => ( is => 'rw' );
 has loadedPlugins  => ( is => 'rw', default => sub { [] } );
 has hLoadedPlugins => ( is => 'rw', default => sub { {} } );
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/Makefile.PL 2.0.14+ds-1/lemonldap-ng-manager/Makefile.PL
--- 2.0.13+ds-3/lemonldap-ng-manager/Makefile.PL	2021-08-20 16:29:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/Makefile.PL	2022-02-21 12:04:41.000000000 +0000
@@ -25,7 +25,7 @@ WriteMakefile(
             },
             MailingList => 'mailto:lemonldap-ng-dev@ow2.org',
             license     => 'http://opensource.org/licenses/GPL-2.0',
-            homepage    => 'http://lemonldap-ng.org/',
+            homepage    => 'https://lemonldap-ng.org/',
             bugtracker =>
               'https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues',
             x_twitter => 'https://twitter.com/lemonldapng',
@@ -34,8 +34,8 @@ WriteMakefile(
     PREREQ_PM => {
         'Convert::PEM'           => 0,
         'Crypt::OpenSSL::RSA'    => 0,
-        'Lemonldap::NG::Common'  => '2.0.13',
-        'Lemonldap::NG::Handler' => '2.0.13',
+        'Lemonldap::NG::Common'  => '2.0.14',
+        'Lemonldap::NG::Handler' => '2.0.14',
         'LWP::UserAgent'         => 0,
     },    # e.g., Module::Name => 1.1
     (
@@ -45,7 +45,7 @@ WriteMakefile(
             ABSTRACT_FROM =>
               'lib/Lemonldap/NG/Manager.pm',    # retrieve abstract from module
             AUTHOR =>
-'Xavier Guimard <x.guimard@free.fr>, Clément Oudot <clement@oodo.net>'
+'Xavier Guimard <x.guimard@free.fr>, Clement Oudot <clement@oodo.net>, Christophe Maudoux <chrmdx@gmail.com>, Maxime Besson <maxime.besson@worteks.com>'
           )
         : ()
     ),
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/MANIFEST 2.0.14+ds-1/lemonldap-ng-manager/MANIFEST
--- 2.0.13+ds-3/lemonldap-ng-manager/MANIFEST	2021-07-22 17:21:57.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/MANIFEST	2022-01-22 15:57:14.000000000 +0000
@@ -211,6 +211,7 @@ site/htdocs/static/logos/it.png
 site/htdocs/static/logos/llng-icon-32.png
 site/htdocs/static/logos/llng-logo-32.png
 site/htdocs/static/logos/pl.png
+site/htdocs/static/logos/pt_BR.png
 site/htdocs/static/logos/tr.png
 site/htdocs/static/logos/vi.png
 site/htdocs/static/logos/zh.png
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/META.json 2.0.14+ds-1/lemonldap-ng-manager/META.json
--- 2.0.13+ds-3/lemonldap-ng-manager/META.json	2021-08-20 16:29:37.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/META.json	2022-02-21 12:04:41.000000000 +0000
@@ -1,7 +1,7 @@
 {
    "abstract" : "Perl extension for managing Lemonldap::NG Web-SSO system.",
    "author" : [
-      "Xavier Guimard <x.guimard@free.fr>, ClÃ©ment Oudot <clement@oodo.net>"
+      "Xavier Guimard <x.guimard@free.fr>, Clement Oudot <clement@oodo.net>, Christophe Maudoux <chrmdx@gmail.com>, Maxime Besson <maxime.besson@worteks.com>"
    ],
    "dynamic_config" : 1,
    "generated_by" : "ExtUtils::MakeMaker version 7.34, CPAN::Meta::Converter version 2.150010",
@@ -41,8 +41,8 @@
             "Convert::PEM" : "0",
             "Crypt::OpenSSL::RSA" : "0",
             "LWP::UserAgent" : "0",
-            "Lemonldap::NG::Common" : "v2.0.13",
-            "Lemonldap::NG::Handler" : "v2.0.13"
+            "Lemonldap::NG::Common" : "v2.0.14",
+            "Lemonldap::NG::Handler" : "v2.0.14"
          }
       }
    },
@@ -52,12 +52,12 @@
       "bugtracker" : {
          "web" : "https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues"
       },
-      "homepage" : "http://lemonldap-ng.org/",
+      "homepage" : "https://lemonldap-ng.org/",
       "license" : [
          "http://opensource.org/licenses/GPL-2.0"
       ],
       "x_MailingList" : "mailto:lemonldap-ng-dev@ow2.org"
    },
-   "version" : "v2.0.13",
+   "version" : "v2.0.14",
    "x_serialization_backend" : "JSON::PP version 4.04"
 }
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/META.yml 2.0.14+ds-1/lemonldap-ng-manager/META.yml
--- 2.0.13+ds-3/lemonldap-ng-manager/META.yml	2021-08-20 16:29:37.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/META.yml	2022-02-21 12:04:41.000000000 +0000
@@ -1,7 +1,7 @@
 ---
 abstract: 'Perl extension for managing Lemonldap::NG Web-SSO system.'
 author:
-  - 'Xavier Guimard <x.guimard@free.fr>, ClÃ©ment Oudot <clement@oodo.net>'
+  - 'Xavier Guimard <x.guimard@free.fr>, Clement Oudot <clement@oodo.net>, Christophe Maudoux <chrmdx@gmail.com>, Maxime Besson <maxime.besson@worteks.com>'
 build_requires:
   Email::Sender: '0'
   IO::String: '0'
@@ -26,13 +26,13 @@ requires:
   Convert::PEM: '0'
   Crypt::OpenSSL::RSA: '0'
   LWP::UserAgent: '0'
-  Lemonldap::NG::Common: v2.0.13
-  Lemonldap::NG::Handler: v2.0.13
+  Lemonldap::NG::Common: v2.0.14
+  Lemonldap::NG::Handler: v2.0.14
 resources:
   MailingList: mailto:lemonldap-ng-dev@ow2.org
   X_twitter: https://twitter.com/lemonldapng
   bugtracker: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues
-  homepage: http://lemonldap-ng.org/
+  homepage: https://lemonldap-ng.org/
   license: http://opensource.org/licenses/GPL-2.0
-version: v2.0.13
+version: v2.0.14
 x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/README 2.0.14+ds-1/lemonldap-ng-manager/README
--- 2.0.13+ds-3/lemonldap-ng-manager/README	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/README	2022-02-21 12:04:41.000000000 +0000
@@ -3,7 +3,7 @@ LemonLDAP::NG
 
 LemonLDAP::NG is a modular Web-SSO based on Apache::Session modules.
 This is the manager part of it. You can find documentation here:
- * for administrators: http://lemonldap-ng.org/
+ * for administrators: https://lemonldap-ng.org/
  * for developers: see embedded perldoc
 
 LemonLDAP::NG is a free software; you can redistribute it and/or modify
@@ -20,7 +20,9 @@ You should have received a copy of the G
 along with this program.  If not, see L<http://www.gnu.org/licenses/>.
 
 Copyright:
- * 2005-2015 by Xavier Guimard and Clément Oudot
+ * 2005-2022 by Xavier Guimard and Clément Oudot
+ * 2018-2022 by Christophe Maudoux
+ * 2019-2022 by Maxime Besson
  * 2008-2011 by Thomas Chemineau
  * 2012-2015 by François-Xavier Deltombe and Sandro Cazzaniga
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/coffee/2ndfa.coffee 2.0.14+ds-1/lemonldap-ng-manager/site/coffee/2ndfa.coffee
--- 2.0.13+ds-3/lemonldap-ng-manager/site/coffee/2ndfa.coffee	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/coffee/2ndfa.coffee	2022-02-19 16:04:21.000000000 +0000
@@ -73,6 +73,7 @@ llapp.controller 'SessionsExplorerCtrl',
 	$scope.U2FCheck = "1"
 	$scope.TOTPCheck = "1"
 	$scope.UBKCheck = "1"
+	$scope.WebAuthnCheck = "1"
 
 	# Import translations functions
 	$scope.translateP = $translator.translateP
@@ -201,7 +202,7 @@ llapp.controller 'SessionsExplorerCtrl',
 				subres = []
 				for attr in attrs
 					if session[attr]
-						if session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK)"/)
+						if session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK|WebAuthn)"/)
 							subres.push
 								title: "type"
 								value: "name"
@@ -295,7 +296,7 @@ llapp.controller 'SessionsExplorerCtrl',
 			over = 0
 
 		# Launch HTTP query
-		$http.get("#{scriptname}sfa/#{sessionType}?#{query}&U2FCheck=#{$scope.U2FCheck}&TOTPCheck=#{$scope.TOTPCheck}&UBKCheck=#{$scope.UBKCheck}").then (response) ->
+		$http.get("#{scriptname}sfa/#{sessionType}?#{query}&U2FCheck=#{$scope.U2FCheck}&TOTPCheck=#{$scope.TOTPCheck}&UBKCheck=#{$scope.UBKCheck}&WebAuthnCheck=#{$scope.WebAuthnCheck}").then (response) ->
 			data = response.data
 			if data.result
 				for n in data.values
@@ -346,7 +347,7 @@ llapp.controller 'SessionsExplorerCtrl',
 			over = 0
 
 		# Launch HTTP
-		$http.get("#{scriptname}sfa/#{sessionType}?_session_uid=#{$scope.searchString}*&groupBy=substr(_session_uid,#{$scope.searchString.length})&U2FCheck=#{$scope.U2FCheck}&TOTPCheck=#{$scope.TOTPCheck}&UBKCheck=#{$scope.UBKCheck}").then (response) ->
+		$http.get("#{scriptname}sfa/#{sessionType}?_session_uid=#{$scope.searchString}*&groupBy=substr(_session_uid,#{$scope.searchString.length})&U2FCheck=#{$scope.U2FCheck}&TOTPCheck=#{$scope.TOTPCheck}&UBKCheck=#{$scope.UBKCheck}&WebAuthnCheck=#{$scope.WebAuthnCheck}").then (response) ->
 			data = response.data
 			if data.result
 				for n in data.values
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/coffee/manager.coffee 2.0.14+ds-1/lemonldap-ng-manager/site/coffee/manager.coffee
--- 2.0.13+ds-3/lemonldap-ng-manager/site/coffee/manager.coffee	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/coffee/manager.coffee	2022-02-19 16:04:21.000000000 +0000
@@ -665,12 +665,13 @@ llapp.controller 'TreeCtrl', [
 			#  menuApp
 			#  menuCat
 			#  rule
+			#  oidcAttribute
 			#  samlAttribute
 			#  samlIDPMetaDataNode
 			#  samlSPMetaDataNode
 			#  sfExtra
 			#  virtualHost
-			return if node.type and node.type.match /^(?:s(?:aml(?:(?:ID|S)PMetaDataNod|Attribut)e|fExtra)|(?:(?:cmbMod|r)ul|authChoic)e|(?:virtualHos|keyTex)t|menu(?:App|Cat))$/ then true else false
+			return if node.type and node.type.match /^(?:s(?:aml(?:(?:ID|S)PMetaDataNod|Attribut)e|fExtra)|oidcAttribute|(?:(?:cmbMod|r)ul|authChoic)e|(?:virtualHos|keyTex)t|menu(?:App|Cat))$/ then true else false
 
 		# Send test Email
 		$scope.sendTestMail = ->
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/coffee/sessions.coffee 2.0.14+ds-1/lemonldap-ng-manager/site/coffee/sessions.coffee
--- 2.0.13+ds-3/lemonldap-ng-manager/site/coffee/sessions.coffee	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/coffee/sessions.coffee	2022-02-19 16:04:21.000000000 +0000
@@ -246,7 +246,7 @@ llapp.controller 'SessionsExplorerCtrl',
 				subres = []
 				for attr in attrs
 					if session[attr]
-						if session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK)"/)
+						if session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK|WebAuthn)"/)
 							subres.push
 								title: "type"
 								value: "name"
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/js/2ndfa.js 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/js/2ndfa.js
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/js/2ndfa.js	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/js/2ndfa.js	2022-02-19 16:04:21.000000000 +0000
@@ -89,6 +89,7 @@
       $scope.U2FCheck = "1";
       $scope.TOTPCheck = "1";
       $scope.UBKCheck = "1";
+      $scope.WebAuthnCheck = "1";
       $scope.translateP = $translator.translateP;
       $scope.translate = $translator.translate;
       $scope.translateTitle = function(node) {
@@ -205,7 +206,7 @@
             for (i = 0, len = attrs.length; i < len; i++) {
               attr = attrs[i];
               if (session[attr]) {
-                if (session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK)"/)) {
+                if (session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK|WebAuthn)"/)) {
                   subres.push({
                     title: "type",
                     value: "name",
@@ -303,7 +304,7 @@
         } else {
           over = 0;
         }
-        return $http.get(scriptname + "sfa/" + sessionType + "?" + query + "&U2FCheck=" + $scope.U2FCheck + "&TOTPCheck=" + $scope.TOTPCheck + "&UBKCheck=" + $scope.UBKCheck).then(function(response) {
+        return $http.get(scriptname + "sfa/" + sessionType + "?" + query + "&U2FCheck=" + $scope.U2FCheck + "&TOTPCheck=" + $scope.TOTPCheck + "&UBKCheck=" + $scope.UBKCheck + "&WebAuthnCheck=" + $scope.WebAuthnCheck).then(function(response) {
           var data, i, len, n, ref;
           data = response.data;
           if (data.result) {
@@ -345,7 +346,7 @@
         } else {
           over = 0;
         }
-        return $http.get(scriptname + "sfa/" + sessionType + "?_session_uid=" + $scope.searchString + "*&groupBy=substr(_session_uid," + $scope.searchString.length + ")&U2FCheck=" + $scope.U2FCheck + "&TOTPCheck=" + $scope.TOTPCheck + "&UBKCheck=" + $scope.UBKCheck).then(function(response) {
+        return $http.get(scriptname + "sfa/" + sessionType + "?_session_uid=" + $scope.searchString + "*&groupBy=substr(_session_uid," + $scope.searchString.length + ")&U2FCheck=" + $scope.U2FCheck + "&TOTPCheck=" + $scope.TOTPCheck + "&UBKCheck=" + $scope.UBKCheck + "&WebAuthnCheck=" + $scope.WebAuthnCheck).then(function(response) {
           var data, i, len, n, ref;
           data = response.data;
           if (data.result) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/js/manager.js 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/js/manager.js
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/js/manager.js	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/js/manager.js	2022-02-19 16:04:21.000000000 +0000
@@ -811,7 +811,7 @@ This file contains:
       $scope.keyWritable = function(scope) {
         var node;
         node = scope.$modelValue;
-        if (node.type && node.type.match(/^(?:s(?:aml(?:(?:ID|S)PMetaDataNod|Attribut)e|fExtra)|(?:(?:cmbMod|r)ul|authChoic)e|(?:virtualHos|keyTex)t|menu(?:App|Cat))$/)) {
+        if (node.type && node.type.match(/^(?:s(?:aml(?:(?:ID|S)PMetaDataNod|Attribut)e|fExtra)|oidcAttribute|(?:(?:cmbMod|r)ul|authChoic)e|(?:virtualHos|keyTex)t|menu(?:App|Cat))$/)) {
           return true;
         } else {
           return false;
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/js/sessions.js 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/js/sessions.js
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/js/sessions.js	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/js/sessions.js	2022-02-19 16:04:21.000000000 +0000
@@ -280,7 +280,7 @@
             for (i = 0, len = attrs.length; i < len; i++) {
               attr = attrs[i];
               if (session[attr]) {
-                if (session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK)"/)) {
+                if (session[attr].toString().match(/"type":\s*"(?:TOTP|U2F|UBK|WebAuthn)"/)) {
                   subres.push({
                     title: "type",
                     value: "name",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/ar.json 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/ar.json
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/ar.json	2021-08-21 17:42:59.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/ar.json	2022-02-19 16:04:21.000000000 +0000
@@ -126,7 +126,9 @@
 "bruteForceProtection":"تفعيل",
 "bruteForceProtectionIncrementalTempo":"Incremental lock",
 "bruteForceProtectionLockTimes":"Incremental lock times",
+"bruteForceProtectionMaxAge":"الحد الأقصى للعمر",
 "bruteForceProtectionMaxFailed":"Allowed failed logins",
+"bruteForceProtectionMaxLockTime":"Maximum lock time",
 "bruteForceProtectionTempo":"Lock time",
 "cancel":"إلغاء",
 "captcha_login_enabled":"التفعيل في استمارة تسجيل الدخول",
@@ -165,6 +167,7 @@
 "casStorage":"اسم وحدة جلسات كاس",
 "casStorageOptions":" خيارات وحدة جلسات كاس",
 "casStrictMatching":"Use strict URL matching",
+"casTicketExpiration":"Temporary ticket lifetime",
 "categoryName":"اسم الفئة",
 "cda":"نطاقات متعددة",
 "certificateMailContent":"محتوى البريد",
@@ -180,6 +183,8 @@
 "cfgLog":"Summary",
 "cfgVersion":"عملية ضبط الإصدارات",
 "checkDevOps":"تفعيل",
+"checkDevOpsCheckSessionAttributes":"Check session attributes",
+"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
 "checkDevOpsDownload":"Download file",
 "checkState":"تفعيل",
 "checkStateSecret":"سر مشترك",
@@ -188,6 +193,8 @@
 "checkUserDisplayComputedSession":"Computed sessions",
 "checkUserDisplayEmptyHeaders":"Empty headers",
 "checkUserDisplayEmptyValues":"Empty values",
+"checkUserDisplayHiddenAttributes":"Hidden attributes",
+"checkUserDisplayHistory":"History",
 "checkUserDisplayNormalizedHeaders":"Normalized headers",
 "checkUserDisplayPersistentInfo":"Persistent session data",
 "checkUserHiddenAttributes":"السمات المخفية",
@@ -349,7 +356,7 @@
 "facebookExportedVars":"المتغيرات المصدرة",
 "facebookParams":"معاييرفاسيبوك",
 "facebookUserField":"Field containing user identifier",
-"failedLoginNumber":"عدد عمليات تسجيل الدخول الفاشلة المسجلة",
+"failedLoginNumber":"Max failed logins count",
 "fileToUpload":"الملف الذي ستحمله",
 "findUser":"تفعيل",
 "findUserControl":"Parameters control",
@@ -567,6 +574,14 @@
 "newEntry":"أنتري جديد",
 "newGrantRule":"قاعدة منح جديدة",
 "newHost":"خادم جديد",
+"newLocationWarning":"تفعيل",
+"newLocationWarningLocationAttribute":"Session attribute containing location",
+"newLocationWarningLocationDisplayAttribute":"Session attribute to display",
+"newLocationWarningMailAttribute":"Session mail attribute",
+"newLocationWarningMailBody":"Warning mail content",
+"newLocationWarningMailSubject":"Warning mail subject",
+"newLocationWarningMaxValues":"Maximum number of locations to consider",
+"newLocationWarnings":"New location warning",
 "newPost":"استمارة وظيفة replay جديدة",
 "newPostVar":"متغير جديد",
 "newRSAKey":"مفاتيح جديدة",
@@ -647,13 +662,13 @@
 "oidcParams":"معاييرأوبين أيدي كونيكت",
 "oidcRP":"الطرف المعتمد  لي أوبين أيدي كونيكت",
 "oidcRPCallbackGetParam":"استدعاء معايير GET",
-"oidcRPMetaDataExportedVars":"السمات المصدرة",
+"oidcRPMetaDataExportedVars":"Exported attributes (claims)",
 "oidcRPMetaDataMacros":"ماكرو",
 "oidcRPMetaDataNode":"الأطراف المعتمد لي أوبين أيدي كونيكت",
 "oidcRPMetaDataNodes":"الأطراف المعتمد لي أوبين أيدي كونيكت",
 "oidcRPMetaDataOptions":"الخيارات",
 "oidcRPMetaDataOptionsAccessTokenClaims":"Release claims in Access Token",
-"oidcRPMetaDataOptionsAccessTokenExpiration":"انتهاء صلاحية التوكن",
+"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Tokens",
 "oidcRPMetaDataOptionsAccessTokenJWT":"Use JWT format for Access Token",
 "oidcRPMetaDataOptionsAccessTokenSignAlg":"Access Token signature algorithm",
 "oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
@@ -662,22 +677,22 @@
 "oidcRPMetaDataOptionsAllowOffline":"Allow offline access",
 "oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
 "oidcRPMetaDataOptionsAuthnLevel":"مستوى إثبات الهوية",
-"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Code expiration",
+"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Codes",
 "oidcRPMetaDataOptionsBasic":"Basic",
 "oidcRPMetaDataOptionsBypassConsent":"تخطى الموافقة ",
 "oidcRPMetaDataOptionsClientID":"معرف العميل",
 "oidcRPMetaDataOptionsClientSecret":"سرالعميل",
 "oidcRPMetaDataOptionsDisplay":"عرض",
 "oidcRPMetaDataOptionsDisplayName":"عرض الاسم",
-"oidcRPMetaDataOptionsExtraClaims":"ادعاءات إضافي",
-"oidcRPMetaDataOptionsIDTokenExpiration":" انتهاء صلاحية تعريف التوكن",
+"oidcRPMetaDataOptionsExtraClaims":"Scope values content",
+"oidcRPMetaDataOptionsIDTokenExpiration":"ID Tokens",
 "oidcRPMetaDataOptionsIDTokenForceClaims":"Force claims to be returned in ID Token",
 "oidcRPMetaDataOptionsIDTokenSignAlg":"خوارزمية توقيع آي دي التوكن",
 "oidcRPMetaDataOptionsIcon":"شعار",
 "oidcRPMetaDataOptionsLogoutSessionRequired":"جلسة مطلوب",
 "oidcRPMetaDataOptionsLogoutType":"نوع",
 "oidcRPMetaDataOptionsLogoutUrl":"يو آر إل",
-"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline session expiration",
+"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline sessions",
 "oidcRPMetaDataOptionsPostLogoutRedirectUris":"عناوين إعادة التوجيه المسموح بها للخروج",
 "oidcRPMetaDataOptionsPublic":"Public client",
 "oidcRPMetaDataOptionsRedirectUris":"عناوين إعادة التوجيه المسموح بها لتسجيل الدخول",
@@ -690,17 +705,18 @@
 "oidcRPMetaDataScopeRules":"Scope rules",
 "oidcRPName":"اسم أوبين أيدي كونيكت RP",
 "oidcRPStateTimeout":"حالة مهلة الجلسة",
-"oidcServiceAccessTokenExpiration":"انتهاء صلاحية التوكن",
+"oidcServiceAccessTokenExpiration":"Access Token",
 "oidcServiceAllowAuthorizationCodeFlow":"ترخيص كود التدفق",
-"oidcServiceAllowDynamicRegistration":"التسجيل الديناميكي",
+"oidcServiceAllowDynamicRegistration":"Activation",
 "oidcServiceAllowHybridFlow":"تدفق هجين",
 "oidcServiceAllowImplicitFlow":"التدفق الضمني",
 "oidcServiceAllowOffline":"Allow offline access",
 "oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
-"oidcServiceAuthorizationCodeExpiration":"Authorization Code expiration",
-"oidcServiceDynamicRegistrationExportedVars":"Exported vars for dynamic registration",
-"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",
-"oidcServiceIDTokenExpiration":" انتهاء صلاحية تعريف التوكن",
+"oidcServiceAuthorizationCodeExpiration":"Authorization Code",
+"oidcServiceDynamicRegistration":"Dynamic registration",
+"oidcServiceDynamicRegistrationExportedVars":"Exported vars",
+"oidcServiceDynamicRegistrationExtraClaims":"Extra claims",
+"oidcServiceIDTokenExpiration":"ID Token",
 "oidcServiceKeyIdSig":"توقيع على هوية المفتاح ",
 "oidcServiceMetaData":"خدمة أوبين أيدي كونيكت",
 "oidcServiceMetaDataAuthnContext":"سياق إثبات الهوية",
@@ -717,9 +733,10 @@
 "oidcServiceMetaDataRegistrationURI":"التسجيل",
 "oidcServiceMetaDataSecurity":"الحماية",
 "oidcServiceMetaDataSessions":"الجلسات",
+"oidcServiceMetaDataTimeouts":"Timeouts",
 "oidcServiceMetaDataTokenURI":"التوكن",
 "oidcServiceMetaDataUserInfoURI":"معلومات المستخدم",
-"oidcServiceOfflineSessionExpiration":"Offline session expiration",
+"oidcServiceOfflineSessionExpiration":"Offline session",
 "oidcServicePrivateKeySig":"توقيع على المفتاح الخاص",
 "oidcServicePublicKeySig":"توقيع على المفتاح العمومي",
 "oidcStorage":"اسم وحدة الجلسات",
@@ -812,8 +829,13 @@
 "postedVars":"متغيرات للنشر",
 "previous":"السابق",
 "privateKey":"مفتاح الخاصة",
-"proxyAuthService":"يو آر إل البوابة الداخلي",
+"proxyAuthService":"URL",
+"proxyAuthServiceChoiceParam":"Choice parameter",
+"proxyAuthServiceChoiceValue":"Choice value",
+"proxyAuthServiceImpersonation":"Impersonation",
 "proxyAuthnLevel":"مستوى إثبات الهوية",
+"proxyCookieName":"اسم ملف تعريف الارتباط",
+"proxyInternalPortal":"Internal Portal",
 "proxyParams":"معايير البروكسي",
 "proxySessionService":"رابط اليورال لخدمة الجلسة",
 "proxyUseSoap":"استخدام سواب بدلا من ريست",
@@ -861,11 +883,11 @@
 "rest2f":"REST second factor",
 "rest2fActivation":"تفعيل",
 "rest2fAuthnLevel":"مستوى إثبات الهوية",
-"rest2fInitArgs":"Init Arguments",
+"rest2fInitArgs":"Init arguments",
 "rest2fInitUrl":"Init URL",
 "rest2fLabel":"Label",
 "rest2fLogo":"شعار",
-"rest2fVerifyArgs":"Verify Arguments",
+"rest2fVerifyArgs":"Verify arguments",
 "rest2fVerifyUrl":"Verify URL",
 "restAuthServer":"Authentication server",
 "restAuthUrl":"يو آر إل إثبات الهوية",
@@ -1089,12 +1111,13 @@
 "stateCheck":"State Check",
 "stayConnect":"الاتصالات المستمرة",
 "stayConnected":"تفعيل",
+"stayConnectedBypassFG":"Do not check fingerprint",
 "stayConnectedCookieName":"اسم ملف تعريف الارتباط",
 "stayConnectedTimeout":"Expiration time",
 "storePassword":"تخزين كلمة مرور المستخدم في بيانات الجلسة",
 "string":"String",
 "subtitle":"Subtitle",
-"successLoginNumber":"عدد تسجيلات الدخول المسجلة",
+"successLoginNumber":"Max successful logins count",
 "successfullySaved":"تم الحفظ بنجاح",
 "sympaHandler":"لطيف",
 "sympaMailKey":"مفتاح جلسة البريد",
@@ -1110,10 +1133,11 @@
 "tooltip":"Tooltip",
 "totp2f":"TOTP",
 "totp2fActivation":"تفعيل",
-"totp2fAuthnLevel":"TOTP authentication level",
+"totp2fAuthnLevel":"مستوى إثبات الهوية",
 "totp2fDigits":"Number of digits",
+"totp2fEncryptSecret":"Encrypt TOTP secrets",
 "totp2fInterval":"Interval",
-"totp2fIssuer":"TOTP Issuer name",
+"totp2fIssuer":"Issuer name",
 "totp2fLabel":"Label",
 "totp2fLogo":"شعار",
 "totp2fRange":"Range of attempts",
@@ -1131,7 +1155,7 @@
 "type":"نوع",
 "u2f":"U2F",
 "u2fActivation":"تفعيل",
-"u2fAuthnLevel":"U2F  مستوى إثبات الهوية",
+"u2fAuthnLevel":"مستوى إثبات الهوية",
 "u2fLabel":"Label",
 "u2fLogo":"شعار",
 "u2fSelfRegistration":"التسجيل الذاتي",
@@ -1172,6 +1196,7 @@
 "vhostAccessToTrace":"Access to trace",
 "vhostAliases":"اسماء مستعارة",
 "vhostAuthnLevel":"مستوى إثبات الهوية  واجب",
+"vhostDevOpsRulesUrl":"DevOps rules file URL",
 "vhostHttps":"إتش تي تي بي س",
 "vhostMaintenance":"وضع الصيانة",
 "vhostOptions":"الخيارات",
@@ -1190,6 +1215,16 @@
 "webIDAuthnLevel":"مستوى إثبات الهوية",
 "webIDExportedVars":"المتغيرات المصدرة",
 "webIDWhitelist":"القائمة البيضاء للويب آي دي",
+"webauthn2f":"WebAuthn",
+"webauthn2fActivation":"تفعيل",
+"webauthn2fAuthnLevel":"مستوى إثبات الهوية",
+"webauthn2fLabel":"Label",
+"webauthn2fLogo":"شعار",
+"webauthn2fSelfRegistration":"التسجيل الذاتي",
+"webauthn2fUserCanRemoveKey":"Allow user to remove WebAuthn",
+"webauthn2fUserVerification":"User verification",
+"webauthnDisplayNameAttr":"User Display Name attribute",
+"webauthnRpName":"Relying Party display name",
 "webidParams":"معايير ويب أي دي",
 "whatToTrace":"المستخدم_البعيد",
 "whiteList":"القائمة البيضاء",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/de.json 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/de.json
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/de.json	2021-08-21 17:42:59.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/de.json	2022-02-19 16:04:21.000000000 +0000
@@ -126,7 +126,9 @@
 "bruteForceProtection":"Activation",
 "bruteForceProtectionIncrementalTempo":"Incremental lock",
 "bruteForceProtectionLockTimes":"Incremental lock times",
+"bruteForceProtectionMaxAge":"Maximum age",
 "bruteForceProtectionMaxFailed":"Allowed failed logins",
+"bruteForceProtectionMaxLockTime":"Maximum lock time",
 "bruteForceProtectionTempo":"Lock time",
 "cancel":"Abbrechen",
 "captcha_login_enabled":"Activation in login form",
@@ -165,6 +167,7 @@
 "casStorage":"CAS sessions module name",
 "casStorageOptions":"CAS sessions module options",
 "casStrictMatching":"Use strict URL matching",
+"casTicketExpiration":"Temporary ticket lifetime",
 "categoryName":"Category name",
 "cda":"Mehrere Domains",
 "certificateMailContent":"Mail content",
@@ -180,6 +183,8 @@
 "cfgLog":"Summary",
 "cfgVersion":"Configuration version",
 "checkDevOps":"Activation",
+"checkDevOpsCheckSessionAttributes":"Check session attributes",
+"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
 "checkDevOpsDownload":"Download file",
 "checkState":"Activation",
 "checkStateSecret":"Shared secret",
@@ -188,6 +193,8 @@
 "checkUserDisplayComputedSession":"Computed sessions",
 "checkUserDisplayEmptyHeaders":"Empty headers",
 "checkUserDisplayEmptyValues":"Empty values",
+"checkUserDisplayHiddenAttributes":"Hidden attributes",
+"checkUserDisplayHistory":"History",
 "checkUserDisplayNormalizedHeaders":"Normalized headers",
 "checkUserDisplayPersistentInfo":"Persistent session data",
 "checkUserHiddenAttributes":"Hidden attributes",
@@ -349,7 +356,7 @@
 "facebookExportedVars":"Exported variables",
 "facebookParams":"Facebook parameters",
 "facebookUserField":"Field containing user identifier",
-"failedLoginNumber":"Number of registered failed logins",
+"failedLoginNumber":"Max failed logins count",
 "fileToUpload":"File to upload",
 "findUser":"Activation",
 "findUserControl":"Parameters control",
@@ -567,6 +574,14 @@
 "newEntry":"New entry",
 "newGrantRule":"New grant rule",
 "newHost":"New host",
+"newLocationWarning":"Activation",
+"newLocationWarningLocationAttribute":"Session attribute containing location",
+"newLocationWarningLocationDisplayAttribute":"Session attribute to display",
+"newLocationWarningMailAttribute":"Session mail attribute",
+"newLocationWarningMailBody":"Warning mail content",
+"newLocationWarningMailSubject":"Warning mail subject",
+"newLocationWarningMaxValues":"Maximum number of locations to consider",
+"newLocationWarnings":"New location warning",
 "newPost":"New form replay",
 "newPostVar":"New variable",
 "newRSAKey":"New keys",
@@ -647,13 +662,13 @@
 "oidcParams":"OpenID Connect parameters",
 "oidcRP":"OpenID Connect Relying Party",
 "oidcRPCallbackGetParam":"Callback GET parameter",
-"oidcRPMetaDataExportedVars":"Exported attributes",
+"oidcRPMetaDataExportedVars":"Exported attributes (claims)",
 "oidcRPMetaDataMacros":"Macros",
 "oidcRPMetaDataNode":"OpenID Connect Relying Parties",
 "oidcRPMetaDataNodes":"OpenID Connect Relying Parties",
 "oidcRPMetaDataOptions":"Options",
 "oidcRPMetaDataOptionsAccessTokenClaims":"Release claims in Access Token",
-"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Token expiration",
+"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Tokens",
 "oidcRPMetaDataOptionsAccessTokenJWT":"Use JWT format for Access Token",
 "oidcRPMetaDataOptionsAccessTokenSignAlg":"Access Token signature algorithm",
 "oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
@@ -662,22 +677,22 @@
 "oidcRPMetaDataOptionsAllowOffline":"Allow offline access",
 "oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
 "oidcRPMetaDataOptionsAuthnLevel":"Authentication level",
-"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Code expiration",
+"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Codes",
 "oidcRPMetaDataOptionsBasic":"Basic",
 "oidcRPMetaDataOptionsBypassConsent":"Bypass consent",
 "oidcRPMetaDataOptionsClientID":"Client ID",
 "oidcRPMetaDataOptionsClientSecret":"Client secret",
 "oidcRPMetaDataOptionsDisplay":"Display",
 "oidcRPMetaDataOptionsDisplayName":"Display name",
-"oidcRPMetaDataOptionsExtraClaims":"Extra claims",
-"oidcRPMetaDataOptionsIDTokenExpiration":"ID Token expiration",
+"oidcRPMetaDataOptionsExtraClaims":"Scope values content",
+"oidcRPMetaDataOptionsIDTokenExpiration":"ID Tokens",
 "oidcRPMetaDataOptionsIDTokenForceClaims":"Force claims to be returned in ID Token",
 "oidcRPMetaDataOptionsIDTokenSignAlg":"ID Token signature algorithm",
 "oidcRPMetaDataOptionsIcon":"Logo",
 "oidcRPMetaDataOptionsLogoutSessionRequired":"Session required",
 "oidcRPMetaDataOptionsLogoutType":"Type",
 "oidcRPMetaDataOptionsLogoutUrl":"URL",
-"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline session expiration",
+"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline sessions",
 "oidcRPMetaDataOptionsPostLogoutRedirectUris":"Allowed redirection addresses for logout",
 "oidcRPMetaDataOptionsPublic":"Public client",
 "oidcRPMetaDataOptionsRedirectUris":"Allowed redirection addresses for login",
@@ -690,24 +705,25 @@
 "oidcRPMetaDataScopeRules":"Scope rules",
 "oidcRPName":"OpenID Connect RP Name",
 "oidcRPStateTimeout":"State session timeout",
-"oidcServiceAccessTokenExpiration":"Access Token expiration",
+"oidcServiceAccessTokenExpiration":"Access Token",
 "oidcServiceAllowAuthorizationCodeFlow":"Authorization Code Flow",
-"oidcServiceAllowDynamicRegistration":"Dynamic Registration",
+"oidcServiceAllowDynamicRegistration":"Activation",
 "oidcServiceAllowHybridFlow":"Hybrid Flow",
 "oidcServiceAllowImplicitFlow":"Implicit Flow",
 "oidcServiceAllowOffline":"Allow offline access",
 "oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
-"oidcServiceAuthorizationCodeExpiration":"Authorization Code expiration",
-"oidcServiceDynamicRegistrationExportedVars":"Exported vars for dynamic registration",
-"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",
-"oidcServiceIDTokenExpiration":"ID Token expiration",
+"oidcServiceAuthorizationCodeExpiration":"Authorization Code",
+"oidcServiceDynamicRegistration":"Dynamic registration",
+"oidcServiceDynamicRegistrationExportedVars":"Exported vars",
+"oidcServiceDynamicRegistrationExtraClaims":"Extra claims",
+"oidcServiceIDTokenExpiration":"ID Token",
 "oidcServiceKeyIdSig":"Signing key ID",
 "oidcServiceMetaData":"OpenID Connect Service",
 "oidcServiceMetaDataAuthnContext":"Authentication context",
 "oidcServiceMetaDataAuthorizeURI":"Authorization",
 "oidcServiceMetaDataBackChannelURI":"Back-Channel URI",
 "oidcServiceMetaDataCheckSessionURI":"Check Session",
-"oidcServiceMetaDataEndPoints":"End points",
+"oidcServiceMetaDataEndPoints":"Endpoints",
 "oidcServiceMetaDataEndSessionURI":"End of session",
 "oidcServiceMetaDataFrontChannelURI":"Front-Channel URI",
 "oidcServiceMetaDataIntrospectionURI":"Introspection",
@@ -717,9 +733,10 @@
 "oidcServiceMetaDataRegistrationURI":"Registration",
 "oidcServiceMetaDataSecurity":"Security",
 "oidcServiceMetaDataSessions":"Sessions",
-"oidcServiceMetaDataTokenURI":"Token",
+"oidcServiceMetaDataTimeouts":"Timeouts",
+"oidcServiceMetaDataTokenURI":"Tokens",
 "oidcServiceMetaDataUserInfoURI":"User Info",
-"oidcServiceOfflineSessionExpiration":"Offline session expiration",
+"oidcServiceOfflineSessionExpiration":"Offline session",
 "oidcServicePrivateKeySig":"Signing private key",
 "oidcServicePublicKeySig":"Signing public key",
 "oidcStorage":"Sessions module name",
@@ -812,8 +829,13 @@
 "postedVars":"Variables to post",
 "previous":"Previous",
 "privateKey":"Private key",
-"proxyAuthService":"Internal portal URL",
+"proxyAuthService":"URL",
+"proxyAuthServiceChoiceParam":"Choice parameter",
+"proxyAuthServiceChoiceValue":"Choice value",
+"proxyAuthServiceImpersonation":"Impersonation",
 "proxyAuthnLevel":"Authentication level",
+"proxyCookieName":"Cookie name",
+"proxyInternalPortal":"Internal Portal",
 "proxyParams":"Proxy parameters",
 "proxySessionService":"Session service URL",
 "proxyUseSoap":"Use SOAP instead of REST",
@@ -861,11 +883,11 @@
 "rest2f":"REST second factor",
 "rest2fActivation":"Activation",
 "rest2fAuthnLevel":"Authentication level",
-"rest2fInitArgs":"Init Arguments",
+"rest2fInitArgs":"Init arguments",
 "rest2fInitUrl":"Init URL",
 "rest2fLabel":"Label",
 "rest2fLogo":"Logo",
-"rest2fVerifyArgs":"Verify Arguments",
+"rest2fVerifyArgs":"Verify arguments",
 "rest2fVerifyUrl":"Verify URL",
 "restAuthServer":"Authentication server",
 "restAuthUrl":"Authentication URL",
@@ -1089,12 +1111,13 @@
 "stateCheck":"State Check",
 "stayConnect":"Persistent connections",
 "stayConnected":"Activation",
+"stayConnectedBypassFG":"Do not check fingerprint",
 "stayConnectedCookieName":"Cookie name",
 "stayConnectedTimeout":"Expiration time",
 "storePassword":"Store user password in session",
 "string":"String",
 "subtitle":"Subtitle",
-"successLoginNumber":"Number of registered logins",
+"successLoginNumber":"Max successful logins count",
 "successfullySaved":"Successfully saved",
 "sympaHandler":"Sympa",
 "sympaMailKey":"Mail session key",
@@ -1110,10 +1133,11 @@
 "tooltip":"Tooltip",
 "totp2f":"TOTP",
 "totp2fActivation":"Activation",
-"totp2fAuthnLevel":"TOTP authentication level",
+"totp2fAuthnLevel":"Authentication level",
 "totp2fDigits":"Number of digits",
+"totp2fEncryptSecret":"Encrypt TOTP secrets",
 "totp2fInterval":"Interval",
-"totp2fIssuer":"TOTP Issuer name",
+"totp2fIssuer":"Issuer name",
 "totp2fLabel":"Label",
 "totp2fLogo":"Logo",
 "totp2fRange":"Range of attempts",
@@ -1131,7 +1155,7 @@
 "type":"Type",
 "u2f":"U2F",
 "u2fActivation":"Activation",
-"u2fAuthnLevel":"U2F authentication level",
+"u2fAuthnLevel":"Authentication level",
 "u2fLabel":"Label",
 "u2fLogo":"Logo",
 "u2fSelfRegistration":"Self registration",
@@ -1172,6 +1196,7 @@
 "vhostAccessToTrace":"Access to trace",
 "vhostAliases":"Aliases",
 "vhostAuthnLevel":"Required authentication level",
+"vhostDevOpsRulesUrl":"DevOps rules file URL",
 "vhostHttps":"HTTPS",
 "vhostMaintenance":"Maintenance mode",
 "vhostOptions":"Options",
@@ -1190,6 +1215,16 @@
 "webIDAuthnLevel":"Authentication level",
 "webIDExportedVars":"Exported variables",
 "webIDWhitelist":"WebID whitelist",
+"webauthn2f":"WebAuthn",
+"webauthn2fActivation":"Activation",
+"webauthn2fAuthnLevel":"Authentication level",
+"webauthn2fLabel":"Label",
+"webauthn2fLogo":"Logo",
+"webauthn2fSelfRegistration":"Self registration",
+"webauthn2fUserCanRemoveKey":"Allow user to remove WebAuthn",
+"webauthn2fUserVerification":"User verification",
+"webauthnDisplayNameAttr":"User Display Name attribute",
+"webauthnRpName":"Relying Party display name",
 "webidParams":"WebID parameters",
 "whatToTrace":"REMOTE_USER",
 "whiteList":"White list",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/en.json 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/en.json
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/en.json	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/en.json	2022-02-19 16:04:21.000000000 +0000
@@ -126,7 +126,9 @@
 "bruteForceProtection":"Activation",
 "bruteForceProtectionIncrementalTempo":"Incremental lock",
 "bruteForceProtectionLockTimes":"Incremental lock times",
+"bruteForceProtectionMaxAge":"Maximum age",
 "bruteForceProtectionMaxFailed":"Allowed failed logins",
+"bruteForceProtectionMaxLockTime":"Maximum lock time",
 "bruteForceProtectionTempo":"Lock time",
 "cancel":"Cancel",
 "captcha_login_enabled":"Activation in login form",
@@ -165,6 +167,7 @@
 "casStorage":"CAS sessions module name",
 "casStorageOptions":"CAS sessions module options",
 "casStrictMatching":"Use strict URL matching",
+"casTicketExpiration":"Temporary ticket lifetime",
 "categoryName":"Category name",
 "cda":"Multiple domains",
 "certificateMailContent":"Mail content",
@@ -180,6 +183,8 @@
 "cfgLog":"Summary",
 "cfgVersion":"Configuration version",
 "checkDevOps":"Activation",
+"checkDevOpsCheckSessionAttributes":"Check session attributes",
+"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
 "checkDevOpsDownload":"Download file",
 "checkState":"Activation",
 "checkStateSecret":"Shared secret",
@@ -188,6 +193,8 @@
 "checkUserDisplayComputedSession":"Computed sessions",
 "checkUserDisplayEmptyHeaders":"Empty headers",
 "checkUserDisplayEmptyValues":"Empty values",
+"checkUserDisplayHiddenAttributes":"Hidden attributes",
+"checkUserDisplayHistory":"History",
 "checkUserDisplayNormalizedHeaders":"Normalized headers",
 "checkUserDisplayPersistentInfo":"Persistent session data",
 "checkUserHiddenAttributes":"Hidden attributes",
@@ -349,7 +356,7 @@
 "facebookExportedVars":"Exported variables",
 "facebookParams":"Facebook parameters",
 "facebookUserField":"Field containing user identifier",
-"failedLoginNumber":"Number of registered failed logins",
+"failedLoginNumber":"Max failed logins count",
 "fileToUpload":"File to upload",
 "findUser":"Activation",
 "findUserControl":"Parameters control",
@@ -567,6 +574,14 @@
 "newEntry":"New entry",
 "newGrantRule":"New grant rule",
 "newHost":"New host",
+"newLocationWarning":"Activation",
+"newLocationWarningLocationAttribute":"Session attribute containing location",
+"newLocationWarningLocationDisplayAttribute":"Session attribute to display",
+"newLocationWarningMailAttribute":"Session mail attribute",
+"newLocationWarningMailBody":"Warning mail content",
+"newLocationWarningMailSubject":"Warning mail subject",
+"newLocationWarningMaxValues":"Maximum number of locations to consider",
+"newLocationWarnings":"New location warning",
 "newPost":"New form replay",
 "newPostVar":"New variable",
 "newRSAKey":"New keys",
@@ -647,13 +662,13 @@
 "oidcParams":"OpenID Connect parameters",
 "oidcRP":"OpenID Connect Relying Party",
 "oidcRPCallbackGetParam":"Callback GET parameter",
-"oidcRPMetaDataExportedVars":"Exported attributes",
+"oidcRPMetaDataExportedVars":"Exported attributes (claims)",
 "oidcRPMetaDataMacros":"Macros",
 "oidcRPMetaDataNode":"OpenID Connect Relying Parties",
 "oidcRPMetaDataNodes":"OpenID Connect Relying Parties",
 "oidcRPMetaDataOptions":"Options",
 "oidcRPMetaDataOptionsAccessTokenClaims":"Release claims in Access Token",
-"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Token expiration",
+"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Tokens",
 "oidcRPMetaDataOptionsAccessTokenJWT":"Use JWT format for Access Token",
 "oidcRPMetaDataOptionsAccessTokenSignAlg":"Access Token signature algorithm",
 "oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
@@ -662,22 +677,22 @@
 "oidcRPMetaDataOptionsAllowOffline":"Allow offline access",
 "oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
 "oidcRPMetaDataOptionsAuthnLevel":"Authentication level",
-"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Code expiration",
+"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Codes",
 "oidcRPMetaDataOptionsBasic":"Basic",
 "oidcRPMetaDataOptionsBypassConsent":"Bypass consent",
 "oidcRPMetaDataOptionsClientID":"Client ID",
 "oidcRPMetaDataOptionsClientSecret":"Client secret",
 "oidcRPMetaDataOptionsDisplay":"Display",
 "oidcRPMetaDataOptionsDisplayName":"Display name",
-"oidcRPMetaDataOptionsExtraClaims":"Extra claims",
-"oidcRPMetaDataOptionsIDTokenExpiration":"ID Token expiration",
+"oidcRPMetaDataOptionsExtraClaims":"Scope values content",
+"oidcRPMetaDataOptionsIDTokenExpiration":"ID Tokens",
 "oidcRPMetaDataOptionsIDTokenForceClaims":"Force claims to be returned in ID Token",
 "oidcRPMetaDataOptionsIDTokenSignAlg":"ID Token signature algorithm",
 "oidcRPMetaDataOptionsIcon":"Logo",
 "oidcRPMetaDataOptionsLogoutSessionRequired":"Session required",
 "oidcRPMetaDataOptionsLogoutType":"Type",
 "oidcRPMetaDataOptionsLogoutUrl":"URL",
-"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline session expiration",
+"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline sessions",
 "oidcRPMetaDataOptionsPostLogoutRedirectUris":"Allowed redirection addresses for logout",
 "oidcRPMetaDataOptionsPublic":"Public client",
 "oidcRPMetaDataOptionsRedirectUris":"Allowed redirection addresses for login",
@@ -690,24 +705,25 @@
 "oidcRPMetaDataScopeRules":"Scope rules",
 "oidcRPName":"OpenID Connect RP Name",
 "oidcRPStateTimeout":"State session timeout",
-"oidcServiceAccessTokenExpiration":"Access Token expiration",
+"oidcServiceAccessTokenExpiration":"Access Token",
 "oidcServiceAllowAuthorizationCodeFlow":"Authorization Code Flow",
-"oidcServiceAllowDynamicRegistration":"Dynamic Registration",
+"oidcServiceAllowDynamicRegistration":"Activation",
 "oidcServiceAllowHybridFlow":"Hybrid Flow",
 "oidcServiceAllowImplicitFlow":"Implicit Flow",
 "oidcServiceAllowOffline":"Allow offline access",
 "oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
-"oidcServiceAuthorizationCodeExpiration":"Authorization Code expiration",
-"oidcServiceDynamicRegistrationExportedVars":"Exported vars for dynamic registration",
-"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",
-"oidcServiceIDTokenExpiration":"ID Token expiration",
+"oidcServiceAuthorizationCodeExpiration":"Authorization Code",
+"oidcServiceDynamicRegistration":"Dynamic registration",
+"oidcServiceDynamicRegistrationExportedVars":"Exported vars",
+"oidcServiceDynamicRegistrationExtraClaims":"Extra claims",
+"oidcServiceIDTokenExpiration":"ID Token",
 "oidcServiceKeyIdSig":"Signing key ID",
 "oidcServiceMetaData":"OpenID Connect Service",
 "oidcServiceMetaDataAuthnContext":"Authentication context",
 "oidcServiceMetaDataAuthorizeURI":"Authorization",
 "oidcServiceMetaDataBackChannelURI":"Back-Channel URI",
 "oidcServiceMetaDataCheckSessionURI":"Check Session",
-"oidcServiceMetaDataEndPoints":"End points",
+"oidcServiceMetaDataEndPoints":"Endpoints",
 "oidcServiceMetaDataEndSessionURI":"End of session",
 "oidcServiceMetaDataFrontChannelURI":"Front-Channel URI",
 "oidcServiceMetaDataIntrospectionURI":"Introspection",
@@ -717,9 +733,10 @@
 "oidcServiceMetaDataRegistrationURI":"Registration",
 "oidcServiceMetaDataSecurity":"Security",
 "oidcServiceMetaDataSessions":"Sessions",
-"oidcServiceMetaDataTokenURI":"Token",
+"oidcServiceMetaDataTimeouts":"Timeouts",
+"oidcServiceMetaDataTokenURI":"Tokens",
 "oidcServiceMetaDataUserInfoURI":"User Info",
-"oidcServiceOfflineSessionExpiration":"Offline session expiration",
+"oidcServiceOfflineSessionExpiration":"Offline session",
 "oidcServicePrivateKeySig":"Signing private key",
 "oidcServicePublicKeySig":"Signing public key",
 "oidcStorage":"Sessions module name",
@@ -812,8 +829,13 @@
 "postedVars":"Variables to post",
 "previous":"Previous",
 "privateKey":"Private key",
-"proxyAuthService":"Internal portal URL",
+"proxyAuthService":"URL",
+"proxyAuthServiceChoiceParam":"Choice parameter",
+"proxyAuthServiceChoiceValue":"Choice value",
+"proxyAuthServiceImpersonation":"Impersonation",
 "proxyAuthnLevel":"Authentication level",
+"proxyCookieName":"Cookie name",
+"proxyInternalPortal":"Internal Portal",
 "proxyParams":"Proxy parameters",
 "proxySessionService":"Session service URL",
 "proxyUseSoap":"Use SOAP instead of REST",
@@ -861,11 +883,11 @@
 "rest2f":"REST second factor",
 "rest2fActivation":"Activation",
 "rest2fAuthnLevel":"Authentication level",
-"rest2fInitArgs":"Init Arguments",
+"rest2fInitArgs":"Init arguments",
 "rest2fInitUrl":"Init URL",
 "rest2fLabel":"Label",
 "rest2fLogo":"Logo",
-"rest2fVerifyArgs":"Verify Arguments",
+"rest2fVerifyArgs":"Verify arguments",
 "rest2fVerifyUrl":"Verify URL",
 "restAuthServer":"Authentication server",
 "restAuthUrl":"Authentication URL",
@@ -1089,12 +1111,13 @@
 "stateCheck":"State Check",
 "stayConnect":"Persistent connections",
 "stayConnected":"Activation",
+"stayConnectedBypassFG":"Do not check fingerprint",
 "stayConnectedCookieName":"Cookie name",
 "stayConnectedTimeout":"Expiration time",
 "storePassword":"Store user password in session",
 "string":"String",
 "subtitle":"Subtitle",
-"successLoginNumber":"Number of registered logins",
+"successLoginNumber":"Max successful logins count",
 "successfullySaved":"Successfully saved",
 "sympaHandler":"Sympa",
 "sympaMailKey":"Mail session key",
@@ -1110,10 +1133,11 @@
 "tooltip":"Tooltip",
 "totp2f":"TOTP",
 "totp2fActivation":"Activation",
-"totp2fAuthnLevel":"TOTP authentication level",
+"totp2fAuthnLevel":"Authentication level",
 "totp2fDigits":"Number of digits",
+"totp2fEncryptSecret":"Encrypt TOTP secrets",
 "totp2fInterval":"Interval",
-"totp2fIssuer":"TOTP Issuer name",
+"totp2fIssuer":"Issuer name",
 "totp2fLabel":"Label",
 "totp2fLogo":"Logo",
 "totp2fRange":"Range of attempts",
@@ -1131,7 +1155,7 @@
 "type":"Type",
 "u2f":"U2F",
 "u2fActivation":"Activation",
-"u2fAuthnLevel":"U2F authentication level",
+"u2fAuthnLevel":"Authentication level",
 "u2fLabel":"Label",
 "u2fLogo":"Logo",
 "u2fSelfRegistration":"Self registration",
@@ -1172,6 +1196,7 @@
 "vhostAccessToTrace":"Access to trace",
 "vhostAliases":"Aliases",
 "vhostAuthnLevel":"Required authentication level",
+"vhostDevOpsRulesUrl":"DevOps rules file URL",
 "vhostHttps":"HTTPS",
 "vhostMaintenance":"Maintenance mode",
 "vhostOptions":"Options",
@@ -1190,6 +1215,16 @@
 "webIDAuthnLevel":"Authentication level",
 "webIDExportedVars":"Exported variables",
 "webIDWhitelist":"WebID whitelist",
+"webauthn2f":"WebAuthn",
+"webauthn2fActivation":"Activation",
+"webauthn2fAuthnLevel":"Authentication level",
+"webauthn2fLabel":"Label",
+"webauthn2fLogo":"Logo",
+"webauthn2fSelfRegistration":"Self registration",
+"webauthn2fUserCanRemoveKey":"Allow user to remove WebAuthn",
+"webauthn2fUserVerification":"User verification",
+"webauthnDisplayNameAttr":"User Display Name attribute",
+"webauthnRpName":"Relying Party display name",
 "webidParams":"WebID parameters",
 "whatToTrace":"REMOTE_USER",
 "whiteList":"White list",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/es.json 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/es.json
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/es.json	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/es.json	2022-02-19 16:04:21.000000000 +0000
@@ -126,7 +126,9 @@
 "bruteForceProtection":"Activación",
 "bruteForceProtectionIncrementalTempo":"Incremental lock",
 "bruteForceProtectionLockTimes":"Incremental lock times",
+"bruteForceProtectionMaxAge":"Maximum age",
 "bruteForceProtectionMaxFailed":"Allowed failed logins",
+"bruteForceProtectionMaxLockTime":"Maximum lock time",
 "bruteForceProtectionTempo":"Lock time",
 "cancel":"Cancelar",
 "captcha_login_enabled":"Activación en formulario de acceso",
@@ -139,7 +141,7 @@
 "casAppMetaDataMacros":"Macros",
 "casAppMetaDataNodes":"Aplicaciones CAS",
 "casAppMetaDataOptions":"Opciones",
-"casAppMetaDataOptionsAuthnLevel":"Authentication level",
+"casAppMetaDataOptionsAuthnLevel":"Nivel de autentificación",
 "casAppMetaDataOptionsRule":"Regla",
 "casAppMetaDataOptionsService":"URL de servicio",
 "casAppMetaDataOptionsUserAttribute":"Atributo de usuario",
@@ -165,6 +167,7 @@
 "casStorage":"CAS sessions module name",
 "casStorageOptions":"CAS sessions module options",
 "casStrictMatching":"Use strict URL matching",
+"casTicketExpiration":"Temporary ticket lifetime",
 "categoryName":"Nombre de categoría",
 "cda":"Dominios múltiples",
 "certificateMailContent":"Contenido de correo",
@@ -180,6 +183,8 @@
 "cfgLog":"Sumario",
 "cfgVersion":"Configuration version",
 "checkDevOps":"Activación",
+"checkDevOpsCheckSessionAttributes":"Check session attributes",
+"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
 "checkDevOpsDownload":"Download file",
 "checkState":"Activación",
 "checkStateSecret":"Secreto compartido",
@@ -188,6 +193,8 @@
 "checkUserDisplayComputedSession":"Computed sessions",
 "checkUserDisplayEmptyHeaders":"Empty headers",
 "checkUserDisplayEmptyValues":"Empty values",
+"checkUserDisplayHiddenAttributes":"Hidden attributes",
+"checkUserDisplayHistory":"History",
 "checkUserDisplayNormalizedHeaders":"Normalized headers",
 "checkUserDisplayPersistentInfo":"Persistent session data",
 "checkUserHiddenAttributes":"Atributos ocultos",
@@ -228,7 +235,7 @@
 "contextSwitchingStopWithLogout":"Stop by logout",
 "contextSwitchingUnrestrictedUsersRule":"Unrestricted users rule",
 "cookieExpiration":"Hora de caducidad de la cookie",
-"cookieName":"Cookie name",
+"cookieName":"Nombre de la cookie",
 "cookieParams":"Cookies",
 "corsAllow_Credentials":"Access-Control-Allow-Credentials",
 "corsAllow_Headers":"Access-Control-Allow-Headers",
@@ -349,7 +356,7 @@
 "facebookExportedVars":"Variables exportadas",
 "facebookParams":"Parámetros Facebook",
 "facebookUserField":"Campo que contiene identificador de usuario",
-"failedLoginNumber":"Número de fallos en la identificación",
+"failedLoginNumber":"Max failed logins count",
 "fileToUpload":"Fichero a cargar",
 "findUser":"Activation",
 "findUserControl":"Parameters control",
@@ -567,6 +574,14 @@
 "newEntry":"Nueva entrada",
 "newGrantRule":"Nueva regla de admisión",
 "newHost":"Nuevo host",
+"newLocationWarning":"Activation",
+"newLocationWarningLocationAttribute":"Session attribute containing location",
+"newLocationWarningLocationDisplayAttribute":"Session attribute to display",
+"newLocationWarningMailAttribute":"Session mail attribute",
+"newLocationWarningMailBody":"Warning mail content",
+"newLocationWarningMailSubject":"Warning mail subject",
+"newLocationWarningMaxValues":"Maximum number of locations to consider",
+"newLocationWarnings":"New location warning",
 "newPost":"New form replay",
 "newPostVar":"Nueva variable",
 "newRSAKey":"Nuevas claves",
@@ -647,13 +662,13 @@
 "oidcParams":"OpenID Connect parameters",
 "oidcRP":"OpenID Connect Relying Party",
 "oidcRPCallbackGetParam":"Callback GET parameter",
-"oidcRPMetaDataExportedVars":"Atributos exportados",
+"oidcRPMetaDataExportedVars":"Exported attributes (claims)",
 "oidcRPMetaDataMacros":"Macros",
 "oidcRPMetaDataNode":"OpenID Connect Relying Parties",
 "oidcRPMetaDataNodes":"OpenID Connect Relying Parties",
 "oidcRPMetaDataOptions":"Opciones",
 "oidcRPMetaDataOptionsAccessTokenClaims":"Release claims in Access Token",
-"oidcRPMetaDataOptionsAccessTokenExpiration":"Caducidad del token de acceso",
+"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Tokens",
 "oidcRPMetaDataOptionsAccessTokenJWT":"Use JWT format for Access Token",
 "oidcRPMetaDataOptionsAccessTokenSignAlg":"Access Token signature algorithm",
 "oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
@@ -661,23 +676,23 @@
 "oidcRPMetaDataOptionsAllowClientCredentialsGrant":"Allow OAuth2.0 Client Credentials Grant",
 "oidcRPMetaDataOptionsAllowOffline":"Permitir acceso sin conexión",
 "oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
-"oidcRPMetaDataOptionsAuthnLevel":"Authentication level",
-"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Caducidad del código de autorización",
+"oidcRPMetaDataOptionsAuthnLevel":"Nivel de autentificación",
+"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Codes",
 "oidcRPMetaDataOptionsBasic":"Basic",
 "oidcRPMetaDataOptionsBypassConsent":"Bypass consent",
 "oidcRPMetaDataOptionsClientID":"ID del cliente",
 "oidcRPMetaDataOptionsClientSecret":"Secreto del cliente",
 "oidcRPMetaDataOptionsDisplay":"Display",
 "oidcRPMetaDataOptionsDisplayName":"Mostrar nombre",
-"oidcRPMetaDataOptionsExtraClaims":"Extra claims",
-"oidcRPMetaDataOptionsIDTokenExpiration":"Caducidad del token ID",
+"oidcRPMetaDataOptionsExtraClaims":"Scope values content",
+"oidcRPMetaDataOptionsIDTokenExpiration":"ID Tokens",
 "oidcRPMetaDataOptionsIDTokenForceClaims":"Force claims to be returned in ID Token",
 "oidcRPMetaDataOptionsIDTokenSignAlg":"Algoritmo de firma del token ID",
 "oidcRPMetaDataOptionsIcon":"Logotipo",
 "oidcRPMetaDataOptionsLogoutSessionRequired":"Se requiere sesión",
 "oidcRPMetaDataOptionsLogoutType":"Tipo",
 "oidcRPMetaDataOptionsLogoutUrl":"URL",
-"oidcRPMetaDataOptionsOfflineSessionExpiration":"Caducidad de sesión desconectada",
+"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline sessions",
 "oidcRPMetaDataOptionsPostLogoutRedirectUris":"Allowed redirection addresses for logout",
 "oidcRPMetaDataOptionsPublic":"Cliente público",
 "oidcRPMetaDataOptionsRedirectUris":"Allowed redirection addresses for login",
@@ -690,24 +705,25 @@
 "oidcRPMetaDataScopeRules":"Scope rules",
 "oidcRPName":"OpenID Connect RP Name",
 "oidcRPStateTimeout":"Caducidad de estado de sesión",
-"oidcServiceAccessTokenExpiration":"Caducidad del token de acceso",
+"oidcServiceAccessTokenExpiration":"Access Token",
 "oidcServiceAllowAuthorizationCodeFlow":"Authorization Code Flow",
-"oidcServiceAllowDynamicRegistration":"Registro dinámico",
+"oidcServiceAllowDynamicRegistration":"Activation",
 "oidcServiceAllowHybridFlow":"Flujo híbrido",
 "oidcServiceAllowImplicitFlow":"Flujo implícito",
 "oidcServiceAllowOffline":"Permitir acceso offline",
 "oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
-"oidcServiceAuthorizationCodeExpiration":"Caducidad del código de autorización",
-"oidcServiceDynamicRegistrationExportedVars":"Variables exportadas para registro dinámico",
-"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",
-"oidcServiceIDTokenExpiration":"Caducidad del token ID",
+"oidcServiceAuthorizationCodeExpiration":"Authorization Code",
+"oidcServiceDynamicRegistration":"Dynamic registration",
+"oidcServiceDynamicRegistrationExportedVars":"Variables exportadas",
+"oidcServiceDynamicRegistrationExtraClaims":"Extra claims",
+"oidcServiceIDTokenExpiration":"ID Token",
 "oidcServiceKeyIdSig":"ID de la clave firmante",
 "oidcServiceMetaData":"OpenID Connect Service",
 "oidcServiceMetaDataAuthnContext":"Contexto de autentificación",
 "oidcServiceMetaDataAuthorizeURI":"Autorización",
 "oidcServiceMetaDataBackChannelURI":"URI de Back-Channel",
 "oidcServiceMetaDataCheckSessionURI":"Check Session",
-"oidcServiceMetaDataEndPoints":"End points",
+"oidcServiceMetaDataEndPoints":"Endpoints",
 "oidcServiceMetaDataEndSessionURI":"Fin de sesión",
 "oidcServiceMetaDataFrontChannelURI":"URI de Front-Channel",
 "oidcServiceMetaDataIntrospectionURI":"Instrospección",
@@ -717,9 +733,10 @@
 "oidcServiceMetaDataRegistrationURI":"Registro",
 "oidcServiceMetaDataSecurity":"Seguridad",
 "oidcServiceMetaDataSessions":"Sesiones",
-"oidcServiceMetaDataTokenURI":"Token",
+"oidcServiceMetaDataTimeouts":"Timeouts",
+"oidcServiceMetaDataTokenURI":"Tokens",
 "oidcServiceMetaDataUserInfoURI":"Información del usuario",
-"oidcServiceOfflineSessionExpiration":"Caducidad de la sesión offline",
+"oidcServiceOfflineSessionExpiration":"Offline session",
 "oidcServicePrivateKeySig":"Clave privada firmante",
 "oidcServicePublicKeySig":"Clave pública firmante",
 "oidcStorage":"Nombre del módulo de sesiones",
@@ -812,8 +829,13 @@
 "postedVars":"Variables to post",
 "previous":"Previous",
 "privateKey":"Clave privada",
-"proxyAuthService":"URL de portal interno",
+"proxyAuthService":"URL",
+"proxyAuthServiceChoiceParam":"Choice parameter",
+"proxyAuthServiceChoiceValue":"Choice value",
+"proxyAuthServiceImpersonation":"Suplantación",
 "proxyAuthnLevel":"Nivel de autentificación",
+"proxyCookieName":"Nombre de la cookie",
+"proxyInternalPortal":"Internal Portal",
 "proxyParams":"Parámetros del proxy",
 "proxySessionService":"Session service URL",
 "proxyUseSoap":"Utilizar SOAP en lugar de REST",
@@ -980,7 +1002,7 @@
 "samlSPMetaDataMacros":"Macros",
 "samlSPMetaDataNodes":"SAML Service Providers",
 "samlSPMetaDataOptions":"Options",
-"samlSPMetaDataOptionsAuthnLevel":"Authentication level",
+"samlSPMetaDataOptionsAuthnLevel":"Nivel de autentificación",
 "samlSPMetaDataOptionsAuthnResponse":"Authentication response",
 "samlSPMetaDataOptionsCheckSLOMessageSignature":"Check SLO message signature",
 "samlSPMetaDataOptionsCheckSSOMessageSignature":"Check SSO message signature",
@@ -1089,12 +1111,13 @@
 "stateCheck":"Comprobación de estado",
 "stayConnect":"Persistent connections",
 "stayConnected":"Activation",
-"stayConnectedCookieName":"Cookie name",
+"stayConnectedBypassFG":"Do not check fingerprint",
+"stayConnectedCookieName":"Nombre de la cookie",
 "stayConnectedTimeout":"Expiration time",
 "storePassword":"Almacenar contraseña de usuario en la sesión",
 "string":"String",
 "subtitle":"Subtítulo",
-"successLoginNumber":"Número de login registrados",
+"successLoginNumber":"Max successful logins count",
 "successfullySaved":"Salvado con éxito",
 "sympaHandler":"Sympa",
 "sympaMailKey":"Clave de sesión de correo",
@@ -1110,10 +1133,11 @@
 "tooltip":"Tooltip",
 "totp2f":"TOTP",
 "totp2fActivation":"Activación",
-"totp2fAuthnLevel":"Nivel de autentificación TOTP",
+"totp2fAuthnLevel":"Nivel de autentificación",
 "totp2fDigits":"Cantidad de dígitos",
+"totp2fEncryptSecret":"Encrypt TOTP secrets",
 "totp2fInterval":"Intervalo",
-"totp2fIssuer":"Nombre de emisor TOTP",
+"totp2fIssuer":"Issuer name",
 "totp2fLabel":"Etiqueta",
 "totp2fLogo":"Logotipo",
 "totp2fRange":"Rango de intentos",
@@ -1131,7 +1155,7 @@
 "type":"Tipo",
 "u2f":"U2F",
 "u2fActivation":"Activación",
-"u2fAuthnLevel":"Nivel de autentificación U2F",
+"u2fAuthnLevel":"Nivel de autentificación",
 "u2fLabel":"Etiqueta",
 "u2fLogo":"Logotipo",
 "u2fSelfRegistration":"Auto registro",
@@ -1172,6 +1196,7 @@
 "vhostAccessToTrace":"Access to trace",
 "vhostAliases":"Aliases",
 "vhostAuthnLevel":"Nivel de autentificación requerido",
+"vhostDevOpsRulesUrl":"DevOps rules file URL",
 "vhostHttps":"HTTPS",
 "vhostMaintenance":"Maintenance mode",
 "vhostOptions":"Options",
@@ -1190,6 +1215,16 @@
 "webIDAuthnLevel":"Nivel de autentificación",
 "webIDExportedVars":"Exported variables",
 "webIDWhitelist":"WebID whitelist",
+"webauthn2f":"WebAuthn",
+"webauthn2fActivation":"Activación",
+"webauthn2fAuthnLevel":"Nivel de autentificación",
+"webauthn2fLabel":"Etiqueta",
+"webauthn2fLogo":"Logotipo",
+"webauthn2fSelfRegistration":"Auto registro",
+"webauthn2fUserCanRemoveKey":"Allow user to remove WebAuthn",
+"webauthn2fUserVerification":"User verification",
+"webauthnDisplayNameAttr":"User Display Name attribute",
+"webauthnRpName":"Relying Party display name",
 "webidParams":"WebID parameters",
 "whatToTrace":"REMOTE_USER",
 "whiteList":"White list",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/fr.json 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/fr.json
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/fr.json	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/fr.json	2022-02-19 16:04:21.000000000 +0000
@@ -126,7 +126,9 @@
 "bruteForceProtection":"Activation",
 "bruteForceProtectionIncrementalTempo":"Verrouillage incrémentiel",
 "bruteForceProtectionLockTimes":"Temps de verrouillage incrémentiel",
+"bruteForceProtectionMaxAge":"Age maximum des échecs",
 "bruteForceProtectionMaxFailed":"Nombre d'échecs de connexion autorisés",
+"bruteForceProtectionMaxLockTime":"Temps maximum de verrouillage",
 "bruteForceProtectionTempo":"Temps de verrouillage",
 "cancel":"Annuler",
 "captcha_login_enabled":"Activation dans le formulaire d'authentification",
@@ -165,6 +167,7 @@
 "casStorage":"Nom du module des sessions CAS",
 "casStorageOptions":"Options du module des sessions CAS",
 "casStrictMatching":"Filtrage strict des URL",
+"casTicketExpiration":"Expiration des tickets temporaires",
 "categoryName":"Nom de la catégorie",
 "cda":"Domaines multiples",
 "certificateMailContent":"Contenu du mail",
@@ -180,6 +183,8 @@
 "cfgLog":"Résumé",
 "cfgVersion":"Version de la configuration",
 "checkDevOps":"Activation",
+"checkDevOpsCheckSessionAttributes":"Vérifier les attributs de session",
+"checkDevOpsDisplayNormalizedHeaders":"Afficher les entêtes normalisés",
 "checkDevOpsDownload":"Télécharger un fichier",
 "checkState":"Activation",
 "checkStateSecret":"Secret partagé",
@@ -188,9 +193,11 @@
 "checkUserDisplayComputedSession":"Sessions évaluées",
 "checkUserDisplayEmptyHeaders":"Entêtes nuls",
 "checkUserDisplayEmptyValues":"Valeurs nulles",
+"checkUserDisplayHiddenAttributes":"Attributs cachés",
+"checkUserDisplayHistory":"Historique",
 "checkUserDisplayNormalizedHeaders":"Entêtes normalisés",
 "checkUserDisplayPersistentInfo":"Données de session persistante",
-"checkUserHiddenAttributes":"Attributs masqués",
+"checkUserHiddenAttributes":"Attributs cachés",
 "checkUserHiddenHeaders":"Entêtes masqués",
 "checkUserIdRule":"Règle d'utilisation des identités",
 "checkUserSearchAttributes":"Attributs utilisés pour rechercher les sessions",
@@ -567,6 +574,14 @@
 "newEntry":"Nouvelle entrée",
 "newGrantRule":"Nouvelle règle d'accès",
 "newHost":"Nouvel hôte",
+"newLocationWarning":"Activation",
+"newLocationWarningLocationAttribute":"Attribut de session contenant la localisation",
+"newLocationWarningLocationDisplayAttribute":"Attribut de session à afficher",
+"newLocationWarningMailAttribute":"Attribut utilisateur contenant le mail ",
+"newLocationWarningMailBody":"Contenu du mail d'avertissement",
+"newLocationWarningMailSubject":"Sujet du mail d'avertissement",
+"newLocationWarningMaxValues":"Nombre maximum de localisations à mémoriser",
+"newLocationWarnings":"Avertissement de nouvelle connexion",
 "newPost":"Nouveau rejeu de formulaire",
 "newPostVar":"Nouvelle variable",
 "newRSAKey":"Nouvelles clefs",
@@ -631,10 +646,10 @@
 "oidcOPMetaDataOptionsDisplay":"Affichage",
 "oidcOPMetaDataOptionsDisplayName":"Nom d'affichage",
 "oidcOPMetaDataOptionsDisplayParams":"Affichage",
-"oidcOPMetaDataOptionsIDTokenMaxAge":"Âge maximum du jeton ID",
+"oidcOPMetaDataOptionsIDTokenMaxAge":"Age maximum du jeton d'identité",
 "oidcOPMetaDataOptionsIcon":"Logo",
 "oidcOPMetaDataOptionsJWKSTimeout":"Durée de vie des données JWKS",
-"oidcOPMetaDataOptionsMaxAge":"Âge maximum",
+"oidcOPMetaDataOptionsMaxAge":"Age maximum",
 "oidcOPMetaDataOptionsPrompt":"Interaction",
 "oidcOPMetaDataOptionsProtocol":"Protocole",
 "oidcOPMetaDataOptionsScope":"Scope",
@@ -647,41 +662,41 @@
 "oidcParams":"Paramètres OpenID Connect",
 "oidcRP":"Client OpenID Connect",
 "oidcRPCallbackGetParam":"Paramètre GET callback",
-"oidcRPMetaDataExportedVars":"Attributs exportés",
+"oidcRPMetaDataExportedVars":"Attributs exportés (claims)",
 "oidcRPMetaDataMacros":"Macros",
 "oidcRPMetaDataNode":"Clients OpenID Connect",
 "oidcRPMetaDataNodes":"Clients OpenID Connect",
 "oidcRPMetaDataOptions":"Options",
-"oidcRPMetaDataOptionsAccessTokenClaims":"Publier les attributs dans l'Access Token",
-"oidcRPMetaDataOptionsAccessTokenExpiration":"Expiration des jetons d'accès",
+"oidcRPMetaDataOptionsAccessTokenClaims":"Publier les attributs dans le jeton d'accès",
+"oidcRPMetaDataOptionsAccessTokenExpiration":"Jetons d'accès",
 "oidcRPMetaDataOptionsAccessTokenJWT":"Format JWT pour les jetons d'accès",
-"oidcRPMetaDataOptionsAccessTokenSignAlg":"Algorithme de signature des Access Token",
+"oidcRPMetaDataOptionsAccessTokenSignAlg":"Algorithme de signature des jetons d'accès",
 "oidcRPMetaDataOptionsAdditionalAudiences":"Audiences supplémentaires",
 "oidcRPMetaDataOptionsAdvanced":"Avancées",
 "oidcRPMetaDataOptionsAllowClientCredentialsGrant":"Autoriser le Client Credentials Grant OAuth2.0",
 "oidcRPMetaDataOptionsAllowOffline":"Autoriser l'accès hors ligne",
 "oidcRPMetaDataOptionsAllowPasswordGrant":"Autoriser le Password Grant OAuth2.0",
 "oidcRPMetaDataOptionsAuthnLevel":"Niveau d'authentification",
-"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Expiration des codes d'autorisation",
+"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Codes d'autorisation",
 "oidcRPMetaDataOptionsBasic":"Basiques",
 "oidcRPMetaDataOptionsBypassConsent":"Contourner le consentement",
 "oidcRPMetaDataOptionsClientID":"ID client",
 "oidcRPMetaDataOptionsClientSecret":"Secret client",
 "oidcRPMetaDataOptionsDisplay":"Affichage",
 "oidcRPMetaDataOptionsDisplayName":"Nom d'affichage",
-"oidcRPMetaDataOptionsExtraClaims":"Déclarations (scopes/claims)",
-"oidcRPMetaDataOptionsIDTokenExpiration":"Expiration des jetons d'identité",
-"oidcRPMetaDataOptionsIDTokenForceClaims":"Forcer la publication des attributs dans l'ID Token",
+"oidcRPMetaDataOptionsExtraClaims":"Contenu des scopes",
+"oidcRPMetaDataOptionsIDTokenExpiration":"Jetons d'identité",
+"oidcRPMetaDataOptionsIDTokenForceClaims":"Forcer la publication des attributs dans le jeton d'identité",
 "oidcRPMetaDataOptionsIDTokenSignAlg":"Algorithme de signature des jetons d'identité",
 "oidcRPMetaDataOptionsIcon":"Logo",
 "oidcRPMetaDataOptionsLogoutSessionRequired":"Session requise",
 "oidcRPMetaDataOptionsLogoutType":"Type",
 "oidcRPMetaDataOptionsLogoutUrl":"URL",
-"oidcRPMetaDataOptionsOfflineSessionExpiration":"Expiration des sessions hors-ligne",
+"oidcRPMetaDataOptionsOfflineSessionExpiration":"Sessions hors-ligne",
 "oidcRPMetaDataOptionsPostLogoutRedirectUris":"Adresses de redirection autorisées pour la déconnexion",
 "oidcRPMetaDataOptionsPublic":"Client public",
 "oidcRPMetaDataOptionsRedirectUris":"Adresses de redirection autorisées pour la connexion",
-"oidcRPMetaDataOptionsRefreshToken":"Utiliser les refresh tokens",
+"oidcRPMetaDataOptionsRefreshToken":"Utiliser les jetons de renouvellement",
 "oidcRPMetaDataOptionsRequirePKCE":"PKCE requis",
 "oidcRPMetaDataOptionsRule":"Règle d'accès",
 "oidcRPMetaDataOptionsTimeouts":"Expiration",
@@ -690,17 +705,18 @@
 "oidcRPMetaDataScopeRules":"Règles de scope",
 "oidcRPName":"Nom du client OpenID Connect",
 "oidcRPStateTimeout":"Durée d'une session state",
-"oidcServiceAccessTokenExpiration":"Expiration des jetons d'accès",
+"oidcServiceAccessTokenExpiration":"Jetons d'accès",
 "oidcServiceAllowAuthorizationCodeFlow":"Authorization Code Flow",
-"oidcServiceAllowDynamicRegistration":"Enregistrement dynamique",
+"oidcServiceAllowDynamicRegistration":"Activation",
 "oidcServiceAllowHybridFlow":"Hybrid Flow",
 "oidcServiceAllowImplicitFlow":"Implicit Flow",
 "oidcServiceAllowOffline":"Autoriser l'accès hors ligne",
 "oidcServiceAllowOnlyDeclaredScopes":"N'autoriser que les scopes déclarés",
-"oidcServiceAuthorizationCodeExpiration":"Expiration des codes d'autorisation",
-"oidcServiceDynamicRegistrationExportedVars":"Variables exportées pour l'enregistrement dynamique",
-"oidcServiceDynamicRegistrationExtraClaims":"Claims supplémentaires pour l'enregistrement dynamique",
-"oidcServiceIDTokenExpiration":"Expiration des jetons d'identité",
+"oidcServiceAuthorizationCodeExpiration":"Codes d'autorisation",
+"oidcServiceDynamicRegistration":"Enregistrement dynamique",
+"oidcServiceDynamicRegistrationExportedVars":"Variables exportées",
+"oidcServiceDynamicRegistrationExtraClaims":"Claims supplémentaires",
+"oidcServiceIDTokenExpiration":"Jetons d'identité",
 "oidcServiceKeyIdSig":"Identifiant de clef de signature",
 "oidcServiceMetaData":"Service OpenID Connect",
 "oidcServiceMetaDataAuthnContext":"Contexte d'authentification",
@@ -717,9 +733,10 @@
 "oidcServiceMetaDataRegistrationURI":"Enregistrement",
 "oidcServiceMetaDataSecurity":"Sécurité",
 "oidcServiceMetaDataSessions":"Sessions",
-"oidcServiceMetaDataTokenURI":"Jeton",
+"oidcServiceMetaDataTimeouts":"Expiration",
+"oidcServiceMetaDataTokenURI":"Jetons",
 "oidcServiceMetaDataUserInfoURI":"Informations Utilisateur",
-"oidcServiceOfflineSessionExpiration":"Expiration des sessions hors-ligne",
+"oidcServiceOfflineSessionExpiration":"Sessions hors-ligne",
 "oidcServicePrivateKeySig":"Clef privée de signature",
 "oidcServicePublicKeySig":"Clef publique de signature",
 "oidcStorage":"Nom du module des sessions",
@@ -812,8 +829,13 @@
 "postedVars":"Variables à poster",
 "previous":"Précédente",
 "privateKey":"Clef privée",
-"proxyAuthService":"URL du portail interne",
+"proxyAuthService":"URL",
+"proxyAuthServiceChoiceParam":"Paramètre du choix d'authentification",
+"proxyAuthServiceChoiceValue":"Valeur du choix d'authentification",
+"proxyAuthServiceImpersonation":"Simulation d'identité",
 "proxyAuthnLevel":"Niveau d'authentification",
+"proxyCookieName":"Nom du cookie",
+"proxyInternalPortal":"Portail interne",
 "proxyParams":"Paramètres Proxy",
 "proxySessionService":"URL du service de session",
 "proxyUseSoap":"Utiliser SOAP au lieu de REST",
@@ -1089,6 +1111,7 @@
 "stateCheck":"Vérification de l'état",
 "stayConnect":"Connexions persistantes",
 "stayConnected":"Activation",
+"stayConnectedBypassFG":"Ne pas vérifier l'empreinte",
 "stayConnectedCookieName":"Nom du cookie",
 "stayConnectedTimeout":"Durée de validité",
 "storePassword":"Stocke le mot de passe de l'utilisateur en session",
@@ -1110,10 +1133,11 @@
 "tooltip":"Info-bulle",
 "totp2f":"TOTP",
 "totp2fActivation":"Activation",
-"totp2fAuthnLevel":"Niveau d'authentification TOTP",
+"totp2fAuthnLevel":"Niveau d'authentification",
 "totp2fDigits":"Nombre de chiffres",
+"totp2fEncryptSecret":"Chiffrer le secret TOTP",
 "totp2fInterval":"Intervalle",
-"totp2fIssuer":"Nom du fournisseur TOTP",
+"totp2fIssuer":"Nom du fournisseur",
 "totp2fLabel":"Label",
 "totp2fLogo":"Logo",
 "totp2fRange":"Nombre d'intervalles à tester",
@@ -1131,7 +1155,7 @@
 "type":"Type",
 "u2f":"U2F",
 "u2fActivation":"Activation",
-"u2fAuthnLevel":"Niveau d'authentification U2F",
+"u2fAuthnLevel":"Niveau d'authentification",
 "u2fLabel":"Label",
 "u2fLogo":"Logo",
 "u2fSelfRegistration":"Auto-enregistrement",
@@ -1172,6 +1196,7 @@
 "vhostAccessToTrace":"Accès à tracer",
 "vhostAliases":"Alias",
 "vhostAuthnLevel":"Niveau d'authentification requis",
+"vhostDevOpsRulesUrl":"URL du fichier de règles DevOps",
 "vhostHttps":"HTTPS",
 "vhostMaintenance":"Mode maintenance",
 "vhostOptions":"Options",
@@ -1190,6 +1215,16 @@
 "webIDAuthnLevel":"Niveau d'authentification",
 "webIDExportedVars":"Variables exportées",
 "webIDWhitelist":"Liste blanche WebID",
+"webauthn2f":"WebAuthn",
+"webauthn2fActivation":"Activation",
+"webauthn2fAuthnLevel":"Niveau d'authentification",
+"webauthn2fLabel":"Label",
+"webauthn2fLogo":"Logo",
+"webauthn2fSelfRegistration":"Auto-enregistrement",
+"webauthn2fUserCanRemoveKey":"Autoriser les utilisateurs à effacer leur WebAuthn",
+"webauthn2fUserVerification":"Vérification de l'utilisateur",
+"webauthnDisplayNameAttr":"Attribut du nom d'affichage de l'utilisateur",
+"webauthnRpName":"Nom d'affichage du portail",
 "webidParams":"Paramètres WebID",
 "whatToTrace":"REMOTE_USER",
 "whiteList":"Liste blanche",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/it.json 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/it.json
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/it.json	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/it.json	2022-02-19 16:04:21.000000000 +0000
@@ -126,7 +126,9 @@
 "bruteForceProtection":"Attivazione",
 "bruteForceProtectionIncrementalTempo":"Incremental lock",
 "bruteForceProtectionLockTimes":"Incremental lock times",
+"bruteForceProtectionMaxAge":"Età massima",
 "bruteForceProtectionMaxFailed":"Allowed failed logins",
+"bruteForceProtectionMaxLockTime":"Maximum lock time",
 "bruteForceProtectionTempo":"Lock time",
 "cancel":"Cancella",
 "captcha_login_enabled":"Attivazione nel modulo di login",
@@ -165,6 +167,7 @@
 "casStorage":"Nome del modulo sessioni CAS",
 "casStorageOptions":"Opzioni del modulo sessioni CAS",
 "casStrictMatching":"Use strict URL matching",
+"casTicketExpiration":"Temporary ticket lifetime",
 "categoryName":"Nome della categoria",
 "cda":"Domini multipli",
 "certificateMailContent":"Contenuto della mail",
@@ -180,6 +183,8 @@
 "cfgLog":"Summary",
 "cfgVersion":"Versione configurazione",
 "checkDevOps":"Activation",
+"checkDevOpsCheckSessionAttributes":"Check session attributes",
+"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
 "checkDevOpsDownload":"Download file",
 "checkState":"Attivazione",
 "checkStateSecret":"Segreto condiviso",
@@ -188,6 +193,8 @@
 "checkUserDisplayComputedSession":"Computed sessions",
 "checkUserDisplayEmptyHeaders":"Empty headers",
 "checkUserDisplayEmptyValues":"Empty values",
+"checkUserDisplayHiddenAttributes":"Hidden attributes",
+"checkUserDisplayHistory":"History",
 "checkUserDisplayNormalizedHeaders":"Normalized headers",
 "checkUserDisplayPersistentInfo":"Persistent session data",
 "checkUserHiddenAttributes":"Attributi nascosti",
@@ -349,7 +356,7 @@
 "facebookExportedVars":"Variabili esportate",
 "facebookParams":"Parametri di Facebook",
 "facebookUserField":"Campo contenente l'identificatore dell'utente",
-"failedLoginNumber":"Numero di login registrati non riusciti",
+"failedLoginNumber":"Max failed logins count",
 "fileToUpload":"File da caricare",
 "findUser":"Activation",
 "findUserControl":"Parameters control",
@@ -567,6 +574,14 @@
 "newEntry":"Nuova entrata",
 "newGrantRule":"Nuova regola di autorizzazione",
 "newHost":"Nuovo host",
+"newLocationWarning":"Activation",
+"newLocationWarningLocationAttribute":"Session attribute containing location",
+"newLocationWarningLocationDisplayAttribute":"Session attribute to display",
+"newLocationWarningMailAttribute":"Session mail attribute",
+"newLocationWarningMailBody":"Warning mail content",
+"newLocationWarningMailSubject":"Warning mail subject",
+"newLocationWarningMaxValues":"Maximum number of locations to consider",
+"newLocationWarnings":"New location warning",
 "newPost":"Nuovo formulario di risposta",
 "newPostVar":"Nuova variabile",
 "newRSAKey":"Nuove chiavi",
@@ -647,13 +662,13 @@
 "oidcParams":"Parametri di OpenID Connect",
 "oidcRP":"Parte basata su OpenID Connect",
 "oidcRPCallbackGetParam":"Parametri Callback GET",
-"oidcRPMetaDataExportedVars":"Attributi esportati",
+"oidcRPMetaDataExportedVars":"Exported attributes (claims)",
 "oidcRPMetaDataMacros":"Macro",
 "oidcRPMetaDataNode":"Parti basate su OpenID Connect",
 "oidcRPMetaDataNodes":"Parti basate su OpenID Connect",
 "oidcRPMetaDataOptions":"Opzioni",
 "oidcRPMetaDataOptionsAccessTokenClaims":"Release claims in Access Token",
-"oidcRPMetaDataOptionsAccessTokenExpiration":"Scadenza accesso token",
+"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Tokens",
 "oidcRPMetaDataOptionsAccessTokenJWT":"Use JWT format for Access Token",
 "oidcRPMetaDataOptionsAccessTokenSignAlg":"Access Token signature algorithm",
 "oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
@@ -662,22 +677,22 @@
 "oidcRPMetaDataOptionsAllowOffline":"Allow offline access",
 "oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
 "oidcRPMetaDataOptionsAuthnLevel":"Livello di autenticazione",
-"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Code expiration",
+"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Codes",
 "oidcRPMetaDataOptionsBasic":"Basic",
 "oidcRPMetaDataOptionsBypassConsent":"Consenso di bypass",
 "oidcRPMetaDataOptionsClientID":"ID Client",
 "oidcRPMetaDataOptionsClientSecret":"Segreto Client",
 "oidcRPMetaDataOptionsDisplay":"Visualizza",
 "oidcRPMetaDataOptionsDisplayName":"Mostra nome",
-"oidcRPMetaDataOptionsExtraClaims":"Richieste supplementari",
-"oidcRPMetaDataOptionsIDTokenExpiration":"Scadenza ID Token",
+"oidcRPMetaDataOptionsExtraClaims":"Scope values content",
+"oidcRPMetaDataOptionsIDTokenExpiration":"ID Tokens",
 "oidcRPMetaDataOptionsIDTokenForceClaims":"Force claims to be returned in ID Token",
 "oidcRPMetaDataOptionsIDTokenSignAlg":"Algoritmo di firma di identificazione di Token",
 "oidcRPMetaDataOptionsIcon":"Logo",
 "oidcRPMetaDataOptionsLogoutSessionRequired":"Sessione necessaria",
 "oidcRPMetaDataOptionsLogoutType":"Tipo",
 "oidcRPMetaDataOptionsLogoutUrl":"URL",
-"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline session expiration",
+"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline sessions",
 "oidcRPMetaDataOptionsPostLogoutRedirectUris":"Indirizzi di reindirizzazione consentiti per il logout",
 "oidcRPMetaDataOptionsPublic":"Cliente pubblico",
 "oidcRPMetaDataOptionsRedirectUris":"Indirizzi di reindirizzazione consentiti per l'accesso",
@@ -690,24 +705,25 @@
 "oidcRPMetaDataScopeRules":"Scope rules",
 "oidcRPName":"Nome di OpenID Connect RP",
 "oidcRPStateTimeout":"Durata della sessione stato",
-"oidcServiceAccessTokenExpiration":"Scadenza accesso token",
+"oidcServiceAccessTokenExpiration":"Access Token",
 "oidcServiceAllowAuthorizationCodeFlow":"Flusso del codice di autorizzazione",
-"oidcServiceAllowDynamicRegistration":"Registrazione dinamica",
+"oidcServiceAllowDynamicRegistration":"Activation",
 "oidcServiceAllowHybridFlow":"Flusso ibrido",
 "oidcServiceAllowImplicitFlow":"Flusso implicito",
 "oidcServiceAllowOffline":"Allow offline access",
 "oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
-"oidcServiceAuthorizationCodeExpiration":"Authorization Code expiration",
-"oidcServiceDynamicRegistrationExportedVars":"Exported vars for dynamic registration",
-"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",
-"oidcServiceIDTokenExpiration":"Scadenza ID Token",
+"oidcServiceAuthorizationCodeExpiration":"Authorization Code",
+"oidcServiceDynamicRegistration":"Dynamic registration",
+"oidcServiceDynamicRegistrationExportedVars":"Exported vars",
+"oidcServiceDynamicRegistrationExtraClaims":"Extra claims",
+"oidcServiceIDTokenExpiration":"ID Token",
 "oidcServiceKeyIdSig":"ID del codice di accesso",
 "oidcServiceMetaData":"Servizio di OpenID Connect",
 "oidcServiceMetaDataAuthnContext":"Contesto di autenticazione",
 "oidcServiceMetaDataAuthorizeURI":"Autorizzazione",
 "oidcServiceMetaDataBackChannelURI":"URI Back-Channel",
 "oidcServiceMetaDataCheckSessionURI":"Controlla sessione",
-"oidcServiceMetaDataEndPoints":"Endpoint",
+"oidcServiceMetaDataEndPoints":"Endpoints",
 "oidcServiceMetaDataEndSessionURI":"Fine sessione",
 "oidcServiceMetaDataFrontChannelURI":"URI Front-Channel ",
 "oidcServiceMetaDataIntrospectionURI":"Introspection",
@@ -717,9 +733,10 @@
 "oidcServiceMetaDataRegistrationURI":"Registrazione",
 "oidcServiceMetaDataSecurity":"Sicurezza",
 "oidcServiceMetaDataSessions":"Sessioni",
-"oidcServiceMetaDataTokenURI":"Token",
+"oidcServiceMetaDataTimeouts":"Timeouts",
+"oidcServiceMetaDataTokenURI":"Tokens",
 "oidcServiceMetaDataUserInfoURI":"Informazioni utente",
-"oidcServiceOfflineSessionExpiration":"Offline session expiration",
+"oidcServiceOfflineSessionExpiration":"Offline session",
 "oidcServicePrivateKeySig":"Firma della chiave privata",
 "oidcServicePublicKeySig":"Firma della chiave pubblica",
 "oidcStorage":"Nome del modulo Sessioni",
@@ -812,8 +829,13 @@
 "postedVars":"Variabili da inviare",
 "previous":"Precedente",
 "privateKey":"Chiave privata",
-"proxyAuthService":"URL del portale interno",
+"proxyAuthService":"URL",
+"proxyAuthServiceChoiceParam":"Choice parameter",
+"proxyAuthServiceChoiceValue":"Choice value",
+"proxyAuthServiceImpersonation":"Impersonation",
 "proxyAuthnLevel":"Livello di autenticazione",
+"proxyCookieName":"Nome del cookie",
+"proxyInternalPortal":"Internal Portal",
 "proxyParams":"Parametri Proxy",
 "proxySessionService":"URL del servizio di sessione",
 "proxyUseSoap":"Usa SOAP invece di REST",
@@ -865,7 +887,7 @@
 "rest2fInitUrl":"URL iniziale",
 "rest2fLabel":"Label",
 "rest2fLogo":"Logo",
-"rest2fVerifyArgs":"Verifica Argomenti",
+"rest2fVerifyArgs":"Verifica argomenti",
 "rest2fVerifyUrl":"Verifica UR",
 "restAuthServer":"Authentication server",
 "restAuthUrl":"URL di autenticazione",
@@ -1089,12 +1111,13 @@
 "stateCheck":"Controllo dello stato",
 "stayConnect":"Connessioni persistenti",
 "stayConnected":"Attivazione",
+"stayConnectedBypassFG":"Do not check fingerprint",
 "stayConnectedCookieName":"Nome del cookie",
 "stayConnectedTimeout":"Expiration time",
 "storePassword":"Memorizzare la password dell'utente nei dati di sessione",
 "string":"String",
 "subtitle":"Subtitle",
-"successLoginNumber":"Numero di login registrati",
+"successLoginNumber":"Max successful logins count",
 "successfullySaved":"Salvato con successo",
 "sympaHandler":"Sympa",
 "sympaMailKey":"Chiave della sessione di posta",
@@ -1110,10 +1133,11 @@
 "tooltip":"Tooltip",
 "totp2f":"TOTP",
 "totp2fActivation":"Attivazione",
-"totp2fAuthnLevel":"Livello di autenticazione TOTP",
+"totp2fAuthnLevel":"Livello di autenticazione",
 "totp2fDigits":"Numero di cifre",
+"totp2fEncryptSecret":"Encrypt TOTP secrets",
 "totp2fInterval":"Intervallo",
-"totp2fIssuer":"Nome dell'emittente TOTP",
+"totp2fIssuer":"Issuer name",
 "totp2fLabel":"Label",
 "totp2fLogo":"Logo",
 "totp2fRange":"Gamma di tentativi",
@@ -1131,7 +1155,7 @@
 "type":"Tipo",
 "u2f":"U2F",
 "u2fActivation":"Attivazione",
-"u2fAuthnLevel":"Livello di autenticazione U2F",
+"u2fAuthnLevel":"Livello di autenticazione",
 "u2fLabel":"Label",
 "u2fLogo":"Logo",
 "u2fSelfRegistration":"Auto-registrazione",
@@ -1172,6 +1196,7 @@
 "vhostAccessToTrace":"Access to trace",
 "vhostAliases":"Alias",
 "vhostAuthnLevel":"Livello di autenticazione richiesto",
+"vhostDevOpsRulesUrl":"DevOps rules file URL",
 "vhostHttps":"HTTPS",
 "vhostMaintenance":"Modalità di manutenzione",
 "vhostOptions":"Opzioni",
@@ -1190,6 +1215,16 @@
 "webIDAuthnLevel":"Livello di autenticazione",
 "webIDExportedVars":"Variabili esportate",
 "webIDWhitelist":"Whitelist WebID",
+"webauthn2f":"WebAuthn",
+"webauthn2fActivation":"Attivazione",
+"webauthn2fAuthnLevel":"Livello di autenticazione",
+"webauthn2fLabel":"Label",
+"webauthn2fLogo":"Logo",
+"webauthn2fSelfRegistration":"Auto-registrazione",
+"webauthn2fUserCanRemoveKey":"Autorizza l'utente a rimuovere la WebAuthn",
+"webauthn2fUserVerification":"User verification",
+"webauthnDisplayNameAttr":"User Display Name attribute",
+"webauthnRpName":"Relying Party display name",
 "webidParams":"Parametri di WebID",
 "whatToTrace":"\nREMOTE_USER",
 "whiteList":"Lista bianca",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/pl.json 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/pl.json
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/pl.json	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/pl.json	2022-02-19 16:04:21.000000000 +0000
@@ -126,7 +126,9 @@
 "bruteForceProtection":"Aktywacja",
 "bruteForceProtectionIncrementalTempo":"Blokada przyrostowa",
 "bruteForceProtectionLockTimes":"Przyrostowe czasy blokady",
+"bruteForceProtectionMaxAge":"Maksymalny czas ważności",
 "bruteForceProtectionMaxFailed":"Dozwolone nieudane logowania",
+"bruteForceProtectionMaxLockTime":"Maximum lock time",
 "bruteForceProtectionTempo":"Czas blokady",
 "cancel":"Anuluj",
 "captcha_login_enabled":"Aktywacja w formularzu logowania",
@@ -165,6 +167,7 @@
 "casStorage":"Nazwa modułu sesji CAS",
 "casStorageOptions":"Opcje modułu sesji CAS",
 "casStrictMatching":"Use strict URL matching",
+"casTicketExpiration":"Temporary ticket lifetime",
 "categoryName":"Nazwa Kategorii",
 "cda":"Wiele domen",
 "certificateMailContent":"Treść wiadomości",
@@ -180,6 +183,8 @@
 "cfgLog":"Podsumowanie",
 "cfgVersion":"Wersja konfiguracji",
 "checkDevOps":"Aktywacja",
+"checkDevOpsCheckSessionAttributes":"Check session attributes",
+"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
 "checkDevOpsDownload":"Download file",
 "checkState":"Aktywacja",
 "checkStateSecret":"Współdzielony sekret",
@@ -188,6 +193,8 @@
 "checkUserDisplayComputedSession":"Sesje obliczane",
 "checkUserDisplayEmptyHeaders":"Puste nagłówki",
 "checkUserDisplayEmptyValues":"Puste wartości",
+"checkUserDisplayHiddenAttributes":"Ukryte atrybuty",
+"checkUserDisplayHistory":"History",
 "checkUserDisplayNormalizedHeaders":"Znormalizowane nagłówki",
 "checkUserDisplayPersistentInfo":"Trwałe dane sesji",
 "checkUserHiddenAttributes":"Ukryte atrybuty",
@@ -293,7 +300,7 @@
 "dbiUserChain":"Łańcuch",
 "dbiUserPassword":"Hasło",
 "dbiUserTable":"Tabela użytkowników",
-"dbiUserUser":"User",
+"dbiUserUser":"Użytkownik",
 "decryptValue":"Odszyfruj wartość",
 "decryptValueFunctions":"Odszyfruj funkcje",
 "decryptValueRule":"Użyj reguły",
@@ -349,7 +356,7 @@
 "facebookExportedVars":"Wyeksportowane zmienne",
 "facebookParams":"Parametry Facebooka",
 "facebookUserField":"Pole zawierające identyfikator użytkownika",
-"failedLoginNumber":"Liczba zarejestrowanych nieudanych prób logowania",
+"failedLoginNumber":"Max failed logins count",
 "fileToUpload":"Plik do przesłania",
 "findUser":"Aktywacja",
 "findUserControl":"Kontrola parametrów",
@@ -567,6 +574,14 @@
 "newEntry":"Nowy wpis",
 "newGrantRule":"Nowa reguła przyznawania",
 "newHost":"Nowy host",
+"newLocationWarning":"Aktywacja",
+"newLocationWarningLocationAttribute":"Session attribute containing location",
+"newLocationWarningLocationDisplayAttribute":"Session attribute to display",
+"newLocationWarningMailAttribute":"Session mail attribute",
+"newLocationWarningMailBody":"Warning mail content",
+"newLocationWarningMailSubject":"Warning mail subject",
+"newLocationWarningMaxValues":"Maximum number of locations to consider",
+"newLocationWarnings":"New location warning",
 "newPost":"Nowy formularz powtórzenia",
 "newPostVar":"Nowa zmienna",
 "newRSAKey":"Nowe klucze",
@@ -647,13 +662,13 @@
 "oidcParams":"Parametry OpenID Connect",
 "oidcRP":"Strona zależna od OpenID Connect",
 "oidcRPCallbackGetParam":"Parametr GET wywołania zwrotnego",
-"oidcRPMetaDataExportedVars":"Wyeksportowane atrybuty",
+"oidcRPMetaDataExportedVars":"Exported attributes (claims)",
 "oidcRPMetaDataMacros":"Makra",
 "oidcRPMetaDataNode":"Strony zależne od OpenID Connect",
 "oidcRPMetaDataNodes":"Strony zależne od OpenID Connect",
 "oidcRPMetaDataOptions":"Opcje",
 "oidcRPMetaDataOptionsAccessTokenClaims":"Zwolnij oświadczenia w tokenie dostępu",
-"oidcRPMetaDataOptionsAccessTokenExpiration":"Wygaśnięcie tokena dostępu",
+"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Tokens",
 "oidcRPMetaDataOptionsAccessTokenJWT":"Użyj formatu JWT dla tokenu dostępu",
 "oidcRPMetaDataOptionsAccessTokenSignAlg":"Algorytm podpisu tokena dostępu",
 "oidcRPMetaDataOptionsAdditionalAudiences":"Dodatkowi odbiorcy",
@@ -662,22 +677,22 @@
 "oidcRPMetaDataOptionsAllowOffline":"Zezwalaj na dostęp offline",
 "oidcRPMetaDataOptionsAllowPasswordGrant":"Zezwól na przyznanie hasła OAuth2.0",
 "oidcRPMetaDataOptionsAuthnLevel":"Poziom uwierzytelnienia",
-"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Wygaśnięcie kodu autoryzacji",
+"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Codes",
 "oidcRPMetaDataOptionsBasic":"Podstawowy",
 "oidcRPMetaDataOptionsBypassConsent":"Obejdź zgodę",
 "oidcRPMetaDataOptionsClientID":"Identyfikator klienta",
 "oidcRPMetaDataOptionsClientSecret":"Sekret klienta",
 "oidcRPMetaDataOptionsDisplay":"Wyświetlanie",
 "oidcRPMetaDataOptionsDisplayName":"Wyświetlana nazwa",
-"oidcRPMetaDataOptionsExtraClaims":"Dodatkowe roszczenia",
-"oidcRPMetaDataOptionsIDTokenExpiration":"Data ważności tokena identyfikacyjnego",
+"oidcRPMetaDataOptionsExtraClaims":"Scope values content",
+"oidcRPMetaDataOptionsIDTokenExpiration":"ID Tokens",
 "oidcRPMetaDataOptionsIDTokenForceClaims":"Wymuś zwrot roszczeń w tokenie identyfikacyjnym",
 "oidcRPMetaDataOptionsIDTokenSignAlg":"Algorytm podpisu tokena identyfikacyjnego",
 "oidcRPMetaDataOptionsIcon":"Logo",
 "oidcRPMetaDataOptionsLogoutSessionRequired":"Wymagana sesja",
 "oidcRPMetaDataOptionsLogoutType":"Rodzaj",
 "oidcRPMetaDataOptionsLogoutUrl":"URL",
-"oidcRPMetaDataOptionsOfflineSessionExpiration":"Wygaśnięcie sesji offline",
+"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline sessions",
 "oidcRPMetaDataOptionsPostLogoutRedirectUris":"Dozwolone adresy przekierowań dla wylogowania",
 "oidcRPMetaDataOptionsPublic":"Klient publiczny",
 "oidcRPMetaDataOptionsRedirectUris":"Dozwolone adresy przekierowań dla logowania",
@@ -690,17 +705,18 @@
 "oidcRPMetaDataScopeRules":"Zasady dotyczące zakresu",
 "oidcRPName":"Nazwa RP OpenID Connect",
 "oidcRPStateTimeout":"Limit czasu sesji stanowej",
-"oidcServiceAccessTokenExpiration":"Wygaśnięcie tokena dostępu",
+"oidcServiceAccessTokenExpiration":"Token dostępowy",
 "oidcServiceAllowAuthorizationCodeFlow":"Przepływ kodu autoryzacji",
-"oidcServiceAllowDynamicRegistration":"Rejestracja dynamiczna",
+"oidcServiceAllowDynamicRegistration":"Activation",
 "oidcServiceAllowHybridFlow":"Przepływ hybrydowy",
 "oidcServiceAllowImplicitFlow":"Implikowany przepływ",
 "oidcServiceAllowOffline":"Zezwalaj na dostęp offline",
 "oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
-"oidcServiceAuthorizationCodeExpiration":"Wygaśnięcie kodu autoryzacji",
-"oidcServiceDynamicRegistrationExportedVars":"Zmienne wyeksportowane do dynamicznej rejestracji",
-"oidcServiceDynamicRegistrationExtraClaims":"Dodatkowe roszczenia dotyczące rejestracji dynamicznej",
-"oidcServiceIDTokenExpiration":"Data ważności tokena identyfikacyjnego",
+"oidcServiceAuthorizationCodeExpiration":"Authorization Code",
+"oidcServiceDynamicRegistration":"Dynamic registration",
+"oidcServiceDynamicRegistrationExportedVars":"Exported vars",
+"oidcServiceDynamicRegistrationExtraClaims":"Extra claims",
+"oidcServiceIDTokenExpiration":"Token identyfikacyjny",
 "oidcServiceKeyIdSig":"Identyfikator klucza podpisu",
 "oidcServiceMetaData":"Usługa OpenID Connect",
 "oidcServiceMetaDataAuthnContext":"Kontekst uwierzytelnienia",
@@ -717,9 +733,10 @@
 "oidcServiceMetaDataRegistrationURI":"Rejestracja",
 "oidcServiceMetaDataSecurity":"Bezpieczeństwo",
 "oidcServiceMetaDataSessions":"Sesje",
-"oidcServiceMetaDataTokenURI":"Token",
+"oidcServiceMetaDataTimeouts":"Limit czasu",
+"oidcServiceMetaDataTokenURI":"Tokens",
 "oidcServiceMetaDataUserInfoURI":"Informacja o użytkowniku",
-"oidcServiceOfflineSessionExpiration":"Wygaśnięcie sesji offline",
+"oidcServiceOfflineSessionExpiration":"Offline session",
 "oidcServicePrivateKeySig":"Klucz prywatny podpisu",
 "oidcServicePublicKeySig":"Klucz publiczny podpisu",
 "oidcStorage":"Nazwa modułu sesji",
@@ -812,8 +829,13 @@
 "postedVars":"Zmienne do opublikowania",
 "previous":"Poprzedni",
 "privateKey":"Prywatny klucz",
-"proxyAuthService":"Wewnętrzny adres URL portalu",
+"proxyAuthService":"URL",
+"proxyAuthServiceChoiceParam":"Choice parameter",
+"proxyAuthServiceChoiceValue":"Choice value",
+"proxyAuthServiceImpersonation":"Personifikacja",
 "proxyAuthnLevel":"Poziom uwierzytelnienia",
+"proxyCookieName":"Nazwa ciasteczka",
+"proxyInternalPortal":"Internal Portal",
 "proxyParams":"Parametry proxy",
 "proxySessionService":"Adres URL usługi sesji",
 "proxyUseSoap":"Użyj SOAP zamiast REST",
@@ -1089,12 +1111,13 @@
 "stateCheck":"Kontrola stanu",
 "stayConnect":"Trwałe połączenia",
 "stayConnected":"Aktywacja",
+"stayConnectedBypassFG":"Do not check fingerprint",
 "stayConnectedCookieName":"Nazwa ciasteczka",
 "stayConnectedTimeout":"Data ważności",
 "storePassword":"Przechowuj hasło użytkownika w sesji",
 "string":"Łańcuch znaków",
 "subtitle":"Podtytuł",
-"successLoginNumber":"Liczba zarejestrowanych loginów",
+"successLoginNumber":"Max successful logins count",
 "successfullySaved":"Pomyślnie zapisano",
 "sympaHandler":"Sympa",
 "sympaMailKey":"Klucz sesji e-mail",
@@ -1110,10 +1133,11 @@
 "tooltip":"Etykietka",
 "totp2f":"TOTP",
 "totp2fActivation":"Aktywacja",
-"totp2fAuthnLevel":"Poziom uwierzytelnienia TOTP",
+"totp2fAuthnLevel":"Poziom uwierzytelnienia",
 "totp2fDigits":"Ilość cyfr",
+"totp2fEncryptSecret":"Encrypt TOTP secrets",
 "totp2fInterval":"Interwał",
-"totp2fIssuer":"TOTP Nazwa wystawcy",
+"totp2fIssuer":"Issuer name",
 "totp2fLabel":"Etykieta",
 "totp2fLogo":"Logo",
 "totp2fRange":"Zakres prób",
@@ -1131,7 +1155,7 @@
 "type":"Rodzaj",
 "u2f":"U2F",
 "u2fActivation":"Aktywacja",
-"u2fAuthnLevel":"Poziom uwierzytelnienia U2F",
+"u2fAuthnLevel":"Poziom uwierzytelnienia",
 "u2fLabel":"Etykieta",
 "u2fLogo":"Logo",
 "u2fSelfRegistration":"Samodzielna rejestracja",
@@ -1172,6 +1196,7 @@
 "vhostAccessToTrace":"Dostęp do śledzenia",
 "vhostAliases":"Aliasy",
 "vhostAuthnLevel":"Wymagany poziom uwierzytelnienia",
+"vhostDevOpsRulesUrl":"DevOps rules file URL",
 "vhostHttps":"HTTPS",
 "vhostMaintenance":"Tryb konserwacji",
 "vhostOptions":"Opcje",
@@ -1190,6 +1215,16 @@
 "webIDAuthnLevel":"Poziom uwierzytelnienia",
 "webIDExportedVars":"Wyeksportowane zmienne",
 "webIDWhitelist":"Biała lista WebID",
+"webauthn2f":"WebAuthn",
+"webauthn2fActivation":"Aktywacja",
+"webauthn2fAuthnLevel":"Poziom uwierzytelnienia",
+"webauthn2fLabel":"Etykieta",
+"webauthn2fLogo":"Logo",
+"webauthn2fSelfRegistration":"Samodzielna rejestracja",
+"webauthn2fUserCanRemoveKey":"Pozwól użytkownikowi usunąć WebAuthn",
+"webauthn2fUserVerification":"User verification",
+"webauthnDisplayNameAttr":"User Display Name attribute",
+"webauthnRpName":"Relying Party display name",
 "webidParams":"Parametry WebID",
 "whatToTrace":"REMOTE_USER",
 "whiteList":"Biała lista",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/tr.json 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/tr.json
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/tr.json	2021-08-21 17:42:59.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/tr.json	2022-02-19 16:04:21.000000000 +0000
@@ -126,7 +126,9 @@
 "bruteForceProtection":"Aktivasyon",
 "bruteForceProtectionIncrementalTempo":"Artan gecikme",
 "bruteForceProtectionLockTimes":"Artan gecikme zamanı",
+"bruteForceProtectionMaxAge":"Maksimum ömür",
 "bruteForceProtectionMaxFailed":"İzin verilen başarısız girişler",
+"bruteForceProtectionMaxLockTime":"Maksimum kilit süresi",
 "bruteForceProtectionTempo":"Kilit süresi",
 "cancel":"İptal Et",
 "captcha_login_enabled":"Giriş formunda aktivasyon",
@@ -165,6 +167,7 @@
 "casStorage":"CAS oturumları modül adı",
 "casStorageOptions":"CAS oturumları modül seçenekleri",
 "casStrictMatching":"Katı URL eşleşmesi kullan",
+"casTicketExpiration":"Geçici bilet ömrü",
 "categoryName":"Kategori ismi",
 "cda":"Çoklu alan adları",
 "certificateMailContent":"E-posta içeriği",
@@ -180,6 +183,8 @@
 "cfgLog":"Özet",
 "cfgVersion":"Yapılandırma sürümü",
 "checkDevOps":"Aktivasyon",
+"checkDevOpsCheckSessionAttributes":"Oturum niteliklerini kontrol et",
+"checkDevOpsDisplayNormalizedHeaders":"Normalleştirilmiş başlıkları görüntüle",
 "checkDevOpsDownload":"Dosyayı indir",
 "checkState":"Aktivasyon",
 "checkStateSecret":"Paylaşılan sır",
@@ -188,6 +193,8 @@
 "checkUserDisplayComputedSession":"Hesaplanan oturumlar",
 "checkUserDisplayEmptyHeaders":"Boş başlıklar",
 "checkUserDisplayEmptyValues":"Boş değerler",
+"checkUserDisplayHiddenAttributes":"Gizli nitelikler",
+"checkUserDisplayHistory":"Geçmiş",
 "checkUserDisplayNormalizedHeaders":"Normalleştirilmiş başlıklar",
 "checkUserDisplayPersistentInfo":"Kalıcı oturum verisi",
 "checkUserHiddenAttributes":"Gizli nitelikler",
@@ -349,7 +356,7 @@
 "facebookExportedVars":"Dışa aktarılan değişkenler",
 "facebookParams":"Facebook parametreleri",
 "facebookUserField":"Alan kullanıcı kimliği içeriyor",
-"failedLoginNumber":"Kayıtlı başarısız giriş sayısı",
+"failedLoginNumber":"Maksimum başarısız giriş sayısı",
 "fileToUpload":"Yüklenecek dosya",
 "findUser":"Aktivasyon",
 "findUserControl":"Parametre kontrolü",
@@ -567,6 +574,14 @@
 "newEntry":"Yeni kayıt",
 "newGrantRule":"Yeni imtiyaz kuralı",
 "newHost":"Yeni konak",
+"newLocationWarning":"Aktivasyon",
+"newLocationWarningLocationAttribute":"Oturum özelliği konum içerir",
+"newLocationWarningLocationDisplayAttribute":"Görüntülenecek oturum özelliği",
+"newLocationWarningMailAttribute":"Oturum postası özelliği",
+"newLocationWarningMailBody":"Uyarı postası içeriği",
+"newLocationWarningMailSubject":"Uyarı postası konusu",
+"newLocationWarningMaxValues":"Dikkate alınacak maksimum konum sayısı",
+"newLocationWarnings":"Yeni konum uyarısı",
 "newPost":"Yeni form tekrarı",
 "newPostVar":"Yeni değişken",
 "newRSAKey":"Yeni anahtarlar",
@@ -647,13 +662,13 @@
 "oidcParams":"OpenID Connect parametreleri",
 "oidcRP":"OpenID Connect Relying Party",
 "oidcRPCallbackGetParam":"GET parametresini geri çağır",
-"oidcRPMetaDataExportedVars":"Dışa aktarılan nitelikler",
+"oidcRPMetaDataExportedVars":"Dışa aktarılan nitelikler (talepler)",
 "oidcRPMetaDataMacros":"Makrolar",
 "oidcRPMetaDataNode":"OpenID Connect Relying Parties",
 "oidcRPMetaDataNodes":"OpenID Connect Relying Parties",
 "oidcRPMetaDataOptions":"Seçenekler",
 "oidcRPMetaDataOptionsAccessTokenClaims":"Erişim Jetonundaki isteklere izin ver",
-"oidcRPMetaDataOptionsAccessTokenExpiration":"Erişim jetonu sona erme",
+"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Tokens",
 "oidcRPMetaDataOptionsAccessTokenJWT":"Erişim Jetonu için JWT formatını kullan",
 "oidcRPMetaDataOptionsAccessTokenSignAlg":"Erişim jetonu imzalama algoritması",
 "oidcRPMetaDataOptionsAdditionalAudiences":"Ek hedef kitleler",
@@ -662,22 +677,22 @@
 "oidcRPMetaDataOptionsAllowOffline":"Çevrimdışı erişime izin ver",
 "oidcRPMetaDataOptionsAllowPasswordGrant":"OAuth2.0 Password Grant İzin Ver",
 "oidcRPMetaDataOptionsAuthnLevel":"Doğrulama seviyesi",
-"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Yetkilendirme Kodu sona erme",
+"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Codes",
 "oidcRPMetaDataOptionsBasic":"Temel",
 "oidcRPMetaDataOptionsBypassConsent":"İzni es geç",
 "oidcRPMetaDataOptionsClientID":"İstemci ID",
 "oidcRPMetaDataOptionsClientSecret":"İstemci sırrı",
 "oidcRPMetaDataOptionsDisplay":"Görüntüle",
 "oidcRPMetaDataOptionsDisplayName":"Görüntülenen ad",
-"oidcRPMetaDataOptionsExtraClaims":"Ekstra haklar",
-"oidcRPMetaDataOptionsIDTokenExpiration":"ID Jetonu sona erme",
+"oidcRPMetaDataOptionsExtraClaims":"Kapsam değerleri içeriği",
+"oidcRPMetaDataOptionsIDTokenExpiration":"ID Tokens",
 "oidcRPMetaDataOptionsIDTokenForceClaims":"ID Jetonunda özelliklerin yayınlanmasını zorla",
 "oidcRPMetaDataOptionsIDTokenSignAlg":"ID Token imzalama algoritması",
 "oidcRPMetaDataOptionsIcon":"Logo",
 "oidcRPMetaDataOptionsLogoutSessionRequired":"Oturum gerekli",
 "oidcRPMetaDataOptionsLogoutType":"Tür",
 "oidcRPMetaDataOptionsLogoutUrl":"URL",
-"oidcRPMetaDataOptionsOfflineSessionExpiration":"Çevrimdışı oturum sona erme",
+"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline sessions",
 "oidcRPMetaDataOptionsPostLogoutRedirectUris":"Çıkış için izin verilen yönlendirme adresleri",
 "oidcRPMetaDataOptionsPublic":"Açık istemci",
 "oidcRPMetaDataOptionsRedirectUris":"Giriş için izin verilen yönlendirme adresleri",
@@ -690,17 +705,18 @@
 "oidcRPMetaDataScopeRules":"Kapsam kuralları",
 "oidcRPName":"OpenID Connect RP Adı",
 "oidcRPStateTimeout":"Oturum zaman aşımını belirle",
-"oidcServiceAccessTokenExpiration":"Erişim Jetonu sona erme",
+"oidcServiceAccessTokenExpiration":"Erişim Jetonu",
 "oidcServiceAllowAuthorizationCodeFlow":"Yetkilendirme Kodu Akışı",
-"oidcServiceAllowDynamicRegistration":"Dinamik Kayıtlanma",
+"oidcServiceAllowDynamicRegistration":"Activation",
 "oidcServiceAllowHybridFlow":"Hibrit Akış",
 "oidcServiceAllowImplicitFlow":"Kapalı Akış",
 "oidcServiceAllowOffline":"Çevrimdışı erişime izin ver",
 "oidcServiceAllowOnlyDeclaredScopes":"Sadece belirli kapsamlara izin ver",
-"oidcServiceAuthorizationCodeExpiration":"Yetkilendirme Kodu sona erme",
-"oidcServiceDynamicRegistrationExportedVars":"Dinamik kayıtlanma için dışa aktarılan değişkenler",
-"oidcServiceDynamicRegistrationExtraClaims":"Dinamik kayıtlanma için ekstra talepler",
-"oidcServiceIDTokenExpiration":"ID Jetonu sona erme",
+"oidcServiceAuthorizationCodeExpiration":"Yetkilendirme Kodu",
+"oidcServiceDynamicRegistration":"Dynamic registration",
+"oidcServiceDynamicRegistrationExportedVars":"Exported vars",
+"oidcServiceDynamicRegistrationExtraClaims":"Extra claims",
+"oidcServiceIDTokenExpiration":"ID Jetonu",
 "oidcServiceKeyIdSig":"Anahtar ID imzalama",
 "oidcServiceMetaData":"OpenID Connect Servisi",
 "oidcServiceMetaDataAuthnContext":"Doğrulama bağlamı",
@@ -717,9 +733,10 @@
 "oidcServiceMetaDataRegistrationURI":"Kayıt",
 "oidcServiceMetaDataSecurity":"Güvenlik",
 "oidcServiceMetaDataSessions":"Oturumlar",
-"oidcServiceMetaDataTokenURI":"Jeton",
+"oidcServiceMetaDataTimeouts":"Zaman aşımları",
+"oidcServiceMetaDataTokenURI":"Jetons",
 "oidcServiceMetaDataUserInfoURI":"Kullanıcı Bilgisi",
-"oidcServiceOfflineSessionExpiration":"Çevrimdışı oturum sona erme",
+"oidcServiceOfflineSessionExpiration":"Çevrimdışı oturum",
 "oidcServicePrivateKeySig":"Özel anahtar imzalama",
 "oidcServicePublicKeySig":"Açık anahtar imzalama",
 "oidcStorage":"Oturumlar modülü adı",
@@ -812,8 +829,13 @@
 "postedVars":"Gönderilecek değişkenler",
 "previous":"Önceki",
 "privateKey":"Özel anahtar",
-"proxyAuthService":"Dahili portal URL'si",
+"proxyAuthService":"URL",
+"proxyAuthServiceChoiceParam":"Tercih parametresi",
+"proxyAuthServiceChoiceValue":"Tercih değeri",
+"proxyAuthServiceImpersonation":"Başka bir kullanıcı gibi davran",
 "proxyAuthnLevel":"Doğrulama seviyesi",
+"proxyCookieName":"Çerez adı",
+"proxyInternalPortal":"Dahili Portal",
 "proxyParams":"Proxy parametreleri",
 "proxySessionService":"Oturum servis URL'si",
 "proxyUseSoap":"REST yerine SOAP kullan",
@@ -841,11 +863,11 @@
 "regexp":"Düzenli ifade",
 "regexps":"Düzenli ifadeler",
 "register":"Yeni hesap kaydet",
-"registerConfirmBody":"Body for verification mail",
-"registerConfirmSubject":"Subject for verification mail",
+"registerConfirmBody":"Doğrulama postası için gövde",
+"registerConfirmSubject":"Doğrulama postası için konu",
 "registerDB":"Kayıt modülü",
-"registerDoneBody":"Body for credentials mail",
-"registerDoneSubject":"Subject for credentials mail",
+"registerDoneBody":"Kimlik bilgileri postası için gövde",
+"registerDoneSubject":"Kimlik bilgileri postası için konu",
 "registerTimeout":"Kayıt isteğinin geçerlilik süresi",
 "registerUrl":"Kayıt sayfası URL'si",
 "reloadParams":"Yapılandırma yeniden yüklendi",
@@ -861,7 +883,7 @@
 "rest2f":"REST ile ikinci faktör",
 "rest2fActivation":"Aktivasyon",
 "rest2fAuthnLevel":"Doğrulama seviyesi",
-"rest2fInitArgs":"Başlangıç Argümanları",
+"rest2fInitArgs":"Başlangıç argümanları",
 "rest2fInitUrl":"Başlangıç URL",
 "rest2fLabel":"Etiket",
 "rest2fLogo":"Logo",
@@ -1089,12 +1111,13 @@
 "stateCheck":"Durum Kontrolü",
 "stayConnect":"Kalıcı bağlantılar",
 "stayConnected":"Aktivasyon",
+"stayConnectedBypassFG":"Parmak izini kontrol etme",
 "stayConnectedCookieName":"Çerez adı",
 "stayConnectedTimeout":"Son kullanma süresi",
 "storePassword":"Kullanıcı parolasını oturumda sakla",
 "string":"Dize",
 "subtitle":"Altyazı",
-"successLoginNumber":"Kayıtlı girişlerin sayısı",
+"successLoginNumber":"Maksimum başarılı giriş sayısı",
 "successfullySaved":"Başarıyla kaydedildi",
 "sympaHandler":"Sympa",
 "sympaMailKey":"E-posta oturum anahtarı",
@@ -1110,10 +1133,11 @@
 "tooltip":"Araç ipucu",
 "totp2f":"TOTP",
 "totp2fActivation":"Aktivasyon",
-"totp2fAuthnLevel":"TOTP doğrulama seviyesi",
+"totp2fAuthnLevel":"Doğrulama seviyesi",
 "totp2fDigits":"Rakam sayısı",
+"totp2fEncryptSecret":"TOTP sırlarını şifreleyin",
 "totp2fInterval":"Süre aralığı",
-"totp2fIssuer":"TOTP Düzenleyici adı",
+"totp2fIssuer":"Düzenleyici adı",
 "totp2fLabel":"Etiket",
 "totp2fLogo":"Logo",
 "totp2fRange":"Deneme sayısı",
@@ -1131,7 +1155,7 @@
 "type":"Tür",
 "u2f":"U2F",
 "u2fActivation":"Aktivasyon",
-"u2fAuthnLevel":"U2F doğrulama seviyesi",
+"u2fAuthnLevel":"Doğrulama seviyesi",
 "u2fLabel":"Etiket",
 "u2fLogo":"Logo",
 "u2fSelfRegistration":"Kendi kendine kayıt",
@@ -1172,6 +1196,7 @@
 "vhostAccessToTrace":"İzlemeye erişim",
 "vhostAliases":"Takma adlar",
 "vhostAuthnLevel":"Gereken doğrulama seviyesi",
+"vhostDevOpsRulesUrl":"DevOps kuralları dosya URL'si",
 "vhostHttps":"HTTPS",
 "vhostMaintenance":"Bakım modu",
 "vhostOptions":"Seçenekler",
@@ -1190,6 +1215,16 @@
 "webIDAuthnLevel":"Doğrulama seviyesi",
 "webIDExportedVars":"Dışa aktarılan değişkenler",
 "webIDWhitelist":"WebID beyaz listesi",
+"webauthn2f":"WebAuthn",
+"webauthn2fActivation":"Aktivasyon",
+"webauthn2fAuthnLevel":"Doğrulama seviyesi",
+"webauthn2fLabel":"Etiket",
+"webauthn2fLogo":"Logo",
+"webauthn2fSelfRegistration":"Kendi kendine kayıt",
+"webauthn2fUserCanRemoveKey":"WebAuthn'i kaldırmak için kullanıcıya izin ver",
+"webauthn2fUserVerification":"Kullanıcı doğrulama",
+"webauthnDisplayNameAttr":"Kullanıcı Görüntülenen İsim niteliği",
+"webauthnRpName":"Bağlı Partinin görünen adı",
 "webidParams":"WebID parametreleri",
 "whatToTrace":"REMOTE_USER",
 "whiteList":"Beyaz liste",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/vi.json 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/vi.json
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/vi.json	2021-08-21 17:42:59.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/vi.json	2022-02-19 16:04:21.000000000 +0000
@@ -126,7 +126,9 @@
 "bruteForceProtection":"Kích hoạt",
 "bruteForceProtectionIncrementalTempo":"Incremental lock",
 "bruteForceProtectionLockTimes":"Incremental lock times",
+"bruteForceProtectionMaxAge":"Thời hạn tối đa",
 "bruteForceProtectionMaxFailed":"Allowed failed logins",
+"bruteForceProtectionMaxLockTime":"Maximum lock time",
 "bruteForceProtectionTempo":"Lock time",
 "cancel":"Hủy",
 "captcha_login_enabled":"Kích hoạt ở dạng đăng nhập",
@@ -165,6 +167,7 @@
 "casStorage":"Tên mô-đun phiên CAS",
 "casStorageOptions":"Các tùy chọn mô-đun phiên CAS",
 "casStrictMatching":"Use strict URL matching",
+"casTicketExpiration":"Temporary ticket lifetime",
 "categoryName":"Tên thể loại",
 "cda":"Nhiều tên miền",
 "certificateMailContent":"Nội dung thư",
@@ -180,6 +183,8 @@
 "cfgLog":"Summary",
 "cfgVersion":"Phiên bản cấu hình",
 "checkDevOps":"Activation",
+"checkDevOpsCheckSessionAttributes":"Check session attributes",
+"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
 "checkDevOpsDownload":"Download file",
 "checkState":"Kích hoạt",
 "checkStateSecret":"Chia sẻ bí mật",
@@ -188,6 +193,8 @@
 "checkUserDisplayComputedSession":"Computed sessions",
 "checkUserDisplayEmptyHeaders":"Empty headers",
 "checkUserDisplayEmptyValues":"Empty values",
+"checkUserDisplayHiddenAttributes":"Hidden attributes",
+"checkUserDisplayHistory":"History",
 "checkUserDisplayNormalizedHeaders":"Normalized headers",
 "checkUserDisplayPersistentInfo":"Persistent session data",
 "checkUserHiddenAttributes":"Thuộc tính ẩn",
@@ -349,7 +356,7 @@
 "facebookExportedVars":"Biến đã được xuất",
 "facebookParams":"Tham số Facebook",
 "facebookUserField":"Field containing user identifier",
-"failedLoginNumber":"Số lượt đăng nhập thất bại",
+"failedLoginNumber":"Max failed logins count",
 "fileToUpload":"Tập tin để tải lên",
 "findUser":"Activation",
 "findUserControl":"Parameters control",
@@ -567,6 +574,14 @@
 "newEntry":"Mục nhập mới",
 "newGrantRule":"Quy tắc cấp mới",
 "newHost":"Máy chủ mới",
+"newLocationWarning":"Activation",
+"newLocationWarningLocationAttribute":"Session attribute containing location",
+"newLocationWarningLocationDisplayAttribute":"Session attribute to display",
+"newLocationWarningMailAttribute":"Session mail attribute",
+"newLocationWarningMailBody":"Warning mail content",
+"newLocationWarningMailSubject":"Warning mail subject",
+"newLocationWarningMaxValues":"Maximum number of locations to consider",
+"newLocationWarnings":"New location warning",
 "newPost":"Phát lại mẫu mới",
 "newPostVar":"Biến mới",
 "newRSAKey":"Khóa mới",
@@ -647,13 +662,13 @@
 "oidcParams":"Các tham số kết nối OpenID",
 "oidcRP":"OpenID Connect Relying Party",
 "oidcRPCallbackGetParam":"Callback GET tham số",
-"oidcRPMetaDataExportedVars":"Biến đã được xuất",
+"oidcRPMetaDataExportedVars":"Exported attributes (claims)",
 "oidcRPMetaDataMacros":"Macros",
 "oidcRPMetaDataNode":"OpenID Connect Relying Parties",
 "oidcRPMetaDataNodes":"OpenID Connect Relying Parties",
 "oidcRPMetaDataOptions":"Tùy chọn",
 "oidcRPMetaDataOptionsAccessTokenClaims":"Release claims in Access Token",
-"oidcRPMetaDataOptionsAccessTokenExpiration":"Hết hạn truy cập Token",
+"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Tokens",
 "oidcRPMetaDataOptionsAccessTokenJWT":"Use JWT format for Access Token",
 "oidcRPMetaDataOptionsAccessTokenSignAlg":"Access Token signature algorithm",
 "oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
@@ -662,22 +677,22 @@
 "oidcRPMetaDataOptionsAllowOffline":"Allow offline access",
 "oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
 "oidcRPMetaDataOptionsAuthnLevel":"Mức xác thực",
-"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Code expiration",
+"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Codes",
 "oidcRPMetaDataOptionsBasic":"Basic",
 "oidcRPMetaDataOptionsBypassConsent":"Bỏ qua sự đồng ý",
 "oidcRPMetaDataOptionsClientID":"Client ID",
 "oidcRPMetaDataOptionsClientSecret":"Trình khách bí mật",
 "oidcRPMetaDataOptionsDisplay":"Hiển thị",
 "oidcRPMetaDataOptionsDisplayName":"Tên hiển thị",
-"oidcRPMetaDataOptionsExtraClaims":"Xác nhận bổ sung",
-"oidcRPMetaDataOptionsIDTokenExpiration":"ID Token hết hạn",
+"oidcRPMetaDataOptionsExtraClaims":"Scope values content",
+"oidcRPMetaDataOptionsIDTokenExpiration":"ID Tokens",
 "oidcRPMetaDataOptionsIDTokenForceClaims":"Force claims to be returned in ID Token",
 "oidcRPMetaDataOptionsIDTokenSignAlg":"Thuật toán chữ ký ID Token",
 "oidcRPMetaDataOptionsIcon":"Logo",
 "oidcRPMetaDataOptionsLogoutSessionRequired":"Phiên yêu cầu",
 "oidcRPMetaDataOptionsLogoutType":"Loại",
 "oidcRPMetaDataOptionsLogoutUrl":"URL",
-"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline session expiration",
+"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline sessions",
 "oidcRPMetaDataOptionsPostLogoutRedirectUris":"Allowed redirection addresses for logout",
 "oidcRPMetaDataOptionsPublic":"Public client",
 "oidcRPMetaDataOptionsRedirectUris":"Allowed redirection addresses for login",
@@ -690,17 +705,18 @@
 "oidcRPMetaDataScopeRules":"Scope rules",
 "oidcRPName":"OpenID Connect RP Name",
 "oidcRPStateTimeout":"Thời gian chờ của trạng thái phiên làm việc",
-"oidcServiceAccessTokenExpiration":"Hết hạn truy cập Token",
+"oidcServiceAccessTokenExpiration":"Access Token",
 "oidcServiceAllowAuthorizationCodeFlow":"Dòng mã ủy quyền",
-"oidcServiceAllowDynamicRegistration":"Đăng ký động",
+"oidcServiceAllowDynamicRegistration":"Activation",
 "oidcServiceAllowHybridFlow":"Dòng chảy hỗn hợp",
 "oidcServiceAllowImplicitFlow":"Dòng chảy ngầm",
 "oidcServiceAllowOffline":"Allow offline access",
 "oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
-"oidcServiceAuthorizationCodeExpiration":"Authorization Code expiration",
-"oidcServiceDynamicRegistrationExportedVars":"Exported vars for dynamic registration",
-"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",
-"oidcServiceIDTokenExpiration":"ID Token hết hạn",
+"oidcServiceAuthorizationCodeExpiration":"Authorization Code",
+"oidcServiceDynamicRegistration":"Dynamic registration",
+"oidcServiceDynamicRegistrationExportedVars":"Exported vars",
+"oidcServiceDynamicRegistrationExtraClaims":"Extra claims",
+"oidcServiceIDTokenExpiration":"ID Token",
 "oidcServiceKeyIdSig":"Khóa ID chính",
 "oidcServiceMetaData":"Dịch vụ Kết nối OpenID",
 "oidcServiceMetaDataAuthnContext":"Ngữ cảnh xác thực",
@@ -717,9 +733,10 @@
 "oidcServiceMetaDataRegistrationURI":"Đăng ký",
 "oidcServiceMetaDataSecurity":"Bảo mật",
 "oidcServiceMetaDataSessions":"Phiên",
-"oidcServiceMetaDataTokenURI":"Token",
+"oidcServiceMetaDataTimeouts":"Timeouts",
+"oidcServiceMetaDataTokenURI":"Tokens",
 "oidcServiceMetaDataUserInfoURI":"Thông tin người dùng",
-"oidcServiceOfflineSessionExpiration":"Offline session expiration",
+"oidcServiceOfflineSessionExpiration":"Offline session",
 "oidcServicePrivateKeySig":"Ký khóa cá nhân",
 "oidcServicePublicKeySig":"Ký khóa công khai",
 "oidcStorage":"Tên mô-đun phiên",
@@ -812,8 +829,13 @@
 "postedVars":"Các biến gửi lên",
 "previous":"Trước",
 "privateKey":"Khóa cá nhân",
-"proxyAuthService":"URL cổng nội bộ",
+"proxyAuthService":"URL",
+"proxyAuthServiceChoiceParam":"Choice parameter",
+"proxyAuthServiceChoiceValue":"Choice value",
+"proxyAuthServiceImpersonation":"Impersonation",
 "proxyAuthnLevel":"Mức xác thực",
+"proxyCookieName":"Tên cookie",
+"proxyInternalPortal":"Internal Portal",
 "proxyParams":"Các tham số proxy",
 "proxySessionService":"URL dịch vụ phiên",
 "proxyUseSoap":"Sử dụng SOAP thay vì REST",
@@ -861,11 +883,11 @@
 "rest2f":"REST second factor",
 "rest2fActivation":"Kích hoạt",
 "rest2fAuthnLevel":"Mức xác thực",
-"rest2fInitArgs":"Init Arguments",
+"rest2fInitArgs":"Init arguments",
 "rest2fInitUrl":"Init URL",
 "rest2fLabel":"Label",
 "rest2fLogo":"Logo",
-"rest2fVerifyArgs":"Verify Arguments",
+"rest2fVerifyArgs":"Verify arguments",
 "rest2fVerifyUrl":"Verify URL",
 "restAuthServer":"Authentication server",
 "restAuthUrl":"URL xác thực",
@@ -1089,12 +1111,13 @@
 "stateCheck":"State Check",
 "stayConnect":"Duy trì kết nối",
 "stayConnected":"Activation",
+"stayConnectedBypassFG":"Do not check fingerprint",
 "stayConnectedCookieName":"Tên cookie",
 "stayConnectedTimeout":"Expiration time",
 "storePassword":"Lưu trữ mật khẩu người dùng trong các dữ liệu phiên",
 "string":"String",
 "subtitle":"Subtitle",
-"successLoginNumber":"Số lượng đăng nhập đã đăng ký",
+"successLoginNumber":"Max successful logins count",
 "successfullySaved":"Lưu thành công",
 "sympaHandler":"Sympa",
 "sympaMailKey":"Khóa phiên qua thư",
@@ -1110,10 +1133,11 @@
 "tooltip":"Tooltip",
 "totp2f":"TOTP",
 "totp2fActivation":"Kích hoạt",
-"totp2fAuthnLevel":"TOTP authentication level",
+"totp2fAuthnLevel":"Mức xác thực",
 "totp2fDigits":"Number of digits",
+"totp2fEncryptSecret":"Encrypt TOTP secrets",
 "totp2fInterval":"Interval",
-"totp2fIssuer":"TOTP Issuer name",
+"totp2fIssuer":"Issuer name",
 "totp2fLabel":"Label",
 "totp2fLogo":"Logo",
 "totp2fRange":"Range of attempts",
@@ -1131,7 +1155,7 @@
 "type":"Loại",
 "u2f":"U2F",
 "u2fActivation":"Kích hoạt",
-"u2fAuthnLevel":"Mức xác thực U2F",
+"u2fAuthnLevel":"Mức xác thực",
 "u2fLabel":"Label",
 "u2fLogo":"Logo",
 "u2fSelfRegistration":"Tự đăng ký ",
@@ -1172,6 +1196,7 @@
 "vhostAccessToTrace":"Access to trace",
 "vhostAliases":"Bí danh",
 "vhostAuthnLevel":"Mức xác thực bắt buộc",
+"vhostDevOpsRulesUrl":"DevOps rules file URL",
 "vhostHttps":"HTTPS",
 "vhostMaintenance":"Chế độ bảo trì",
 "vhostOptions":"Tùy chọn",
@@ -1190,6 +1215,16 @@
 "webIDAuthnLevel":"Mức xác thực",
 "webIDExportedVars":"Xuất khẩu biến",
 "webIDWhitelist":"WebID white list",
+"webauthn2f":"WebAuthn",
+"webauthn2fActivation":"Kích hoạt",
+"webauthn2fAuthnLevel":"Mức xác thực",
+"webauthn2fLabel":"Label",
+"webauthn2fLogo":"Logo",
+"webauthn2fSelfRegistration":"Tự đăng ký",
+"webauthn2fUserCanRemoveKey":"Allow user to remove WebAuthn",
+"webauthn2fUserVerification":"User verification",
+"webauthnDisplayNameAttr":"User Display Name attribute",
+"webauthnRpName":"Relying Party display name",
 "webidParams":"Tham số WebID",
 "whatToTrace":"REMOTE_USER",
 "whiteList":"Danh sách trắng",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/zh.json 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/zh.json
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/zh.json	2021-08-21 17:42:59.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/zh.json	2022-02-19 16:04:21.000000000 +0000
@@ -126,7 +126,9 @@
 "bruteForceProtection":"激活",
 "bruteForceProtectionIncrementalTempo":"Incremental lock",
 "bruteForceProtectionLockTimes":"Incremental lock times",
+"bruteForceProtectionMaxAge":"最大時間",
 "bruteForceProtectionMaxFailed":"Allowed failed logins",
+"bruteForceProtectionMaxLockTime":"Maximum lock time",
 "bruteForceProtectionTempo":"Lock time",
 "cancel":"取消",
 "captcha_login_enabled":" 登录激活",
@@ -165,6 +167,7 @@
 "casStorage":"CAS 会话模块名称",
 "casStorageOptions":"CAS 会话模块选项",
 "casStrictMatching":"Use strict URL matching",
+"casTicketExpiration":"Temporary ticket lifetime",
 "categoryName":"分类名称",
 "cda":"Multiple domains",
 "certificateMailContent":"Mail content",
@@ -179,7 +182,9 @@
 "certificateResetByMailValidityDelay":"Minimum duration before expiration",
 "cfgLog":"Summary",
 "cfgVersion":"配置信息",
-"checkDevOps":"Activation",
+"checkDevOps":"激活",
+"checkDevOpsCheckSessionAttributes":"Check session attributes",
+"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
 "checkDevOpsDownload":"Download file",
 "checkState":"激活",
 "checkStateSecret":"Shared secret",
@@ -188,6 +193,8 @@
 "checkUserDisplayComputedSession":"Computed sessions",
 "checkUserDisplayEmptyHeaders":"Empty headers",
 "checkUserDisplayEmptyValues":"Empty values",
+"checkUserDisplayHiddenAttributes":"Hidden attributes",
+"checkUserDisplayHistory":"History",
 "checkUserDisplayNormalizedHeaders":"Normalized headers",
 "checkUserDisplayPersistentInfo":"Persistent session data",
 "checkUserHiddenAttributes":"Hidden attributes",
@@ -239,7 +246,7 @@
 "corsMax_Age":"Access-Control-Max-Age",
 "create":"创建",
 "crossOrigineResourceSharing":"Cross-Origin Resource Sharing",
-"crowdsec":"Activation",
+"crowdsec":"激活",
 "crowdsecAction":"Action",
 "crowdsecKey":"API key",
 "crowdsecUrl":"Base URL of local API",
@@ -281,7 +288,7 @@
 "dbiConnectionAuth":"Authentication process",
 "dbiConnectionUser":"User process",
 "dbiDynamicHash":"dynamic hashing",
-"dbiDynamicHashEnabled":"dynamic hash activation",
+"dbiDynamicHashEnabled":"動態雜湊值啟用",
 "dbiDynamicHashNewPasswordScheme":"Dynamic hash scheme for new passwords",
 "dbiDynamicHashValidSaltedSchemes":"Supported salted schemes",
 "dbiDynamicHashValidSchemes":"Supported non-salted schemes",
@@ -318,8 +325,8 @@
 "domain":"域",
 "done":"完成",
 "dones":"完成",
-"doubleCookie":"Double cookie (HTTP and HTTPS)",
-"doubleCookieForSingleSession":"Double cookie for a single session",
+"doubleCookie":"雙 cookie（HTTP 與 HTTPS）",
+"doubleCookieForSingleSession":"單次工作階段使用雙 cookie",
 "down":"Move down",
 "download":"下载",
 "downloadIt":"下载它",
@@ -349,9 +356,9 @@
 "facebookExportedVars":"Exported variables",
 "facebookParams":"Facebook 参数",
 "facebookUserField":"Field containing user identifier",
-"failedLoginNumber":"Number of registered failed logins",
+"failedLoginNumber":"Max failed logins count",
 "fileToUpload":"上传的文件",
-"findUser":"Activation",
+"findUser":"激活",
 "findUserControl":"Parameters control",
 "findUserExcludingAttributes":"Excluding attributes",
 "findUserSearchingAttributes":"Searching attributes",
@@ -567,6 +574,14 @@
 "newEntry":"New entry",
 "newGrantRule":"New grant rule",
 "newHost":"New host",
+"newLocationWarning":"激活",
+"newLocationWarningLocationAttribute":"Session attribute containing location",
+"newLocationWarningLocationDisplayAttribute":"Session attribute to display",
+"newLocationWarningMailAttribute":"Session mail attribute",
+"newLocationWarningMailBody":"Warning mail content",
+"newLocationWarningMailSubject":"Warning mail subject",
+"newLocationWarningMaxValues":"Maximum number of locations to consider",
+"newLocationWarnings":"New location warning",
 "newPost":"New form replay",
 "newPostVar":"New variable",
 "newRSAKey":"New keys",
@@ -647,13 +662,13 @@
 "oidcParams":"OpenID Connect parameters",
 "oidcRP":"OpenID Connect Relying Party",
 "oidcRPCallbackGetParam":"Callback GET parameter",
-"oidcRPMetaDataExportedVars":"Exported attributes",
+"oidcRPMetaDataExportedVars":"Exported attributes (claims)",
 "oidcRPMetaDataMacros":"Macros",
 "oidcRPMetaDataNode":"OpenID Connect Relying Parties",
 "oidcRPMetaDataNodes":"OpenID Connect Relying Parties",
 "oidcRPMetaDataOptions":"Options",
 "oidcRPMetaDataOptionsAccessTokenClaims":"Release claims in Access Token",
-"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Token expiration",
+"oidcRPMetaDataOptionsAccessTokenExpiration":"Access Tokens",
 "oidcRPMetaDataOptionsAccessTokenJWT":"Use JWT format for Access Token",
 "oidcRPMetaDataOptionsAccessTokenSignAlg":"Access Token signature algorithm",
 "oidcRPMetaDataOptionsAdditionalAudiences":"Additional audiences",
@@ -662,22 +677,22 @@
 "oidcRPMetaDataOptionsAllowOffline":"Allow offline access",
 "oidcRPMetaDataOptionsAllowPasswordGrant":"Allow OAuth2.0 Password Grant",
 "oidcRPMetaDataOptionsAuthnLevel":"认证级别",
-"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Code expiration",
+"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Codes",
 "oidcRPMetaDataOptionsBasic":"Basic",
 "oidcRPMetaDataOptionsBypassConsent":"Bypass consent",
 "oidcRPMetaDataOptionsClientID":"Client ID",
 "oidcRPMetaDataOptionsClientSecret":"Client secret",
 "oidcRPMetaDataOptionsDisplay":"Display",
 "oidcRPMetaDataOptionsDisplayName":"Display name",
-"oidcRPMetaDataOptionsExtraClaims":"Extra claims",
-"oidcRPMetaDataOptionsIDTokenExpiration":"ID Token expiration",
+"oidcRPMetaDataOptionsExtraClaims":"Scope values content",
+"oidcRPMetaDataOptionsIDTokenExpiration":"ID Tokens",
 "oidcRPMetaDataOptionsIDTokenForceClaims":"Force claims to be returned in ID Token",
 "oidcRPMetaDataOptionsIDTokenSignAlg":"ID Token signature algorithm",
 "oidcRPMetaDataOptionsIcon":"Logo",
 "oidcRPMetaDataOptionsLogoutSessionRequired":"Session required",
 "oidcRPMetaDataOptionsLogoutType":"Type",
 "oidcRPMetaDataOptionsLogoutUrl":"URL",
-"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline session expiration",
+"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline sessions",
 "oidcRPMetaDataOptionsPostLogoutRedirectUris":"Allowed redirection addresses for logout",
 "oidcRPMetaDataOptionsPublic":"Public client",
 "oidcRPMetaDataOptionsRedirectUris":"Allowed redirection addresses for login",
@@ -690,24 +705,25 @@
 "oidcRPMetaDataScopeRules":"Scope rules",
 "oidcRPName":"OpenID Connect RP Name",
 "oidcRPStateTimeout":"State session timeout",
-"oidcServiceAccessTokenExpiration":"Access Token expiration",
+"oidcServiceAccessTokenExpiration":"Access Token",
 "oidcServiceAllowAuthorizationCodeFlow":"Authorization Code Flow",
-"oidcServiceAllowDynamicRegistration":"Dynamic Registration",
+"oidcServiceAllowDynamicRegistration":"Activation",
 "oidcServiceAllowHybridFlow":"Hybrid Flow",
 "oidcServiceAllowImplicitFlow":"Implicit Flow",
 "oidcServiceAllowOffline":"Allow offline access",
 "oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
-"oidcServiceAuthorizationCodeExpiration":"Authorization Code expiration",
-"oidcServiceDynamicRegistrationExportedVars":"Exported vars for dynamic registration",
-"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",
-"oidcServiceIDTokenExpiration":"ID Token expiration",
+"oidcServiceAuthorizationCodeExpiration":"Authorization Code",
+"oidcServiceDynamicRegistration":"Dynamic registration",
+"oidcServiceDynamicRegistrationExportedVars":"Exported vars",
+"oidcServiceDynamicRegistrationExtraClaims":"Extra claims",
+"oidcServiceIDTokenExpiration":"ID Token",
 "oidcServiceKeyIdSig":"Signing key ID",
 "oidcServiceMetaData":"OpenID Connect Service",
 "oidcServiceMetaDataAuthnContext":"Authentication context",
 "oidcServiceMetaDataAuthorizeURI":"Authorization",
 "oidcServiceMetaDataBackChannelURI":"Back-Channel URI",
 "oidcServiceMetaDataCheckSessionURI":"Check Session",
-"oidcServiceMetaDataEndPoints":"End points",
+"oidcServiceMetaDataEndPoints":"Endpoints",
 "oidcServiceMetaDataEndSessionURI":"End of session",
 "oidcServiceMetaDataFrontChannelURI":"Front-Channel URI",
 "oidcServiceMetaDataIntrospectionURI":"Introspection",
@@ -717,9 +733,10 @@
 "oidcServiceMetaDataRegistrationURI":"Registration",
 "oidcServiceMetaDataSecurity":"Security",
 "oidcServiceMetaDataSessions":"Sessions",
+"oidcServiceMetaDataTimeouts":"Timeouts",
 "oidcServiceMetaDataTokenURI":"令牌",
 "oidcServiceMetaDataUserInfoURI":"用户信息",
-"oidcServiceOfflineSessionExpiration":"Offline session expiration",
+"oidcServiceOfflineSessionExpiration":"Offline session",
 "oidcServicePrivateKeySig":"Signing private key",
 "oidcServicePublicKeySig":"Signing public key",
 "oidcStorage":"Sessions module name",
@@ -793,7 +810,7 @@
 "portalForceAuthnInterval":"Force authentication interval",
 "portalMainLogo":"Main logo",
 "portalMenu":"Menu",
-"portalModules":"Modules activation",
+"portalModules":"模組啟用",
 "portalOpenLinkInNewWindow":"New window",
 "portalOther":"Other",
 "portalParams":"Portal",
@@ -812,8 +829,13 @@
 "postedVars":"Variables to post",
 "previous":"Previous",
 "privateKey":"Private key",
-"proxyAuthService":"Internal portal URL",
+"proxyAuthService":"URL",
+"proxyAuthServiceChoiceParam":"Choice parameter",
+"proxyAuthServiceChoiceValue":"Choice value",
+"proxyAuthServiceImpersonation":"模擬",
 "proxyAuthnLevel":"认证等级",
+"proxyCookieName":"Cookie 名称",
+"proxyInternalPortal":"Internal Portal",
 "proxyParams":"Proxy parameters",
 "proxySessionService":"Session service URL",
 "proxyUseSoap":"Use SOAP instead of REST",
@@ -861,11 +883,11 @@
 "rest2f":"REST second factor",
 "rest2fActivation":"激活",
 "rest2fAuthnLevel":"认证等级",
-"rest2fInitArgs":"Init Arguments",
+"rest2fInitArgs":"Init arguments",
 "rest2fInitUrl":"Init URL",
 "rest2fLabel":"Label",
 "rest2fLogo":"Logo",
-"rest2fVerifyArgs":"Verify Arguments",
+"rest2fVerifyArgs":"Verify arguments",
 "rest2fVerifyUrl":"Verify URL",
 "restAuthServer":"Authentication server",
 "restAuthUrl":"Authentication URL",
@@ -889,7 +911,7 @@
 "ruleAuthnLevel":"Required authentication level",
 "rules":"Rules",
 "rulesAuthnLevel":"Required auth levels",
-"sameSite":"Cookie SameSite value",
+"sameSite":"Cookie SameSite 值",
 "saml":"SAML",
 "samlAdvanced":"高级",
 "samlAttribute":"SAML attribute",
@@ -901,7 +923,7 @@
 "samlAuthnContextMapPassword":"密码",
 "samlAuthnContextMapPasswordProtectedTransport":"Password protected transport",
 "samlAuthnContextMapTLSClient":"TLS client",
-"samlCommonDomainCookie":"Common Domain Cookie",
+"samlCommonDomainCookie":"通用網域 Cookie",
 "samlCommonDomainCookieActivation":"激活",
 "samlCommonDomainCookieDomain":"Common domain",
 "samlCommonDomainCookieReader":"Reader URL",
@@ -1033,7 +1055,7 @@
 "scope":"Scope",
 "search":"Search...",
 "secondFactors":"Second factors",
-"securedCookie":"Secured Cookie (SSL)",
+"securedCookie":"安全 Cookie (SSL)",
 "security":"Security",
 "sendTestMail":"Send test email",
 "sendTestMailSuccess":"Test email successfully sent",
@@ -1088,13 +1110,14 @@
 "ssoSessions":"SSO sessions",
 "stateCheck":"State Check",
 "stayConnect":"Persistent connections",
-"stayConnected":"Activation",
+"stayConnected":"激活",
+"stayConnectedBypassFG":"Do not check fingerprint",
 "stayConnectedCookieName":"Cookie 名称",
 "stayConnectedTimeout":"Expiration time",
 "storePassword":"Store user password in session",
 "string":"String",
 "subtitle":"Subtitle",
-"successLoginNumber":"Number of registered logins",
+"successLoginNumber":"Max successful logins count",
 "successfullySaved":"Successfully saved",
 "sympaHandler":"Sympa",
 "sympaMailKey":"Mail session key",
@@ -1110,10 +1133,11 @@
 "tooltip":"Tooltip",
 "totp2f":"TOTP",
 "totp2fActivation":"激活",
-"totp2fAuthnLevel":"TOTP authentication level",
+"totp2fAuthnLevel":"认证级别",
 "totp2fDigits":"Number of digits",
+"totp2fEncryptSecret":"Encrypt TOTP secrets",
 "totp2fInterval":"Interval",
-"totp2fIssuer":"TOTP Issuer name",
+"totp2fIssuer":"Issuer name",
 "totp2fLabel":"Label",
 "totp2fLogo":"Logo",
 "totp2fRange":"Range of attempts",
@@ -1131,7 +1155,7 @@
 "type":"Type",
 "u2f":"U2F",
 "u2fActivation":"激活",
-"u2fAuthnLevel":"U2F authentication level",
+"u2fAuthnLevel":"认证级别",
 "u2fLabel":"Label",
 "u2fLogo":"Logo",
 "u2fSelfRegistration":"Self registration",
@@ -1141,7 +1165,7 @@
 "unknownAttrOrMacro":"Unknown attribute or macro",
 "unknownError":"Unknown error",
 "unknownKey":"Unknown key",
-"unsecuredCookie":"Unsecured cookie",
+"unsecuredCookie":"不安全的 cookie",
 "up":"Move up",
 "upgradeSession":"Session upgrade",
 "uploadDenied":"Upload denied",
@@ -1172,6 +1196,7 @@
 "vhostAccessToTrace":"Access to trace",
 "vhostAliases":"Aliases",
 "vhostAuthnLevel":"Required authentication level",
+"vhostDevOpsRulesUrl":"DevOps rules file URL",
 "vhostHttps":"HTTPS",
 "vhostMaintenance":"Maintenance mode",
 "vhostOptions":"Options",
@@ -1190,6 +1215,16 @@
 "webIDAuthnLevel":"认证等级",
 "webIDExportedVars":"Exported variables",
 "webIDWhitelist":"WebID whitelist",
+"webauthn2f":"WebAuthn",
+"webauthn2fActivation":"激活",
+"webauthn2fAuthnLevel":"认证级别",
+"webauthn2fLabel":"Label",
+"webauthn2fLogo":"Logo",
+"webauthn2fSelfRegistration":"Self registration",
+"webauthn2fUserCanRemoveKey":"Allow user to remove WebAuthn",
+"webauthn2fUserVerification":"User verification",
+"webauthnDisplayNameAttr":"User Display Name attribute",
+"webauthnRpName":"Relying Party display name",
 "webidParams":"WebID parameters",
 "whatToTrace":"REMOTE_USER",
 "whiteList":"White list",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/zh_TW.json 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/zh_TW.json
--- 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/languages/zh_TW.json	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/languages/zh_TW.json	2022-02-19 16:04:21.000000000 +0000
@@ -126,7 +126,9 @@
 "bruteForceProtection":"啟用",
 "bruteForceProtectionIncrementalTempo":"增量鎖",
 "bruteForceProtectionLockTimes":"增量鎖時間",
+"bruteForceProtectionMaxAge":"最大時間",
 "bruteForceProtectionMaxFailed":"允許的失敗登入",
+"bruteForceProtectionMaxLockTime":"Maximum lock time",
 "bruteForceProtectionTempo":"鎖時間",
 "cancel":"取消",
 "captcha_login_enabled":"在登入表單中啟用",
@@ -165,6 +167,7 @@
 "casStorage":"CAS 工作階段模組名稱",
 "casStorageOptions":"CAS 工作階段模組選項",
 "casStrictMatching":"Use strict URL matching",
+"casTicketExpiration":"Temporary ticket lifetime",
 "categoryName":"分類名稱",
 "cda":"多域名",
 "certificateMailContent":"郵件內容",
@@ -180,6 +183,8 @@
 "cfgLog":"概要",
 "cfgVersion":"設定版本",
 "checkDevOps":"啟用",
+"checkDevOpsCheckSessionAttributes":"Check session attributes",
+"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
 "checkDevOpsDownload":"Download file",
 "checkState":"啟用",
 "checkStateSecret":"已分享的祕密",
@@ -188,6 +193,8 @@
 "checkUserDisplayComputedSession":"Computed sessions",
 "checkUserDisplayEmptyHeaders":"Empty headers",
 "checkUserDisplayEmptyValues":"Empty values",
+"checkUserDisplayHiddenAttributes":"隱藏屬性",
+"checkUserDisplayHistory":"History",
 "checkUserDisplayNormalizedHeaders":"Normalized headers",
 "checkUserDisplayPersistentInfo":"Persistent session data",
 "checkUserHiddenAttributes":"隱藏屬性",
@@ -349,7 +356,7 @@
 "facebookExportedVars":"已匯出的變數",
 "facebookParams":"Facebook 參數",
 "facebookUserField":"包含使用者識別符號的欄位",
-"failedLoginNumber":"已註冊的失敗登入數",
+"failedLoginNumber":"Max failed logins count",
 "fileToUpload":"上傳失敗",
 "findUser":"啟用",
 "findUserControl":"Parameters control",
@@ -567,6 +574,14 @@
 "newEntry":"新項目",
 "newGrantRule":"新授權規則",
 "newHost":"新主機",
+"newLocationWarning":"啟用",
+"newLocationWarningLocationAttribute":"Session attribute containing location",
+"newLocationWarningLocationDisplayAttribute":"Session attribute to display",
+"newLocationWarningMailAttribute":"Session mail attribute",
+"newLocationWarningMailBody":"Warning mail content",
+"newLocationWarningMailSubject":"Warning mail subject",
+"newLocationWarningMaxValues":"Maximum number of locations to consider",
+"newLocationWarnings":"New location warning",
 "newPost":"新表單重新進行",
 "newPostVar":"新變數",
 "newRSAKey":"新金鑰",
@@ -647,13 +662,13 @@
 "oidcParams":"OpenID 連線參數",
 "oidcRP":"OpenID 連線提供方",
 "oidcRPCallbackGetParam":"Callback GET 參數",
-"oidcRPMetaDataExportedVars":"已匯出屬性",
+"oidcRPMetaDataExportedVars":"Exported attributes (claims)",
 "oidcRPMetaDataMacros":"巨集",
 "oidcRPMetaDataNode":"OpenID 連線提供方",
 "oidcRPMetaDataNodes":"OpenID 連線提供方",
 "oidcRPMetaDataOptions":"選項",
 "oidcRPMetaDataOptionsAccessTokenClaims":"Release claims in Access Token",
-"oidcRPMetaDataOptionsAccessTokenExpiration":"存取權杖到期",
+"oidcRPMetaDataOptionsAccessTokenExpiration":"存取權杖",
 "oidcRPMetaDataOptionsAccessTokenJWT":"Use JWT format for Access Token",
 "oidcRPMetaDataOptionsAccessTokenSignAlg":"Access Token signature algorithm",
 "oidcRPMetaDataOptionsAdditionalAudiences":"額外聽眾",
@@ -662,22 +677,22 @@
 "oidcRPMetaDataOptionsAllowOffline":"允許離線存取",
 "oidcRPMetaDataOptionsAllowPasswordGrant":"允許 OAuth2.0 密碼授權",
 "oidcRPMetaDataOptionsAuthnLevel":"驗證等級",
-"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"授權碼到期",
+"oidcRPMetaDataOptionsAuthorizationCodeExpiration":"Authorization Codes",
 "oidcRPMetaDataOptionsBasic":"基本",
 "oidcRPMetaDataOptionsBypassConsent":"繞過同意",
 "oidcRPMetaDataOptionsClientID":"客戶端 ID",
 "oidcRPMetaDataOptionsClientSecret":"客戶端祕密",
 "oidcRPMetaDataOptionsDisplay":"顯示",
 "oidcRPMetaDataOptionsDisplayName":"顯示名稱",
-"oidcRPMetaDataOptionsExtraClaims":"額外的聲明",
-"oidcRPMetaDataOptionsIDTokenExpiration":"ID 權杖到期",
+"oidcRPMetaDataOptionsExtraClaims":"Scope values content",
+"oidcRPMetaDataOptionsIDTokenExpiration":"ID Token",
 "oidcRPMetaDataOptionsIDTokenForceClaims":"強制要求以 ID 權杖回傳",
 "oidcRPMetaDataOptionsIDTokenSignAlg":"ID 權杖簽章演算法",
 "oidcRPMetaDataOptionsIcon":"圖示",
 "oidcRPMetaDataOptionsLogoutSessionRequired":"需要工作階段",
 "oidcRPMetaDataOptionsLogoutType":"類型",
 "oidcRPMetaDataOptionsLogoutUrl":"URL",
-"oidcRPMetaDataOptionsOfflineSessionExpiration":"離線工作階段到期",
+"oidcRPMetaDataOptionsOfflineSessionExpiration":"Offline session",
 "oidcRPMetaDataOptionsPostLogoutRedirectUris":"允許登出的重新導向地址",
 "oidcRPMetaDataOptionsPublic":"公開客戶端",
 "oidcRPMetaDataOptionsRedirectUris":"允許登入的重新導向地址",
@@ -690,17 +705,18 @@
 "oidcRPMetaDataScopeRules":"Scope rules",
 "oidcRPName":"OpenID 連線 RP 名稱",
 "oidcRPStateTimeout":"狀態工作階段逾時",
-"oidcServiceAccessTokenExpiration":"存取權杖到期",
+"oidcServiceAccessTokenExpiration":"存取權杖",
 "oidcServiceAllowAuthorizationCodeFlow":"授權碼流程",
-"oidcServiceAllowDynamicRegistration":"動態註冊",
+"oidcServiceAllowDynamicRegistration":"Activation",
 "oidcServiceAllowHybridFlow":"混合流程",
 "oidcServiceAllowImplicitFlow":"內含流程",
 "oidcServiceAllowOffline":"允許離線存取",
 "oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
-"oidcServiceAuthorizationCodeExpiration":"授權碼到期",
-"oidcServiceDynamicRegistrationExportedVars":"用於動態註冊的已匯出變數",
-"oidcServiceDynamicRegistrationExtraClaims":"動態註冊的額外聲明",
-"oidcServiceIDTokenExpiration":"ID 權杖到期",
+"oidcServiceAuthorizationCodeExpiration":"Authorization Code",
+"oidcServiceDynamicRegistration":"Dynamic registration",
+"oidcServiceDynamicRegistrationExportedVars":"Exported vars",
+"oidcServiceDynamicRegistrationExtraClaims":"Extra claims",
+"oidcServiceIDTokenExpiration":"ID 權杖",
 "oidcServiceKeyIdSig":"簽署金鑰 ID",
 "oidcServiceMetaData":"OpenID 連線服務",
 "oidcServiceMetaDataAuthnContext":"驗證內容",
@@ -717,9 +733,10 @@
 "oidcServiceMetaDataRegistrationURI":"註冊",
 "oidcServiceMetaDataSecurity":"安全",
 "oidcServiceMetaDataSessions":"工作階段",
+"oidcServiceMetaDataTimeouts":"逾時",
 "oidcServiceMetaDataTokenURI":"權杖",
 "oidcServiceMetaDataUserInfoURI":"使用者資訊",
-"oidcServiceOfflineSessionExpiration":"離線工作階段到期",
+"oidcServiceOfflineSessionExpiration":"Offline session",
 "oidcServicePrivateKeySig":"簽署私鑰",
 "oidcServicePublicKeySig":"簽署公鑰",
 "oidcStorage":"工作階段模組名稱",
@@ -812,8 +829,13 @@
 "postedVars":"要發佈的變數",
 "previous":"前一個",
 "privateKey":"私鑰",
-"proxyAuthService":"內部首頁 URL",
+"proxyAuthService":"URL",
+"proxyAuthServiceChoiceParam":"Choice parameter",
+"proxyAuthServiceChoiceValue":"Choice value",
+"proxyAuthServiceImpersonation":"模擬",
 "proxyAuthnLevel":"驗證等級",
+"proxyCookieName":"Cookie 名稱",
+"proxyInternalPortal":"Internal Portal",
 "proxyParams":"代理伺服器參數",
 "proxySessionService":"工作階段服務 URL",
 "proxyUseSoap":"使用 SOAP 而非 REST",
@@ -1089,12 +1111,13 @@
 "stateCheck":"狀態檢查",
 "stayConnect":"持久連線",
 "stayConnected":"啟用",
+"stayConnectedBypassFG":"Do not check fingerprint",
 "stayConnectedCookieName":"Cookie 名稱",
 "stayConnectedTimeout":"過期名稱",
 "storePassword":"在工作階段中儲存使用者密碼",
 "string":"字串",
 "subtitle":"副標題",
-"successLoginNumber":"已註冊的登入數",
+"successLoginNumber":"Max successful logins count",
 "successfullySaved":"成功儲存",
 "sympaHandler":"Sympa",
 "sympaMailKey":"郵件工作階段金鑰",
@@ -1110,10 +1133,11 @@
 "tooltip":"工具提示",
 "totp2f":"TOTP",
 "totp2fActivation":"啟用",
-"totp2fAuthnLevel":"TOTP 驗證等級",
+"totp2fAuthnLevel":"驗證等級",
 "totp2fDigits":"位數",
+"totp2fEncryptSecret":"Encrypt TOTP secrets",
 "totp2fInterval":"間隔",
-"totp2fIssuer":"TOTP 發行者名稱",
+"totp2fIssuer":"Issuer name",
 "totp2fLabel":"標籤",
 "totp2fLogo":"圖示",
 "totp2fRange":"嘗試範圍",
@@ -1131,7 +1155,7 @@
 "type":"類型",
 "u2f":"U2F",
 "u2fActivation":"啟用",
-"u2fAuthnLevel":"U2F 驗證等級",
+"u2fAuthnLevel":"驗證等級",
 "u2fLabel":"標籤",
 "u2fLogo":"圖示",
 "u2fSelfRegistration":"自行註冊",
@@ -1172,6 +1196,7 @@
 "vhostAccessToTrace":"存取追蹤",
 "vhostAliases":"別名",
 "vhostAuthnLevel":"需要的驗證等級",
+"vhostDevOpsRulesUrl":"DevOps rules file URL",
 "vhostHttps":"HTTPS",
 "vhostMaintenance":"維護模式",
 "vhostOptions":"選項",
@@ -1190,6 +1215,16 @@
 "webIDAuthnLevel":"驗證等級",
 "webIDExportedVars":"已匯出的變數",
 "webIDWhitelist":"WebID 白名單",
+"webauthn2f":"WebAuthn",
+"webauthn2fActivation":"啟用",
+"webauthn2fAuthnLevel":"驗證等級",
+"webauthn2fLabel":"標籤",
+"webauthn2fLogo":"圖示",
+"webauthn2fSelfRegistration":"自行註冊",
+"webauthn2fUserCanRemoveKey":"允許使用者移除 WebAuthn",
+"webauthn2fUserVerification":"User verification",
+"webauthnDisplayNameAttr":"User Display Name attribute",
+"webauthnRpName":"Relying Party display name",
 "webidParams":"WebID 參數",
 "whatToTrace":"REMOTE_USER",
 "whiteList":"白名單",
Binary files 2.0.13+ds-3/lemonldap-ng-manager/site/htdocs/static/logos/pt_BR.png and 2.0.14+ds-1/lemonldap-ng-manager/site/htdocs/static/logos/pt_BR.png differ
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/templates/2ndfa.tpl 2.0.14+ds-1/lemonldap-ng-manager/site/templates/2ndfa.tpl
--- 2.0.13+ds-3/lemonldap-ng-manager/site/templates/2ndfa.tpl	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/templates/2ndfa.tpl	2022-02-19 16:04:21.000000000 +0000
@@ -22,13 +22,16 @@
                 &nbsp;&nbsp;&&nbsp;&nbsp;
                 <input type="checkbox" ng-model="UBKCheck" class="form-check-input" ng-true-value="2" ng-false-value="1" ng-change="search2FA()"/>
                 <label class="form-check-label" for="UBKCheck">UBK</label>
+                &nbsp;&nbsp;&&nbsp;&nbsp;
+                <input type="checkbox" ng-model="WebAuthnCheck" class="form-check-input" ng-true-value="2" ng-false-value="1" ng-change="search2FA()"/>
+                <label class="form-check-label" for="WebAuthnCheck">WebAuthn</label>
               </div>
             </form>
           </ul>
 		      <div class="col-lg-6 col-md-6 col-sm-8 col-xs-14" >
 		        <form class="navbar-form" role="search">
               <div class="input-group add-on">
-                <input class="form-control" placeholder="{{translate('search')}}" type="text" ng-model="searchString" ng-init="" ng-keyup="search2FA()"/>
+                <input class="form-control" trplaceholder="search" type="text" ng-model="searchString" ng-init="" ng-keyup="search2FA()"/>
                 <div class="input-group-btn">
                 <button class="btn btn-default" ng-click="search2FA(1)"><i class="glyphicon glyphicon-search"></i></button>
                 </div>
@@ -104,16 +107,16 @@
       </table>
     </div>
     <div ng-if="!node.nodes" >
-	  <th class="col-md-3" ng-if="node.title!='UBK' && node.title!='TOTP' && node.title!='U2F'">{{translate(node.title)}}</th>
-      <td class="data-{{node.epoch}}" ng-if="node.title=='TOTP' || node.title=='UBK' || node.title=='U2F'" >{{node.title}}</td>
+	  <th class="col-md-3" ng-if="node.title!='UBK' && node.title!='TOTP' && node.title!='U2F' && node.title!='WebAuthn'">{{translate(node.title)}}</th>
+      <td class="data-{{node.epoch}}" ng-if="node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' || node.title=='WebAuthn'" >{{node.title}}</td>
 	  <th class="col-md-3" ng-if="node.title=='type'">{{translate(node.value)}}</th>
 	  <td class="col-md-3 data-{{node.epoch}}" ng-if="node.title!='type'" >{{node.value}}</td>
 	  <th class="col-md-3" ng-if="node.title=='type'">{{translate(node.epoch)}}</th>
-	  <td class="col-md-3 data-{{node.epoch}}" ng-if="node.title=='TOTP' || node.title=='UBK' || node.title=='U2F'">{{localeDate(node.epoch)}}</td>
+	  <td class="col-md-3 data-{{node.epoch}}" ng-if="node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' || node.title=='WebAuthn'">{{localeDate(node.epoch)}}</td>
       <td class="data-{{node.epoch}}">
-	  <span ng-if="node.title=='TOTP' || node.title=='UBK' || node.title=='U2F'" class="link text-danger glyphicon glyphicon-minus-sign" ng-click="delete2FA(node.title, node.epoch)"></span>
+	  <span ng-if="node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' || node.title=='WebAuthn'" class="link text-danger glyphicon glyphicon-minus-sign" ng-click="delete2FA(node.title, node.epoch)"></span>
 		  <!--
-		  <span ng-if="$last && ( node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' )" class="link text-success glyphicon glyphicon-plus-sign" ng-click="menuClick({title:'newRule'})"></span>
+		  <span ng-if="$last && ( node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' || node.title=='WebAuthn' )" class="link text-success glyphicon glyphicon-plus-sign" ng-click="menuClick({title:'newRule'})"></span>
 		  -->
       </td>
     </div>
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/site/templates/sessions.tpl 2.0.14+ds-1/lemonldap-ng-manager/site/templates/sessions.tpl
--- 2.0.13+ds-3/lemonldap-ng-manager/site/templates/sessions.tpl	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/site/templates/sessions.tpl	2022-02-19 16:04:21.000000000 +0000
@@ -108,7 +108,7 @@
        <td class="data-{{node.epoch}}">
 	      <span ng-if="node.td=='2'" class="link text-danger glyphicon glyphicon-minus-sign" ng-click="deleteOIDCConsent(node.title, node.epoch)"></span>
 		  <!--
-		  <span ng-if="$last && ( node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' )" class="link text-success glyphicon glyphicon-plus-sign" ng-click="menuClick({title:'newRule'})"></span>
+		  <span ng-if="$last && ( node.title=='TOTP' || node.title=='UBK' || node.title=='U2F' || node.title=='WebAuthn' )" class="link text-success glyphicon glyphicon-plus-sign" ng-click="menuClick({title:'newRule'})"></span>
 		  -->
        </td>
     </div>
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/04-2F-api.t 2.0.14+ds-1/lemonldap-ng-manager/t/04-2F-api.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/04-2F-api.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/04-2F-api.t	2022-02-19 16:04:21.000000000 +0000
@@ -20,9 +20,9 @@ sub newSession {
         $tmp = Lemonldap::NG::Common::Session->new( {
                 storageModule        => 'Apache::Session::File',
                 storageModuleOptions => {
-                    Directory     => 't/sessions',
-                    LockDirectory => 't/sessions',
-                    backend       => 'Apache::Session::File',
+                    Directory      => 't/sessions',
+                    LockDirectory  => 't/sessions',
+                    backend        => 'Apache::Session::File',
                     generateModule =>
 'Lemonldap::NG::Common::Apache::Session::Generate::SHA256',
                 },
@@ -241,7 +241,15 @@ $sfaDevices = [ {
         "type"    => "UBK",
         "_secret" => "123456",
         "epoch"   => time
-    }
+    },
+    {
+        "_credentialId"        => "abc",
+        "_credentialPublicKey" => "abc",
+        "_signCount"           => "65",
+        "epoch"                => "1643201784",
+        "name"                 => "MyFidoKey",
+        "type"                 => "WebAuthn"
+    },
 ];
 newSession( 'dwho', '127.10.0.1', 'SSO',        $sfaDevices );
 newSession( 'dwho', '127.10.0.1', 'Persistent', $sfaDevices );
@@ -304,12 +312,15 @@ newSession( 'tof', '127.10.0.1', 'Persis
 checkGetList( 1, 'dwho', 'U2F' );
 checkGetList( 1, 'dwho', 'TOTP' );
 checkGetList( 1, 'dwho', 'UBK' );
+checkGetList( 1, 'dwho', 'WebAuthn' );
 checkGetBadType( 'dwho', 'UBKIKI' );
-$ret = checkGetList( 3, 'dwho' );
+$ret = checkGetList( 4, 'dwho' );
 checkGetOnIds( 'dwho', $ret );
 checkDelete( 'dwho', @$ret[0]->{id} );
 checkDelete404( 'dwho', @$ret[0]->{id} );
-checkGetList( 2, 'dwho' );
+checkGetList( 3, 'dwho' );
+checkDeleteList( 1, 'dwho', 'WebAuthn' );
+checkGetList( 0, 'dwho', 'WebAuthn' );
 checkDeleteList( 2, 'dwho' );
 checkGetList( 0, 'dwho' );
 checkDeleteList( 0, 'dwho' );
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/04-menu-api.t 2.0.14+ds-1/lemonldap-ng-manager/t/04-menu-api.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/04-menu-api.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/04-menu-api.t	2022-02-19 16:04:21.000000000 +0000
@@ -129,7 +129,7 @@ sub checkGet {
     my $res = get( $test, $type, $confKey );
     check200( $test, $res );
     my @path = split '/', $attrPath;
-    my $key = from_json( $res->[2]->[0] );
+    my $key  = from_json( $res->[2]->[0] );
     for (@path) {
         if ( ref($key) eq 'ARRAY' ) {
             $key = $key->[$_];
@@ -326,7 +326,7 @@ checkAddFailsOnInvalidConfkey( $test, 'c
 
 checkAddFailsOnInvalidConfkey
 
-$test = "Cat - Update should succeed and keep existing values";
+  $test = "Cat - Update should succeed and keep existing values";
 $cat1->{order} = 3;
 delete $cat1->{catname};
 checkUpdate( $test, 'cat', 'mycat1', $cat1 );
@@ -419,7 +419,7 @@ $test = "App - Get app myapp1 from mycat
 checkGetNotFound( $test, 'app/mycat3', 'myapp1' );
 
 $test = "App - Add app myapp1 to mycat3 should err on not found";
-checkAddNotFound( $test, 'app/mycat3', $app1);
+checkAddNotFound( $test, 'app/mycat3', $app1 );
 
 $test = "App - Add app1 to cat1 should succeed";
 checkAdd( $test, 'app/mycat1', $app1 );
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/04-misc-api.t 2.0.14+ds-1/lemonldap-ng-manager/t/04-misc-api.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/04-misc-api.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/04-misc-api.t	2022-02-19 16:04:21.000000000 +0000
@@ -23,10 +23,10 @@ is( $brokenconfig->{status},        'ko'
 is( $brokenconfig->{status_config}, 'ko', 'Got expected config status' );
 rename 't/conf/lmConf-1.json.broken', 't/conf/lmConf-1.json';
 
-my $allfine = getStatus( "Back to normal" );
-is( $allfine->{status},        'ok', 'Got expected global status' );
-is( $allfine->{status_config}, 'ok', 'Got expected config status' );
-is( $allfine->{status_sessions}, 'unknown', 'Not implemented yet' );
+my $allfine = getStatus("Back to normal");
+is( $allfine->{status},           'ok',      'Got expected global status' );
+is( $allfine->{status_config},    'ok',      'Got expected config status' );
+is( $allfine->{status_sessions},  'unknown', 'Not implemented yet' );
 is( $allfine->{status_psessions}, 'unknown', 'Not implemented yet' );
 
 # Clean up generated files, except for "lmConf-1.json"
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/04-providers-api.t 2.0.14+ds-1/lemonldap-ng-manager/t/04-providers-api.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/04-providers-api.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/04-providers-api.t	2022-02-19 16:04:21.000000000 +0000
@@ -276,7 +276,7 @@ sub checkFindByProviderId {
         ($gotProviderId) = $result->{metadata} =~ m/entityID=['"](.+?)['"]/i;
     }
     elsif ( $providerIdName eq 'serviceUrl' ) {
-        $gotProviderId = $result->{options}->{service};
+        $gotProviderId = shift @{ $result->{options}->{service} };
     }
     else {
         $gotProviderId = $result->{$providerIdName};
@@ -337,8 +337,8 @@ my $oidcRp = {
         email => 'mail',
     },
     options => {
-        clientSecret => 'secret',
-        icon         => 'web.png',
+        clientSecret           => 'secret',
+        icon                   => 'web.png',
         postLogoutRedirectUris =>
           [ "http://url/logout1", "http://url/logout2" ],
     }
@@ -534,7 +534,7 @@ $samlSp->{options}->{checkSLOMessageSign
 $samlSp->{options}->{encryptionMode}           = 'nameid';
 delete $samlSp->{options}->{sessionNotOnOrAfterTimeout};
 delete $samlSp->{exportedAttributes};
-$samlSp->{macros}->{family_name}                      = '$sn',
+$samlSp->{macros}->{family_name} = '$sn',
   $samlSp->{exportedAttributes}->{cn}->{name}         = "cn",
   $samlSp->{exportedAttributes}->{cn}->{friendlyName} = "common_name",
   $samlSp->{exportedAttributes}->{cn}->{mandatory}    = "false",
@@ -646,7 +646,9 @@ my $casApp = {
         given_name => '$firstName',
     },
     options => {
-        service       => 'http://mycasapp.example.com',
+        service => [
+            'http://mycasapp.example.com', 'http://mycasapp2.example.com/test'
+        ],
         rule          => '$uid eq \'dwho\'',
         userAttribute => 'uid'
     }
@@ -654,7 +656,7 @@ my $casApp = {
 
 $test = "CasApp - Add should succeed";
 checkAdd( $test, 'cas/app', $casApp );
-checkGet( $test, 'cas/app', 'myCasApp1', 'options/service',
+checkGet( $test, 'cas/app', 'myCasApp1', 'options/service/0',
     'http://mycasapp.example.com' );
 checkGet( $test, 'cas/app', 'myCasApp1', 'options/userAttribute', 'uid' );
 checkGet( $test, 'cas/app', 'myCasApp1', 'options/rule', '$uid eq \'dwho\'' );
@@ -663,7 +665,7 @@ $test = "CasApp - Add should fail on dup
 checkAddFailsIfExists( $test, 'cas/app', $casApp );
 
 $test = "CasApp - Update should succeed and keep existing values";
-$casApp->{options}->{service}       = 'http://mycasapp.acme.com';
+$casApp->{options}->{service}       = ['http://mycasapp.acme.com'];
 $casApp->{options}->{userAttribute} = 'cn';
 delete $casApp->{options}->{rule};
 delete $casApp->{macros};
@@ -671,7 +673,7 @@ delete $casApp->{exportedVars};
 $casApp->{macros}->{given_name} = '$givenName';
 $casApp->{exportedVars}->{cn}   = 'uid';
 checkUpdate( $test, 'cas/app', 'myCasApp1', $casApp );
-checkGet( $test, 'cas/app', 'myCasApp1', 'options/service',
+checkGet( $test, 'cas/app', 'myCasApp1', 'options/service/0',
     'http://mycasapp.acme.com' );
 checkGet( $test, 'cas/app', 'myCasApp1', 'options/userAttribute', 'cn' );
 checkGet( $test, 'cas/app', 'myCasApp1', 'options/rule', '$uid eq \'dwho\'' );
@@ -686,17 +688,17 @@ delete $casApp->{options}->{playingPossu
 
 $test = "CasApp - Add should fail on non existing options";
 $casApp->{confKey} = 'myCasApp2';
-$casApp->{options}->{service}       = 'http://mycasapp.skynet.com';
+$casApp->{options}->{service}       = ['http://mycasapp.skynet.com'];
 $casApp->{options}->{playingPossum} = 'ElephantInTheRoom';
 checkAddWithUnknownAttributes( $test, 'cas/app', $casApp );
 delete $casApp->{options}->{playingPossum};
 
 $test = "CasApp - Add should fail because service host already exists";
-$casApp->{options}->{service} = 'http://mycasapp.acme.com/ignoredbyissuer';
+$casApp->{options}->{service} = ['http://mycasapp.acme.com/ignoredbyissuer'];
 checkAddFailsIfExists( $test, 'cas/app', $casApp );
 
 $test = "CasApp - 2nd add should succeed";
-$casApp->{options}->{service} = 'http://mycasapp.skynet.com';
+$casApp->{options}->{service} = ['http://mycasapp.skynet.com'];
 checkAdd( $test, 'cas/app', $casApp );
 
 $test = "CasApp - Update should fail if confKey not found";
@@ -714,9 +716,13 @@ $test = "CasApp - Replace should fail on
 $casApp->{options}->{playingPossum} = 'elephant';
 checkReplaceWithInvalidAttribute( $test, 'cas/app', 'myCasApp2', $casApp );
 delete $casApp->{options}->{playingPossum};
-$casApp->{options}->{service} = "XXX";
+$casApp->{options}->{service} = ["XXX"];
 checkReplaceWithInvalidAttribute( $test, 'cas/app', 'myCasApp2', $casApp );
 
+$test = "CasApp - Replace should fail if service is not an array";
+$casApp->{options}->{service} = "http://cas.url.string";
+check409( $test, update( $test, 'cas/app', 'myCasApp2', $casApp ) );
+
 $test = "CasApp - Replace should fail if confKey not found";
 $casApp->{confKey} = 'myCasApp3';
 checkReplaceNotFound( $test, 'cas/app', 'myCasApp3', $casApp );
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/05-rest-api.t 2.0.14+ds-1/lemonldap-ng-manager/t/05-rest-api.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/05-rest-api.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/05-rest-api.t	2022-02-19 16:04:21.000000000 +0000
@@ -58,7 +58,7 @@ while (<F>) {
 close F;
 
 ok( $hstruct = from_json($hstruct), 'struct.json is JSON' );
-ok( ref $hstruct eq 'ARRAY', 'struct.json is an array' )
+ok( ref $hstruct eq 'ARRAY',        'struct.json is an array' )
   or print STDERR "Expected: ARRAY, got: " . ( ref $hstruct ) . "\n";
 count(2);
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/06-rest-api-RSA.t 2.0.14+ds-1/lemonldap-ng-manager/t/06-rest-api-RSA.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/06-rest-api-RSA.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/06-rest-api-RSA.t	2022-02-19 16:04:21.000000000 +0000
@@ -15,7 +15,7 @@ sub checkResult {
     like( $key->{private}, qr/BEGIN/, "is PEM formatted" );
     like( $key->{public},  qr/BEGIN/, "is PEM formatted" );
     ok( $key->{hash}, "hash is non empty" ) if $expecthash;
-    count(1) if $expecthash;
+    count(1)                                if $expecthash;
     count(4);
 }
 
@@ -53,7 +53,7 @@ checkResult($res);
 ok(
     $res = &client->_post(
         '/confs/newCertificate', '', IO::String->new('{"password":"hello"}'),
-        'application/json', 20,
+        'application/json',      20,
     ),
     "Request succeed"
 );
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/11-save-changed-conf-with-confirmation.t 2.0.14+ds-1/lemonldap-ng-manager/t/11-save-changed-conf-with-confirmation.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/11-save-changed-conf-with-confirmation.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/11-save-changed-conf-with-confirmation.t	2022-02-19 16:04:21.000000000 +0000
@@ -20,7 +20,7 @@ mkdir 't/sessions';
 my ( $res, $resBody );
 ok( $res = &client->_post( '/confs/', 'cfgNum=1', &body, 'application/json' ),
     "Request succeed" );
-ok( $res->[0] == 200, "Result code is 200" );
+ok( $res->[0] == 200,                       "Result code is 200" );
 ok( $resBody = from_json( $res->[2]->[0] ), "Result body contains JSON text" );
 
 ok( $resBody->{result} == 0, "JSON response contains \"result:0\"" )
@@ -36,7 +36,7 @@ count(6);
 foreach my $i ( 0 .. 3 ) {
     ok(
         $resBody->{details}->{__warnings__}->[$i]->{message} =~
-          /\b(unprotected|cross-domain-authentication|retries|__badExpressionAssignment__)\b/,
+/\b(unprotected|cross-domain-authentication|retries|__badExpressionAssignment__)\b/,
         "Warning with 'unprotect', 'CDA', 'assignment' or 'retries' found"
     ) or print STDERR Dumper($resBody);
     count(1);
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/12-save-changed-conf.t 2.0.14+ds-1/lemonldap-ng-manager/t/12-save-changed-conf.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/12-save-changed-conf.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/12-save-changed-conf.t	2022-02-19 16:04:21.000000000 +0000
@@ -20,7 +20,7 @@ mkdir 't/sessions';
 my ( $res, $resBody );
 ok( $res = &client->_post( '/confs/', 'cfgNum=1', &body, 'application/json' ),
     "Request succeed" );
-ok( $res->[0] == 200, "Result code is 200" );
+ok( $res->[0] == 200,                       "Result code is 200" );
 ok( $resBody = from_json( $res->[2]->[0] ), "Result body contains JSON text" );
 ok( $resBody->{result} == 1, "JSON response contains \"result:1\"" )
   or print STDERR Dumper($resBody);
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/14-bad-changes-in-conf.t 2.0.14+ds-1/lemonldap-ng-manager/t/14-bad-changes-in-conf.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/14-bad-changes-in-conf.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/14-bad-changes-in-conf.t	2022-02-19 16:04:21.000000000 +0000
@@ -16,7 +16,7 @@ unlink 't/conf/lmConf-2.json';
 my ( $res, $resBody );
 ok( $res = &client->_post( '/confs/', 'cfgNum=1', &body, 'application/json' ),
     "Request succeed" );
-ok( $res->[0] == 200, "Result code is 200" );
+ok( $res->[0] == 200,                       "Result code is 200" );
 ok( $resBody = from_json( $res->[2]->[0] ), "Result body contains JSON text" );
 ok( $resBody->{result} == 0, "JSON response contains \"result:0\"" )
   or print STDERR Dumper($res);
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/15-combination.t 2.0.14+ds-1/lemonldap-ng-manager/t/15-combination.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/15-combination.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/15-combination.t	2022-02-19 16:04:21.000000000 +0000
@@ -17,7 +17,7 @@ mkdir 't/sessions';
 my ( $res, $resBody );
 ok( $res = &client->_post( '/confs/', 'cfgNum=1', &body, 'application/json' ),
     "Request succeed" );
-ok( $res->[0] == 200, "Result code is 200" );
+ok( $res->[0] == 200,                       "Result code is 200" );
 ok( $resBody = from_json( $res->[2]->[0] ), "Result body contains JSON text" );
 ok( $resBody->{result} == 1, "JSON response contains \"result:1\"" )
   or print STDERR Dumper($res);
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/17-extra2f.t 2.0.14+ds-1/lemonldap-ng-manager/t/17-extra2f.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/17-extra2f.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/17-extra2f.t	2022-02-19 16:04:21.000000000 +0000
@@ -17,7 +17,7 @@ mkdir 't/sessions';
 my ( $res, $resBody );
 ok( $res = &client->_post( '/confs/', 'cfgNum=1', &body, 'application/json' ),
     "Request succeed" );
-ok( $res->[0] == 200, "Result code is 200" );
+ok( $res->[0] == 200,                       "Result code is 200" );
 ok( $resBody = from_json( $res->[2]->[0] ), "Result body contains JSON text" );
 ok( $resBody->{result} == 1, "JSON response contains \"result:1\"" )
   or print STDERR Dumper($res);
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/30-DBI-Cli.t 2.0.14+ds-1/lemonldap-ng-manager/t/30-DBI-Cli.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/30-DBI-Cli.t	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/30-DBI-Cli.t	2022-02-19 16:04:21.000000000 +0000
@@ -63,7 +63,7 @@ SKIP: {
     Lemonldap::NG::Manager::Cli->run(@args);
     my $res = $dbh->selectrow_hashref(
         "SELECT * FROM lmConfig WHERE field='ldapSetPassword'");
-    ok( $res,                 'Key inserted' );
+    ok( $res,                          'Key inserted' );
     ok( $res and $res->{value} == '0', 'Value is 0' );
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/40-sessions.t 2.0.14+ds-1/lemonldap-ng-manager/t/40-sessions.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/40-sessions.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/40-sessions.t	2022-02-19 16:04:21.000000000 +0000
@@ -16,8 +16,8 @@ sub newSession {
         $tmp = Lemonldap::NG::Common::Session->new( {
                 storageModule        => 'Apache::Session::File',
                 storageModuleOptions => {
-                    Directory     => 't/sessions',
-                    LockDirectory => 't/sessions',
+                    Directory      => 't/sessions',
+                    LockDirectory  => 't/sessions',
                     generateModule =>
 'Lemonldap::NG::Common::Apache::Session::Generate::SHA256',
                 },
@@ -148,7 +148,7 @@ count(5);
 foreach (@ids) {
     my $res;
     ok( $res = &client->_del("/sessions/global/$_"), "Delete $_" );
-    ok( $res->[0] == 200, 'Result code is 200' );
+    ok( $res->[0] == 200,                            'Result code is 200' );
     ok( from_json( $res->[2]->[0] )->{result} == 1,
         'Body is JSON and result==1' );
     count(3);
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/50-notifications-DBI.t 2.0.14+ds-1/lemonldap-ng-manager/t/50-notifications-DBI.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/50-notifications-DBI.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/50-notifications-DBI.t	2022-02-19 16:04:21.000000000 +0000
@@ -86,7 +86,7 @@ SKIP: {
     $notif = '{"done":1}';
     $res   = $client->jsonPutResponse(
         'notifications/actives/dwho_Test',
-        '', IO::String->new($notif),
+        '',                 IO::String->new($notif),
         'application/json', length($notif)
     );
     ok( $res->{result} == 1, 'Result = 1' );
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/50-notifications.t 2.0.14+ds-1/lemonldap-ng-manager/t/50-notifications.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/50-notifications.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/50-notifications.t	2022-02-19 16:04:21.000000000 +0000
@@ -56,7 +56,7 @@ displayTests('actives');
 $notif = '{"done":1}';
 $res   = &client->jsonPutResponse(
     'notifications/actives/dwho_Test',
-    '', IO::String->new($notif),
+    '',                 IO::String->new($notif),
     'application/json', length($notif)
 );
 ok( $res->{result} == 1, 'Result = 1' );
@@ -137,7 +137,7 @@ sub displayTests {
         ) or diag Dumper($res);
         my $internal_ref = $res->{values}->[0]->{notification};
         my $ref          = $res->{values}->[0]->{reference};
-        $res = &client->jsonResponse( "notifications/$type/$internal_ref" );
+        $res = &client->jsonResponse("notifications/$type/$internal_ref");
         ok( $res->{done} eq $internal_ref, 'Internal reference found' )
           or diag Dumper($res);
         ok( $res = eval { from_json( $res->{notifications}->[0] ) },
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/60-2ndfa.t 2.0.14+ds-1/lemonldap-ng-manager/t/60-2ndfa.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/60-2ndfa.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/60-2ndfa.t	2022-02-19 16:04:21.000000000 +0000
@@ -16,8 +16,8 @@ sub newSession {
         $tmp = Lemonldap::NG::Common::Session->new( {
                 storageModule        => 'Apache::Session::File',
                 storageModuleOptions => {
-                    Directory     => 't/sessions',
-                    LockDirectory => 't/sessions',
+                    Directory      => 't/sessions',
+                    LockDirectory  => 't/sessions',
                     generateModule =>
 'Lemonldap::NG::Common::Apache::Session::Generate::SHA256',
                 },
@@ -277,7 +277,7 @@ $res = &client->jsonResponse( '/sfa/pers
     'uid=*&groupBy=substr(uid,0)&U2FCheck=2&TOTPCheck=2&UBKCheck=2' );
 ok( $res->{result} == 1,
     'Search "uid"=* & UBK & TOTP & UBK - Result code = 1' );
-ok( $res->{count} == 1, 'Found 1 result' ) or print STDERR Dumper($res);
+ok( $res->{count} == 1,       'Found 1 result' ) or print STDERR Dumper($res);
 ok( @{ $res->{values} } == 1, 'List 1 result' );
 ok( $res->{values}->[0]->{value} && $res->{values}->[0]->{value} eq 'd',
     'Result match "uid=d"' )
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/70-viewer.t 2.0.14+ds-1/lemonldap-ng-manager/t/70-viewer.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/70-viewer.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/70-viewer.t	2022-02-19 16:04:21.000000000 +0000
@@ -18,7 +18,8 @@ sub body {
 
 # Test that key value is sent
 my $res = &client->jsonResponse('/view/1/portalDisplayOidcConsents');
-ok( $res->{value} eq '$_oidcConsents && $_oidcConsents =~ /\\w+/', 'Key found' );
+ok( $res->{value} eq '$_oidcConsents && $_oidcConsents =~ /\\w+/',
+    'Key found' );
 count(1);
 
 # Test that hidden key values are NOT sent
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/80-attributes.t 2.0.14+ds-1/lemonldap-ng-manager/t/80-attributes.t
--- 2.0.13+ds-3/lemonldap-ng-manager/t/80-attributes.t	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/80-attributes.t	2022-02-19 16:04:21.000000000 +0000
@@ -38,15 +38,14 @@ my @notManagedAttributes = (
     'syslogFacility',   'userLogger',         'logLevel',
 
     # Plugins parameters
-    'notificationsMaxRetrieve',   'persistentSessionAttributes',
-    'bruteForceProtectionMaxAge', 'bruteForceProtectionMaxLockTime',
+    'notificationsMaxRetrieve', 'persistentSessionAttributes',
 
     # PSGI/CGI protection (must be set in lemonldap-ng.ini)
     'protection',
 
     # SecureToken handler
     'secureTokenAllowOnError', 'secureTokenAttribute', 'secureTokenExpiration',
-    'secureTokenHeader', 'secureTokenMemcachedServers', 'secureTokenUrls',
+    'secureTokenHeader',       'secureTokenMemcachedServers', 'secureTokenUrls',
 
     # Sessions and OTT storage
     'configStorage', 'localStorageOptions', 'localStorage',
@@ -158,7 +157,7 @@ sub scanTree {
 
             # Nodes must have a title
             ok( $name = $leaf->{title}, "Node has a name" );
-            ok( $name =~ /^\w+$/, "Name is a string" );
+            ok( $name =~ /^\w+$/,       "Name is a string" );
 
             # Nodes must have leafs or subnodes
             ok( (
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/01-base-tree.json 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/01-base-tree.json
--- 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/01-base-tree.json	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/01-base-tree.json	2022-01-22 14:30:19.000000000 +0000
@@ -1595,12 +1595,12 @@
       "title": "samlSPSSODescriptorSingleLogoutService"
     }, {
       "_nodes": [{
-        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
+        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "type": "samlAssertion"
       }, {
-        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
+        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "type": "samlAssertion"
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/02-base-tree-all-nodes-opened.json 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/02-base-tree-all-nodes-opened.json
--- 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/02-base-tree-all-nodes-opened.json	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/02-base-tree-all-nodes-opened.json	2022-01-22 14:30:19.000000000 +0000
@@ -2021,17 +2021,17 @@
       "id": "samlSPSSODescriptorAssertionConsumerService",
       "title": "samlSPSSODescriptorAssertionConsumerService",
       "nodes": [{
-        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
+        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "type": "samlAssertion",
-        "data": ["1", "0", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", "#PORTAL#/saml/proxySingleSignOnArtifact"]
+        "data": ["0", "1", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", "#PORTAL#/saml/proxySingleSignOnArtifact"]
       }, {
-        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
+        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "type": "samlAssertion",
-        "data": ["0", "1", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "#PORTAL#/saml/proxySingleSignOnPost"]
+        "data": ["1", "0", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "#PORTAL#/saml/proxySingleSignOnPost"]
       }]
     }, {
       "id": "samlSPSSODescriptorArtifactResolutionService",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/03-base-tree-appCat-modifed.json 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/03-base-tree-appCat-modifed.json
--- 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/03-base-tree-appCat-modifed.json	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/03-base-tree-appCat-modifed.json	2022-01-22 14:30:19.000000000 +0000
@@ -1996,17 +1996,17 @@
       "id": "samlSPSSODescriptorAssertionConsumerService",
       "title": "samlSPSSODescriptorAssertionConsumerService",
       "nodes": [{
-        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
+        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "type": "samlAssertion",
-        "data": ["1", "0", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", "#PORTAL#/saml/proxySingleSignOnArtifact"]
+        "data": ["0", "1", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", "#PORTAL#/saml/proxySingleSignOnArtifact"]
       }, {
-        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
+        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "type": "samlAssertion",
-        "data": ["0", "1", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "#PORTAL#/saml/proxySingleSignOnPost"]
+        "data": ["1", "0", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "#PORTAL#/saml/proxySingleSignOnPost"]
       }]
     }, {
       "id": "samlSPSSODescriptorArtifactResolutionService",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/11-modified-with-confirmation.json 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/11-modified-with-confirmation.json
--- 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/11-modified-with-confirmation.json	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/11-modified-with-confirmation.json	2022-01-22 14:30:19.000000000 +0000
@@ -2126,17 +2126,17 @@
       "id": "samlSPSSODescriptorAssertionConsumerService",
       "title": "samlSPSSODescriptorAssertionConsumerService",
       "nodes": [{
-        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
+        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "type": "samlAssertion",
-        "data": ["1", "0", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", "#PORTAL#/saml/proxySingleSignOnArtifact"]
+        "data": ["0", "1", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", "#PORTAL#/saml/proxySingleSignOnArtifact"]
       }, {
-        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
+        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "type": "samlAssertion",
-        "data": ["0", "1", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "#PORTAL#/saml/proxySingleSignOnPost"]
+        "data": ["1", "0", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "#PORTAL#/saml/proxySingleSignOnPost"]
       }]
     }, {
       "id": "samlSPSSODescriptorArtifactResolutionService",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/12-modified.json 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/12-modified.json
--- 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/12-modified.json	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/12-modified.json	2022-01-22 14:30:19.000000000 +0000
@@ -2133,17 +2133,17 @@
       "id": "samlSPSSODescriptorAssertionConsumerService",
       "title": "samlSPSSODescriptorAssertionConsumerService",
       "nodes": [{
-        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
+        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "type": "samlAssertion",
-        "data": ["1", "0", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", "#PORTAL#/saml/proxySingleSignOnArtifact"]
+        "data": ["0", "1", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", "#PORTAL#/saml/proxySingleSignOnArtifact"]
       }, {
-        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
+        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "type": "samlAssertion",
-        "data": ["0", "1", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "#PORTAL#/saml/proxySingleSignOnPost"]
+        "data": ["1", "0", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "#PORTAL#/saml/proxySingleSignOnPost"]
       }]
     }, {
       "id": "samlSPSSODescriptorArtifactResolutionService",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/14-bad.json 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/14-bad.json
--- 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/14-bad.json	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/14-bad.json	2022-01-22 14:30:19.000000000 +0000
@@ -2012,17 +2012,17 @@
       "id": "samlSPSSODescriptorAssertionConsumerService",
       "title": "samlSPSSODescriptorAssertionConsumerService",
       "nodes": [{
-        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
+        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "type": "samlAssertion",
-        "data": ["1", "0", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", "#PORTAL#/saml/proxySingleSignOnArtifact"]
+        "data": ["0", "1", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact", "#PORTAL#/saml/proxySingleSignOnArtifact"]
       }, {
-        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
+        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "type": "samlAssertion",
-        "data": ["0", "1", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "#PORTAL#/saml/proxySingleSignOnPost"]
+        "data": ["1", "0", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "#PORTAL#/saml/proxySingleSignOnPost"]
       }]
     }, {
       "id": "samlSPSSODescriptorArtifactResolutionService",
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/15-combination.json 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/15-combination.json
--- 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/15-combination.json	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/15-combination.json	2022-01-22 14:30:19.000000000 +0000
@@ -2547,13 +2547,13 @@
     },
     {
       "_nodes": [{
-        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
+        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "type": "samlAssertion"
       },
       {
-        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
+        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "type": "samlAssertion"
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/17-extra2f.json 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/17-extra2f.json
--- 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/17-extra2f.json	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/17-extra2f.json	2022-01-22 14:30:19.000000000 +0000
@@ -3691,13 +3691,13 @@
                {
                   "_nodes" : [
                      {
-                        "default" : "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
+                        "default" : "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
                         "id" : "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
                         "title" : "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
                         "type" : "samlAssertion"
                      },
                      {
-                        "default" : "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
+                        "default" : "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
                         "id" : "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
                         "title" : "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
                         "type" : "samlAssertion"
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/70-diff.json 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/70-diff.json
--- 2.0.13+ds-3/lemonldap-ng-manager/t/jsonfiles/70-diff.json	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/jsonfiles/70-diff.json	2022-01-22 14:30:19.000000000 +0000
@@ -1606,12 +1606,12 @@
       "title": "samlSPSSODescriptorSingleLogoutService"
     }, {
       "_nodes": [{
-        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
+        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact",
         "type": "samlAssertion"
       }, {
-        "default": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
+        "default": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost",
         "id": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "title": "samlSPSSODescriptorAssertionConsumerServiceHTTPPost",
         "type": "samlAssertion"
diff -pruN 2.0.13+ds-3/lemonldap-ng-manager/t/lemonldap-ng.ini 2.0.14+ds-1/lemonldap-ng-manager/t/lemonldap-ng.ini
--- 2.0.13+ds-3/lemonldap-ng-manager/t/lemonldap-ng.ini	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-manager/t/lemonldap-ng.ini	2022-01-22 14:30:19.000000000 +0000
@@ -30,7 +30,7 @@ staticPrefix = app/
 languages    = fr, en, vi, ar
 templateDir  = site/templates/
 enabledModules = conf, sessions, notifications, 2ndFA, viewer, api
-viewerHiddenKeys = samlIDPMetaDataNodes samlSPMetaDataNodes portalDisplayLogout captcha_login_enabled
+viewerHiddenKeys = samlIDPMetaDataNodes samlSPMetaDataNodes, portalDisplayLogout   captcha_login_enabled
 viewerAllowBrowser = $env->{REMOTE_ADDR} eq '127.0.0.1'
 viewerAllowDiff = 1
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm	2022-02-19 16:04:21.000000000 +0000
@@ -23,7 +23,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_NO_SECOND_FACTORS
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Portal::Main::Plugin';
 with 'Lemonldap::NG::Portal::Lib::OverConf';
@@ -293,8 +293,8 @@ sub run {
                   || 'Second factor notification';
                 my $msg = $self->conf->{sfRemovedNotifMsg}
                   || "$removed expired second factor(s) has/have been removed ($name)!";
-                $msg =~ s/_removedSF_/$removed/;
-                $msg =~ s/_nameSF_/$name/;
+                $msg =~ s/\b_removedSF_\b/$removed/;
+                $msg =~ s/\b_nameSF_\b/$name/;
                 my $params =
                   $removed > 1
                   ? { trspan => "expired2Fremoved, $removed, $name" }
@@ -385,7 +385,8 @@ sub run {
             MSG           => $self->canUpdateSfa($req) || 'choose2f',
             ALERT   => ( $self->canUpdateSfa($req) ? 'warning' : 'positive' ),
             MODULES => [
-                map { {
+                map {
+                    {
                         CODE  => $_->prefix,
                         LOGO  => $_->logo,
                         LABEL => $_->label
@@ -440,7 +441,7 @@ sub _choice {
             return $self->p->do(
                 $req,
                 [
-                    sub { $res }, 'controlUrl',
+                    sub { $res },  'controlUrl',
                     'buildCookie', @{ $self->p->endAuth },
                 ]
             );
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/TOTP.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/TOTP.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/TOTP.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/TOTP.pm	2022-02-07 19:06:14.000000000 +0000
@@ -5,7 +5,7 @@ use strict;
 use Mouse;
 use JSON qw(from_json to_json);
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Portal::Main::Plugin
@@ -157,12 +157,19 @@ sub run {
                 400 );
         }
 
+        my $storable_secret =
+          $self->get_storable_secret( $token->{_totp2fSecret} );
+        unless ($storable_secret) {
+            $self->logger->error("Unable to encrypt TOTP secret");
+            return $self->p->sendError( $req, "serverError", 500 );
+        }
+
         # Store TOTP secret
         push @keep,
           {
             type    => 'TOTP',
             name    => $TOTPName,
-            _secret => $token->{_totp2fSecret},
+            _secret => $storable_secret,
             epoch   => $epoch
           };
         $self->logger->debug(
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/U2F.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/U2F.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/U2F.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/U2F.pm	2022-02-19 16:04:21.000000000 +0000
@@ -280,8 +280,11 @@ sub run {
 
         # Delete U2F device
         @$_2fDevices = map {
-            if ( $_->{epoch} eq $epoch ) { $keyName = $_->{name}; () }
-            else                         { $_ }
+            if ( $_->{epoch} eq $epoch and $_->{type} eq "U2F" ) {
+                $keyName = $_->{name};
+                ();
+            }
+            else { $_ }
         } @$_2fDevices;
         if ($keyName) {
             $self->logger->debug(
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/WebAuthn.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/WebAuthn.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/WebAuthn.pm	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/WebAuthn.pm	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,352 @@
+# Self WebAuthn registration
+package Lemonldap::NG::Portal::2F::Register::WebAuthn;
+
+use strict;
+use Mouse;
+use JSON qw(from_json to_json);
+use MIME::Base64 qw(encode_base64url decode_base64url);
+use Crypt::URandom;
+
+our $VERSION = '2.0.12';
+
+extends 'Lemonldap::NG::Portal::Main::Plugin';
+with 'Lemonldap::NG::Portal::Lib::WebAuthn';
+
+# INITIALIZATION
+
+has prefix   => ( is => 'rw', default => 'webauthn' );
+has template => ( is => 'ro', default => 'webauthn2fregister' );
+has welcome  => ( is => 'ro', default => 'webauthn2fWelcome' );
+has logo     => ( is => 'rw', default => 'webauthn.png' );
+has ott      => (
+    is      => 'ro',
+    lazy    => 1,
+    default => sub {
+        my $ott =
+          $_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken');
+        my $timeout = $_[0]->{conf}->{sfRegisterTimeout}
+          // $_[0]->{conf}->{formTimeout};
+        $ott->timeout($timeout);
+        return $ott;
+    }
+);
+
+has displayname_attr => (
+    is      => 'rw',
+    lazy    => 1,
+    default => sub {
+        my $self = shift;
+             $self->conf->{webauthnDisplayNameAttr}
+          || $self->conf->{portalUserAttr}
+          || $self->conf->{whatToTrace}
+          || '_user';
+    }
+);
+
+has rpName => (
+    is      => 'rw',
+    lazy    => 1,
+    default => sub {
+        my $self = shift;
+        $self->conf->{webauthnRpName} || "LemonLDAP::NG";
+    }
+);
+
+sub init { return 1; }
+
+# RUNNING METHODS
+
+# Return a Base64url encoded user handle
+sub getRegistrationUserHandle {
+    my ( $self, $req ) = @_;
+
+    my $current_user_handle = $self->getUserHandle( $req, $req->userData );
+    if ($current_user_handle) {
+        return $current_user_handle;
+    }
+    else {
+        my $new_user_handle = $self->_generate_user_handle;
+        $self->setUserHandle( $req, $new_user_handle );
+        return $new_user_handle;
+    }
+}
+
+# https://www.w3.org/TR/webauthn-2/#sctn-user-handle-privacy
+# It is RECOMMENDED to let the user handle be 64 random bytes, and store this
+# value in the user’s account.
+sub _generate_user_handle {
+    my ($self) = @_;
+    return encode_base64url( Crypt::URandom::urandom(64) );
+}
+
+sub _registrationchallenge {
+    my ( $self, $req, $user ) = @_;
+
+    my @_2fDevices = $self->find2fByType( $req, $req->userData );
+
+    # Check if user can register one more 2F device
+    my $size    = @_2fDevices;
+    my $maxSize = $self->conf->{max2FDevices};
+    $self->logger->debug("Registered 2F Device(s): $size / $maxSize");
+    if ( $size >= $maxSize ) {
+        $self->userLogger->warn("Max number of 2F devices is reached");
+        return $self->p->sendError( $req, 'maxNumberof2FDevicesReached', 400 );
+    }
+
+    my $challenge_base64 = encode_base64url( Crypt::URandom::urandom(32) );
+
+    # Challenge is persisted on the server
+    my $token = $self->ott->createToken( {
+            registration_options => {
+                challenge => $challenge_base64,
+            }
+        }
+    );
+
+    my $displayName      = $req->userData->{ $self->displayname_attr } || $user;
+    my $userVerification = $self->conf->{webauthn2fUserVerification};
+
+    my $request = {
+        rp => {
+            name => $self->rpName,
+        },
+        user => {
+            name        => $user,
+            id          => $self->getRegistrationUserHandle($req),
+            displayName => $displayName,
+        },
+        challenge              => $challenge_base64,
+        pubKeyCredParams       => [],
+        authenticatorSelection => { (
+                $userVerification
+                ? ( userVerification => $userVerification )
+                : ()
+            )
+        }
+    };
+
+    $self->logger->debug( "Register parameters " . to_json($request) );
+    return $self->p->sendJSONresponse( $req,
+        { request => $request, state_id => $token } );
+}
+
+sub _registration {
+    my ( $self, $req, $user ) = @_;
+
+    # Recover creation parameters, including challenge
+    my $state_id = $req->param('state_id');
+    unless ($state_id) {
+        $self->logger->error("Could not find state ID in WebAuthn response");
+        return $self->p->sendError( $req, 'Invalid response', 400 );
+    }
+    my $state_data;
+    unless ( $state_data = $self->ott->getToken($state_id) ) {
+        $self->logger->error(
+            "Expired or invalid state ID in WebAuthn response: $state_id");
+        return $self->p->sendError( $req, 'PE82', 400 );
+    }
+    my $registration_options = ( $state_data->{registration_options} );
+    return $self->p->sendError( $req,
+        'Registration options missing from state data', 400 )
+      unless ($registration_options);
+
+    # Data required for WebAuthn verification
+    my $credential_json = $req->param('credential');
+    $self->logger->debug("Get registered credential data $credential_json");
+
+    return $self->p->sendError( $req, 'Missing credential parameter', 400 )
+      unless ($credential_json);
+
+    my $validation = eval {
+        $self->validateCredential( $req, $registration_options,
+            $credential_json );
+    };
+    if ($@) {
+        $self->logger->error("Credential validation error: $@");
+        return $self->p->sendError( $req, "webAuthnRegisterFailed", 400 );
+    }
+
+    my $credential_id     = $validation->{credential_id};
+    my $credential_pubkey = $validation->{credential_pubkey};
+    my $signature_count   = $validation->{signature_count};
+
+    $self->logger->debug( "Registering new credential : \n" . "ID: "
+          . $credential_id . "\n"
+          . "Public key "
+          . $credential_pubkey . "\n"
+          . "Signature count "
+          . $signature_count
+          . "\n" );
+
+    if (
+        $self->find2fByKey(
+            $req, $req->userData, $self->type,
+            "_credentialId", $credential_id
+        )
+      )
+    {
+        return $self->p->sendError( $req, 'webauthnAlreadyRegistered', 400 );
+    }
+
+    my $keyName = $req->param('keyName');
+    my $epoch   = time();
+
+    # Set default name if empty, check characters and truncate name if too long
+    $keyName ||= $epoch;
+    unless ( $keyName =~ /^[\w]+$/ ) {
+        $self->userLogger->error('WebAuthn name with bad character(s)');
+        return $self->p->sendError( $req, 'badName', 200 );
+    }
+    $keyName = substr( $keyName, 0, $self->conf->{max2FDevicesNameLength} );
+    $self->logger->debug("Key name: $keyName");
+
+    if (
+        $self->add2fDevice(
+            $req,
+            $req->userData,
+            {
+                type                 => $self->type,
+                name                 => $keyName,
+                _credentialId        => $credential_id,
+                _credentialPublicKey => $credential_pubkey,
+                _signCount           => $signature_count,
+                epoch                => $epoch
+            }
+        )
+      )
+    {
+        $self->userLogger->notice(
+            "Webauthn key registration of $keyName succeeds for $user");
+
+        return $self->p->sendJSONresponse( $req, { result => 1 } );
+    }
+    else {
+        return $self->p->sendError( $req, 'Server Error', 500 );
+    }
+
+}
+
+sub _verificationchallenge {
+    my ( $self, $req, $user ) = @_;
+
+    $self->logger->debug('Verification challenge req');
+
+    my $request = $self->generateChallenge( $req, $req->userData );
+
+    unless ($request) {
+        return $self->p->sendError( $req, "No registered devices", 400 );
+    }
+
+    # Request is persisted on the server
+    my $token = $self->ott->createToken( {
+            authentication_options => $request,
+        }
+    );
+
+    $self->logger->debug( "Authentication parameters: " . to_json($request) );
+    return $self->p->sendJSONresponse( $req,
+        { request => $request, state_id => $token } );
+}
+
+sub _verification {
+    my ( $self, $req, $user ) = @_;
+
+    my $credential_json = $req->param('credential');
+
+    return $self->p->sendError( $req, 'Missing credential parameter', 400 )
+      unless ($credential_json);
+
+    my $state_id = $req->param('state_id');
+    unless ($state_id) {
+        $self->logger->error(
+            "Could not find state ID in WebAuthn response: $credential_json");
+        return $self->p->sendError( $req, 'Invalid response', 400 );
+    }
+
+    # Recover challenge
+    my $state_data;
+    unless ( $state_data = $self->ott->getToken($state_id) ) {
+        $self->logger->error(
+            "Expired or invalid state ID in WebAuthn response: $state_id");
+        return $self->p->sendError( $req, 'PE82', 400 );
+    }
+
+    my $signature_options = ( $state_data->{authentication_options} );
+
+    my $validation_result = eval {
+        $self->validateAssertion( $req, $req->userData, $signature_options,
+            $credential_json );
+    };
+    if ($@) {
+        $self->logger->error("Webauthn validation error for $user: $@");
+        return $self->p->sendJSONresponse( $req, { result => 0 } );
+    }
+
+    if ( $validation_result->{success} == 1 ) {
+        return $self->p->sendJSONresponse( $req, { result => 1 } );
+    }
+    else {
+        return $self->p->sendJSONresponse( $req, { result => 0 } );
+    }
+}
+
+sub _delete {
+    my ( $self, $req, $user ) = @_;
+
+    # Check if unregistration is allowed
+    return $self->p->sendError( $req, 'notAuthorized', 200 )
+      unless $self->conf->{webauthn2fUserCanRemoveKey};
+
+    my $epoch = $req->param('epoch')
+      or
+      return $self->p->sendError( $req, '"epoch" parameter is missing', 400 );
+
+    if ( $self->del2fDevice( $req, $req->userData, $self->type, $epoch ) ) {
+        return $self->p->sendJSONresponse( $req, { result => 1 } );
+    }
+    else {
+        return $self->p->sendError( $req, '2FDeviceNotFound', 400 );
+    }
+}
+
+# Main method
+sub run {
+    my ( $self, $req, $action ) = @_;
+    my $user = $req->userData->{ $self->conf->{whatToTrace} };
+
+    return $self->p->sendError( $req,
+        'No ' . $self->conf->{whatToTrace} . ' found in user data', 500 )
+      unless $user;
+
+    # Check if U2F key can be updated
+    my $msg = $self->canUpdateSfa( $req, $action );
+    return $self->p->sendError( $req, $msg, 400 ) if $msg;
+
+    if ( $action eq 'registrationchallenge' ) {
+        return $self->_registrationchallenge( $req, $user );
+    }
+
+    elsif ( $action eq 'registration' ) {
+        return $self->_registration( $req, $user );
+    }
+
+    elsif ( $action eq 'verificationchallenge' ) {
+        return $self->_verificationchallenge( $req, $user );
+    }
+
+    elsif ( $action eq 'verification' ) {
+        return $self->_verification( $req, $user );
+    }
+
+    elsif ( $action eq 'delete' ) {
+        return $self->_delete( $req, $user );
+    }
+
+    else {
+        $self->logger->error("Unknown WebAuthn action -> $action");
+        return $self->p->sendError( $req, 'unknownAction', 400 );
+    }
+
+}
+
+1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/U2F.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/U2F.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/U2F.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/U2F.pm	2022-02-07 19:06:14.000000000 +0000
@@ -187,6 +187,7 @@ sub fail {
                 MAIN_LOGO       => $self->conf->{portalMainLogo},
                 AUTH_ERROR      => $req->error,
                 AUTH_ERROR_TYPE => $req->error_type,
+                AUTH_ERROR_ROLE => $req->error_role,
                 SKIN            => $self->p->getSkin($req),
                 FAILED          => 1
             }
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/WebAuthn.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/WebAuthn.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/WebAuthn.pm	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/WebAuthn.pm	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,122 @@
+# WebAuthn second factor authentication
+#
+# This plugin handle authentications to ask WebAuthn second factor for users that
+# have registered their WebAuthn authenticators
+package Lemonldap::NG::Portal::2F::WebAuthn;
+
+use strict;
+use Mouse;
+use JSON qw(from_json to_json);
+use MIME::Base64 qw(encode_base64url decode_base64url);
+use Crypt::URandom;
+
+use Lemonldap::NG::Portal::Main::Constants qw(
+  PE_OK
+  PE_ERROR
+  PE_SENDRESPONSE
+  PE_BADCREDENTIALS
+);
+
+our $VERSION = '2.0.12';
+
+extends 'Lemonldap::NG::Portal::Main::SecondFactor';
+with 'Lemonldap::NG::Portal::Lib::WebAuthn';
+
+# INITIALIZATION
+
+has rule   => ( is => 'rw' );
+has prefix => ( is => 'ro', default => 'webauthn' );
+has logo   => ( is => 'rw', default => 'webauthn.png' );
+
+sub init {
+    my ($self) = @_;
+
+    # If self registration is enabled and "activation" is just set to
+    # "enabled", replace the rule to detect if user has registered its key
+    if (    $self->conf->{webauthn2fSelfRegistration}
+        and $self->conf->{webauthn2fActivation} eq '1' )
+    {
+        $self->conf->{webauthn2fActivation} =
+          '$_2fDevices && $_2fDevices =~ /"type"\s*:\s*"WebAuthn"/s';
+    }
+    return 0
+      unless ( $self->Lemonldap::NG::Portal::Main::SecondFactor::init() );
+
+    return 1;
+}
+
+# RUNNING METHODS
+
+# Main method
+sub run {
+    my ( $self, $req, $token ) = @_;
+
+    my $user        = $req->user;
+    my $checkLogins = $req->param('checkLogins');
+    $self->logger->debug("WebAuthn: checkLogins set") if $checkLogins;
+
+    my $stayconnected = $req->param('stayconnected');
+    $self->logger->debug("WebAuthn: stayconnected set") if $stayconnected;
+
+    my $request = $self->generateChallenge( $req, $req->sessionInfo );
+
+    unless ($request) {
+        $self->logger->error(
+            "No registered WebAuthn devices for " . $req->user );
+        return PE_ERROR;
+    }
+
+    $self->ott->updateToken( $token, _webauthn_request => $request );
+
+    my $tmp = $self->p->sendHtml(
+        $req,
+        'webauthn2fcheck',
+        params => {
+            MAIN_LOGO     => $self->conf->{portalMainLogo},
+            SKIN          => $self->p->getSkin($req),
+            DATA          => to_json( { request => $request } ),
+            TOKEN         => $token,
+            CHECKLOGINS   => $checkLogins,
+            STAYCONNECTED => $stayconnected
+        }
+    );
+
+    $req->response($tmp);
+    return PE_SENDRESPONSE;
+}
+
+sub verify {
+    my ( $self, $req, $session ) = @_;
+
+    my $user = $session->{ $self->conf->{whatToTrace} };
+
+    my $credential_json = $req->param('credential');
+
+    unless ($credential_json) {
+        $self->logger->error('Missing signature parameter');
+        return PE_ERROR;
+    }
+
+    my $signature_options = $session->{_webauthn_request};
+    delete $session->{_webauthn_request};
+
+    my $validation_result = eval {
+        $self->validateAssertion( $req, $session, $signature_options,
+            $credential_json );
+    };
+    if ($@) {
+        $self->logger->error("Webauthn validation error for $user: $@");
+        return PE_ERROR;
+    }
+
+    if ( $validation_result->{success} == 1 ) {
+        return PE_OK;
+    }
+    else {
+        $self->logger->error(
+            "Webauthn validation did not return success for $user");
+        return PE_ERROR;
+    }
+}
+
+1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Yubikey.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Yubikey.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Yubikey.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Yubikey.pm	2022-02-19 16:04:21.000000000 +0000
@@ -122,7 +122,7 @@ sub _findYubikey {
         }
     }
 
-    return $yubikey;
+    return $yubikey || '';
 
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/AD.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/AD.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/AD.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/AD.pm	2022-02-07 19:06:14.000000000 +0000
@@ -5,10 +5,14 @@ package Lemonldap::NG::Portal::Auth::AD;
 
 use strict;
 use Mouse;
-use Lemonldap::NG::Portal::Main::Constants
-  qw(PE_OK PE_PP_PASSWORD_EXPIRED PE_PP_CHANGE_AFTER_RESET PE_BADCREDENTIALS);
+use Lemonldap::NG::Portal::Main::Constants qw(
+  PE_OK
+  PE_PP_PASSWORD_EXPIRED
+  PE_PP_CHANGE_AFTER_RESET
+  PE_BADCREDENTIALS
+);
 
-our $VERSION = '2.0.6';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Portal::Auth::LDAP';
 
@@ -158,4 +162,16 @@ sub authenticate {
     return $res;
 }
 
+# Define which error codes will stop Combination process
+# @param res error code
+# @return result 1 if stop is needed
+sub stop {
+    my ( $self, $res ) = @_;
+
+    return 1
+      if ( $res == PE_PP_PASSWORD_EXPIRED
+        or $res == PE_PP_CHANGE_AFTER_RESET );
+    return 0;
+}
+
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/CAS.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/CAS.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/CAS.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/CAS.pm	2022-02-19 16:04:21.000000000 +0000
@@ -255,8 +255,7 @@ sub authenticate {
 sub setAuthSessionInfo {
     my ( $self, $req ) = @_;
     $req->{sessionInfo}->{authenticationLevel} = $self->conf->{casAuthnLevel};
-    $req->{sessionInfo}->{_casSrv}  
-               = $req->data->{_casSrvCurrent};
+    $req->{sessionInfo}->{_casSrv}             = $req->data->{_casSrvCurrent};
     return PE_OK;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Combination.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Combination.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Combination.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Combination.pm	2022-02-07 19:06:14.000000000 +0000
@@ -3,12 +3,18 @@ package Lemonldap::NG::Portal::Auth::Com
 use strict;
 use Mouse;
 use Lemonldap::NG::Common::Combination::Parser;
-use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_ERROR PE_FIRSTACCESS);
+use Lemonldap::NG::Portal::Main::Constants qw(
+  PE_CONFIRM
+  PE_ERROR
+  PE_FIRSTACCESS
+  PE_FORMEMPTY
+  PE_PASSWORD_OK
+  PE_OK
+);
 use Scalar::Util 'weaken';
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
-# TODO: See Lib::Wrapper
 extends 'Lemonldap::NG::Portal::Main::Auth';
 with 'Lemonldap::NG::Portal::Lib::OverConf';
 
@@ -126,7 +132,7 @@ sub getDisplayType {
         $req->data->{dataKeep}->{combinationTry},
         $req->data->{combinationStack}
     );
-    my ( $res, $name ) = $stack->[$nb]->[0]->( 'getDisplayType', @_ );
+    my $res = $stack->[$nb]->[0]->( 'getDisplayType', @_ );
     return $res;
 }
 
@@ -231,13 +237,14 @@ sub try {
         return PE_ERROR;
     }
 
+    my $stop = 0;
     if ( $nb < @$stack - 1 ) {
 
         # TODO: change logLevel for userLog()
         ( $res, $name ) = $stack->[$nb]->[$type]->( $subname, $req, @args );
 
         # On error, restart authentication with next scheme
-        if ( $res > PE_OK ) {
+        unless ( $stop = $self->stop( $stack->[$nb]->[$type], $res ) ) {
             $self->logger->info(qq'Scheme "$name" returned $res, trying next');
             $req->data->{dataKeep}->{combinationTry}++;
             $req->steps( [ @{ $req->data->{combinationSteps} } ] );
@@ -251,11 +258,17 @@ sub try {
     $req->sessionInfo->{ [ '_auth', '_userDB' ]->[$type] } = $name;
     $req->sessionInfo->{_combinationTry} =
       $req->data->{dataKeep}->{combinationTry};
-    if ( $res > 0 and $res != PE_FIRSTACCESS ) {
-        $self->userLogger->warn( 'All schemes failed'
-              . ( $req->user ? ' for user ' . $req->user : '' ) . ' ('
-              . $req->address
-              . ')' );
+    if ( $res > 0 ) {
+        if ($stop) {
+            $self->userLogger->info(
+                "Combination stopped by plugin $name (code $res)");
+        }
+        elsif ( $res != PE_FIRSTACCESS ) {
+            $self->userLogger->warn( 'All schemes failed'
+                  . ( $req->user ? ' for user ' . $req->user : '' ) . ' ('
+                  . $req->address
+                  . ')' );
+        }
     }
     return $res;
 }
@@ -269,6 +282,32 @@ sub name {
       || 'Combination';
 }
 
+sub stop {
+    my ( $self, $mod, $res ) = @_;
+    return 1
+      if (
+        $res <= 0    # PE_OK
+        or $res == PE_CONFIRM
+        or $res == PE_PASSWORD_OK
+
+        # TODO: adding this may generate behavior change
+        #or $res == PE_FIRSTACCESS
+        #or $res == PE_FORMEMPTY
+      );
+    my ( $ret, $name );
+    $ret = $mod->( 'can', 'stop' );
+    if ($ret) {
+        eval { ( $ret, $name ) = $mod->( 'stop', $res ) };
+        if ($@) {
+
+            $self->logger->error(
+                "Optional ${name}::stop() method failed: " . $@ );
+            return 0;
+        }
+    }
+    return $ret;
+}
+
 package Lemonldap::NG::Portal::Lib::Combination::UserLogger;
 
 # This logger rewrite "warn" to "notice"
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Custom.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Custom.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Custom.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Custom.pm	2022-01-22 14:30:25.000000000 +0000
@@ -1,35 +1,12 @@
 package Lemonldap::NG::Portal::Auth::Custom;
+use Lemonldap::NG::Portal::Lib::CustomModule;
 
 use strict;
 
-# Fake 'new' method here. Return Lemonldap::NG::Portal::Auth::Custom::{CustomAuth}->new
-sub new {
-    my ( $class, $self ) = @_;
-    unless ( $self->{conf}->{customAuth} ) {
-        die 'Custom Auth module not defined';
-    }
-
-    my $res;
-    eval { $res = $self->{p}->loadModule( $self->{conf}->{customAuth} ) };
-    die 'Unable to load Auth module ' . $self->{conf}->{customAuth} if ($@);
-    return $res;
-}
-
-sub getDisplayType {
-
-    # Warning : $self passed here is the Portal itself
-    my ($self) = @_;
-    my $logo = ( $self->{conf}->{customAuth} =~ /::(\w+)$/ )[0];
-
-    if (  -e $self->{conf}->{templateDir}
-        . "/../htdocs/static/common/modules/"
-        . $logo
-        . ".png" )
-    {
-        $self->logger->debug("CustomAuth $logo.png found");
-        return "logo";
-    }
-    return "standardform";
-}
+our @ISA = qw(Lemonldap::NG::Portal::Lib::CustomModule);
+use constant {
+    custom_name       => "Auth",
+    custom_config_key => "customAuth",
+};
 
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/GitHub.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/GitHub.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/GitHub.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/GitHub.pm	2022-02-19 16:04:21.000000000 +0000
@@ -155,8 +155,7 @@ sub extractFormInfo {
 
         $self->logger->debug("Response from GitHub User API: $user_content");
 
-        eval {
-            $json_hash = from_json( $user_content, { allow_nonref => 1 } ); };
+        eval { $json_hash = from_json( $user_content, { allow_nonref => 1 } ); };
         if ($@) {
             $self->logger->error("Unable to decode JSON $user_content");
             return PE_ERROR;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/LDAP.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/LDAP.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/LDAP.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/LDAP.pm	2022-02-07 19:06:14.000000000 +0000
@@ -7,11 +7,12 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_DONE
   PE_ERROR
   PE_LDAPCONNECTFAILED
+  PE_PP_ACCOUNT_LOCKED
   PE_PP_PASSWORD_EXPIRED
   PE_PP_CHANGE_AFTER_RESET
 );
 
-our $VERSION = '2.0.10';
+our $VERSION = '2.0.14';
 
 # Inheritance: UserDB::LDAP provides all needed ldap functions
 extends qw(
@@ -99,4 +100,17 @@ sub authLogout {
     return PE_OK;
 }
 
+# Define which error codes will stop Combination process
+# @param res error code
+# @return result 1 if stop is needed
+sub stop {
+    my ( $self, $res ) = @_;
+
+    return 1
+      if ( $res == PE_PP_PASSWORD_EXPIRED
+        or $res == PE_PP_ACCOUNT_LOCKED
+        or $res == PE_PP_CHANGE_AFTER_RESET );
+    return 0;
+}
+
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/OpenIDConnect.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/OpenIDConnect.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/OpenIDConnect.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/OpenIDConnect.pm	2022-02-19 16:04:21.000000000 +0000
@@ -6,11 +6,11 @@ use MIME::Base64 qw/encode_base64 decode
 use Lemonldap::NG::Common::JWT qw(getJWTPayload);
 use Lemonldap::NG::Portal::Main::Constants qw(
   PE_OK
-  PE_ERROR
+  PE_OIDC_AUTH_ERROR
   PE_IDPCHOICE
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Portal::Main::Auth
@@ -110,7 +110,7 @@ sub extractFormInfo {
             }
             else {
                 $self->userLogger->error("Unable to extract state $state");
-                return PE_ERROR;
+                return PE_OIDC_AUTH_ERROR;
             }
         }
 
@@ -118,11 +118,11 @@ sub extractFormInfo {
         my $op = $req->data->{_oidcOPCurrent};
 
         unless ($op) {
-            $self->userLogger->error("OpenID Provider not found");
-            return PE_ERROR;
+            $self->userLogger->error("OpenIDConnect Provider not found");
+            return PE_OIDC_AUTH_ERROR;
         }
 
-        $self->logger->debug("Using OpenID Provider $op");
+        $self->logger->debug("Using OpenIDConnect Provider $op");
 
         # Check error
         my $error = $req->param("error");
@@ -135,7 +135,7 @@ sub extractFormInfo {
               if $error_description;
             $self->logger->error("Error URI: $error_uri") if $error_uri;
 
-            return PE_ERROR;
+            return PE_OIDC_AUTH_ERROR;
         }
 
         # Get access_token and id_token
@@ -148,19 +148,19 @@ sub extractFormInfo {
         my $content =
           $self->getAuthorizationCodeAccessToken( $req, $op, $code,
             $auth_method );
-        return PE_ERROR unless $content;
+        return PE_OIDC_AUTH_ERROR unless $content;
 
         my $token_response = $self->decodeTokenResponse($content);
 
         unless ($token_response) {
             $self->logger->error("Could not decode Token Response: $content");
-            return PE_ERROR;
+            return PE_OIDC_AUTH_ERROR;
         }
 
         # Check validity of token response
         unless ( $self->checkTokenResponseValidity($token_response) ) {
             $self->logger->error("Token response is not valid");
-            return PE_ERROR;
+            return PE_OIDC_AUTH_ERROR;
         }
         else {
             $self->logger->debug("Token response is valid");
@@ -178,7 +178,7 @@ sub extractFormInfo {
         {
             unless ( $self->verifyJWTSignature( $id_token, $op ) ) {
                 $self->logger->error("JWT signature verification failed");
-                return PE_ERROR;
+                return PE_OIDC_AUTH_ERROR;
             }
             $self->logger->debug("JWT signature verified");
         }
@@ -190,7 +190,7 @@ sub extractFormInfo {
         unless ( defined $id_token_payload_hash ) {
             $self->logger->error(
                 "Could not decode incoming ID token: $id_token");
-            return PE_ERROR;
+            return PE_OIDC_AUTH_ERROR;
         }
 
         # Check validity of Access Token (optional)
@@ -199,7 +199,7 @@ sub extractFormInfo {
             unless ( $self->verifyHash( $access_token, $at_hash, $id_token ) ) {
                 $self->userLogger->error(
                     "Access token hash verification failed");
-                return PE_ERROR;
+                return PE_OIDC_AUTH_ERROR;
             }
             $self->logger->debug("Access token hash verified");
         }
@@ -211,7 +211,7 @@ sub extractFormInfo {
         # Check validity of ID Token
         unless ( $self->checkIDTokenValidity( $op, $id_token_payload_hash ) ) {
             $self->userLogger->error('ID Token not valid');
-            return PE_ERROR;
+            return PE_OIDC_AUTH_ERROR;
         }
         else {
             $self->logger->debug('ID Token is valid');
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Radius.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Radius.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Radius.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/Radius.pm	2022-01-22 14:30:19.000000000 +0000
@@ -4,14 +4,14 @@ use strict;
 use Mouse;
 use Authen::Radius;
 use Lemonldap::NG::Portal::Main::Constants qw(
-  PE_BADCREDENTIALS
   PE_OK
+  PE_BADCREDENTIALS
   PE_RADIUSCONNECTFAILED
 );
 
 extends qw(Lemonldap::NG::Portal::Auth::_WebForm);
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 # PROPERTIES
 
@@ -39,24 +39,28 @@ sub initRadius {
 sub init {
     my $self = shift;
     unless ( $self->initRadius ) {
-        $self->error('Radius connect failed');
+        $self->error('Radius initialisation failed');
     }
-    return 1;
+
+    return $self->Lemonldap::NG::Portal::Auth::_WebForm::init();
 }
 
 # RUNNING METHODS
 
 sub authenticate {
     my ( $self, $req ) = @_;
-    $self->initRadius unless ( $self->radius );
+    $self->initRadius unless $self->radius;
     unless ( $self->radius ) {
         $self->setSecurity($req);
         return PE_RADIUSCONNECTFAILED;
     }
 
+    $self->logger->debug(
+"Send authentication request ($req->{user}) to Radius server ($self->{conf}->{radiusServer})"
+    );
     my $res = $self->radius->check_pwd( $req->user, $req->data->{password} );
     unless ( $res == 1 ) {
-        $self->userLogger->warn("Unable to authenticate $req->{user} !");
+        $self->userLogger->warn("Unable to authenticate $req->{user}!");
         $self->setSecurity($req);
         return PE_BADCREDENTIALS;
     }
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/SAML.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/SAML.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/SAML.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/SAML.pm	2022-01-22 14:30:19.000000000 +0000
@@ -3,6 +3,7 @@ package Lemonldap::NG::Portal::Auth::SAM
 use strict;
 use MIME::Base64 qw/encode_base64/;
 use Mouse;
+use HTML::Entities qw(encode_entities);
 use Lemonldap::NG::Portal::Lib::SAML;
 use Lemonldap::NG::Common::FormEncode;
 use Lemonldap::NG::Portal::Main::Constants qw(
@@ -24,7 +25,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_SENDRESPONSE
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Portal::Main::Auth
@@ -342,9 +343,14 @@ sub extractFormInfo {
                         $self->logger->debug(
                             "Found value $value for attribute $userAttribute");
                     }
+                    else {
+                        $self->logger->warn(
+"No value for $userAttribute found in SAML assertion"
+                        );
+                    }
                 }
                 else {
-                    $self->logger->debug(
+                    $self->logger->warn(
                         "No attributes found in SAML assertion");
                 }
             }
@@ -767,8 +773,11 @@ sub extractFormInfo {
                 $req->postFields( { 'SAMLResponse' => $slo_body } );
 
                 # RelayState
-                $req->postFields->{'RelayState'} = $relaystate
-                  if ($relaystate);
+                if ($relaystate) {
+                    $req->{postFields}->{'RelayState'} =
+                      encode_entities($relaystate);
+                    $req->data->{safeHiddenFormValues}->{RelayState} = 1;
+                }
 
                 # TODO: verify this
                 push @{ $req->steps }, 'autoPost';
@@ -1127,8 +1136,11 @@ sub extractFormInfo {
         }
 
         # RelayState
-        $req->{postFields}->{'RelayState'} = $login->msg_relayState
-          if ( $login->msg_relayState );
+        if ( $login->msg_relayState ) {
+            $req->{postFields}->{'RelayState'} =
+              encode_entities( $login->msg_relayState );
+            $req->data->{safeHiddenFormValues}->{RelayState} = 1;
+        }
 
         # TODO: verify this
         $req->steps( ['autoPost'] );
@@ -1389,8 +1401,11 @@ sub authLogout {
         $req->postFields( { 'SAMLRequest' => $slo_body } );
 
         # RelayState
-        $req->postFields->{'RelayState'} = $logout->msg_relayState
-          if ( $logout->msg_relayState );
+        if ( $logout->msg_relayState ) {
+            $req->{postFields}->{'RelayState'} =
+              encode_entities( $logout->msg_relayState );
+            $req->data->{safeHiddenFormValues}->{RelayState} = 1;
+        }
 
         # Post done in Portal
         $req->steps( [ 'deleteSession', 'autoPost' ] );
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/_WebForm.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/_WebForm.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/_WebForm.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/_WebForm.pm	2022-02-19 16:04:21.000000000 +0000
@@ -19,7 +19,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_PASSWORDFORMEMPTY
 );
 
-our $VERSION = '2.0.10';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Portal::Main::Auth
@@ -73,7 +73,10 @@ sub extractFormInfo {
     my $res            = PE_OK;
 
     # 1. No user defined at all -> first access
-    unless ( $defUser and $req->method =~ /^POST$/i ) {
+    # _pwdCheck is a workaround to make CheckUser work while using a GET
+    unless ( $defUser
+        and ( uc( $req->method ) eq "POST" or $req->data->{_pwdCheck} ) )
+    {
         $res = PE_FIRSTACCESS;
     }
 
@@ -87,11 +90,13 @@ sub extractFormInfo {
     # 3. If user and oldpassword defined -> password form
     elsif ( $defUser and $defOldPassword ) {
         $res = PE_PASSWORDFORMEMPTY
-          unless ( ( $req->{user} = $req->param('user') )
+          unless (
+               ( $req->{user} = $req->param('user') )
             && ( $req->data->{oldpassword} = $req->param('oldpassword') )
             && ( $req->data->{newpassword} = $req->param('newpassword') )
             && ( $req->data->{confirmpassword} =
-                $req->param('confirmpassword') ) );
+                $req->param('confirmpassword') )
+          );
     }
 
     # If form seems empty
@@ -170,6 +175,7 @@ sub getDisplayType {
 
 sub setSecurity {
     my ( $self, $req ) = @_;
+    return if $req->data->{skipToken};
 
     # If captcha is enable, prepare it
     if ( $self->captcha ) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/WebID.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/WebID.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/WebID.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/WebID.pm	2022-02-19 16:04:21.000000000 +0000
@@ -33,7 +33,7 @@ has reWebIDWhitelist => ( is => 'rw' );
 
 sub init {
     my ($self) = @_;
-    my @hosts = split /\s+/, $self->{conf}->{webIDWhitelist};
+    my @hosts  = split /\s+/, $self->{conf}->{webIDWhitelist};
     unless (@hosts) {
         $self->error(
 'WebID white list is empty. Set it in manager, use * to accept all FOAF providers'
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/CertificateResetByMail/Custom.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/CertificateResetByMail/Custom.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/CertificateResetByMail/Custom.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/CertificateResetByMail/Custom.pm	2022-01-22 14:30:25.000000000 +0000
@@ -1,23 +1,20 @@
 package Lemonldap::NG::Portal::CertificateResetByMail::Custom;
+use Lemonldap::NG::Portal::Lib::CustomModule;
 
 use strict;
-use Mouse;
 
-extends 'Lemonldap::NG::Portal::Main::Plugin';
+our @ISA = qw(Lemonldap::NG::Portal::Lib::CustomModule);
+use constant {
+    custom_name       => "CertificateResetByMail",
+    custom_config_key => "customResetCertByMail",
+};
 
 sub new {
     my ( $class, $self ) = @_;
     unless ( $self->{conf}->{customRegister} ) {
         die 'Custom register module not defined';
     }
-
-    my $res = $self->{p}->loadModule( $self->{conf}->{customResetCertByMail} );
-    unless ($res) {
-        die 'Unable to load register module '
-          . $self->{conf}->{customResetCertByMail};
-    }
-
-    return $res;
+    return $class->Lemonldap::NG::Portal::Lib::CustomModule::new($self);
 }
 
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/CertificateResetByMail/LDAP.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/CertificateResetByMail/LDAP.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/CertificateResetByMail/LDAP.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/CertificateResetByMail/LDAP.pm	2022-02-07 19:06:14.000000000 +0000
@@ -11,7 +11,7 @@ use Lemonldap::NG::Portal::Main::Constan
 
 extends 'Lemonldap::NG::Portal::Lib::LDAP';
 
-our $VERSION = '2.0.8';
+our $VERSION = '2.0.14';
 
 # PRIVATE METHOD
 sub modifCertificate {
@@ -50,8 +50,6 @@ sub modifCertificate {
     unless ( $result->code == 0 ) {
         $self->logger->debug( "LDAP modify Error: " . $result->code );
         $self->ldap->unbind;
-        $self->{flags}->{ldapActive} = 0;
-        $self->ldap->unbind;
         return PE_LDAPERROR;
     }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/CAS.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/CAS.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/CAS.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/CAS.pm	2022-01-24 21:26:10.000000000 +0000
@@ -15,7 +15,7 @@ use Lemonldap::NG::Portal::Main::Constan
   URIRE
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Portal::Main::Issuer',
   'Lemonldap::NG::Portal::Lib::CAS';
@@ -61,8 +61,12 @@ sub init {
 
     # Add CAS Services, so we can check service= parameter on logout
     foreach my $casSrv ( keys %{ $self->casAppList } ) {
-        if ( my $serviceUrl =
-            $self->casAppList->{$casSrv}->{casAppMetaDataOptionsService} )
+        for my $serviceUrl (
+            split(
+                /\s+/,
+                $self->casAppList->{$casSrv}->{casAppMetaDataOptionsService}
+            )
+          )
         {
             push @{ $self->p->{additionalTrustedDomains} }, $serviceUrl;
             $self->logger->debug(
@@ -83,15 +87,22 @@ sub storeEnvAndCheckGateway {
       || $req->param('gateway');
 
     if ( $gateway and $gateway eq "true" ) {
-        $self->logger->debug(
-            "Gateway mode requested, redirect without authentication");
-        $req->response( [ 302, [ Location => $service ], [] ] );
-        for my $s ( $self->ipath, $self->ipath . 'Path' ) {
-            $self->logger->debug("Removing $s from pdata")
-              if delete $req->pdata->{$s};
-        }
+        if ( $self->_gatewayAllowedRedirect( $req, $service ) ) {
+            $self->logger->debug(
+                "Gateway mode requested, redirect without authentication");
+            $req->response( [ 302, [ Location => $service ], [] ] );
+            for my $s ( $self->ipath, $self->ipath . 'Path' ) {
+                $self->logger->debug("Removing $s from pdata")
+                  if delete $req->pdata->{$s};
+            }
 
-        return PE_SENDRESPONSE;
+            return PE_SENDRESPONSE;
+        }
+        else {
+            $self->logger->error(
+                "Disallowing redirection to unknown service $service");
+            return PE_CAS_SERVICE_NOT_ALLOWED;
+        }
     }
 
     if ( $service and $service =~ URIRE ) {
@@ -112,6 +123,24 @@ sub storeEnvAndCheckGateway {
     return PE_OK;
 }
 
+sub _gatewayAllowedRedirect {
+    my ( $self, $req, $service ) = @_;
+
+    my $app                    = $self->getCasApp($service);
+    my $casAccessControlPolicy = $self->conf->{casAccessControlPolicy};
+
+    # Redirect is allowed if there is no access control or if
+    # the service is declared in CAS apps
+    if ( $casAccessControlPolicy !~ /^(error|faketicket)$/i ) {
+        return 1;
+    }
+    if ($app) {
+        return 1;
+    }
+
+    return 0;
+}
+
 # Main method (launched only for authenticated users, see Main/Issuer)
 sub run {
     my ( $self, $req, $target ) = @_;
@@ -138,9 +167,6 @@ sub run {
     # Session ID
     my $session_id = $req->{sessionInfo}->{_session_id} || $req->id;
 
-    # Session creation timestamp
-    my $time = $req->{sessionInfo}->{_utime} || time();
-
     # 1. LOGIN
     if ( $target eq $cas_login ) {
 
@@ -281,12 +307,20 @@ sub run {
             $self->logger->debug(
                 "Create a CAS service ticket for service $service");
 
+            my $_utime =
+              $self->conf->{casTicketExpiration}
+              ? (
+                time +
+                  $self->conf->{casTicketExpiration} -
+                  $self->conf->{timeout} )
+              : ( $req->{sessionInfo}->{_utime} || time() );
+
             my $Sinfos;
             $Sinfos->{type}    = 'casService';
             $Sinfos->{service} = $service;
             $Sinfos->{renew}   = $casRenewFlag;
             $Sinfos->{_cas_id} = $session_id;
-            $Sinfos->{_utime}  = $time;
+            $Sinfos->{_utime}  = $_utime;
             $Sinfos->{_casApp} = $app;
 
             my $h = $self->p->processHook( $req, 'casGenerateServiceTicket',
@@ -491,6 +525,18 @@ sub validate {
         return $self->returnCasValidateError();
     }
 
+    # Make sure the token is still valid, we already compensated for
+    # different TTLs when storing _utime
+    if ( $casServiceSession->{data}->{_utime} ) {
+        if (
+            time >
+            ( $casServiceSession->{data}->{_utime} + $self->conf->{timeout} ) )
+        {
+            $self->logger->error("Session $ticket has expired");
+            return $self->returnCasValidateError();
+        }
+    }
+
     $self->logger->debug("Service ticket session $ticket found");
 
     my $service1_uri = URI->new($service);
@@ -612,11 +658,16 @@ sub proxy {
             'Error in proxy session management' );
     }
 
+    my $_utime =
+      $self->conf->{casTicketExpiration}
+      ? ( time + $self->conf->{casTicketExpiration} - $self->conf->{timeout} )
+      : $casProxyGrantingSession->data->{_utime};
+
     my $Pinfos;
     $Pinfos->{type}    = 'casProxy';
     $Pinfos->{service} = $targetService;
     $Pinfos->{_cas_id} = $casProxyGrantingSession->data->{_cas_id};
-    $Pinfos->{_utime}  = $casProxyGrantingSession->data->{_utime};
+    $Pinfos->{_utime}  = $_utime;
     $Pinfos->{proxies} = $casProxyGrantingSession->data->{proxies};
 
     $casProxySession->update($Pinfos);
@@ -686,6 +737,20 @@ sub _validate2 {
         return $self->returnCasServiceValidateError( $req, 'INVALID_TICKET',
             'Ticket not found' );
     }
+
+    # Make sure the token is still valid, we already compensated for
+    # different TTLs when storing _utime
+    if ( $casServiceSession->{data}->{_utime} ) {
+        if (
+            time >
+            ( $casServiceSession->{data}->{_utime} + $self->conf->{timeout} ) )
+        {
+            $self->logger->error("$urlType ticket session $ticket has expired");
+            return $self->returnCasServiceValidateError( $req, 'INVALID_TICKET',
+                'Ticket expired' );
+        }
+    }
+
     my $app = $casServiceSession->data->{_casApp};
 
     $self->logger->debug("$urlType ticket session $ticket found");
@@ -752,7 +817,7 @@ sub _validate2 {
         $PGinfos->{type}    = 'casProxyGranting';
         $PGinfos->{service} = $service;
         $PGinfos->{_cas_id} = $casServiceSession->data->{_cas_id};
-        $PGinfos->{_utime}  = $casServiceSession->data->{_utime};
+        $PGinfos->{_utime}  = time;
         $PGinfos->{_casApp} = $app;
 
         # Trace proxies
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm	2022-02-19 16:04:21.000000000 +0000
@@ -20,7 +20,7 @@ use Lemonldap::NG::Portal::Main::Constan
 );
 use String::Random qw/random_string/;
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Portal::Main::Issuer
@@ -263,7 +263,9 @@ sub run {
 
             # Check if this RP is authorized
             if ( my $rule = $self->spRules->{$rp} ) {
-                unless ( $rule->( $req, $req->sessionInfo ) ) {
+                my $ruleVariables =
+                  { %{ $req->sessionInfo || {} }, _oidc_grant_type => $flow };
+                unless ( $rule->( $req, $ruleVariables ) ) {
                     $self->userLogger->warn( 'User '
                           . $req->sessionInfo->{ $self->conf->{whatToTrace} }
                           . " is not authorized to access to $rp" );
@@ -386,10 +388,10 @@ sub run {
 
             # Check openid scope
             unless ( $self->_hasScope( 'openid', $oidc_request->{'scope'} ) ) {
-                $self->logger->debug("No openid scope found");
+                $self->logger->error("No openid scope found");
 
                 #TODO manage standard OAuth request
-                return PE_OK;
+                return PE_ERROR;
             }
 
             # Check Request JWT signature
@@ -701,7 +703,7 @@ sub run {
 
                 # Store data in session
                 my $code_payload = {
-                    code_challenge => $oidc_request->{'code_challenge'},
+                    code_challenge        => $oidc_request->{'code_challenge'},
                     code_challenge_method =>
                       $oidc_request->{'code_challenge_method'},
                     nonce           => $oidc_request->{'nonce'},
@@ -755,6 +757,7 @@ sub run {
                             scope           => $scope,
                             rp              => $rp,
                             user_session_id => $req->id,
+                            grant_type      => $flow,
                         }
                     );
 
@@ -762,7 +765,7 @@ sub run {
                         $self->logger->error("Unable to create Access Token");
                         $self->returnRedirectError( $req,
                             $oidc_request->{'redirect_uri'},
-                            "server_error", undef, undef,
+                            "server_error",           undef, undef,
                             $oidc_request->{'state'}, 1 );
                     }
 
@@ -862,6 +865,7 @@ sub run {
                             scope           => $scope,
                             rp              => $rp,
                             user_session_id => $req->id,
+                            grant_type      => $flow,
                         }
                     );
 
@@ -869,7 +873,7 @@ sub run {
                         $self->logger->error("Unable to create Access Token");
                         return $self->returnRedirectError( $req,
                             $oidc_request->{'redirect_uri'},
-                            "server_error", undef, undef,
+                            "server_error",           undef, undef,
                             $oidc_request->{'state'}, 1 );
                     }
 
@@ -1096,6 +1100,13 @@ sub _handleClientCredentialsGrant {
     my $req_scope = $req->param('scope') || '';
     my $scope     = $self->getScope( $req, $rp, $req_scope );
 
+    unless ($scope) {
+        $self->userLogger->warn( 'Client '
+              . $client_id
+              . " was not granted any requested scopes ($req_scope) for $rp" );
+        return $self->sendOIDCError( $req, 'invalid_scope', 400 );
+    }
+
     my $infos = {
         $self->conf->{whatToTrace} => $client_id,
         _clientId                  => $client_id,
@@ -1110,7 +1121,9 @@ sub _handleClientCredentialsGrant {
 
     # Run rule against session info
     if ( my $rule = $self->spRules->{$rp} ) {
-        unless ( $rule->( $req, $infos ) ) {
+        my $ruleVariables =
+          { %{ $infos || {} }, _oidc_grant_type => "clientcredentials", };
+        unless ( $rule->( $req, $ruleVariables ) ) {
             $self->userLogger->warn(
                     "Relying party $rp did not validate the provided "
                   . "Access Rule during Client Credentials Grant" );
@@ -1131,6 +1144,7 @@ sub _handleClientCredentialsGrant {
             scope           => $scope,
             rp              => $rp,
             user_session_id => $session->id,
+            grant_type      => "clientcredentials",
         }
     );
     unless ($access_token) {
@@ -1208,7 +1222,9 @@ sub _handlePasswordGrant {
 
     ## Make sure the current user is allowed to use this RP
     if ( my $rule = $self->spRules->{$rp} ) {
-        unless ( $rule->( $req, $req->sessionInfo ) ) {
+        my $ruleVariables =
+          { %{ $req->sessionInfo || {} }, _oidc_grant_type => "password", };
+        unless ( $rule->( $req, $ruleVariables ) ) {
             $self->userLogger->warn( 'User '
                   . $req->sessionInfo->{ $self->conf->{whatToTrace} }
                   . " is not authorized to access to $rp" );
@@ -1219,6 +1235,12 @@ sub _handlePasswordGrant {
 
     # Resolve scopes
     my $scope = $self->getScope( $req, $rp, $req_scope );
+    unless ($scope) {
+        $self->userLogger->warn( 'User '
+              . $req->sessionInfo->{ $self->conf->{whatToTrace} }
+              . " was not granted any requested scopes ($req_scope) for $rp" );
+        return $self->sendOIDCError( $req, 'invalid_scope', 400 );
+    }
 
     my $user_id = $self->getUserIDForRP( $req, $rp, $req->sessionInfo );
 
@@ -1233,6 +1255,7 @@ sub _handlePasswordGrant {
         $req, $rp, $scope,
         $req->sessionInfo,
         {
+            grant_type      => "password",
             user_session_id => $req->id,
         }
     );
@@ -1256,6 +1279,7 @@ sub _handlePasswordGrant {
                 scope           => $scope,
                 client_id       => $client_id,
                 user_session_id => $req->id,
+                grant_type      => "password",
             },
             0,
         );
@@ -1304,7 +1328,7 @@ sub _handlePasswordGrant {
         access_token => "$access_token",
         token_type   => 'Bearer',
         expires_in   => $expires_in + 0,
-        ( ( $scope ne $req_scope ) ? ( scope => "$scope" ) : () ),
+        ( ( $scope ne $req_scope ) ? ( scope => "$scope" )       : () ),
         ( $refresh_token ? ( refresh_token => "$refresh_token" ) : () ),
         ( $id_token      ? ( id_token      => "$id_token" )      : () ),
     };
@@ -1385,6 +1409,7 @@ sub _handleAuthorizationCodeGrant {
         $req, $rp, $scope,
         $codeSession->data->{user_session_id},
         {
+            grant_type      => "authorizationcode",
             user_session_id => $apacheSession->id,
         }
     );
@@ -1419,6 +1444,7 @@ sub _handleAuthorizationCodeGrant {
                 client_id    => $client_id,
                 _session_uid => $apacheSession->data->{_user},
                 auth_time    => $apacheSession->data->{_lastAuthnUTime},
+                grant_type   => "authorizationcode",
             },
             1,
         );
@@ -1445,6 +1471,7 @@ sub _handleAuthorizationCodeGrant {
                 scope           => $scope,
                 client_id       => $client_id,
                 user_session_id => $codeSession->data->{user_session_id},
+                grant_type      => "authorizationcode",
             },
             0,
         );
@@ -1499,7 +1526,7 @@ sub _handleAuthorizationCodeGrant {
         expires_in   => $expires_in + 0,
         id_token     => "$id_token",
         ( $refresh_token ? ( refresh_token => "$refresh_token" ) : () ),
-        ( ( $req_scope ne $scope ) ? ( scope => "$scope" ) : () ),
+        ( ( $req_scope ne $scope ) ? ( scope => "$scope" )       : () ),
     };
 
     my $cRP = $apacheSession->data->{_oidcConnectedRP} || '';
@@ -1560,6 +1587,7 @@ sub _handleRefreshTokenGrant {
             $user_session_id,
             {
                 user_session_id => $user_session_id,
+                grant_type      => $refreshSession->data->{grant_type},
             }
         );
 
@@ -1618,6 +1646,7 @@ sub _handleRefreshTokenGrant {
             $refreshSession->data,
             {
                 offline_session_id => $refreshSession->id,
+                grant_type         => $refreshSession->data->{grant_type},
             }
         );
 
@@ -2149,6 +2178,7 @@ sub metadata {
             jwks_uri               => $baseUrl . $jwks_uri,
             authorization_endpoint => $baseUrl . $authorize_uri,
             end_session_endpoint   => $baseUrl . $endsession_uri,
+
             #check_session_iframe   => $baseUrl . $checksession_uri,
             introspection_endpoint => $baseUrl . $introspection_uri,
 
@@ -2165,10 +2195,10 @@ sub metadata {
 
             # Scopes
             scopes_supported => [qw/openid profile email address phone/],
-            response_types_supported => $response_types,
-            grant_types_supported    => $grant_types,
-            acr_values_supported     => \@acr,
-            subject_types_supported  => ["public"],
+            response_types_supported              => $response_types,
+            grant_types_supported                 => $grant_types,
+            acr_values_supported                  => \@acr,
+            subject_types_supported               => ["public"],
             token_endpoint_auth_methods_supported =>
               [qw/client_secret_post client_secret_basic/],
             introspection_endpoint_auth_methods_supported =>
@@ -2350,9 +2380,9 @@ sub _generateIDToken {
         exp       => $id_token_exp,                      # expiration
         iat       => time,                               # Issued time
         auth_time => $sessionInfo->{_lastAuthnUTime},    # Authentication time
-        acr => $id_token_acr,    # Authentication Context Class Reference
-        azp => $client_id,       # Authorized party
-                                 # TODO amr
+        acr       => $id_token_acr,    # Authentication Context Class Reference
+        azp       => $client_id,       # Authorized party
+                                       # TODO amr
     };
 
     for ( keys %{$extra_claims} ) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/SAML.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/SAML.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/SAML.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/SAML.pm	2022-02-19 16:04:21.000000000 +0000
@@ -4,6 +4,7 @@ use strict;
 use Mouse;
 use URI;
 use URI::QueryParam;
+use HTML::Entities qw(encode_entities);
 use Lemonldap::NG::Portal::Lib::SAML;
 use Lemonldap::NG::Portal::Main::Constants qw(
   PE_OK
@@ -21,7 +22,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_UNAUTHORIZEDPARTNER
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Portal::Main::Issuer',
   'Lemonldap::NG::Portal::Lib::SAML';
@@ -280,7 +281,7 @@ sub run {
             $req->data->{_proxiedRequest}     = $request;
             $req->data->{_proxiedMethod}      = $method;
             $req->data->{_proxiedRelayState}  = $relaystate,
-              $req->data->{_proxiedArtifact}  = $artifact;
+              $req->data->{_proxiedArtifact} = $artifact;
         }
 
         # Process the request or use IDP initiated mode
@@ -596,8 +597,8 @@ sub run {
             # Get session key associated with NameIDFormat
             # Not for unspecified, transient, persistent, entity, encrypted
             my $nameIDFormatConfiguration = {
-                $self->getNameIDFormat("email") => 'samlNameIDFormatMapEmail',
-                $self->getNameIDFormat("x509")  => 'samlNameIDFormatMapX509',
+                $self->getNameIDFormat("email")   => 'samlNameIDFormatMapEmail',
+                $self->getNameIDFormat("x509")    => 'samlNameIDFormatMapX509',
                 $self->getNameIDFormat("windows") =>
                   'samlNameIDFormatMapWindows',
                 $self->getNameIDFormat("kerberos") =>
@@ -1048,8 +1049,11 @@ sub run {
                 }
 
                 # RelayState
-                $req->{postFields}->{'RelayState'} = $relaystate
-                  if ($relaystate);
+                if ($relaystate) {
+                    $req->{postFields}->{'RelayState'} =
+                      encode_entities($relaystate);
+                    $req->data->{safeHiddenFormValues}->{RelayState} = 1;
+                }
 
                 $req->steps( ['autoPost'] );
                 return PE_OK;
@@ -1515,9 +1519,15 @@ sub sloRelayPost {
     $self->logger->debug("Found relay session $relayID");
 
     # Get data to build POST form
-    $req->{postUrl}                     = $relayInfos->data->{url};
+    $req->{postUrl} = $relayInfos->data->{url};
     $req->{postFields}->{'SAMLRequest'} = $relayInfos->data->{body};
-    $req->{postFields}->{'RelayState'}  = $relayInfos->data->{relayState};
+
+    # RelayState
+    if ( $relayInfos->data->{relayState} ) {
+        $req->{postFields}->{'RelayState'} =
+          encode_entities( $relayInfos->data->{relayState} );
+        $req->data->{safeHiddenFormValues}->{RelayState} = 1;
+    }
 
     # Delete relay session
     $relayInfos->remove();
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/CAS.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/CAS.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/CAS.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/CAS.pm	2022-01-24 21:26:10.000000000 +0000
@@ -7,7 +7,7 @@ use XML::Simple;
 use Lemonldap::NG::Common::UserAgent;
 use URI;
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 # PROPERTIES
 
@@ -226,8 +226,10 @@ sub returnCasServiceValidateSuccess {
     }
     if ($proxies) {
         $self->logger->debug("Add proxies $proxies in response");
-        $s .= "\t\t<cas:proxies>\n\t\t\t<cas:proxy>$_</cas:proxy>\n"
-          foreach ( split( /$self->conf->{multiValuesSeparator}/, $proxies ) );
+        $s .= "\t\t<cas:proxies>\n";
+        $s .= "\t\t\t<cas:proxy>$_</cas:proxy>\n"
+          foreach (
+            reverse( split( $self->conf->{multiValuesSeparator}, $proxies ) ) );
         $s .= "\t\t</cas:proxies>\n";
     }
     $s .= "\t</cas:authenticationSuccess>\n</cas:serviceResponse>\n";
@@ -529,22 +531,28 @@ sub getCasApp {
 
     for my $app ( keys %{ $self->casAppList } ) {
 
-        my $candidateUri =
-          URI->new( $self->casAppList->{$app}->{casAppMetaDataOptionsService} );
-        my $candidateHost  = $candidateUri->authority;
-        my $candidateCanon = $candidateUri->canonical;
-
-        # Try to match prefix, remembering the longest match found
-        if ( index( $uriCanon, $candidateCanon ) == 0 ) {
-            if ( length($longestCandidate) < length($candidateCanon) ) {
-                $longestCandidate = $candidateCanon;
-                $prefixConfKey    = $app;
+        for my $appservice (
+            split(
+                /\s+/, $self->casAppList->{$app}->{casAppMetaDataOptionsService}
+            )
+          )
+        {
+            my $candidateUri   = URI->new($appservice);
+            my $candidateHost  = $candidateUri->authority;
+            my $candidateCanon = $candidateUri->canonical;
+
+            # Try to match prefix, remembering the longest match found
+            if ( index( $uriCanon, $candidateCanon ) == 0 ) {
+                if ( length($longestCandidate) < length($candidateCanon) ) {
+                    $longestCandidate = $candidateCanon;
+                    $prefixConfKey    = $app;
+                }
             }
-        }
 
-        # Try to match host, only if strict matching is disabled
-        unless ( $self->conf->{casStrictMatching} ) {
-            $hostnameConfKey = $app if ( $hostname eq $candidateHost );
+            # Try to match host, only if strict matching is disabled
+            unless ( $self->conf->{casStrictMatching} ) {
+                $hostnameConfKey = $app if ( $hostname eq $candidateHost );
+            }
         }
     }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Choice.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Choice.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Choice.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Choice.pm	2022-02-19 16:04:21.000000000 +0000
@@ -7,7 +7,7 @@ use Safe;
 extends 'Lemonldap::NG::Portal::Lib::Wrapper';
 with 'Lemonldap::NG::Portal::Lib::OverConf';
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 has modules    => ( is => 'rw', default => sub { {} } );
 has rules      => ( is => 'rw', default => sub { {} } );
@@ -244,7 +244,7 @@ sub _buildAuthLoop {
                 # Get displayType for this module
                 no strict 'refs';
                 my $displayType = eval {
-                    "Lemonldap::NG::Portal::Auth::${auth}"
+                    $self->_authentication->modules->{$_}
                       ->can('getDisplayType')->( $self, $req );
                 } || 'logo';
 
@@ -252,10 +252,8 @@ sub _buildAuthLoop {
                     "Display type $displayType for module $auth");
                 $optionsLoop->{$displayType} = 1;
                 my $logo = $_;
-                if ( $auth eq 'Custom' ) {
-                    $logo =
-                      ( $self->{conf}->{customAuth} =~ /::(\w+)$/ )[0];
-                }
+
+                my $foundLogo = 0;
 
                 # If displayType is logo, check if key.png is available
                 if (  -e $self->conf->{templateDir}
@@ -264,11 +262,30 @@ sub _buildAuthLoop {
                     . ".png" )
                 {
                     $optionsLoop->{logoFile} = $logo . ".png";
+                    $foundLogo = 1;
                 }
                 else {
                     $optionsLoop->{logoFile} = $auth . ".png";
                 }
 
+                # Compatibility, with Custom, try the module name if
+                # key was not found
+                if ( $auth eq 'Custom' and not $foundLogo ) {
+                    $logo =
+                      ( ( $self->{conf}->{customAuth} || "" ) =~ /::(\w+)$/ )
+                      [0];
+                    if (
+                        $logo
+                        and ( -e $self->conf->{templateDir}
+                            . "/../htdocs/static/common/modules/"
+                            . $logo
+                            . ".png" )
+                      )
+                    {
+                        $optionsLoop->{logoFile} = $logo . ".png";
+                    }
+                }
+
                 # Register item in loop
                 push @authLoop, $optionsLoop;
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/CustomModule.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/CustomModule.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/CustomModule.pm	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/CustomModule.pm	2022-01-22 14:30:25.000000000 +0000
@@ -0,0 +1,30 @@
+package Lemonldap::NG::Portal::Lib::CustomModule;
+
+# Fake 'new' method here
+sub new {
+    my ( $class, $self ) = @_;
+
+    my $configKey = $class->custom_config_key;
+    my $name      = $class->custom_name;
+
+    my $module = $self->{conf}->{$configKey};
+    unless ($module) {
+        die "Custom $name module not defined";
+    }
+
+    $module = "Lemonldap::NG::Portal$module" if ( $module =~ /^::/ );
+
+    eval "require $module";
+    if ($@) {
+        die "Custom $name module failed to compile: $@";
+    }
+
+    my $obj = eval { $module->new($self); };
+    if ($@) {
+        die "Custom $name module failed to create instance: $@";
+    }
+
+    $self->{p}->logger->debug("Custom $name module loaded");
+    return $obj;
+}
+1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/DBI.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/DBI.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/DBI.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/DBI.pm	2022-01-23 15:41:18.000000000 +0000
@@ -12,7 +12,7 @@ use Mouse;
 
 extends 'Lemonldap::NG::Common::Module';
 
-our $VERSION = '2.0.0';
+our $VERSION = '2.0.14';
 
 # PROPERTIES
 
@@ -91,16 +91,14 @@ sub init {
 # @return SQL statement string
 sub hash_password {
     my ( $self, $password, $hash ) = @_;
-    if ( $hash =~ /^(md5|sha|sha1|encrypt)$/i ) {
+    if ($hash) {
         $self->logger->debug( "Using " . uc($hash) . " to hash password" );
         return uc($hash) . "($password)";
     }
     else {
-        $self->logger->notice(
-            "No valid password hash, using clear text for password");
+        $self->logger->debug("No password hash, using clear text for password");
         return $password;
     }
-
 }
 
 # Return hashed password for use in SQL SELECT statement
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/LDAP.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/LDAP.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/LDAP.pm	2021-08-21 17:42:59.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/LDAP.pm	2022-02-19 16:04:21.000000000 +0000
@@ -13,7 +13,7 @@ use Lemonldap::NG::Portal::Main::Constan
 
 extends 'Lemonldap::NG::Common::Module';
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 
 # PROPERTIES
 
@@ -45,7 +45,6 @@ sub newLdap {
         )
       )
     {
-        $self->logger->error("LDAP initialization error: $@");
         return undef;
     }
 
@@ -54,9 +53,6 @@ sub newLdap {
     if ( $msg->code ) {
         $self->logger->error( 'LDAP test has failed: ' . $msg->error );
     }
-    elsif ( $self->{conf}->{ldapPpolicyControl} and not $ldap->loadPP() ) {
-        $self->logger->error("LDAP password policy error");
-    }
     return $ldap;
 }
 
@@ -76,8 +72,8 @@ has findUserFilter => (
     is      => 'ro',
     lazy    => 1,
     builder => sub {
-        $_[0]->conf->{AuthLDAPFilter} ||
-        $_[0]->conf->{LDAPFilter}
+        $_[0]->conf->{AuthLDAPFilter}
+          || $_[0]->conf->{LDAPFilter}
           || '(&(uid=$user)(objectClass=inetOrgPerson))';
     }
 );
@@ -132,7 +128,7 @@ sub getUser {
     $self->validateLdap;
     return PE_LDAPCONNECTFAILED unless $self->ldap;
 
-    $self->bind();
+    return PE_LDAPERROR unless $self->bind();
 
     my $mesg = $self->ldap->search(
         base   => $self->conf->{ldapBase},
@@ -234,6 +230,10 @@ sub findUser {
 # Validate LDAP connection before use
 sub validateLdap {
     my ($self) = @_;
+    local $SIG{'PIPE'} = sub {
+        $self->logger->info("Reconnecting to LDAP server due to broken socket");
+    };
+
     unless ($self->ldap
         and $self->ldap->root_dse( attrs => ['supportedLDAPVersion'] ) )
     {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Net/LDAP.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Net/LDAP.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Net/LDAP.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Net/LDAP.pm	2022-02-19 16:04:21.000000000 +0000
@@ -7,21 +7,14 @@ use Net::LDAP;    #inherits
 use Net::LDAP::Util qw(escape_filter_value);
 use base qw(Net::LDAP);
 use Lemonldap::NG::Portal::Main::Constants ':all';
+use Net::LDAP::Control::PasswordPolicy;
 use Encode;
 use Unicode::String qw(utf8);
 use Scalar::Util 'weaken';
 use IO::Socket::Timeout;
 use utf8;
 
-our $VERSION  = '2.0.10';
-our $ppLoaded = 0;
-
-BEGIN {
-    eval {
-        require threads::shared;
-        threads::shared::share($ppLoaded);
-    };
-}
+our $VERSION = '2.0.14';
 
 # INITIALIZATION
 
@@ -59,7 +52,7 @@ sub new {
         ( $conf->{ldapVerify}  ? ( verify  => $conf->{ldapVerify} )  : () ),
     );
     unless ($self) {
-        $portal->logger->error($@);
+        $portal->logger->error( "LDAP initialization error: " . $@ );
         return 0;
     }
     elsif ( $Net::LDAP::VERSION < '0.64' ) {
@@ -72,7 +65,7 @@ sub new {
             and $self->socket->errstr < 0 )
         {
             $portal->logger->error(
-                "SSL connection error: " . $self->socket->errstr );
+                "LDAP SSL connection failed: " . $self->socket->errstr );
             return 0;
         }
     }
@@ -91,7 +84,7 @@ sub new {
         $h{verify} ||= $conf->{ldapVerify} if ( $conf->{ldapVerify} );
         my $mesg = $self->start_tls(%h);
         if ( $mesg->code ) {
-            $portal->logger->error( 'StartTLS failed: ' . $mesg->error );
+            $portal->logger->error( 'LDAP StartTLS failed: ' . $mesg->error );
             return 0;
         }
     }
@@ -135,7 +128,43 @@ sub bind {
             };
             print STDERR "$@\n" if ($@);
         }
+
+        if ( $self->{conf}->{ldapPpolicyControl} ) {
+            my $pp = Net::LDAP::Control::PasswordPolicy->new();
+            $args{control} = [$pp];
+        }
+
         $mesg = $self->SUPER::bind( $dn, %args );
+
+        if ( $mesg->code ) {
+            my ($resp) = $mesg->control("1.3.6.1.4.1.42.2.27.8.5.1");
+
+            # Check for ppolicy error
+            my $pp_error = $resp->pp_error if ( defined($resp) );
+            if ( defined $pp_error ) {
+                my $ppolicy_error = [
+                    "password expired",
+                    "account locked",
+                    "change after reset",
+                    "password mod not allowed",
+                    "supply old password",
+                    "insufficient password quality",
+                    "password too short",
+                    "password too young",
+                    "password in history"
+                ]->[$pp_error];
+
+                $self->{portal}
+                  ->logger->error( "Error when binding to LDAP server: "
+                      . $mesg->error
+                      . " | extended ppolicy control response error: $ppolicy_error"
+                  );
+            }
+            else {
+                $self->{portal}->logger->error(
+                    "Error when binding to LDAP server: " . $mesg->error );
+            }
+        }
     }
     else {
         $mesg = $self->SUPER::bind();
@@ -158,30 +187,6 @@ sub unbind {
     return $mesg;
 }
 
-## @method private boolean loadPP ()
-# Load Net::LDAP::Control::PasswordPolicy
-# @return true if succeed.
-sub loadPP {
-    my $self = shift;
-    return 1 if ($ppLoaded);
-
-    # Minimal version of Net::LDAP required
-    if ( $Net::LDAP::VERSION < 0.38 ) {
-        die(
-"Module Net::LDAP is too old for password policy, please install version 0.38 or higher"
-        );
-    }
-
-    # Require Perl module
-    eval { require Net::LDAP::Control::PasswordPolicy };
-    if ($@) {
-        $self->{portal}->logger->error(
-            "Module Net::LDAP::Control::PasswordPolicy not found in @INC");
-        return 0;
-    }
-    $ppLoaded = 1;
-}
-
 ## @method protected int userBind(string dn, hash args)
 # Call bind() with dn/password and return
 # @param $dn LDAP distinguish name
@@ -202,7 +207,7 @@ sub userBind {
         # Get server control response
         my ($resp) = $mesg->control("1.3.6.1.4.1.42.2.27.8.5.1");
 
-        # Return direct unless control resonse
+        # Return direct unless control response
         unless ( defined $resp ) {
             if ( $mesg->code == 49 ) {
                 $self->{portal}->userLogger->warn(
@@ -623,39 +628,6 @@ sub userModifyPassword {
     }
 }
 
-## @method protected Lemonldap::NG::Portal::_LDAP ldap()
-# @return Lemonldap::NG::Portal::_LDAP object
-sub ldap {
-    my $self = shift;
-    return $self->{ldap}
-      if ( ref( $self->{ldap} )
-        and $self->{flags}->{ldapActive} );
-    if ( $self->{ldap} = Lemonldap::NG::Portal::_LDAP->new($self)
-        and my $mesg = $self->{ldap}->bind )
-    {
-        if ( $mesg->code != 0 ) {
-            $self->logger->error( "LDAP error: " . $mesg->error );
-            $self->{ldap}->unbind;
-        }
-        else {
-            if ( $self->{ldapPpolicyControl}
-                and not $self->{ldap}->loadPP() )
-            {
-                $self->logger->error("LDAP password policy error");
-                $self->{ldap}->unbind;
-            }
-            else {
-                $self->{flags}->{ldapActive} = 1;
-                return $self->{ldap};
-            }
-        }
-    }
-    else {
-        $self->logger->error("LDAP error: $@");
-    }
-    return 0;
-}
-
 ## @method string searchGroups(string base, string key, string value, string attributes, hashref dupcheck)
 # Get groups from LDAP directory
 # @param base LDAP search base
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Notifications/XML.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Notifications/XML.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Notifications/XML.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Notifications/XML.pm	2022-02-19 16:04:21.000000000 +0000
@@ -69,7 +69,7 @@ sub checkForNotifications {
     }
 
     # Transform notifications
-    my $i   = 0;                                # Files count
+    my $i   = 0;    # Files count
     my $now = strftime "%Y-%m-%d", localtime;
 
     foreach my $file ( values %$notifs ) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm	2022-02-19 16:04:21.000000000 +0000
@@ -22,14 +22,14 @@ use Mouse;
 
 use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_REDIRECT);
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 
 # OpenID Connect standard claims
 use constant PROFILE => [
     qw/name family_name given_name middle_name nickname preferred_username
       profile picture website gender birthdate zoneinfo locale updated_at/
 ];
-use constant EMAIL => [qw/email email_verified/];
+use constant EMAIL   => [qw/email email_verified/];
 use constant ADDRESS =>
   [qw/formatted street_address locality region postal_code country/];
 use constant PHONE => [qw/phone_number phone_number_verified/];
@@ -1031,7 +1031,7 @@ sub storeState {
     # check if there are data to store
     my $infos;
     foreach (@data) {
-        $infos->{$_} = $req->{$_} if $req->{$_};
+        $infos->{$_}        = $req->{$_}       if $req->{$_};
         $infos->{"data_$_"} = $req->data->{$_} if $req->data->{$_};
     }
     return unless ($infos);
@@ -1064,7 +1064,9 @@ sub extractState {
 
     # Push values in $self
     foreach ( keys %{$stateSession} ) {
-        next if $_ =~ /(type|_session_id|_session_kind|_utime)/;
+        next
+          if $_ =~
+/^(?:type|_session_id|_session_kind|_utime|tokenTimeoutTimestamp|tokenSessionStartTimestamp)$/;
         my $tmp = $stateSession->{$_};
         if (s/^data_//) {
             $req->data->{$_} = $tmp;
@@ -1524,7 +1526,7 @@ sub getScope {
 
             # Set a magic "$requested" variable that contains true if the
             # scope was requested by the application
-            my $requested = grep { $_ eq $dynamicScope } @scope_values;
+            my $requested  = grep { $_ eq $dynamicScope } @scope_values;
             my $attributes = { %{ $req->userData }, requested => $requested };
 
             # If scope is granted by the rule
@@ -1546,7 +1548,10 @@ sub getScope {
     }
 
     $self->p->processHook( $req, 'oidcResolveScope', \@scope_values, $rp );
-    return join( ' ', @scope_values );
+
+    my $scope_str = join( ' ', @scope_values );
+    $self->logger->debug("Resolved scopes: $scope_str");
+    return $scope_str;
 }
 
 # Return Hash of UserInfo data
@@ -1647,7 +1652,7 @@ sub buildUserInfoResponseFromData {
     }
 
     my $h = $self->p->processHook( $req, 'oidcGenerateUserInfoResponse',
-        $userinfo_response );
+        $userinfo_response, $rp );
     return {} if ( $h != PE_OK );
 
     return $userinfo_response;
@@ -1745,7 +1750,7 @@ sub createJWT {
     my $jwt_payload = encode_base64url( to_json($payload), "" );
 
     # JWT header
-    my $typ = $type || "JWT";
+    my $typ             = $type || "JWT";
     my $jwt_header_hash = { typ => $typ, alg => $alg };
     if ( $alg eq "RS256" or $alg eq "RS384" or $alg eq "RS512" ) {
         $jwt_header_hash->{kid} = $self->conf->{oidcServiceKeyIdSig}
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Remote.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Remote.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Remote.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Remote.pm	2022-02-19 16:04:21.000000000 +0000
@@ -2,11 +2,18 @@ package Lemonldap::NG::Portal::Lib::Remo
 
 use strict;
 use Mouse;
-use Lemonldap::NG::Common::Session;
-use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_ERROR PE_REDIRECT);
 use MIME::Base64;
+use Lemonldap::NG::Common::Session;
+use Lemonldap::NG::Portal::Main::Constants qw(
+  URIRE
+  PE_OK
+  PE_ERROR
+  PE_REDIRECT
+);
+
+our $VERSION = '2.0.14';
 
-our $VERSION = '2.0.0';
+has cookieName => ( is => 'rw' );
 
 # INITIALIZATION
 
@@ -20,31 +27,39 @@ sub init {
         $self->error( "Missing required parameters" . join( ', ', @missing ) );
         return 0;
     }
+
+    unless ( $self->conf->{remotePortal} =~ URIRE ) {
+        $self->error("Bad remotePortal URL");
+        return 0;
+    }
+
     eval "require " . $self->conf->{remoteGlobalStorage};
     if ($@) {
         $self->error($@);
         return 0;
     }
-    $self->conf->{remoteCookieName} ||= $self->conf->{cookieName};
+    $self->cookieName( $self->conf->{remoteCookieName}
+          || $self->conf->{cookieName} );
+
+    return 1;
 }
 
 # RUNNING METHODS
 
 ## @apmethod int checkRemoteId()
 # check if a CDA mechanism has been instantiated and if session is available.
-# Redirect the user to the remote portal else by calling goToPortal().
+# Redirect user to remote portal else by calling goToPortal().
 # @return Lemonldap::NG::Portal constant
 sub checkRemoteId {
     my ( $self, $req ) = @_;
     my %h;
 
-    if ( my $rId = $req->param( $self->conf->{remoteCookieName} ) ) {
+    if ( my $rId = $req->param( $self->cookieName ) ) {
         $req->mustRedirect(1);
 
         # Trying to recover session from global session storage
-
         my $remoteSession = Lemonldap::NG::Common::Session->new( {
-                storageModule => $self->conf->{remoteGlobalStorage},
+                storageModule        => $self->conf->{remoteGlobalStorage},
                 storageModuleOptions =>
                   $self->conf->{remoteGlobalStorageOptions},
                 cacheModule        => $self->conf->{localSessionStorage},
@@ -53,7 +68,6 @@ sub checkRemoteId {
                 kind               => "SSO",
             }
         );
-
         if ( $remoteSession->error ) {
             $self->logger->error("Remote session error");
             $self->logger->error( $remoteSession->error );
@@ -62,24 +76,26 @@ sub checkRemoteId {
 
         %{ $req->data->{rSessionInfo} } = %{ $remoteSession->data() };
         delete( $req->data->{rSessionInfo}->{'_password'} )
-          unless ( $self->conf->{storePassword} );
+          unless $self->conf->{storePassword};
         return PE_OK;
     }
+
     return $self->goToPortal($req);
 }
 
 ## @method protected void goToPortal()
-# Redirect the user to the remote portal.
+# Redirect user to remote portal.
 sub goToPortal {
     my ( $self, $req ) = @_;
     $req->urldc(
-        $self->conf->{remotePortal} . "?url="
+        $self->conf->{remotePortal} . '?url='
           . encode_base64(
             $self->conf->{portal}
               . ( $req->query_string ? '?' . $req->query_string : '' ),
             ''
           )
     );
+
     return PE_REDIRECT;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/RESTProxy.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/RESTProxy.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/RESTProxy.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/RESTProxy.pm	2022-01-22 14:30:19.000000000 +0000
@@ -4,28 +4,45 @@ use strict;
 use JSON;
 use Mouse;
 use Lemonldap::NG::Common::UserAgent;
-use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_ERROR PE_BADCREDENTIALS);
+use Lemonldap::NG::Portal::Main::Constants qw(
+  URIRE
+  PE_OK
+  PE_ERROR
+  PE_BADCREDENTIALS
+);
 use Lemonldap::NG::Common::FormEncode;
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
-has ua => ( is => 'rw' );
+has ua             => ( is => 'rw' );
+has cookieName     => ( is => 'rw' );
+has sessionService => ( is => 'rw' );
 
 # INITIALIZATION
 
 sub init {
     my ($self) = @_;
-    $self->conf->{remoteCookieName} ||= $self->conf->{cookieName};
-    $self->conf->{proxySessionService} ||=
-      $self->conf->{proxyAuthService} . '/session/my';
-    $self->conf->{proxySessionService} =~ s#/*$##;
-    $self->ua( Lemonldap::NG::Common::UserAgent->new( $self->conf ) );
-    $self->ua->default_header( Accept => 'application/json' );
 
-    unless ( defined $self->conf->{proxyAuthService} ) {
-        $self->error("Missing proxyAuthService parameter");
+    unless ( defined $self->conf->{proxyAuthService}
+        && $self->conf->{proxyAuthService} =~ URIRE )
+    {
+        $self->error("Bad or missing proxyAuthService parameter");
         return 0;
     }
+
+    my $sessionService = $self->conf->{proxySessionService}
+      || $self->conf->{proxyAuthService} . '/session/my';
+    $sessionService =~ s#/*$##;
+    unless ( $sessionService =~ URIRE ) {
+        $self->error("Malformed proxySessionService parameter");
+        return 0;
+    }
+    $self->sessionService($sessionService);
+    $self->ua( Lemonldap::NG::Common::UserAgent->new( $self->conf ) );
+    $self->ua->default_header( Accept => 'application/json' );
+    $self->cookieName( $self->conf->{proxyCookieName}
+          || $self->conf->{cookieName} );
+
     return 1;
 }
 
@@ -37,8 +54,26 @@ sub getUser {
     return PE_OK if ( $req->data->{_proxyQueryDone} );
     $self->logger->debug(
         'Proxy push auth to ' . $self->conf->{proxyAuthService} );
-    my $resp = $self->ua->post( $self->conf->{proxyAuthService},
-        { user => $req->{user}, password => $req->data->{password} } );
+    my $resp = $self->ua->post(
+        $self->conf->{proxyAuthService},
+        {
+            user     => $req->{user},
+            password => $req->data->{password},
+            (
+                $self->conf->{proxyAuthServiceChoiceParam}
+                  && $self->conf->{proxyAuthServiceChoiceValue}
+                ? ( $self->conf->{proxyAuthServiceChoiceParam} =>
+                      $self->conf->{proxyAuthServiceChoiceValue} )
+                : ()
+            ),
+            (
+                $self->conf->{proxyAuthServiceImpersonation}
+                  && $req->param('spoofId')
+                ? ( spoofId => $req->param('spoofId') )
+                : ()
+            )
+        }
+    );
     unless ( $resp->is_success ) {
         $self->logger->error(
             'Unable to query authentication service: ' . $resp->status_line );
@@ -50,17 +85,23 @@ sub getUser {
         $self->logger->error("Bad content: $@");
         return PE_ERROR;
     }
-    $req->sessionInfo->{_proxyQueryDone}++;
     unless ( $res->{result} ) {
-        $self->userLogger->notice("Authentication refused for $req->{user}");
+        $self->userLogger->warn("Authentication failed for $req->{user}");
         $self->setSecurity($req);
         return PE_BADCREDENTIALS;
     }
+    my $name = $self->cookieName;
+    unless ( grep /\b$name=/, $resp->header('Set-Cookie') ) {
+        $self->logger->error("No cookie named '$name'");
+        return PE_ERROR;
+    }
+
     $req->sessionInfo->{_proxyCookies} = join '; ',
       map { s/;.*$//; $_ } $resp->header('Set-Cookie');
     $self->logger->debug( 'Store remote cookies in session ('
           . $req->sessionInfo->{_proxyCookies}
           . ')' );
+    $req->data->{_proxyQueryDone}++;
 
     return PE_OK;
 }
@@ -74,8 +115,10 @@ sub findUser {
 sub setSessionInfo {
     my ( $self, $req ) = @_;
     return PE_OK if ( $req->data->{_setSessionInfoDone} );
+    $self->logger->debug(
+        'Proxy requests sessionInfo to ' . $self->sessionService . '/global' );
     my $q = HTTP::Request->new(
-        GET => $self->conf->{proxySessionService} . '/global',
+        GET => $self->sessionService . '/global',
         [
             Cookie => $req->sessionInfo->{_proxyCookies},
             Accept => 'application/json'
@@ -97,7 +140,7 @@ sub setSessionInfo {
         $req->{sessionInfo}->{$_} ||= $res->{$_} unless (/^_/);
     }
     $req->data->{_setSessionInfoDone}++;
-    
+
     return PE_OK;
 }
 
@@ -113,7 +156,27 @@ sub authLogout {
         ]
     );
     my $resp = $self->ua->request($q);
-    
+    unless ( $resp->is_success ) {
+        $self->logger->error(
+            'Unable to query authentication service: ' . $resp->status_line );
+        return PE_OK;
+    }
+    $self->logger->debug('Proxy gets a response');
+    my $res = eval { JSON::from_json( $resp->content, { allow_nonref => 1 } ) };
+    if ($@) {
+        $self->logger->error("Bad content: $@");
+        return PE_OK;
+    }
+    my $user = $req->{sessionInfo}->{ $self->conf->{whatToTrace} };
+    unless ( $res->{result} ) {
+        $self->userLogger->warn("Internal Portal logout failed for $user")
+          if $user;
+        return PE_OK;
+    }
+    $self->userLogger->notice(
+        "User $user has been disconnected from internal Portal")
+      if $user;
+
     return PE_OK;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SAML.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SAML.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SAML.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SAML.pm	2022-02-19 16:04:21.000000000 +0000
@@ -7,7 +7,7 @@ use Lemonldap::NG::Common::Session;
 use Lemonldap::NG::Common::UserAgent;
 use Lemonldap::NG::Common::FormEncode;
 use XML::Simple;
-use HTML::Entities qw(decode_entities);
+use HTML::Entities qw(decode_entities encode_entities);
 use MIME::Base64;
 use HTTP::Request;         # SOAP call
 use POSIX qw(strftime);    # Convert SAML2 date into timestamp
@@ -21,7 +21,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_SAML_SLO_ERROR
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 # PROPERTIES
 
@@ -300,7 +300,7 @@ sub loadIDPs {
         }
         $self->logger->debug("Set encryption mode $encryption_mode on IDP $_");
 
-        # Set signature method if overriden
+        # Set signature method if overridden
         my $signature_method = $self->conf->{samlIDPMetaDataOptions}->{$_}
           ->{samlIDPMetaDataOptionsSignatureMethod};
         if ($signature_method) {
@@ -471,7 +471,7 @@ sub loadSPs {
         }
         $self->logger->debug("Set encryption mode $encryption_mode on SP $_");
 
-        # Set signature method if overriden
+        # Set signature method if overridden
         my $signature_method = $self->conf->{samlSPMetaDataOptions}->{$_}
           ->{samlSPMetaDataOptionsSignatureMethod};
         if ($signature_method) {
@@ -550,7 +550,7 @@ sub checkMessage {
             $message = $self->resolveArtifact( $profile, $artifact, $method );
 
             # Request or response ?
-            if ( $message =~ /samlp:response/i ) {
+            if ( $self->_isArtifactSamlResponse($message) ) {
                 $response = $message;
             }
             else {
@@ -598,7 +598,7 @@ sub checkMessage {
                   $self->resolveArtifact( $profile, $artifact, $method );
 
                 # Request or response ?
-                if ( $message =~ /samlp:response/i ) {
+                if ( $self->_isArtifactSamlResponse($message) ) {
                     $response = $message;
                 }
                 else {
@@ -627,6 +627,29 @@ sub checkMessage {
     return ( $request, $response, $method, $relaystate, $artifact ? 1 : 0 );
 }
 
+sub _isArtifactSamlResponse {
+    my ( $self, $message ) = @_;
+
+    my $type = eval {
+        my $resp = Lasso::Samlp2ArtifactResponse->new;
+        $resp->init_from_message($message);
+        $resp->any->get_name;
+    };
+
+    if ($@) {
+        $self->logger->warn("Could not detect type of Artifact response");
+        return;
+    }
+
+    $self->logger->debug("Artifact response type is $type");
+    if ( $type eq "Response" ) {
+        return 1;
+    }
+    else {
+        return 0;
+    }
+}
+
 ## @method boolean checkLassoError(Lasso::Error error, string level)
 # Log Lasso error code and message if this is actually a Lasso::Error with code > 0
 # @param error Lasso error object
@@ -1864,6 +1887,10 @@ sub resolveArtifact {
             $message = $soap_answer->content();
             $self->logger->debug("Get message $message");
         }
+        else {
+            $self->logger->error(
+                "Error while sending message: " . $soap_answer->status_line );
+        }
     }
 
     return $message;
@@ -2533,8 +2560,10 @@ sub sendLogoutResponseToServiceProvider
         $req->{postFields} = { 'SAMLResponse' => $slo_body };
 
         # RelayState
-        $req->{postFields}->{'RelayState'} = $relaystate
-          if ($relaystate);
+        if ($relaystate) {
+            $req->{postFields}->{'RelayState'} = encode_entities($relaystate);
+            $req->data->{safeHiddenFormValues}->{RelayState} = 1;
+        }
 
         return $self->p->do( $req, ['autoPost'] );
     }
@@ -3011,7 +3040,7 @@ sub createAttributeValue {
 
     # Decode UTF-8
     $self->logger->debug("Decode UTF8 value $value") if $force_utf8;
-    $value = decode( "utf8", $value ) if $force_utf8;
+    $value = decode( "utf8", $value )                if $force_utf8;
     $self->logger->debug("Create attribute value $value");
 
     # SAML2 attribute value
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SMTP.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SMTP.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SMTP.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SMTP.pm	2022-02-19 16:04:21.000000000 +0000
@@ -47,7 +47,7 @@ sub loadMailTemplate {
     my ( $self, $req, $name, %prm ) = @_;
 
     # HTML::Template cache interferes with email translation (#1897)
-    $prm{cache} = 0 unless defined $prm{cache};
+    $prm{cache}                   = 0 unless defined $prm{cache};
     $prm{params}->{STATIC_PREFIX} = $self->p->staticPrefix;
     $prm{params}->{MAIN_LOGO}     = $self->conf->{portalMainLogo};
     my %extra =
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SOAPProxy.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SOAPProxy.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SOAPProxy.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SOAPProxy.pm	2022-01-22 14:30:19.000000000 +0000
@@ -3,13 +3,20 @@ package Lemonldap::NG::Portal::Lib::SOAP
 use strict;
 use Mouse;
 use SOAP::Lite;
-use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_ERROR PE_BADCREDENTIALS);
+use Lemonldap::NG::Portal::Main::Constants qw(
+  URIRE
+  PE_OK
+  PE_ERROR
+  PE_BADCREDENTIALS
+);
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 # INITIALIZATION
 
-has urn => (
+has cookieName     => ( is => 'rw' );
+has sessionService => ( is => 'rw' );
+has urn            => (
     is      => 'rw',
     lazy    => 1,
     default => sub {
@@ -19,13 +26,24 @@ has urn => (
 
 sub init {
     my ($self) = @_;
-    $self->conf->{remoteCookieName}    ||= $self->conf->{cookieName};
-    $self->conf->{proxySessionService} ||= $self->conf->{proxyAuthService};
 
-    unless ( defined $self->conf->{proxyAuthService} ) {
-        $self->error("Missing proxyAuthService parameter");
+    unless ( defined $self->conf->{proxyAuthService}
+        && $self->conf->{proxyAuthService} =~ URIRE )
+    {
+        $self->error("Bad or missing proxyAuthService parameter");
+        return 0;
+    }
+
+    my $sessionService = $self->conf->{proxySessionService}
+      || $self->conf->{proxyAuthService};
+    unless ( $sessionService =~ URIRE ) {
+        $self->error("Malformed proxySessionService parameter");
         return 0;
     }
+    $self->sessionService($sessionService);
+    $self->cookieName( $self->conf->{proxyCookieName}
+          || $self->conf->{cookieName} );
+
     return 1;
 }
 
@@ -37,6 +55,8 @@ no warnings 'once';
 sub getUser {
     my ( $self, $req ) = @_;
     return PE_OK if ( $req->data->{_proxyQueryDone} );
+    $self->logger->debug(
+        'Proxy push auth to ' . $self->conf->{proxyAuthService} );
     my $soap =
       SOAP::Lite->proxy( $self->conf->{proxyAuthService} )->uri( $self->urn );
     my $r = $soap->getCookies( $req->{user}, $req->data->{password} );
@@ -45,6 +65,7 @@ sub getUser {
               . $r->fault->{faultstring} );
         return PE_ERROR;
     }
+    $self->logger->debug('Proxy gets a response');
     my $res = $r->result();
 
     # If authentication failed, display error
@@ -54,8 +75,7 @@ sub getUser {
         $self->setSecurity($req);
         return PE_BADCREDENTIALS;
     }
-    unless ( $req->data->{_remoteId} =
-        $res->{cookies}->{ $self->conf->{remoteCookieName} } )
+    unless ( $req->data->{_remoteId} = $res->{cookies}->{ $self->cookieName } )
     {
         $self->logger->error("No cookie named $self->{remoteCookieName}");
         return PE_ERROR;
@@ -74,16 +94,17 @@ sub findUser {
 sub setSessionInfo {
     my ( $self, $req ) = @_;
     return PE_OK if ( $req->data->{_setSessionInfoDone} );
-    my $soap = SOAP::Lite->proxy( $self->conf->{proxySessionService} )
-      ->uri( $self->urn );
-    my $r = $soap->getAttributes( $req->data->{_remoteId} );
-    if ( $r->fault ) {
-        $self->logger->error( "Unable to query authentication service"
-              . $r->fault->{faultstring} );
-    }
+    $self->logger->debug(
+        'Proxy requests sessionInfo to ' . $self->sessionService . '/global' );
+    my $soap = SOAP::Lite->proxy( $self->sessionService )->uri( $self->urn );
+    my $r    = $soap->getAttributes( $req->data->{_remoteId} );
+    $self->logger->error(
+        "Unable to query session service: " . $r->fault->{faultstring} )
+      if ( $r->fault );
+
     my $res = $r->result();
     if ( $res->{error} ) {
-        $self->userLogger->warn("Unable to get attributes for $self->{user} ");
+        $self->userLogger->warn("Unable to get attributes for $self->{user}");
         return PE_ERROR;
     }
     foreach ( keys %{ $res->{attributes} } ) {
@@ -96,6 +117,8 @@ sub setSessionInfo {
 }
 
 sub authLogout {
+
+    # Nothing to do here
     return PE_OK;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/WebAuthn.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/WebAuthn.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/WebAuthn.pm	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/WebAuthn.pm	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,342 @@
+package Lemonldap::NG::Portal::Lib::WebAuthn;
+
+use strict;
+use Mouse::Role;
+use MIME::Base64 qw(encode_base64url decode_base64url);
+use JSON qw(decode_json from_json to_json);
+use Digest::SHA qw(sha256);
+use URI;
+use Carp;
+
+our $VERSION = '2.0.12';
+
+has rp_id    => ( is => 'rw', lazy    => 1, builder => "_build_rp_id" );
+has origin   => ( is => 'rw', lazy    => 1, builder => "_build_origin" );
+has type     => ( is => 'ro', default => 'WebAuthn' );
+has verifier => ( is => 'rw', lazy    => 1, builder => "_build_verifier" );
+
+sub _build_verifier {
+    my $self = shift;
+    return Authen::WebAuthn->new(
+        rp_id  => $self->rp_id,
+        origin => $self->origin,
+    );
+}
+
+sub _build_rp_id {
+    my ($self) = @_;
+
+    # TODO make this configurable
+    my $portal_uri = URI->new( $self->{conf}->{portal} );
+    return $portal_uri->authority;
+}
+
+sub _build_origin {
+    my ($self) = @_;
+    my $portal_uri = URI->new( $self->{conf}->{portal} );
+    return ( $portal_uri->scheme . "://" . $portal_uri->authority );
+}
+
+around 'init' => sub {
+    my $orig = shift;
+    my $self = shift;
+
+    eval { require Authen::WebAuthn };
+    if ($@) {
+        $self->logger->error("Can't load WebAuthn library: $@");
+        $self->error("Can't load WebAuthn library: $@");
+        return 0;
+    }
+
+    return $orig->( $self, @_ );
+};
+
+sub getUserHandle {
+    my ( $self, $req, $data ) = @_;
+    return $data->{_webAuthnUserHandle};
+}
+
+sub setUserHandle {
+    my ( $self, $req, $user_handle ) = @_;
+    $self->p->updatePersistentSession( $req,
+        { _webAuthnUserHandle => $user_handle } );
+    return;
+}
+
+sub generateChallenge {
+    my ( $self, $req, $data ) = @_;
+
+    # Find webauthn devices for user
+    my @webauthn_devices = $self->find2fByType( $req, $data, $self->type );
+    unless (@webauthn_devices) {
+        return;
+    }
+
+    my $challenge_base64 = encode_base64url( Crypt::URandom::urandom(32) );
+    my $userVerification = $self->conf->{webauthn2fUserVerification};
+
+    return {
+        challenge        => $challenge_base64,
+        allowCredentials => [
+            map { { type => "public-key", id => $_->{_credentialId}, } }
+              @webauthn_devices
+        ],
+        ( $userVerification ? ( userVerification => $userVerification ) : () ),
+        extensions => {
+            appid => $self->origin,
+        },
+    };
+}
+
+sub validateCredential {
+    my ( $self, $req, $registration_options, $credential_json ) = @_;
+
+    my $credential = from_json($credential_json);
+
+    my $client_data_json_b64   = $credential->{response}->{clientDataJSON};
+    my $attestation_object_b64 = $credential->{response}->{attestationObject};
+
+    my $requested_uv =
+      $registration_options->{authenticatorSelection}->{userVerification} || "";
+    my $challenge_b64 = $registration_options->{challenge};
+
+    my $token_binding_id_b64 = encode_base64url(
+        $req->headers->header('Sec-Provided-Token-Binding-ID') );
+
+    return $self->verifier->validate_registration(
+        challenge_b64          => $challenge_b64,
+        requested_uv           => $requested_uv,
+        client_data_json_b64   => $client_data_json_b64,
+        attestation_object_b64 => $attestation_object_b64,
+        token_binding_id_b64   => $token_binding_id_b64
+    );
+}
+
+sub validateAssertion {
+    my ( $self, $req, $data, $signature_options, $credential_json ) = @_;
+
+    my $user = $data->{ $self->conf->{whatToTrace} };
+    $self->logger->debug("Get asserted credential $credential_json");
+    my $credential = from_json($credential_json);
+
+    my $credential_id = $credential->{id};
+    croak("Empty credential id in credential response") unless $credential_id;
+
+    # 5. If options.allowCredentials is not empty, verify that credential.id
+    # identifies one of the public key credentials listed in
+    # options.allowCredentials.
+    my @allowed_credential_ids =
+      map { $_->{id} } @{ $signature_options->{allowCredentials} };
+    if ( @allowed_credential_ids
+        and not grep { $_ eq $credential_id } @allowed_credential_ids )
+    {
+        croak("Received credential ID $credential_id was not requested");
+    }
+
+    # 6. Identify the user being authenticated and verify that this user is the
+    # owner of the public key credential source credentialSource identified by
+    # credential.id If the user was identified before the authentication
+    # ceremony was initiated, e.g., via a username or cookie, verify that the
+    # identified user is the owner of credentialSource.
+    my @webauthn_devices = $self->find2fByType( $req, $data, $self->type );
+    my @matching_credentials =
+      grep { $_->{_credentialId} eq $credential_id } @webauthn_devices;
+    if ( @matching_credentials < 1 ) {
+        croak("Received credential ID $credential_id does not belong to user");
+    }
+    if ( @matching_credentials > 1 ) {
+        croak("Found multiple credentials with ID $credential_id for user");
+    }
+    my $matching_credential = $matching_credentials[0];
+
+    # If response.userHandle is present, let userHandle be its value.
+    # Verify that userHandle also maps to the same user.
+    if ( $credential->{response}->{userHandle} ) {
+        my $user_handle         = $credential->{response}->{userHandle};
+        my $current_user_handle = $self->getUserHandle( $req, $data );
+        unless ( $user_handle eq $current_user_handle ) {
+            croak(
+"Received user handle ($user_handle) does not match current user ($current_user_handle)"
+            );
+        }
+    }
+
+    # TODO If the user was not identified before the authentication ceremony
+    # was initiated, verify that response.userHandle is present, and that the
+    # user identified by this value is the owner of credentialSource.
+    # NOTE: irrelevant for now, take this into account when implementing
+    # Auth::WebAuthn
+
+    my $client_data_json_b64   = $credential->{response}->{clientDataJSON};
+    my $authenticator_data_b64 = $credential->{response}->{authenticatorData};
+    my $signature_b64          = $credential->{response}->{signature};
+    my $extension_results      = $credential->{clientExtensionResults};
+    my $requested_uv           = $signature_options->{userVerification} || "";
+
+    my $token_binding_id_b64 = encode_base64url(
+        $req->headers->header('Sec-Provided-Token-Binding-ID') );
+
+    my $validation_result = $self->verifier->validate_assertion(
+        challenge_b64          => $signature_options->{challenge},
+        credential_pubkey_b64  => $matching_credential->{_credentialPublicKey},
+        stored_sign_count      => $matching_credential->{_signCount},
+        requested_uv           => $requested_uv,
+        client_data_json_b64   => $client_data_json_b64,
+        authenticator_data_b64 => $authenticator_data_b64,
+        signature_b64          => $signature_b64,
+        extension_results      => $extension_results,
+        token_binding_id_b64   => $token_binding_id_b64,
+    );
+
+    if ( $validation_result->{success} == 1 ) {
+        my $new_signature_count = $validation_result->{signature_count};
+        $self->userLogger->info(
+                "Successfully verified signature with count "
+              . "$new_signature_count for $user" );
+
+        # Update storedSignCount to be the value of authData.signCount
+        $self->update2fDevice( $req, $data, $self->type,
+            "_credentialId", $credential_id, "_signCount",
+            $new_signature_count );
+    }
+
+    return $validation_result;
+}
+
+sub decode_credential {
+    my ( $self, $json ) = @_;
+    my $credential = decode_json($json);
+
+    # Decode ClientDataJSON
+    if ( $credential->{response}->{clientDataJSON} ) {
+        $credential->{response}->{clientDataJSON} = decode_json(
+            decode_base64url( $credential->{response}->{clientDataJSON} ) );
+    }
+
+    # Decode attestation object
+    if ( $credential->{response}->{attestationObject} ) {
+        $credential->{response}->{attestationObject} =
+          getAttestationObject( $credential->{response}->{attestationObject} );
+    }
+
+    # Decode authenticator data
+    if ( $credential->{response}->{authenticatorData} ) {
+        $credential->{response}->{authenticatorData} =
+          getAuthData(
+            decode_base64url( $credential->{response}->{authenticatorData} ) );
+    }
+
+    # Decode rawID
+    if ( $credential->{rawId} ) {
+        $credential->{rawId} = decode_base64url( $credential->{rawId} );
+    }
+
+    return $credential;
+}
+
+sub update2fDevice {
+    my ( $self, $req, $info, $type, $key, $value, $update_key, $update_value )
+      = @_;
+
+    my $user = $info->{ $self->conf->{whatToTrace} };
+
+    my $_2fDevices = $self->get2fDevices( $req, $info );
+    return 0 unless $_2fDevices;
+
+    my @found =
+      grep { $_->{type} eq $type and $_->{$key} eq $value } @{$_2fDevices};
+
+    for my $device (@found) {
+        $device->{$update_key} = $update_value;
+    }
+
+    if (@found) {
+        $self->p->updatePersistentSession( $req,
+            { _2fDevices => to_json($_2fDevices) }, $user );
+        return 1;
+    }
+    return 0;
+}
+
+sub add2fDevice {
+    my ( $self, $req, $info, $device ) = @_;
+
+    my $_2fDevices = $self->get2fDevices( $req, $info );
+
+    push @{$_2fDevices}, $device;
+    $self->logger->debug(
+        "Append 2F Device: { type => 'Webauthn', name => $device->{name} }");
+    $self->p->updatePersistentSession( $req,
+        { _2fDevices => to_json($_2fDevices) } );
+    return 1;
+}
+
+sub del2fDevice {
+    my ( $self, $req, $info, $type, $epoch ) = @_;
+
+    my $_2fDevices = $self->get2fDevices( $req, $info );
+    return 0 unless $_2fDevices;
+
+    my @updated_2fDevices =
+      grep { not( $_->{type} eq $type and $_->{epoch} eq $epoch ) }
+      @{$_2fDevices};
+    $self->logger->debug(
+        "Deleted 2F Device: { type => $type, epoch => $epoch }");
+    $self->p->updatePersistentSession( $req,
+        { _2fDevices => to_json( [@updated_2fDevices] ) } );
+    return 1;
+}
+
+sub find2fByKey {
+    my ( $self, $req, $info, $type, $key, $value ) = @_;
+
+    my $_2fDevices = $self->get2fDevices( $req, $info );
+    return unless $_2fDevices;
+
+    my @found =
+      grep { $_->{type} eq $type and $_->{$key} eq $value } @{$_2fDevices};
+    return @found;
+}
+
+## @method get2fDevices($req, $info)
+# Validate logout request
+# @param req Request object
+# @param info HashRef of session data
+# @return undef or ArrayRef of second factors
+
+sub get2fDevices {
+    my ( $self, $req, $info ) = @_;
+
+    my $_2fDevices;
+    if ( $info->{_2fDevices} ) {
+        $_2fDevices =
+          eval { from_json( $info->{_2fDevices}, { allow_nonref => 1 } ); };
+        if ($@) {
+            $self->logger->error("Corrupted session (_2fDevices): $@");
+            return;
+        }
+    }
+    else {
+        # Return new ArrayRef
+        return [];
+    }
+    if ( ref($_2fDevices) eq "ARRAY" ) {
+        return $_2fDevices;
+    }
+    else {
+        return;
+    }
+}
+
+sub find2fByType {
+    my ( $self, $req, $info, $type ) = @_;
+
+    my $_2fDevices = $self->get2fDevices( $req, $info );
+    return unless $_2fDevices;
+
+    return @{$_2fDevices} unless $type;
+    my @found = grep { $_->{type} eq $type } @{$_2fDevices};
+    return @found;
+}
+
+1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Wrapper.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Wrapper.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Wrapper.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Wrapper.pm	2022-02-19 16:04:21.000000000 +0000
@@ -32,7 +32,7 @@ sub authCancel         { '_authCancel' }
 sub _betweenAuthAndData { _wrapEntryPoint( @_, 'betweenAuthAndData' ); }
 sub _afterData          { _wrapEntryPoint( @_, 'afterData' ); }
 sub _endAuth            { _wrapEntryPoint( @_, 'endAuth' ); }
-sub _forAuthUser        { _wrapEntryPoint( @_, 'forAuthUser', 1 ); }
+sub _forAuthUser        { _wrapEntryPoint( @_, 'forAuthUser',  1 ); }
 sub _beforeLogout       { _wrapEntryPoint( @_, 'beforeLogout', 1 ); }
 sub _authCancel         { _wrapEntryPoint( @_, 'authCancel' ); }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Auth.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Auth.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Auth.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Auth.pm	2022-02-19 16:04:21.000000000 +0000
@@ -3,7 +3,7 @@ package Lemonldap::NG::Portal::Main::Aut
 use strict;
 use Mouse;
 
-our $VERSION = '2.0.0';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Portal::Main::Plugin';
 
@@ -11,4 +11,6 @@ extends 'Lemonldap::NG::Portal::Main::Pl
 
 has authnLevel => ( is => 'rw' );
 
+sub stop { 0 }
+
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Constants.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Constants.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Constants.pm	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Constants.pm	2022-01-22 14:30:19.000000000 +0000
@@ -4,7 +4,7 @@ package Lemonldap::NG::Portal::Main::Con
 use strict;
 use Exporter 'import';
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 
 use constant HANDLER => 'Lemonldap::NG::Handler::PSGI::Main';
 use constant URIRE =>
@@ -112,6 +112,7 @@ use constant {
     PE_NO_SECOND_FACTORS                 => 103,
     PE_BAD_DEVOPS_FILE                   => 104,
     PE_FILENOTFOUND                      => 105,
+    PE_OIDC_AUTH_ERROR                   => 106,
 };
 
 sub portalConsts {
@@ -130,6 +131,7 @@ sub portalConsts {
         '103' => 'PE_NO_SECOND_FACTORS',
         '104' => 'PE_BAD_DEVOPS_FILE',
         '105' => 'PE_FILENOTFOUND',
+        '106' => 'PE_OIDC_AUTH_ERROR',
         '2'   => 'PE_FORMEMPTY',
         '20'  => 'PE_NO_PASSWORD_BE',
         '21'  => 'PE_PP_ACCOUNT_LOCKED',
@@ -328,7 +330,8 @@ our @EXPORT_OK = (
     'PE_UPGRADESESSION',
     'PE_NO_SECOND_FACTORS',
     'PE_BAD_DEVOPS_FILE',
-    'PE_FILENOTFOUND'
+    'PE_FILENOTFOUND',
+    'PE_OIDC_AUTH_ERROR'
 );
 our %EXPORT_TAGS = ( 'all' => [ @EXPORT_OK, 'import' ], );
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Display.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Display.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Display.pm	2021-08-21 17:42:59.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Display.pm	2022-02-19 16:04:21.000000000 +0000
@@ -2,7 +2,7 @@
 # Display functions for LemonLDAP::NG Portal
 package Lemonldap::NG::Portal::Main::Display;
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 package Lemonldap::NG::Portal::Main;
 use strict;
@@ -13,6 +13,7 @@ use URI;
 has isPP          => ( is => 'rw' );
 has speChars      => ( is => 'rw' );
 has skinRules     => ( is => 'rw' );
+has stayConnected => ( is => 'rw', default => sub { 0 } );
 has requireOldPwd => ( is => 'rw', default => sub { 1 } );
 
 sub displayInit {
@@ -28,17 +29,25 @@ sub displayInit {
             else {
                 $self->logger->error(
                     qq(Skin rule "$skinRule" returns an error: )
-                      . HANDLER->tsv->{jail}->error );
+                      . HANDLER->tsv->{jail}->error
+                      || 'Unable to compile rule' );
             }
         }
     }
     my $rule = HANDLER->buildSub(
         HANDLER->substitute( $self->conf->{portalRequireOldPassword} ) );
     unless ($rule) {
-        my $error = HANDLER->tsv->{jail}->error || '???';
-        $self->logger->error( "Bad requireOldPwd rule: " . $error );
+        my $error = HANDLER->tsv->{jail}->error || 'Unable to compile rule';
+        $self->logger->error("Bad requireOldPwd rule: $error");
     }
     $self->requireOldPwd($rule);
+    $rule =
+      HANDLER->buildSub( HANDLER->substitute( $self->conf->{stayConnected} ) );
+    unless ($rule) {
+        my $error = HANDLER->tsv->{jail}->error || 'Unable to compile rule';
+        $self->logger->error("Bad stayConnected rule: $error");
+    }
+    $self->stayConnected($rule);
 
     my $speChars =
       $self->conf->{passwordPolicySpecialChar} eq '__ALL__'
@@ -77,6 +86,7 @@ sub display {
             MAIN_LOGO       => $self->conf->{portalMainLogo},
             LANGS           => $self->conf->{showLanguages},
             AUTH_ERROR_TYPE => $req->error_type,
+            AUTH_ERROR_ROLE => $req->error_role,
             NOTIFICATION    => $notif,
             HIDDEN_INPUTS   => $self->buildHiddenForm($req),
             AUTH_URL        => $req->{data}->{_url},
@@ -100,6 +110,7 @@ sub display {
             LANGS           => $self->conf->{showLanguages},
             AUTH_ERROR      => $req->error,
             AUTH_ERROR_TYPE => $req->error_type,
+            AUTH_ERROR_ROLE => $req->error_role,
             AUTH_URL        => $req->{data}->{_url},
             MSG             => $req->info,
             HIDDEN_INPUTS   => $self->buildHiddenForm($req),
@@ -131,6 +142,7 @@ sub display {
             LANGS           => $self->conf->{showLanguages},
             AUTH_ERROR      => $req->error,
             AUTH_ERROR_TYPE => $req->error_type,
+            AUTH_ERROR_ROLE => $req->error_role,
             AUTH_URL        => $req->{data}->{_url},
             HIDDEN_INPUTS   => $self->buildHiddenForm($req),
             ACTIVE_TIMER    => $req->data->{activeTimer},
@@ -142,7 +154,7 @@ sub display {
             ASK_LOGINS        => $req->param('checkLogins')   || 0,
             ASK_STAYCONNECTED => $req->param('stayconnected') || 0,
             CONFIRMKEY        => $self->stamp(),
-            LIST => $req->data->{list} || [],
+            LIST              => $req->data->{list} || [],
             (
                 $req->data->{customScript}
                 ? ( CUSTOM_SCRIPT => $req->data->{customScript} )
@@ -165,8 +177,9 @@ sub display {
             LANGS           => $self->conf->{showLanguages},
             AUTH_ERROR      => $self->error,
             AUTH_ERROR_TYPE => $req->error_type,
+            AUTH_ERROR_ROLE => $req->error_role,
             MSG             => $info,
-            URL => $req->{urldc} || $self->conf->{portal},    # Fix 2158
+            URL           => $req->{urldc} || $self->conf->{portal},  # Fix 2158
             HIDDEN_INPUTS => $self->buildOutgoingHiddenForm( $req, $method ),
             ACTIVE_TIMER  => $req->data->{activeTimer},
             CHOICE_PARAM  => $self->conf->{authChoiceParam},
@@ -198,6 +211,7 @@ sub display {
             LANGS           => $self->conf->{showLanguages},
             AUTH_ERROR      => $self->error,
             AUTH_ERROR_TYPE => $req->error_type,
+            AUTH_ERROR_ROLE => $req->error_role,
             PROVIDERURI     => $p,
             MSG             => $req->info(),
             (
@@ -239,11 +253,9 @@ sub display {
             LANGS     => $self->conf->{showLanguages},
             AUTH_USER => $req->{sessionInfo}->{ $self->conf->{portalUserAttr} },
             NEWWINDOW => $self->conf->{portalOpenLinkInNewWindow},
-            LOGOUT_URL     => $self->conf->{portal} . "?logout=1",
-            APPSLIST_ORDER => $req->{sessionInfo}->{'_appsListOrder'},
-            PING           => $self->conf->{portalPingInterval},
-            REQUIRE_OLDPASSWORD =>
-              $self->requireOldPwd->( $req, $req->userData ),
+            LOGOUT_URL          => $self->conf->{portal} . "?logout=1",
+            APPSLIST_ORDER      => $req->{sessionInfo}->{'_appsListOrder'},
+            PING                => $self->conf->{portalPingInterval},
             DONT_STORE_PASSWORD => $self->conf->{browsersDontStorePassword},
             HIDE_OLDPASSWORD    => 0,
             PPOLICY_NOPOLICY    => !$self->isPP(),
@@ -254,6 +266,11 @@ sub display {
             PPOLICY_MINDIGIT    => $self->conf->{passwordPolicyMinDigit},
             PPOLICY_MINSPECHAR  => $self->conf->{passwordPolicyMinSpeChar},
             (
+                $self->requireOldPwd->( $req, $req->userData )
+                ? ( REQUIRE_OLDPASSWORD => 1 )
+                : ()
+            ),
+            (
                 $self->conf->{passwordPolicyMinSpeChar} || $self->speChars()
                 ? ( PPOLICY_ALLOWEDSPECHAR => $self->speChars() )
                 : ()
@@ -352,6 +369,7 @@ sub display {
             LANGS           => $self->conf->{showLanguages},
             AUTH_ERROR      => $req->error,
             AUTH_ERROR_TYPE => $req->error_type,
+            AUTH_ERROR_ROLE => $req->error_role,
             LOCKTIME        => $req->lockTime(),
             (
                 $req->data->{customScript}
@@ -364,43 +382,57 @@ sub display {
     # 3 Authentication has been refused OR first access
     else {
         $skinfile = 'login';
-        my $login = $self->userId($req);
-        if ( $login eq 'anonymous' ) {
-            $login = '';
-        }
-        elsif ( $req->user ) {
-            $login = $req->{user};
-        }
+        my $login = $req->user;
         %templateParams = (
             MAIN_LOGO             => $self->conf->{portalMainLogo},
             LANGS                 => $self->conf->{showLanguages},
             AUTH_ERROR            => $req->error,
             AUTH_ERROR_TYPE       => $req->error_type,
+            AUTH_ERROR_ROLE       => $req->error_role,
             AUTH_URL              => $req->{data}->{_url},
             LOGIN                 => $login,
             DONT_STORE_PASSWORD   => $self->conf->{browsersDontStorePassword},
             CHECK_LOGINS          => $self->conf->{portalCheckLogins},
-            ASK_LOGINS            => $req->param('checkLogins') || 0,
+            ASK_LOGINS            => $req->param('checkLogins')   || 0,
             ASK_STAYCONNECTED     => $req->param('stayconnected') || 0,
             DISPLAY_RESETPASSWORD => $self->conf->{portalDisplayResetPassword},
             DISPLAY_REGISTER      => $self->conf->{portalDisplayRegister},
-            DISPLAY_UPDATECERTIF =>
+            DISPLAY_UPDATECERTIF  =>
               $self->conf->{portalDisplayCertificateResetByMail},
             MAILCERTIF_URL => $self->conf->{certificateResetByMailURL},
             MAIL_URL       => $self->conf->{mailUrl},
             REGISTER_URL   => $self->conf->{registerUrl},
             HIDDEN_INPUTS  => $self->buildHiddenForm($req),
-            STAYCONNECTED  => $self->conf->{stayConnected},
-            IMPERSONATION  => $self->conf->{impersonationRule},
+            IMPERSONATION  => $self->conf->{impersonationRule}
+              || $self->conf->{proxyAuthServiceImpersonation},
+            ENABLE_PASSWORD_DISPLAY =>
+              $self->conf->{portalEnablePasswordDisplay},
+            (
+                $self->stayConnected->( $req, $req->sessionInfo )
+                ? ( STAYCONNECTED => 1 )
+                : ()
+            ),
             (
                 $req->data->{customScript}
                 ? ( CUSTOM_SCRIPT => $req->data->{customScript} )
                 : ()
             ),
-            ENABLE_PASSWORD_DISPLAY =>
-              $self->conf->{portalEnablePasswordDisplay},
         );
 
+        # External links
+        if ( $self->conf->{portalDisplayResetPassword} ) {
+            $templateParams{"MAIL_URL_EXTERNAL"} =
+              $self->_isExternalUrl( $self->conf->{mailUrl} );
+        }
+        if ( $self->conf->{portalDisplayRegister} ) {
+            $templateParams{"REGISTER_URL_EXTERNAL"} =
+              $self->_isExternalUrl( $self->conf->{registerUrl} );
+        }
+        if ( $self->conf->{portalDisplayCertificateResetByMail} ) {
+            $templateParams{MAILCERTIF_URL_EXTERNAL} =
+              $self->_isExternalUrl( $self->conf->{certificateResetByMailURL} );
+        }
+
         # Display captcha if it's enabled
         if ( $req->captcha ) {
             %templateParams = (
@@ -486,15 +518,15 @@ sub display {
             my $plugin =
               $self->loadedModules->{
                 "Lemonldap::NG::Portal::Plugins::FindUser"};
-            my $fields = [];
-            my $slogin;
+            my ( $fields, $slogin, $mandatory ) = ( [], '', 0 );
+
             if (   $plugin
                 && $self->conf->{findUser}
                 && $self->conf->{impersonationRule}
                 && $self->conf->{findUserSearchingAttributes} )
             {
                 $slogin = $req->data->{findUser};
-                $fields = $plugin->buildForm();
+                ( $fields, $mandatory ) = $plugin->buildForm();
             }
 
             # Authentication loop
@@ -511,6 +543,7 @@ sub display {
                     DISPLAY_OPENID_FORM  => 0,
                     DISPLAY_YUBIKEY_FORM => 0,
                     DISPLAY_FINDUSER     => scalar @$fields,
+                    MANDATORY            => $mandatory,
                     FIELDS               => $fields,
                     SPOOFID              => $slogin
                 );
@@ -535,17 +568,18 @@ sub display {
                     : 0,
                     DISPLAY_SSL_FORM  => $displayType =~ /sslform/ ? 1 : 0,
                     DISPLAY_GPG_FORM  => $displayType =~ /gpgform/ ? 1 : 0,
-                    DISPLAY_LOGO_FORM => $displayType eq "logo"    ? 1 : 0,
+                    DISPLAY_LOGO_FORM => $displayType eq "logo" ? 1 : 0,
                     DISPLAY_FINDUSER  => scalar @$fields,
                     module            => $displayType eq "logo"
                     ? $self->getModule( $req, 'auth' )
                     : "",
-                    AUTH_LOOP => [],
+                    AUTH_LOOP  => [],
                     PORTAL_URL =>
                       ( $displayType eq "logo" ? $self->conf->{portal} : 0 ),
-                    MSG     => $req->info(),
-                    FIELDS  => $fields,
-                    SPOOFID => $slogin
+                    MSG       => $req->info(),
+                    MANDATORY => $mandatory,
+                    FIELDS    => $fields,
+                    SPOOFID   => $slogin
                 );
             }
         }
@@ -618,7 +652,9 @@ sub buildHiddenForm {
 
         # Check XSS attacks
         next
-          if $self->checkXSSAttack( $_, $req->{portalHiddenFormValues}->{$_} );
+          if (!$req->data->{safeHiddenFormValues}->{$_}
+            && $self->checkXSSAttack( $_, $req->{portalHiddenFormValues}->{$_} )
+          );
 
         # Build hidden input HTML code
         # 'id' is removed to avoid warning with Choice
@@ -685,7 +721,21 @@ sub mkSessionArray {
 
     return "" unless ( ref $sessions eq "ARRAY" and @$sessions );
 
-    my @fields = sort keys %{ $self->conf->{sessionDataToRemember} };
+    # Merge user configuration with plugin self-configuration
+    my %rememberedData = %{ $self->pluginSessionDataToRemember };
+    @rememberedData{ keys %{ $self->conf->{sessionDataToRemember} } } =
+      values %{ $self->conf->{sessionDataToRemember} };
+
+    # Delete fields with an empty/undef/__hidden__ column name
+    delete @rememberedData{
+        grep {
+                 ( not $rememberedData{$_} )
+              or ( $rememberedData{$_} eq "__hidden__" )
+        } keys %rememberedData
+    };
+
+    my @fields = sort( keys %rememberedData );
+
     return $self->loadTemplate(
         $req,
         'sessionArray',
@@ -693,11 +743,8 @@ sub mkSessionArray {
             title        => $title,
             displayUser  => $displayUser,
             displayError => $displayError,
-            fields       => [
-                map { { name => $self->conf->{sessionDataToRemember}->{$_} } }
-                  @fields
-            ],
-            sessions => [
+            fields       => [ map { { name => $rememberedData{$_} } } @fields ],
+            sessions     => [
                 map {
                     my $session = $_;
                     {
@@ -763,7 +810,8 @@ sub mkOidcConsent {
         'oidcConsents',
         params => {
             partners => [
-                map { {
+                map {
+                    {
                         name        => $_,
                         epoch       => $consents->{$_}->{epoch},
                         scope       => $consents->{$_}->{scope},
@@ -776,4 +824,9 @@ sub mkOidcConsent {
     );
 }
 
+sub _isExternalUrl {
+    my ( $self, $url ) = @_;
+    return ( index( $url, $self->conf->{portal} ) < 0 );
+}
+
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm	2021-08-22 11:14:51.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm	2022-02-19 16:04:21.000000000 +0000
@@ -8,7 +8,7 @@
 #                  of lemonldap-ng.ini) and underlying handler configuration
 package Lemonldap::NG::Portal::Main::Init;
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 
 package Lemonldap::NG::Portal::Main;
 
@@ -94,6 +94,10 @@ has cors => ( is => 'rw' );
 # Cookie SameSite value
 has cookieSameSite => ( is => 'rw' );
 
+# Plugins may declare the session data they want to store in login history here
+has pluginSessionDataToRemember =>
+  ( is => 'rw', isa => "HashRef", default => sub { {} } );
+
 # INITIALIZATION
 
 sub init {
@@ -124,9 +128,9 @@ sub init {
 
     # Purge loaded module list
     $self->loadedModules( {} );
-    $self->afterSub(      {} );
-    $self->aroundSub(     {} );
-    $self->hook(          {} );
+    $self->afterSub( {} );
+    $self->aroundSub( {} );
+    $self->hook( {} );
 
     # Insert `reloadConf` in handler reload stack
     Lemonldap::NG::Handler::Main->onReload( $self, 'reloadConf' );
@@ -152,10 +156,24 @@ sub init {
 
 sub setPortalRoutes {
     my ($self) = @_;
-    $self->authRoutes(
-        { GET => {}, POST => {}, PUT => {}, DELETE => {}, OPTIONS => {} } );
-    $self->unAuthRoutes(
-        { GET => {}, POST => {}, PUT => {}, DELETE => {}, OPTIONS => {} } );
+    $self->authRoutes( {
+            GET     => {},
+            POST    => {},
+            PUT     => {},
+            PATCH   => {},
+            DELETE  => {},
+            OPTIONS => {}
+        }
+    );
+    $self->unAuthRoutes( {
+            GET     => {},
+            POST    => {},
+            PUT     => {},
+            PATCH   => {},
+            DELETE  => {},
+            OPTIONS => {}
+        }
+    );
     $self
 
       # "/" or undeclared paths
@@ -210,10 +228,13 @@ sub reloadConf {
     foreach ( qw(_macros _groups), @entryPoints ) {
         $self->{$_} = [];
     }
-    $self->afterSub(  {} );
+    $self->afterSub( {} );
     $self->aroundSub( {} );
-    $self->spRules(   {} );
-    $self->hook(      {} );
+    $self->spRules( {} );
+    $self->hook( {} );
+
+    # Plugin history fields
+    $self->pluginSessionDataToRemember( {} );
 
     # Load conf in portal object
     foreach my $key ( keys %$conf ) {
@@ -560,11 +581,15 @@ sub loadModule {
         $self->logger->debug("Module $module loaded");
     };
     if ($@) {
-        $self->error("Unable to build $module object: $@");
+        $self->logger->error("Unable to build $module object: $@");
+        return 0;
+    }
+    unless ($obj) {
+        $self->logger->error("$module new() method returned undef");
         return 0;
     }
-    unless ( $obj and $obj->init ) {
-        $self->error("$module init failed");
+    if ( $obj->can("init") and ( !$obj->init ) ) {
+        $self->logger->error("$module init failed");
         return 0;
     }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Issuer.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Issuer.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Issuer.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Issuer.pm	2022-02-07 19:06:14.000000000 +0000
@@ -100,7 +100,7 @@ sub _redirect {
         $self->logger->debug(
             'Add ' . $self->ipath . ', ' . $self->ipath . 'Path in keepPdata' );
         push @{ $req->pdata->{keepPdata} }, $self->ipath, $self->ipath . 'Path';
-        $req->{urldc} = $self->conf->{portal} . '/' . $self->path;
+        $req->{urldc}           = $self->p->buildUrl( $self->path );
         $req->pdata->{_url}     = encode_base64( $req->urldc, '' );
         $req->pdata->{issuerTs} = time;
     }
@@ -128,7 +128,7 @@ sub _redirect {
                     $self->restoreRequest( $_[0], $ir );
                     $self->cleanPdata( $_[0] );
                     return $self->run( @_, @path );
-                }
+                  }
                 : ()
             )
         ]
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Menu.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Menu.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Menu.pm	2021-07-22 12:32:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Menu.pm	2022-02-07 19:06:14.000000000 +0000
@@ -7,7 +7,7 @@ use Mouse;
 use Clone 'clone';
 use Lemonldap::NG::Portal::Main::Constants 'URIRE';
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Common::Module';
 
@@ -116,6 +116,7 @@ sub params {
     $res{DISPLAY_MODULES} = $self->displayModules($req);
     $res{AUTH_ERROR_TYPE} =
       $req->error_type( $res{AUTH_ERROR} = $req->menuError );
+    $res{AUTH_ERROR_ROLE} = $req->error_role;
 
 # Display menu 2fRegisters link only if at least a 2F device is registered and rule
     $res{sfaManager} =
@@ -169,7 +170,7 @@ sub displayModules {
     foreach my $module ( @{ $self->menuModules } ) {
         $self->logger->debug("Check if $module->[0] has to be displayed");
 
-        if ( $module->[1]->( $req, $req->sessionInfo ) ) {
+        if ( $module->[1]->( $req, $req->userData ) ) {
             my $moduleHash = { $module->[0] => 1 };
             if ( $module->[0] eq 'Appslist' ) {
                 $moduleHash->{'APPSLIST_LOOP'} = $self->appslist($req);
@@ -177,16 +178,16 @@ sub displayModules {
             elsif ( $module->[0] eq 'LoginHistory' ) {
                 $moduleHash->{'SUCCESS_LOGIN'} =
                   $self->p->mkSessionArray( $req,
-                    $req->{sessionInfo}->{_loginHistory}->{successLogin},
+                    $req->{userData}->{_loginHistory}->{successLogin},
                     "", 0, 0 );
                 $moduleHash->{'FAILED_LOGIN'} =
                   $self->p->mkSessionArray( $req,
-                    $req->{sessionInfo}->{_loginHistory}->{failedLogin},
+                    $req->{userData}->{_loginHistory}->{failedLogin},
                     "", 0, 1 );
             }
             elsif ( $module->[0] eq 'OidcConsents' ) {
                 $moduleHash->{'OIDC_CONSENTS'} =
-                  $self->p->mkOidcConsent( $req, $req->sessionInfo );
+                  $self->p->mkOidcConsent( $req, $req->userData );
             }
             push @$displayModules, $moduleHash;
         }
@@ -298,7 +299,7 @@ sub _buildApplicationHash {
     my $appuri  = $apphash->{options}->{uri}  || "";
     my $appdesc = $apphash->{options}->{description};
     my $applogo = $apphash->{options}->{logo};
-    my $apptip = $apphash->{options}->{tooltip} || $appname;
+    my $apptip  = $apphash->{options}->{tooltip} || $appname;
 
     # Detect sub applications
     my $subapphash;
@@ -413,7 +414,7 @@ sub _filterHash {
                 if ( my $sub = $self->p->spRules->{$p} ) {
                     eval {
                         delete $apphash->{$key}
-                          unless ( $sub->( $req, $req->sessionInfo ) );
+                          unless ( $sub->( $req, $req->userData ) );
                     };
                     if ($@) {
                         $self->logger->error("Partner rule $p returns: $@");
@@ -438,7 +439,7 @@ sub _filterHash {
             delete $apphash->{$key}
               unless (
                 $self->p->HANDLER->grant(
-                    $req, $req->sessionInfo, $appuri, $cond, $vhost
+                    $req, $req->userData, $appuri, $cond, $vhost
                 )
               );
             next;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm	2022-02-07 19:06:14.000000000 +0000
@@ -11,7 +11,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_ERROR
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Common::Module';
 
@@ -161,6 +161,15 @@ sub canUpdateSfa {
     return $msg;
 }
 
+sub addSessionDataToRemember {
+    my ( $self, $newData ) = @_;
+    for my $sessionAttr ( keys %{ $newData || {} } ) {
+        $self->p->pluginSessionDataToRemember->{$sessionAttr} =
+          $newData->{$sessionAttr};
+    }
+    return;
+}
+
 1;
 __END__
 
@@ -269,7 +278,8 @@ constants set with method name to run. F
 setting C<sessionInfo> provisionning
 
 =item C<afterData>: method called after C<sessionInfo> provisionning
-I<(macros, groups,...)>
+I<(macros, groups,...)>. This entry point is called after 'storeHistory'
+if login process fails and before 'validSession' if succeeds.
 
 =item C<endAuth>: method called when session is validated (after cookie build)
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugins.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugins.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugins.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugins.pm	2022-02-19 16:04:21.000000000 +0000
@@ -2,7 +2,7 @@
 # into "plugins" list in lemonldap-ng.ini, section "portal"
 package Lemonldap::NG::Portal::Main::Plugins;
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 package Lemonldap::NG::Portal::Main;
 
@@ -29,15 +29,15 @@ our @pList = (
     portalForceAuthn                    => '::Plugins::ForceAuthn',
     checkUser                           => '::Plugins::CheckUser',
     checkDevOps                         => '::Plugins::CheckDevOps',
-    impersonationRule                   => '::Plugins::Impersonation',
     contextSwitchingRule                => '::Plugins::ContextSwitching',
     decryptValueRule                    => '::Plugins::DecryptValue',
     findUser                            => '::Plugins::FindUser',
-    adaptativeAuthenticationLevelRules =>
+    newLocationWarning                  => '::Plugins::NewLocationWarning',
+    adaptativeAuthenticationLevelRules  =>
       '::Plugins::AdaptativeAuthenticationLevel',
-    globalLogoutRule => '::Plugins::GlobalLogout',
     refreshSessions  => '::Plugins::Refresh',
     crowdsec         => '::Plugins::CrowdSec',
+    globalLogoutRule => '::Plugins::GlobalLogout',
 );
 
 ##@method list enabledPlugins
@@ -81,7 +81,7 @@ sub enabledPlugins {
       if ( $conf->{soapSessionServer}
         or $conf->{soapConfigServer} );
 
-    # Add REST (check is done by it)
+    # Add REST (check is done by plugin itself)
     push @res, '::Plugins::RESTServer';
 
     # Check if password is enabled
@@ -96,9 +96,16 @@ sub enabledPlugins {
     # Check if custom plugins are required
     if ( $conf->{customPlugins} ) {
         $self->logger->debug( 'Custom plugins: ' . $conf->{customPlugins} );
-        push @res, grep ( /\w+/, split( /,\s*/, $conf->{customPlugins} ) );
+        push @res, grep ( /\w+/, split( /[,\s]+/, $conf->{customPlugins} ) );
     }
-    
+
+    # Impersonation overwrites req->step and pops 'afterData' EP.
+    # Static and custom 'afterData' plugins will be never launched
+    # if they are loaded after Impersonation.
+    # This plugin must be the last 'afterData' loaded plugin. Fix #2655
+    push @res, '::Plugins::Impersonation'
+      if $conf->{impersonationRule};
+
     return @res;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm	2022-02-19 16:04:21.000000000 +0000
@@ -400,7 +400,7 @@ sub authenticate {
     $req->steps( [
             'setSessionInfo',           'setMacros',
             'setPersistentSessionInfo', 'storeHistory',
-            @{ $self->afterData }, sub { PE_BADCREDENTIALS }
+            @{ $self->afterData },      sub { PE_BADCREDENTIALS }
         ]
     );
 
@@ -505,6 +505,8 @@ sub setPersistentSessionInfo {
 
 sub setLocalGroups {
     my ( $self, $req ) = @_;
+    $req->{sessionInfo}->{groups}  //= '';
+    $req->{sessionInfo}->{hGroups} //= {};
     foreach ( sort keys %{ $self->_groups } ) {
         if ( $self->_groups->{$_}->( $req, $req->sessionInfo ) ) {
             $req->{sessionInfo}->{groups} .=
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Request.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Request.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Request.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Request.pm	2022-02-07 19:06:14.000000000 +0000
@@ -85,9 +85,16 @@ has token => ( is => 'rw' );
 has wantErrorRender => ( is => 'rw' );
 
 # Error type
+
+sub error_role {
+    my $req = shift;
+    return $req->error_type(@_) eq 'negative' ? 'alert' : 'status';
+}
+
 sub error_type {
     my $req  = shift;
     my $code = shift || $req->error;
+    $req->error($code);
 
     # Positive errors
     return "positive"
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm	2022-02-07 19:06:14.000000000 +0000
@@ -9,7 +9,7 @@
 #
 package Lemonldap::NG::Portal::Main::Run;
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 
 package Lemonldap::NG::Portal::Main;
 
@@ -17,7 +17,7 @@ use strict;
 use URI::Escape;
 use URI;
 use JSON;
-use Lemonldap::NG::Common::Util qw(getPSessionID);
+use Lemonldap::NG::Common::Util qw(getPSessionID getSameSite);
 
 has trOverCache => ( is => 'rw', default => sub { {} } );
 
@@ -134,7 +134,7 @@ sub login {
     return $self->do(
         $req,
         [
-            'checkUnauthLogout', 'controlUrl',    # Fix 2342
+            'checkUnauthLogout',            'controlUrl',          # Fix 2342
             @{ $self->beforeAuth },         $self->authProcess,
             @{ $self->betweenAuthAndData }, $self->sessionData,
             @{ $self->afterData },          $self->validSession,
@@ -148,7 +148,7 @@ sub postLogin {
     return $self->do(
         $req,
         [
-            'checkUnauthLogout', 'restoreArgs',            # Fix 2342
+            'checkUnauthLogout', 'restoreArgs',                    # Fix 2342
             'controlUrl',        @{ $self->beforeAuth },
             $self->authProcess,  @{ $self->betweenAuthAndData },
             $self->sessionData,  @{ $self->afterData },
@@ -189,7 +189,8 @@ sub refresh {
     $req->user( $data{_user} || $data{ $self->conf->{whatToTrace} } );
     $req->id( $data{_session_id} );
     foreach ( keys %data ) {
-        delete $data{$_} unless ( /^_/ or /^(?:startTime)$/ );
+        delete $data{$_}
+          unless ( /^_/ or /^(?:startTime|authenticationLevel)$/ );
     }
     $data{_updateTime} = strftime( "%Y%m%d%H%M%S", localtime() );
     $self->logger->debug(
@@ -198,14 +199,14 @@ sub refresh {
             'getUser',
             @{ $self->betweenAuthAndData },
             'setSessionInfo',
-            $self->groupsAndMacros,
-            'setLocalGroups',
             sub {
                 $_[0]->sessionInfo->{$_} = $data{$_} foreach ( keys %data );
                 $_[0]->refresh(1);
                 return PE_OK;
             },
-            'store',
+            $self->groupsAndMacros,
+            'setLocalGroups',
+            'store'
         ]
     );
     my $res = $req->error( $self->process($req) );
@@ -289,6 +290,7 @@ sub do {
                     params => {
                         AUTH_ERROR      => $err,
                         AUTH_ERROR_TYPE => $req->error_type,
+                        AUTH_ERROR_ROLE => $req->error_role,
                     }
                 );
             }
@@ -355,6 +357,11 @@ sub do {
 
 sub getModule {
     my ( $self, $req, $type ) = @_;
+    if ( my $val =
+        $req->userData->{ { auth => '_auth', user => '_userDB' }->{$type} } )
+    {
+        return $val;
+    }
     if (
         my $mod = {
             auth     => '_authentication',
@@ -583,7 +590,9 @@ sub updateSession {
             $self->logger->debug("Update sessionInfo $_");
             $self->_dump( $infos->{$_} );
             $req->{sessionInfo}->{$_} = $infos->{$_};
-            if ( $self->HANDLER->data->{_session_id} && $id eq $self->HANDLER->data->{_session_id} ) {
+            if (   $self->HANDLER->data->{_session_id}
+                && $id eq $self->HANDLER->data->{_session_id} )
+            {
                 $self->HANDLER->data->{$_} = $infos->{$_};
             }
         }
@@ -1040,6 +1049,7 @@ sub tplParams {
         SKIN       => $self->getSkin($req),
         PORTAL_URL => $self->conf->{portal},
         SKIN_PATH  => $portalPath . "skins",
+        SAMESITE   => getSameSite( $self->conf ),
         SKIN_BG    => $self->conf->{portalSkinBackground},
         CUSTOM_CSS => $self->conf->{portalCustomCss},
         ( $self->customParameters ? ( %{ $self->customParameters } ) : () ),
@@ -1083,7 +1093,7 @@ sub registerLogin {
     }
 
     my $history = $req->sessionInfo->{_loginHistory} ||= {};
-    my $type = ( $req->authResult > 0 ? 'failed' : 'success' ) . 'Login';
+    my $type    = ( $req->authResult > 0 ? 'failed' : 'success' ) . 'Login';
     $history->{$type} ||= [];
     $self->logger->debug("Current login saved into $type");
 
@@ -1117,9 +1127,11 @@ sub _sumUpSession {
       $withoutUser
       ? {}
       : { user => $session->{ $self->conf->{whatToTrace} } };
-    $res->{$_} = $session->{$_}
-      foreach ( "_utime", "ipAddr",
-        keys %{ $self->conf->{sessionDataToRemember} } );
+    $res->{$_} = $session->{$_} foreach (
+        "_utime", "ipAddr",
+        keys %{ $self->conf->{sessionDataToRemember} },
+        keys %{ $self->pluginSessionDataToRemember }
+    );
     return $res;
 }
 
@@ -1221,4 +1233,29 @@ sub cspGetHost {
         $uri->scheme . "://" . ( $uri->_port ? $uri->host_port : $uri->host ) );
 }
 
+sub buildUrl {
+    my $self = shift;
+    return $self->portal unless @_;
+
+    # URL base is $self->portal unless first arg is an URL
+    my $uri =
+      URI->new( ( $_[0] =~ m#^https?://# ) ? shift(@_) : $self->portal );
+    my @pathSg = grep { $_ ne '' } $uri->path_segments;
+    while (@_) {
+        my $s = shift;
+        if ( ref $s ) {
+            $uri->query_form($s);
+            if (@_) {
+                require Carp;
+                Carp::confess('Query must be the last arg of buildUrl');
+            }
+        }
+        else {
+            push @pathSg, $s;
+        }
+    }
+    $uri->path_segments(@pathSg);
+    return $uri;
+}
+
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/SecondFactor.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/SecondFactor.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/SecondFactor.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/SecondFactor.pm	2022-02-19 16:04:21.000000000 +0000
@@ -10,7 +10,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_BADCREDENTIALS
 );
 
-our $VERSION = '2.0.8';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Portal::Main::Plugin
@@ -30,10 +30,10 @@ has ott => (
     }
 );
 
-has prefix  => ( is => 'rw' );
-has logo    => ( is => 'rw', default => '2f.png' );
-has label   => ( is => 'rw' );
-has noRoute => ( is => 'ro' );
+has prefix     => ( is => 'rw' );
+has logo       => ( is => 'rw', default => '2f.png' );
+has label      => ( is => 'rw' );
+has noRoute    => ( is => 'ro' );
 has authnLevel => (
     is      => 'rw',
     lazy    => 1,
@@ -45,7 +45,7 @@ has authnLevel => (
 sub init {
     my ($self) = @_;
 
-    # Set logo if overriden
+    # Set logo if overridden
     $self->logo( $self->conf->{ $self->prefix . "2fLogo" } )
       if ( $self->conf->{ $self->prefix . "2fLogo" } );
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Password/Combination.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Password/Combination.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Password/Combination.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Password/Combination.pm	2022-02-19 16:04:21.000000000 +0000
@@ -50,9 +50,11 @@ sub init {
 
 sub delegate {
     my ( $self, $req, $name, @args ) = @_;
+
     # The user might want to override which password DB is used with a macro
     # This is useful when using SASL delegation in OpenLDAP
-    my $userDB = $req->sessionInfo->{_cmbPasswordDB} || $req->sessionInfo->{_userDB};
+    my $userDB =
+      $req->sessionInfo->{_cmbPasswordDB} || $req->sessionInfo->{_userDB};
     unless ( $self->mods->{$userDB} ) {
         $self->logger->error("No Password module available for $userDB");
         return PE_ERROR;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Password/Custom.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Password/Custom.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Password/Custom.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Password/Custom.pm	2022-01-22 14:30:25.000000000 +0000
@@ -1,17 +1,12 @@
 package Lemonldap::NG::Portal::Password::Custom;
+use Lemonldap::NG::Portal::Lib::CustomModule;
 
 use strict;
 
-sub new {
-    my ( $class, $self ) = @_;
-    unless ( $self->{conf}->{customPassword} ) {
-        die 'Custom Password module not defined';
-    }
-
-    eval $self->{p}->loadModule( $self->{conf}->{customPassword} );
-    ($@)
-      ? return $self->{p}->loadModule( $self->{conf}->{customPassword} )
-      : die 'Unable to load Password module ' . $self->{conf}->{customPassword};
-}
+our @ISA = qw(Lemonldap::NG::Portal::Lib::CustomModule);
+use constant {
+    custom_name       => "Password",
+    custom_config_key => "customPassword",
+};
 
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Password/LDAP.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Password/LDAP.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Password/LDAP.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Password/LDAP.pm	2022-02-19 16:04:21.000000000 +0000
@@ -38,13 +38,15 @@ sub modifyPassword {
       if $self->conf->{ldapGetUserBeforePasswordChange};
 
     if ( $req->data->{dn} ) {
-        $dn                 = $req->data->{dn};
-        $requireOldPassword = $self->requireOldPwdRule->( $req, $req->userData );
+        $dn = $req->data->{dn};
+        $requireOldPassword =
+          $self->requireOldPwdRule->( $req, $req->userData );
         $self->logger->debug("Get DN from request data: $dn");
     }
     else {
-        $dn                 = $req->sessionInfo->{_dn};
-        $requireOldPassword = $self->requireOldPwdRule->( $req, $req->sessionInfo );
+        $dn = $req->sessionInfo->{_dn};
+        $requireOldPassword =
+          $self->requireOldPwdRule->( $req, $req->sessionInfo );
         $self->logger->debug("Get DN from session data: $dn");
     }
     unless ($dn) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/AutoSignin.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/AutoSignin.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/AutoSignin.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/AutoSignin.pm	2022-02-19 16:04:21.000000000 +0000
@@ -42,7 +42,7 @@ sub init {
             push @{ $self->rules }, [ $sub, $id ];
         }
     }
-    
+
     return 1;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/BruteForceProtection.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/BruteForceProtection.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/BruteForceProtection.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/BruteForceProtection.pm	2022-01-22 14:30:19.000000000 +0000
@@ -7,7 +7,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_WAIT
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Portal::Main::Plugin';
 
@@ -19,12 +19,10 @@ has lockTimes => (
     isa     => 'ArrayRef',
     default => sub { [] }
 );
-
 has maxAge => (
     is  => 'rw',
     isa => 'Int'
 );
-
 has maxFailed => (
     is  => 'rw',
     isa => 'Int'
@@ -56,6 +54,7 @@ sub init {
         return 0;
     }
 
+    my $maxAge = $self->conf->{bruteForceProtectionMaxAge} || 300;
     if ( $self->conf->{bruteForceProtectionIncrementalTempo} ) {
         my $lockTimes = @{ $self->lockTimes } =
           sort { $a <=> $b }
@@ -65,7 +64,7 @@ sub init {
               ? abs $_
               : ()
           }
-          grep { /\d+/ }
+          grep /\d+/,
           split /\s*,\s*/, $self->conf->{bruteForceProtectionLockTimes};
 
         unless ($lockTimes) {
@@ -87,14 +86,13 @@ sub init {
             $lockTimes = $self->conf->{failedLoginNumber};
         }
 
-        my $sum = $self->conf->{bruteForceProtectionMaxAge} *
-          ( 1 + $self->conf->{failedLoginNumber} - $lockTimes );
+        my $sum =
+          $maxAge * ( 1 + $self->conf->{failedLoginNumber} - $lockTimes );
         $sum += $_ foreach @{ $self->lockTimes };
         $self->maxAge($sum);
     }
     else {
-        $self->maxAge( $self->conf->{bruteForceProtectionMaxAge} *
-              ( 1 + $self->maxFailed ) );
+        $self->maxAge( $maxAge * ( 1 + $self->maxFailed ) );
     }
 
     return 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CDA.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CDA.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CDA.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CDA.pm	2022-02-19 16:04:21.000000000 +0000
@@ -9,7 +9,7 @@ use Lemonldap::NG::Portal::Main::Constan
   URIRE
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Common::Module';
 
@@ -18,8 +18,6 @@ extends 'Lemonldap::NG::Common::Module';
 use constant endAuth     => 'changeUrldc';
 use constant forAuthUser => 'changeUrldc';
 
-sub init { 1 }
-
 # RUNNING METHOD
 
 sub changeUrldc {
@@ -27,7 +25,7 @@ sub changeUrldc {
     my $urldc = $req->{urldc} || '';
     if (    $req->id
         and $urldc =~ URIRE
-        and $3 !~ m@\Q$self->{conf}->{domain}\E$@oi
+        and $3     !~ m@\Q$self->{conf}->{domain}\E$@oi
         and $self->p->isTrustedUrl($urldc) )
     {
         my $ssl = $urldc =~ /^https/;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CertificateResetByMail.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CertificateResetByMail.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CertificateResetByMail.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CertificateResetByMail.pm	2022-02-07 19:06:14.000000000 +0000
@@ -600,6 +600,7 @@ sub display {
         MAIN_LOGO       => $self->conf->{portalMainLogo},
         AUTH_ERROR      => $req->error,
         AUTH_ERROR_TYPE => $req->error_type,
+        AUTH_ERROR_ROLE => $req->error_role,
         AUTH_URL        => $req->data->{_url},
         CHOICE_VALUE    => $req->{_authChoice},
         EXPMAILDATE     => $req->data->{expMailDate},
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckDevOps.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckDevOps.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckDevOps.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckDevOps.pm	2022-02-19 16:04:21.000000000 +0000
@@ -7,15 +7,15 @@ use Lemonldap::NG::Common::UserAgent;
 use Lemonldap::NG::Portal::Main::Constants qw(
   URIRE
   PE_OK
-  PE_ERROR
   PE_BADURL
   PE_NOTOKEN
   PE_TOKENEXPIRED
   PE_FILENOTFOUND
   PE_BAD_DEVOPS_FILE
+  PE_REGISTERFORMEMPTY
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Portal::Main::Plugin
@@ -47,9 +47,14 @@ has ua => (
 
 sub init {
     my ($self) = @_;
-    $self->addAuthRoute( checkdevops => 'run', ['POST'] )
+    $self->addAuthRoute( checkdevops => 'parse', ['POST'] )
       ->addAuthRouteWithRedirect( checkdevops => 'display', ['GET'] );
 
+    unless ( $self->conf->{useSafeJail} ) {
+        $self->logger->warn('"CheckDevOps" plugin enabled WITHOUT SafeJail');
+        return 0;
+    }
+
     return 1;
 }
 
@@ -79,10 +84,10 @@ sub display {
     return $self->p->sendHtml( $req, 'checkdevops', params => $params, );
 }
 
-sub run {
-    my ( $self,    $req )   = @_;
-    my ( $headers, $rules ) = ( [], [] );
-    my ( $msg, $json, $url );
+sub parse {
+    my ( $self,    $req ) = @_;
+    my ( $headers, $rules, $unknown ) = ( [], [], [] );
+    my ( $msg,     $json,  $url, $bad_json );
     my $alert = 'alert-danger';
 
     # Check token
@@ -112,16 +117,19 @@ sub run {
             DOWNLOAD  => $self->conf->{checkDevOpsDownload},
             MSG       => "PE$msg",
             ALERTE    => 'alert-warning',
-            TOKEN     => $token,
+            TOKEN     => $token
         };
         return $self->p->sendJSONresponse( $req, $params )
-          if $req->wantJSON && $msg;
+          if ( $req->wantJSON && $msg );
 
         # Display form
         return $self->p->sendHtml( $req, 'checkdevops', params => $params )
           if $msg;
     }
 
+    $msg = 'PE' . PE_REGISTERFORMEMPTY
+      unless ( $req->param('url') || $req->param('checkDevOpsFile') );
+
     # Check URL if allowed and exists
     if ( $self->conf->{checkDevOpsDownload} and $url = $req->param('url') ) {
         undef $url if $self->p->checkXSSAttack( 'CheckDevOps URL', $url );
@@ -169,8 +177,8 @@ sub run {
     unless ( $json || $msg ) {
         $json = eval {
             from_json( $req->param('checkDevOpsFile'), { allow_nonref => 1 } );
-        };
-        if ($@) {
+        } if $req->param('checkDevOpsFile');
+        if ( $@ || !$json->{rules} ) {
 
             # Prepare form params
             undef $json;
@@ -186,10 +194,10 @@ sub run {
         my $handler = $self->p->HANDLER;
         my $vhost   = $handler->resolveAlias($req);
 
-        # Removed forbidden session attributes
-        foreach my $v ( split /\s+/, $self->conf->{hiddenAttributes} ) {
+        # Removed hidden session attributes
+        foreach my $v ( split /[,\s]+/, $self->conf->{hiddenAttributes} ) {
             foreach ( keys %{ $json->{headers} } ) {
-                if ( $json->{headers}->{$_} eq '$' . $v ) {
+                if ( $json->{headers}->{$_} =~ /\$$v/ ) {
                     delete $json->{headers}->{$_};
                     my $user = $req->userData->{ $self->conf->{whatToTrace} };
                     $self->userLogger->warn(
@@ -199,29 +207,99 @@ sub run {
             }
         }
 
+        # Parse rules
+        my $cpt = new Safe;
+        $cpt->share_from( 'MIME::Base64', ['&encode_base64'] );
+        $cpt->share_from(
+            'Lemonldap::NG::Handler::Main::Jail',
+            [
+                '&encrypt', '&token',
+                @Lemonldap::NG::Handler::Main::Jail::builtCustomFunctions
+            ]
+        );
+        $cpt->share_from( 'Lemonldap::NG::Common::Safelib',
+            $Lemonldap::NG::Common::Safelib::functions );
+
+        foreach ( keys %{ $json->{rules} } ) {
+            $cpt->reval("BEGIN { 'warnings'->unimport; } $json->{rules}->{$_}");
+            my $err = join(
+                '',
+                grep(
+                    { $_ =~
+/(?:Undefined subroutine|Devel::StackTrace|trapped by operation mask)/
+                          ? ()
+                          : $_; }
+                    split( /\n/, $@, 0 ) )
+            );
+            if ($err) {
+                $self->userLogger->error(
+                    "Bad rule: $json->{rules}->{$_} ($err)");
+                $bad_json = 1;
+            }
+        }
+
         # Compile headers
         $handler->headersInit( undef, { $vhost => $json->{headers} } );
         $headers = $handler->checkHeaders( $req, $req->userData );
-        my $headers_list = join ', ', map { "$_->{key}:$_->{value}" } @$headers;
-        $self->logger->debug("CheckDevOps compiled headers: $headers_list");
 
-        # Compile rules
-        @$rules = map {
-            my ( $sub, $flag ) = $handler->conditionSub( $json->{rules}->{$_} );
-            {
-                uri    => $_,
-                access => $sub->( $req, $req->userData )
-                ? 'allowed'
-                : 'forbidden'
-            }
-        } sort keys %{ $json->{rules} };
-        my $rules_list = join ', ', map { "$_->{uri}:$_->{access}" } @$rules;
-        $self->logger->debug("CheckDevOps compiled rules: $rules_list");
-
-        # Prepare form params
-        $msg   = 'checkDevOps';
-        $alert = 'alert-info';
-        $json  = JSON->new->ascii->pretty->encode($json);    # Pretty print
+        # Check attributes if required
+        if ( $self->conf->{checkDevOpsCheckSessionAttributes} ) {
+            $unknown  = $self->_checkSessionAttrs($json);
+            $bad_json = 1 if scalar @$unknown;
+        }
+
+        if ( $handler->tsv->{maintenance}->{$vhost} || $bad_json ) {
+
+            # Prepare form params
+            undef $json;
+            $headers = [];
+            $alert   = 'alert-danger';
+            $msg     = 'PE' . PE_BAD_DEVOPS_FILE;
+            $self->userLogger->error("CheckDevOps: bad 'rules.json' file");
+            $handler->tsv->{maintenance}->{$vhost} = 0;
+        }
+        else {
+
+            # Normalize headers name if required
+            if ( $self->conf->{checkDevOpsDisplayNormalizedHeaders} ) {
+                $self->logger->debug("Normalize headers...");
+                @$headers = map {
+                    ;    # Prevent compilation error with old Perl versions
+                    no strict 'refs';
+                    {
+                        key   => &{ $handler . '::cgiName' }( $_->{key} ),
+                        value => $_->{value}
+                    }
+                } @$headers;
+            }
+
+            my $headers_list = join ', ', map "$_->{key}:$_->{value}",
+              @$headers;
+            $self->logger->debug("CheckDevOps compiled headers: $headers_list");
+
+            # Compile rules
+            @$rules = map {
+                my ( $sub, $flag ) =
+                  $handler->conditionSub( $json->{rules}->{$_} );
+                {
+                    uri    => $_,
+                    access => $sub->( $req, $req->userData )
+                    ? 'allowed'
+                    : 'forbidden'
+                }
+            } sort keys %{ $json->{rules} };
+            my $rules_list = join ', ', map "$_->{uri}:$_->{access}", @$rules;
+            $self->logger->debug("CheckDevOps compiled rules: $rules_list");
+
+            # Prepare form params
+            $msg   = 'checkDevOps';
+            $alert = 'alert-info';
+            foreach ( keys %$json ) {
+                delete $json->{$_} unless $_ =~ /\b(?:rules|headers)\b/;
+                delete $json->{$_} unless keys %{ $json->{$_} };
+            }
+            $json = JSON->new->ascii->pretty->encode($json);    # Pretty print
+        }
     }
 
     # Prepare form
@@ -232,6 +310,7 @@ sub run {
         LANGS     => $self->conf->{showLanguages},
         DOWNLOAD  => $self->conf->{checkDevOpsDownload},
         MSG       => $msg,
+        UNKNOWN   => join( $self->conf->{multiValuesSeparator}, @$unknown ),
         ALERTE    => $alert,
         FILE      => $json,
         HEADERS   => $headers,
@@ -246,7 +325,40 @@ sub run {
     return $self->p->sendJSONresponse( $req, $params ) if $req->wantJSON;
 
     # Display form
-    return $self->p->sendHtml( $req, 'checkdevops', params => $params, );
+    return $self->p->sendHtml( $req, 'checkdevops', params => $params );
+}
+
+sub _checkSessionAttrs {
+    my ( $self, $json ) = @_;
+    my $unknown;
+    my %sessionAttrs = map { $_ => 1 } (
+        keys %{ $self->conf->{ldapExportedVars} },
+        keys %{ $self->conf->{exportedVars} },
+        keys %{ $self->conf->{macros} }
+    );
+    $sessionAttrs{groups} = 1 if $self->conf->{groups};
+    $self->logger->debug(
+        "Existing session attributes: "
+          . join $self->conf->{multiValuesSeparator},
+        keys %sessionAttrs
+    );
+
+    my @varh = map { ( $json->{headers}->{$_} =~ /\$(\w+)\b/g ) }
+      keys %{ $json->{headers} };
+    my @varr = map { ( $json->{rules}->{$_} =~ /\$(\w+)\b/g ) }
+      keys %{ $json->{rules} };
+    my %usedAttrs = map { $_ => 1 } ( @varh, @varr );
+    $self->logger->debug(
+        "Used attributs: " . join $self->conf->{multiValuesSeparator},
+        keys %usedAttrs );
+
+    @$unknown = map { $sessionAttrs{$_} ? () : $_ } sort keys %usedAttrs;
+    $self->logger->debug(
+        "Unknown attributes: " . join $self->conf->{multiValuesSeparator},
+        @$unknown )
+      if scalar @$unknown;
+
+    return $unknown;
 }
 
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckState.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckState.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckState.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckState.pm	2022-02-07 19:06:14.000000000 +0000
@@ -7,8 +7,9 @@ package Lemonldap::NG::Portal::Plugins::
 
 use strict;
 use Mouse;
+use Lemonldap::NG::Portal;
 
-our $VERSION = '2.0.10';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Portal::Main::Plugin';
 
@@ -41,16 +42,22 @@ sub check {
 
     if ( my $user = $req->param('user') and my $pwd = $req->param('password') )
     {
-        $req->user($user);
-        $req->data->{password} = $pwd;
+        $req->parameters->{user}     = ($user);
+        $req->parameters->{password} = $pwd;
+        $req->data->{skipToken}      = 1;
+
+        # This makes Auth::Choice use authChoiceAuthBasic if defined
+        $req->data->{_pwdCheck} = 1;
 
         # Not launched methods:
-        #  - "extractFormInfo" due to "token"
         #  - "buildCookie" useless here
         $req->steps( [
-                'getUser',                         'authenticate',
-                @{ $self->p->betweenAuthAndData }, $self->p->sessionData,
-                @{ $self->p->afterData },          'storeHistory',
+                @{ $self->p->beforeAuth },
+                $self->p->authProcess,
+                @{ $self->p->betweenAuthAndData },
+                $self->p->sessionData,
+                @{ $self->p->afterData },
+                'storeHistory',
                 @{ $self->p->endAuth }
             ]
         );
@@ -61,7 +68,8 @@ sub check {
     }
 
     return $self->p->sendError( $req, join( ",\n", @rep ), 500 ) if (@rep);
-    return $self->p->sendJSONresponse( $req, { result => 1 } );
+    return $self->p->sendJSONresponse( $req,
+        { result => 1, version => $Lemonldap::NG::Portal::VERSION } );
 }
 
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckUser.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckUser.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckUser.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckUser.pm	2022-01-22 14:30:19.000000000 +0000
@@ -9,7 +9,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_BADCREDENTIALS
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Portal::Main::Plugin
@@ -28,11 +28,14 @@ has ott => (
         return $ott;
     }
 );
+
+has displayHistoryRule           => ( is => 'rw', default => sub { 0 } );
 has unrestrictedUsersRule        => ( is => 'rw', default => sub { 0 } );
 has displayEmptyValuesRule       => ( is => 'rw', default => sub { 0 } );
 has displayEmptyHeadersRule      => ( is => 'rw', default => sub { 0 } );
 has displayPersistentInfoRule    => ( is => 'rw', default => sub { 0 } );
 has displayComputedSessionRule   => ( is => 'rw', default => sub { 0 } );
+has displayHiddenAttributesRule  => ( is => 'rw', default => sub { 0 } );
 has displayNormalizedHeadersRule => ( is => 'rw', default => sub { 0 } );
 has idRule                       => ( is => 'rw', default => sub { 1 } );
 has sorted                       => ( is => 'rw', default => sub { 0 } );
@@ -97,6 +100,7 @@ sub init {
         )
     );
     return 0 unless $self->displayComputedSessionRule;
+
     $self->displayNormalizedHeadersRule(
         $self->p->buildRule(
             $self->conf->{checkUserDisplayNormalizedHeaders},
@@ -105,6 +109,22 @@ sub init {
     );
     return 0 unless $self->displayNormalizedHeadersRule;
 
+    $self->displayHistoryRule(
+        $self->p->buildRule(
+            $self->conf->{checkUserDisplayHistory},
+            'checkUserDisplayHistory'
+        )
+    );
+    return 0 unless $self->displayHistoryRule;
+
+    $self->displayHiddenAttributesRule(
+        $self->p->buildRule(
+            $self->conf->{checkUserDisplayHiddenAttributes},
+            'checkUserDisplayHiddenAttributes'
+        )
+    );
+    return 0 unless $self->displayHistoryRule;
+
     # Init. other options
     $self->sorted( $self->conf->{impersonationRule}
           || $self->conf->{contextSwitchingRule} );
@@ -116,13 +136,18 @@ sub init {
 
 # RUNNING METHODS
 sub display {
-    my ( $self,  $req )         = @_;
+    my ( $self, $req ) = @_;
+    my $history = [ [], [] ];
     my ( $attrs, $array_attrs ) = ( $req->userData, [] );
 
     $self->logger->debug("Display current session data...");
     $self->userLogger->info("Using spoofed SSO groups if exist")
       if ( $self->conf->{impersonationRule} );
 
+    $history = $self->_concatHistory( $attrs->{_loginHistory} )
+      if $self->displayHistoryRule->( $req, $req->userData )
+      && $self->conf->{loginHistoryEnabled};
+
     $attrs =
       $self->_removeKeys( $attrs, $self->persistentAttrs,
         'Remove persistent session attributes...' )
@@ -141,6 +166,9 @@ sub display {
         MSG        => 'checkUser' . $self->merged,
         ALERTE     => ( $self->merged ? 'alert-warning' : 'alert-info' ),
         LOGIN      => $req->{userData}->{ $self->conf->{whatToTrace} },
+        HISTORY    => ( @{ $history->[0] } || @{ $history->[1] } ) ? 1 : 0,
+        SUCCESS    => $history->[0],
+        FAILED     => $history->[1],
         ATTRIBUTES => $array_attrs->[2],
         MACROS     => $array_attrs->[1],
         GROUPS     => $array_attrs->[0],
@@ -161,7 +189,8 @@ sub check {
     my ( $attrs, $array_attrs, $array_hdrs ) = ( {}, [], [] );
     my $msg           = my $auth = my $computed = '';
     my $savedUserData = $req->userData;
-    my $unUser = $self->unrestrictedUsersRule->( $req, $savedUserData ) || 0;
+    my $unUser  = $self->unrestrictedUsersRule->( $req, $savedUserData ) || 0;
+    my $history = [ [], [] ];
 
     # Check token
     if ( $self->ottRule->( $req, {} ) ) {
@@ -250,7 +279,7 @@ sub check {
           . $self->conf->{checkUserSearchAttributes}
           : $self->conf->{whatToTrace};
 
-        foreach ( split /\s+/, $searchAttrs ) {
+        foreach ( split /[,\s]+/, $searchAttrs ) {
             $self->logger->debug("Searching with: $_ = $user");
             $sessions = $self->module->searchOn( $moduleOptions, $_, $user );
             last if ( keys %$sessions );
@@ -294,7 +323,11 @@ sub check {
         $attrs       = {};
     }
     else {
-        $msg = 'checkUser' . $self->merged;
+        $msg     = 'checkUser' . $self->merged;
+        $history = $self->_concatHistory( $attrs->{_loginHistory} )
+          if $self->displayHistoryRule->( $req, $savedUserData )
+          && $self->conf->{loginHistoryEnabled};
+
         $attrs =
           $self->_removeKeys( $attrs, $self->persistentAttrs,
             'Remove persistent session attributes...' )
@@ -387,6 +420,9 @@ sub check {
         ALLOWED     => $auth,
         ALERTE_AUTH => $alert_auth,
         HEADERS     => $array_hdrs,
+        HISTORY     => ( @{ $history->[0] } || @{ $history->[1] } ) ? 1 : 0,
+        SUCCESS     => $history->[0],
+        FAILED      => $history->[1],
         ATTRIBUTES  => $array_attrs->[2],
         MACROS      => $array_attrs->[1],
         GROUPS      => $array_attrs->[0],
@@ -555,23 +591,16 @@ sub _createArray {
     my ( $self, $req, $attrs, $userData ) = @_;
     my $array_attrs = [];
 
-    if ( $self->displayEmptyValuesRule->( $req, $userData ) ) {
-        $self->logger->debug("Delete hidden attributes...");
-        foreach my $k ( sort keys %$attrs ) {
-
-            # Ignore hidden attributes
-            push @$array_attrs, { key => $k, value => $attrs->{$k} }
-              unless ( $self->hAttr =~ /\b$k\b/ );
-        }
-    }
-    else {
-        $self->logger->debug("Delete hidden and empty attributes...");
-        foreach my $k ( sort keys %$attrs ) {
-
-            # Ignore hidden attributes and empty values
-            push @$array_attrs, { key => $k, value => $attrs->{$k} }
-              unless ( $self->hAttr =~ /\b$k\b/ or !$attrs->{$k} );
-        }
+    foreach my $k ( sort keys %$attrs ) {
+        push @$array_attrs,
+          { key => $k, value => $attrs->{$k} }
+          unless ( (
+                $self->hAttr =~ /\b$k\b/
+                && !$self->displayHiddenAttributesRule->( $req, $userData )
+            )
+            || (   !$attrs->{$k}
+                && !$self->displayEmptyValuesRule->( $req, $userData ) )
+          );
     }
 
     return $array_attrs;
@@ -651,4 +680,32 @@ sub _removeKeys {
     return $attrs;
 }
 
+sub _concatHistory {
+    my ( $self,    $history ) = @_;
+    my ( $success, $failed )  = ( [], [] );
+
+    $self->logger->debug('Concatenate history...');
+    @$success = map {
+        my $element = $_;
+        my $utime   = delete $element->{_utime};
+        {
+            utime  => $utime,
+            values => join $self->conf->{multiValuesSeparator},
+            map "$_=$element->{$_}", sort keys %$element
+        }
+    } @{ $history->{successLogin} };
+
+    @$failed = map {
+        my $element = $_;
+        my $utime   = delete $element->{_utime};
+        {
+            utime  => $utime,
+            values => join $self->conf->{multiValuesSeparator},
+            map "$_=$element->{$_}", sort keys %$element
+        }
+    } @{ $history->{failedLogin} };
+
+    return [ $success, $failed ];
+}
+
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/ContextSwitching.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/ContextSwitching.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/ContextSwitching.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/ContextSwitching.pm	2022-02-19 16:04:21.000000000 +0000
@@ -40,7 +40,7 @@ has unrestrictedUsersRule => ( is => 'rw
 
 sub init {
     my ($self) = @_;
-    $self->addAuthRoute( switchcontext => 'run',  ['POST'] )
+    $self->addAuthRoute( switchcontext => 'run', ['POST'] )
       ->addAuthRoute( switchcontext => 'display', ['GET'] );
 
     # Parse ContextSwitching rules
@@ -138,10 +138,10 @@ sub display {
 
 sub run {
     my ( $self, $req ) = @_;
-    my $statut = PE_OK;
-    my $realId = $req->userData->{ $self->conf->{whatToTrace} };
+    my $statut  = PE_OK;
+    my $realId  = $req->userData->{ $self->conf->{whatToTrace} };
     my $spoofId = $req->param('spoofId') || '';    # ContextSwitching required ?
-    my $unUser = $self->unrestrictedUsersRule->( $req, $req->userData ) || 0;
+    my $unUser  = $self->unrestrictedUsersRule->( $req, $req->userData ) || 0;
 
     # Check token
     if ( $self->ottRule->( $req, {} ) ) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/FindUser.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/FindUser.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/FindUser.pm	2021-08-21 17:42:59.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/FindUser.pm	2022-02-19 16:04:21.000000000 +0000
@@ -9,7 +9,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_TOKENEXPIRED
 );
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Portal::Main::Plugin
@@ -80,9 +80,11 @@ sub retreiveFindUserParams {
     $self->logger->debug("FindUser: reading parameters...");
     @$searching = map {
         my ( $key, $value, $null ) = split '#', $_;
+        $key =~ s/^(?:\d+_)?//;
         my $param  = $req->params($key) // '';
-        my @values = split $self->conf->{multiValuesSeparator},
-          $self->conf->{findUserSearchingAttributes}->{$_} || '';
+        my @values = grep s/^(?:\d+_)?//,
+          split( $self->conf->{multiValuesSeparator},
+            $self->conf->{findUserSearchingAttributes}->{$_} || '' );
         my $select  = scalar @values > 1 && not scalar @values % 2;
         my %values  = @values if $select;
         my $defined = length $param;
@@ -145,16 +147,17 @@ sub retreiveFindUserParams {
 }
 
 sub buildForm {
-    my $self   = shift;
-    my $fields = [];
+    my $self = shift;
+    my ( $fields, @required ) = ( [], () );
 
     $self->logger->debug('Building array ref with searching fields...');
     @$fields =
-      sort { $a->{select} <=> $b->{select} || $a->{value} cmp $b->{value} }
       map {
         my ( $key, $value, $null ) = split '#', $_;
         my @values = split $self->conf->{multiValuesSeparator},
           $self->conf->{findUserSearchingAttributes}->{$_} || $key;
+        $key =~ s/^(?:\d+_)?//;
+        push @required, $key unless $null;
         my $nbr = scalar @values;
         if ( $nbr > 1 ) {
             if ( $nbr % 2 ) { () }
@@ -164,14 +167,17 @@ sub buildForm {
                 $nbr /= 2;
                 $self->logger->debug(
                     "Building $key with type 'select' and $nbr entries...");
-                @$choices = sort { $a->{value} cmp $b->{value} }
-                  map { { key => $_, value => $hash{$_} } } keys %hash;
+                @$choices = map {
+                    my $k = $_;
+                    $k =~ s/^(?:\d+_)?//;
+                    { key => $k, value => $hash{$_} }
+                } sort keys %hash;
                 {
                     select  => 1,
                     key     => $key,
-                    null    => $null,
                     value   => $value ? $value : $key,
-                    choices => $choices
+                    choices => $choices,
+                    null    => $null
                 };
             }
         }
@@ -179,12 +185,14 @@ sub buildForm {
             {
                 select => 0,
                 key    => $key,
-                value  => $values[0]
+                value  => $values[0],
+                null   => $null
             };
         }
-      } keys %{ $self->conf->{findUserSearchingAttributes} };
+      } sort keys %{ $self->conf->{findUserSearchingAttributes} };
+    $self->logger->debug('Mandatory field(s) required') if scalar @required;
 
-    return $fields;
+    return ( $fields, scalar @required );
 }
 
 sub _sendResult {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/ForceAuthn.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/ForceAuthn.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/ForceAuthn.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/ForceAuthn.pm	2022-01-22 14:30:19.000000000 +0000
@@ -7,7 +7,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_MUSTAUTHN
 );
 
-our $VERSION = '2.0.10';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Portal::Main::Plugin';
 
@@ -15,8 +15,6 @@ extends 'Lemonldap::NG::Portal::Main::Pl
 
 use constant forAuthUser => 'run';
 
-sub init { 1 }
-
 # RUNNING METHOD
 
 sub run {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/GlobalLogout.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/GlobalLogout.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/GlobalLogout.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/GlobalLogout.pm	2022-02-19 16:04:21.000000000 +0000
@@ -12,7 +12,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_SENDRESPONSE
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Portal::Main::Plugin
@@ -207,7 +207,8 @@ sub activeSessions {
             }
             $_;
           }
-          sort { $b->{startTime} cmp $a->{startTime} } map { {
+          sort { $b->{startTime} cmp $a->{startTime} } map {
+            {
                 id          => $_,
                 customParam => $sessions->{$_}->{$customParam},
                 ipAddr      => $sessions->{$_}->{ipAddr},
@@ -217,7 +218,7 @@ sub activeSessions {
             };
           } keys %$sessions;
     }
-    
+
     return $activeSessions;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/History.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/History.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/History.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/History.pm	2022-01-22 14:30:19.000000000 +0000
@@ -7,7 +7,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_INFO
 );
 
-our $VERSION = '2.0.10';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Portal::Main::Plugin
@@ -18,8 +18,6 @@ extends qw(
 
 use constant endAuth => 'run';
 
-sub init { 1 }
-
 # RUNNING METHOD
 
 sub run {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/Impersonation.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/Impersonation.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/Impersonation.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/Impersonation.pm	2022-02-19 16:04:21.000000000 +0000
@@ -88,10 +88,10 @@ sub run {
     my $unUser = 0;
     my $loginHistory =
       $req->{sessionInfo}->{_loginHistory};    # Store login history
-    $req->{user} ||= $req->{sessionInfo}->{_impUser};    # If 2FA is enabled
-    my $spoofId = $req->param('spoofId')       # Impersonation required
-      || $req->{sessionInfo}->{_impSpoofId}    # If 2FA is enabled
-      || $req->{user};                         # Impersonation not required
+    $req->{user} ||= $req->{sessionInfo}->{_impUser};   # If 2FA is enabled
+    my $spoofId = $req->param('spoofId')                # Impersonation required
+      || $req->{sessionInfo}->{_impSpoofId}             # If 2FA is enabled
+      || $req->{user};    # Impersonation not required
 
     $self->logger->debug("No impersonation required")
       if ( $spoofId eq $req->{user} );
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/MailPasswordReset.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/MailPasswordReset.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/MailPasswordReset.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/MailPasswordReset.pm	2022-02-07 19:06:14.000000000 +0000
@@ -567,6 +567,7 @@ sub display {
         MAIN_LOGO       => $self->conf->{portalMainLogo},
         AUTH_ERROR      => $req->error,
         AUTH_ERROR_TYPE => $req->error_type,
+        AUTH_ERROR_ROLE => $req->error_role,
         AUTH_URL        => $req->data->{_url},
         CHOICE_VALUE    => $req->{_authChoice},
         EXPMAILDATE     => $req->data->{expMailDate},
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/NewLocationWarning.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/NewLocationWarning.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/NewLocationWarning.pm	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/NewLocationWarning.pm	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,168 @@
+package Lemonldap::NG::Portal::Plugins::NewLocationWarning;
+
+use strict;
+use Mouse;
+use POSIX qw(strftime);
+use Lemonldap::NG::Portal::Main::Constants qw(PE_OK);
+use List::MoreUtils qw/uniq/;
+
+our $VERSION = '2.0.14';
+
+has locationAttribute        => ( is => 'rw' );
+has locationDisplayAttribute => ( is => 'rw' );
+has locationMaxValues        => ( is => 'rw' );
+has mailSessionKey           => (
+    is      => 'rw',
+    lazy    => 1,
+    default => sub {
+        return
+             $_[0]->{conf}->{newLocationWarningMailAttribute}
+          || $_[0]->{conf}->{mailSessionKey}
+          || 'mail';
+    }
+);
+
+extends qw(
+  Lemonldap::NG::Portal::Lib::SMTP
+  Lemonldap::NG::Portal::Main::Plugin
+);
+
+# Entrypoint
+use constant afterSub => { setLocalGroups => 'checkNewLocation' };
+use constant endAuth  => 'sendWarningEmail';
+
+sub init {
+    my ($self) = @_;
+
+    if ( $self->conf->{disablePersistentStorage} ) {
+        $self->logger->error(
+'"NewLocationWarning" plugin enabled WITHOUT persistent session storage"'
+        );
+        return 0;
+    }
+    unless ( $self->conf->{loginHistoryEnabled} ) {
+        $self->logger->error(
+            '"NewLocationWarning" plugin enabled WITHOUT "History" plugin');
+        return 0;
+    }
+
+    $self->locationAttribute( $self->conf->{newLocationWarningLocationAttribute}
+          || 'ipAddr' );
+    $self->locationDisplayAttribute(
+             $self->conf->{newLocationWarningLocationDisplayAttribute}
+          || $self->locationAttribute );
+    $self->locationMaxValues( $self->conf->{newLocationWarningMaxValues} || 0 );
+
+    return 1;
+}
+
+sub checkNewLocation {
+    my ( $self, $req ) = @_;
+    my $successLogin = $req->sessionInfo->{_loginHistory}->{successLogin} || [];
+    my $location     = $req->sessionInfo->{ $self->locationAttribute };
+
+    $self->logger->debug( "Could not find location of user " . $req->user )
+      unless $location;
+
+    # Get all non-empty, unique values of location attribute through list of
+    # successful logins
+    my @envHistory =
+      grep { $_ // "" }
+      uniq( map { $_->{ $self->locationAttribute } // "" } @{$successLogin} );
+
+    # Only consider some of the past unique locations
+    my $maxLocations = $self->locationMaxValues;
+    splice @envHistory, $maxLocations
+      if ( $maxLocations and ( scalar @envHistory > $maxLocations ) );
+
+    if ( grep { $_ eq $location } @envHistory ) {
+        $self->userLogger->debug(
+            "User " . $req->user . " logged in from known location $location" );
+    }
+    else {
+        # Not the first location in history, warn if new location
+        if (@envHistory) {
+            $self->userLogger->info( "User "
+                  . $req->user
+                  . " logged in from unknown location $location" );
+            my $riskLevel = ( $req->sessionInfo->{_riskLevel} || 0 ) + 1;
+            $req->sessionInfo->{_riskLevel} = $riskLevel;
+            $req->sessionInfo->{_riskDetails}->{newLocation} =
+              $req->sessionInfo->{ $self->locationDisplayAttribute };
+        }
+        else {
+            $self->userLogger->info( "User "
+                  . $req->user
+                  . " logged with empty location history from location $location"
+            );
+        }
+    }
+    return PE_OK;
+}
+
+sub sendWarningEmail {
+    my ( $self, $req ) = @_;
+    return $self->_sendMail($req)
+      if $req->sessionInfo->{_riskDetails}->{newLocation};
+
+    return PE_OK;
+}
+
+sub _sendMail {
+    my ( $self, $req ) = @_;
+    my $date     = strftime( '%F %X', localtime );
+    my $location = $req->sessionInfo->{_riskDetails}->{newLocation};
+    my $ua       = $req->env->{HTTP_USER_AGENT};
+    my $mail     = $req->sessionInfo->{ $self->mailSessionKey };
+
+    # Build mail content
+    my $tr      = $self->translate($req);
+    my $subject = $self->conf->{newLocationWarningMailSubject};
+    unless ($subject) {
+        $self->logger->debug('Use default warning subject');
+        $subject = 'newLocationWarningMailSubject';
+        $tr->( \$subject );
+    }
+    my ( $body, $html );
+    if ( $self->conf->{newLocationWarningMailBody} ) {
+
+        # We use a specific text message, no html
+        $self->logger->debug('Use specific warning body message');
+        $body = $self->conf->{newLocationWarningMailBody};
+
+        # Replace variables in body
+        $body =~ s/\$ua\b/$ua/ge;
+        $body =~ s/\$location\b/$location/ge;
+        $body =~ s/\$date\b/$date/ge;
+        $body =~ s/\$(\w+)/$req->{sessionInfo}->{$1} || ''/ge;
+    }
+    else {
+
+        # Use HTML template
+        $body = $self->loadMailTemplate(
+            $req,
+            'mail_new_location_warning',
+            filter => $tr,
+            params => {
+                location => $location,
+                date     => $date,
+                ua       => $ua
+            },
+        );
+        $html = 1;
+    }
+    if ( $mail && $subject && $body ) {
+        $self->logger->warn("User $mail is signing in from a new location");
+
+        # Send mail
+        $self->logger->debug('Unable to send new location warning mail')
+          unless ( $self->send_mail( $mail, $subject, $body, $html ) );
+    }
+    else {
+        $self->logger->error(
+            'Unable to send new location warning mail: missing parameter(s)');
+    }
+    return PE_OK;
+}
+
+1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/PublicPages.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/PublicPages.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/PublicPages.pm	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/PublicPages.pm	2022-02-19 16:04:21.000000000 +0000
@@ -10,7 +10,7 @@ our $VERSION = '2.0.10';
 sub init {
     my ($self) = @_;
     $self->addAuthRoute( public => { ':tpl' => 'run' }, ['GET'] )
-      ->addUnauthRoute( public => { ':tpl'  => 'run' }, ['GET'] );
+      ->addUnauthRoute( public => { ':tpl' => 'run' }, ['GET'] );
 
     return 1;
 }
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/Register.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/Register.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/Register.pm	2021-08-20 16:28:21.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/Register.pm	2022-02-07 19:06:14.000000000 +0000
@@ -436,6 +436,7 @@ sub display {
         MAIN_LOGO       => $self->conf->{portalMainLogo},
         AUTH_ERROR      => $req->error,
         AUTH_ERROR_TYPE => $req->error_type,
+        AUTH_ERROR_ROLE => $req->error_role,
         AUTH_URL        => $req->data->{_url},
         CHOICE_PARAM    => $self->conf->{authChoiceParam},
         CHOICE_VALUE    => $req->data->{_authChoice},
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/RESTServer.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/RESTServer.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/RESTServer.pm	2021-07-22 12:32:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/RESTServer.pm	2022-02-19 16:04:21.000000000 +0000
@@ -15,7 +15,7 @@
 #   * GET /session/my/<type>                     : get session data
 #   * GET /session/my/<type>/key                 : get session key
 #   * DELETE /session/my                         : ask for logout
-#   * DELETE /sessions/my                        : ask for global logout
+#   * DELETE /sessions/my                        : ask for global logout (if GlobalLogout plugin is on)
 #
 # - Authentication
 #   * GET /renewcaptcha                          : get token and captcha image
@@ -49,6 +49,8 @@
 #                                                            (restricted)
 #   * DELETE /mysession/<type>/key                         : delete key in data
 #                                                            (restricted)
+#   * GET    /myapplications                               : get my appplications
+#                                                            list
 #
 # There is no conflict with SOAP server, they can be used together
 
@@ -65,7 +67,7 @@ use Lemonldap::NG::Portal::Main::Constan
   URIRE
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends qw(
   Lemonldap::NG::Portal::Main::Plugin
@@ -105,9 +107,11 @@ has exportedAttr => (
 
             # Convert @attributes into hash to remove duplicates
             my %attributes = map( { $_ => 1 } @attributes );
-            %attributes =
-              ( %attributes, %{ $conf->{exportedVars} }, %{ $conf->{macros} },
-              );
+            %attributes = (
+                %attributes,
+                %{ $conf->{exportedVars} },
+                %{ $conf->{macros} },
+            );
             return '[' . join( ',', keys %attributes ) . ']';
         }
     }
@@ -198,9 +202,16 @@ sub init {
           )
 
           ->addAuthRoute(
-            sessions => { my => { ':sessionType' => 'removeSessions' } },
+            session => { my => 'removeSession' },
             ['DELETE']
           );
+
+        if ( $self->conf->{globalLogoutRule} ) {
+            $self->addAuthRoute(
+                sessions => { my => 'removeSessions' },
+                ['DELETE']
+            );
+        }
     }
 
     if ( $self->conf->{restPasswordServer} ) {
@@ -247,7 +258,10 @@ sub init {
       ->addAuthRoute(
         mysession => { ':sessionType' => 'updateMySession' },
         ['PUT']
-      );
+      )
+
+      ->addAuthRoute( myapplications => 'myApplications', ['GET'] );
+
     extends @parents               if ($add);
     $self->setTypes( $self->conf ) if ( $self->conf->{restSessionServer} );
 
@@ -352,7 +366,7 @@ sub updateSession {
 
     # Get session and store info
     my $session = $self->getApacheSession( $mod, $id, $infos, $force )
-      or return $self->p->sendError( $req, 'Session id does not exists', 400 );
+      or return $self->p->sendError( $req, 'Session Id does not exist', 400 );
 
     return $self->p->sendJSONresponse( $req, { result => 1 } );
 }
@@ -365,7 +379,7 @@ sub delSession {
 
     # Get session
     my $session = $self->getApacheSession( $mod, $id )
-      or return $self->p->sendError( $req, 'Session id does not exists', 400 );
+      or return $self->p->sendError( $req, 'Session Id does not exist', 400 );
 
     # Delete it
     $self->logger->debug("REST request to delete session $id");
@@ -588,16 +602,35 @@ sub getError {
     return $self->p->sendJSONresponse(
         $req,
         {
-            result   => 1,
-            lang     => $lang,
-            errorNum => $errNum ? $errNum : 'all',
+            result        => 1,
+            lang          => $lang,
+            errorNum      => $errNum ? $errNum : 'all',
             errorsFileURL =>
               "$self->{conf}->{staticPrefix}/languages/$lang.json",
-            ( $errNum ? ( errorMsgRef => "PE$errNum" ) : () ),
+            ( $errNum ? ( errorMsgRef => "PE$errNum" ) : () )
         }
     );
 }
 
+sub removeSession {
+    my ( $self, $req ) = @_;
+    my $id = $req->userData->{_session_id};
+    return $self->p->sendError( $req, 'ID is required', 400 ) unless ($id);
+    my $mod = $self->getGlobal()
+      or return $self->p->sendError( $req, undef, 400 );
+
+    # Get session
+    my $session = $self->getApacheSession( $mod, $id )
+      or return $self->p->sendError( $req, 'Session Id does not exist', 400 );
+
+    # Delete it
+    $self->logger->debug("REST request to delete global session $id");
+    my $res = $self->p->_deleteSession( $req, $session );
+    $self->logger->debug(" Result is $res");
+
+    return $self->p->sendJSONresponse( $req, { result => $res } );
+}
+
 sub removeSessions {
     my ( $self, $req ) = @_;
     my $glPlugin =
@@ -698,12 +731,13 @@ sub pwdConfirm {
             400 );
     }
 
-    $req->user($user);
-    $req->data->{password}  = $password;
-    $req->data->{_pwdCheck} = 1;
+    $req->parameters->{user}     = $user;
+    $req->parameters->{password} = $password;
+    $req->data->{_pwdCheck}      = 1;
+    $req->data->{skipToken}      = 1;
 
     if ( $self->p->_userDB ) {
-        $req->steps( [ 'getUser', 'authenticate' ] );
+        $req->steps( [ $self->p->authProcess ] );
         my $result = $self->p->process($req);
         if ( $result == PE_PASSWORD_OK or $result == PE_OK ) {
             return $self->p->sendJSONresponse( $req,
@@ -743,9 +777,8 @@ sub getUser {
 
     # Search user in database
     $req->steps( [
-            'getUser',   'setSessionInfo',
-            'setMacros', 'setGroups',
-            'setLocalGroups'
+            'getUser',                 'setSessionInfo',
+            $self->p->groupsAndMacros, 'setLocalGroups'
         ]
     );
     my $error = $self->p->process( $req, ( $mail ? ( useMail => 1 ) : () ) );
@@ -763,6 +796,24 @@ sub getUser {
     }
 }
 
+sub myApplications {
+    my ( $self, $req ) = @_;
+    my @appslist = map {
+        my @apps = map {
+            {
+                $_->{appname} => {
+                    AppUri  => $_->{appuri},
+                    AppDesc => $_->{appdesc}
+                }
+            }
+        } @{ $_->{applications} };
+        { Category => $_->{catname}, Applications => \@apps },
+    } @{ $self->p->menu->appslist($req) };
+
+    return $self->p->sendJSONresponse( $req,
+        { result => 1, myapplications => \@appslist } );
+}
+
 sub _checkSecret {
     my ( $self, $secret ) = @_;
     my $isValid = 0;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/SOAPServer.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/SOAPServer.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/SOAPServer.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/SOAPServer.pm	2022-02-19 16:04:21.000000000 +0000
@@ -26,7 +26,7 @@ extends qw(
   Lemonldap::NG::Common::Conf::AccessLib
 );
 
-has server => ( is => 'rw' );
+has server        => ( is => 'rw' );
 has configStorage => (
     is      => 'ro',
     lazy    => 1,
@@ -60,9 +60,11 @@ has exportedAttr => (
 
             # Convert @attributes into hash to remove duplicates
             my %attributes = map( { $_ => 1 } @attributes );
-            %attributes =
-              ( %attributes, %{ $conf->{exportedVars} }, %{ $conf->{macros} },
-              );
+            %attributes = (
+                %attributes,
+                %{ $conf->{exportedVars} },
+                %{ $conf->{macros} },
+            );
 
             return [ sort keys %attributes ];
         }
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/StayConnected.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/StayConnected.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/StayConnected.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/StayConnected.pm	2022-02-07 19:06:14.000000000 +0000
@@ -9,7 +9,7 @@ use Lemonldap::NG::Portal::Main::Constan
   PE_SENDRESPONSE
 );
 
-our $VERSION = '2.0.12';
+our $VERSION = '2.0.14';
 
 extends 'Lemonldap::NG::Portal::Main::Plugin';
 
@@ -20,8 +20,8 @@ use constant beforeAuth   => 'check';
 use constant beforeLogout => 'logout';
 
 # INITIALIZATION
-
-has ott => (
+has rule => ( is => 'rw', default => sub { 0 } );
+has ott  => (
     is      => 'rw',
     lazy    => 1,
     default => sub {
@@ -52,6 +52,11 @@ sub init {
     my ($self) = @_;
     $self->addAuthRoute( registerbrowser => 'storeBrowser', ['POST'] );
 
+    # Parse activation rule
+    $self->rule(
+        $self->p->buildRule( $self->conf->{stayConnected}, 'stayConnected' ) );
+    return 0 unless $self->rule;
+
     return 1;
 }
 
@@ -61,11 +66,12 @@ sub init {
 # Then ask for browser fingerprint
 sub newDevice {
     my ( $self, $req ) = @_;
-
     my $checkLogins = $req->param('checkLogins');
     $self->logger->debug("StayConnected: checkLogins set") if $checkLogins;
 
-    if ( $req->param('stayconnected') ) {
+    if (   $req->param('stayconnected')
+        && $self->rule->( $req, $req->sessionInfo ) )
+    {
         my $token = $self->ott->createToken( {
                 name => $req->sessionInfo->{ $self->conf->{whatToTrace} },
                 (
@@ -97,53 +103,59 @@ sub storeBrowser {
     my ( $self, $req ) = @_;
     $req->urldc( $req->param('url') );
     $req->mustRedirect(1);
-    if ( my $token = $req->param('token') ) {
-        if ( my $tmp = $self->ott->getToken($token) ) {
-            my $uid = $req->userData->{ $self->conf->{whatToTrace} };
-            if ( $tmp->{name} eq $uid ) {
-                if ( my $fg = $req->param('fg') ) {
-                    my $ps = Lemonldap::NG::Common::Session->new(
-                        storageModule => $self->conf->{globalStorage},
-                        storageModuleOptions =>
-                          $self->conf->{globalStorageOptions},
-                        kind => "SSO",
-                        info => {
-                            _utime          => time + $self->timeout(),
-                            _session_uid    => $uid,
-                            _connectedSince => time,
-                            dataKeep        => $req->data->{dataToKeep},
-                            fingerprint     => $fg,
-                        },
-                    );
-
-                    # Cookie available 30 days
-                    $req->addCookie(
-                        $self->p->cookie(
-                            name    => $self->cookieName(),
-                            value   => $ps->id,
-                            max_age => $self->timeout(),
-                            secure  => $self->conf->{securedCookie},
-                        )
-                    );
-                    $req->sessionInfo->{_loginHistory} = $tmp->{history}
-                      if exists $tmp->{history};
+    if ( $self->rule->( $req, $req->sessionInfo ) ) {
+        if ( my $token = $req->param('token') ) {
+            if ( my $tmp = $self->ott->getToken($token) ) {
+                my $uid = $req->userData->{ $self->conf->{whatToTrace} };
+                if ( $tmp->{name} eq $uid ) {
+                    if ( my $fg = $req->param('fg') ) {
+                        my $ps = Lemonldap::NG::Common::Session->new(
+                            storageModule => $self->conf->{globalStorage},
+                            storageModuleOptions =>
+                              $self->conf->{globalStorageOptions},
+                            kind => "SSO",
+                            info => {
+                                _utime          => time + $self->timeout,
+                                _session_uid    => $uid,
+                                _connectedSince => time,
+                                dataKeep        => $req->data->{dataToKeep},
+                                fingerprint     => $fg,
+                            },
+                        );
+
+                        # Cookie available 30 days by default
+                        $req->addCookie(
+                            $self->p->cookie(
+                                name    => $self->cookieName,
+                                value   => $ps->id,
+                                max_age => $self->timeout,
+                                secure  => $self->conf->{securedCookie},
+                            )
+                        );
+                        $req->sessionInfo->{_loginHistory} = $tmp->{history}
+                          if exists $tmp->{history};
+                    }
+                    else {
+                        $self->logger->warn(
+                            "Browser did not return fingerprint");
+                    }
                 }
                 else {
-                    $self->logger->warn("Browser hasn't return fingerprint");
+                    $self->userLogger->error(
+                        "StayConnected: mismatch UID ($tmp->{name} / $uid)");
                 }
             }
             else {
                 $self->userLogger->error(
-                    "StayConnected: mismatch UID: $tmp->{name} / $uid");
+                    "StayConnected called with an expired token");
             }
         }
         else {
-            $self->userLogger->error(
-                "StayConnected called with an expired token");
+            $self->userLogger->error('StayConnected called without token');
         }
     }
     else {
-        $self->userLogger->error('StayConnected called without token');
+        $self->userLogger->error('StayConnected not allowed');
     }
 
     # Return persistent connection cookie
@@ -157,73 +169,96 @@ sub storeBrowser {
 # Then delete authentication methods from "steps" array.
 sub check {
     my ( $self, $req ) = @_;
-    if ( my $cid = $req->cookies->{ $self->cookieName() } ) {
-        my $ps = Lemonldap::NG::Common::Session->new(
-            storageModule        => $self->conf->{globalStorage},
-            storageModuleOptions => $self->conf->{globalStorageOptions},
-            kind                 => "SSO",
-            id                   => $cid,
-        );
-        if (    $ps
-            and my $uid = $ps->data->{_session_uid}
-            and time() < $ps->data->{_utime} )
-        {
-            $self->logger->debug('Persistent connection found');
-            if (    my $fg = $req->param('fg')
-                and my $token = $req->param('token') )
+    if ( $self->rule->( $req, $req->sessionInfo ) ) {
+        if ( my $cid = $req->cookies->{ $self->cookieName } ) {
+            my $ps = Lemonldap::NG::Common::Session->new(
+                storageModule        => $self->conf->{globalStorage},
+                storageModuleOptions => $self->conf->{globalStorageOptions},
+                kind                 => "SSO",
+                id                   => $cid,
+            );
+            if (    $ps
+                and my $uid = $ps->data->{_session_uid}
+                and time() < $ps->data->{_utime} )
             {
-                if ( my $prm = $self->ott->getToken($token) ) {
-                    $req->data->{dataKeep} = $ps->data->{dataKeep};
-                    $self->logger->debug('Persistent connection found');
-                    if ( $fg eq $ps->data->{fingerprint} ) {
-                        $req->user($uid);
-                        my @steps =
-                          grep {
-                            !ref $_
-                              and $_ !~ /^(?:extractFormInfo|authenticate)$/
-                          } @{ $req->steps };
-                        $req->steps( \@steps );
-                        $self->userLogger->notice(
-                            "$uid connected by StayConnected cookie");
-                        return PE_OK;
+                $self->logger->debug('Persistent connection found');
+                if (    my $fg = $req->param('fg')
+                    and my $token = $req->param('token') )
+                {
+                    if ( my $prm = $self->ott->getToken($token) ) {
+                        $req->data->{dataKeep} = $ps->data->{dataKeep};
+                        $self->logger->debug('Persistent connection found');
+                        if ( $self->conf->{stayConnectedBypassFG} ) {
+                            $req->user($uid);
+                            my @steps =
+                              grep {
+                                !ref $_
+                                  and $_ !~ /^(?:extractFormInfo|authenticate)$/
+                              } @{ $req->steps };
+                            $req->steps( \@steps );
+                            $self->userLogger->notice(
+"$uid connected by StayConnected cookie without fingerprint checking"
+                            );
+                            return PE_OK;
+                        }
+                        else {
+                            if ( $fg eq $ps->data->{fingerprint} ) {
+                                $req->user($uid);
+                                my @steps =
+                                  grep {
+                                    !ref $_
+                                      and $_ !~
+                                      /^(?:extractFormInfo|authenticate)$/
+                                  } @{ $req->steps };
+                                $req->steps( \@steps );
+                                $self->userLogger->notice(
+                                    "$uid connected by StayConnected cookie");
+                                return PE_OK;
+                            }
+                            else {
+                                $self->userLogger->warn(
+                                    "Fingerprint changed for $uid");
+                                $ps->remove;
+                                $self->logout($req);
+                            }
+                        }
                     }
                     else {
-                        $self->userLogger->warn("Fingerprint changed for $uid");
-                        $ps->remove;
-                        $self->logout($req);
+                        $self->userLogger->notice(
+                            "StayConnected: expired token for $uid");
                     }
                 }
                 else {
-                    $self->userLogger->notice(
-                        "StayConnected: expired token for $uid");
+                    my $token = $self->ott->createToken( $req->parameters );
+                    $req->response(
+                        $self->p->sendHtml(
+                            $req,
+                            '../common/registerBrowser',
+                            params => {
+                                TOKEN  => $token,
+                                ACTION => '#',
+                            }
+                        )
+                    );
+                    return PE_SENDRESPONSE;
                 }
             }
             else {
-                my $token = $self->ott->createToken( $req->parameters );
-                $req->response(
-                    $self->p->sendHtml(
-                        $req,
-                        '../common/registerBrowser',
-                        params => {
-                            TOKEN  => $token,
-                            ACTION => '#',
-                        }
-                    )
-                );
-                return PE_SENDRESPONSE;
-            }
-        }
-        else {
-            $self->userLogger->notice('Persistent connection expired');
-            unless ( $ps->{error} ) {
-                $self->logger->debug(
-                    'Persistent connection session id = ' . $ps->{id} );
-                $self->logger->debug( 'Persistent connection session _utime = '
-                      . $ps->data->{_utime} );
-                $ps->remove;
+                $self->userLogger->notice('Persistent connection expired');
+                unless ( $ps->{error} ) {
+                    $self->logger->debug(
+                        'Persistent connection session id = ' . $ps->{id} );
+                    $self->logger->debug(
+                        'Persistent connection session _utime = '
+                          . $ps->data->{_utime} );
+                    $ps->remove;
+                }
             }
         }
     }
+    else {
+        $self->userLogger->error('StayConnected not allowed');
+    }
     return PE_OK;
 }
 
@@ -231,7 +266,7 @@ sub logout {
     my ( $self, $req ) = @_;
     $req->addCookie(
         $self->p->cookie(
-            name    => $self->cookieName(),
+            name    => $self->cookieName,
             value   => 0,
             expires => 'Wed, 21 Oct 2015 00:00:00 GMT',
             secure  => $self->conf->{securedCookie},
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Register/AD.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Register/AD.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Register/AD.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Register/AD.pm	2022-02-07 19:06:14.000000000 +0000
@@ -9,7 +9,7 @@ use Lemonldap::NG::Portal::Main::Constan
 
 extends 'Lemonldap::NG::Portal::Register::LDAP';
 
-our $VERSION = '2.0.10';
+our $VERSION = '2.0.14';
 
 sub createUser {
     my ( $self, $req ) = @_;
@@ -44,8 +44,6 @@ sub createUser {
             "LDAP error " . $mesg->code . ": " . $mesg->error );
 
         $self->ldap->unbind();
-        $self->{flags}->{ldapActive} = 0;
-
         return PE_LDAPERROR;
     }
     return PE_OK;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Register/Custom.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Register/Custom.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Register/Custom.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Register/Custom.pm	2022-01-22 14:30:25.000000000 +0000
@@ -1,22 +1,12 @@
 package Lemonldap::NG::Portal::Register::Custom;
+use Lemonldap::NG::Portal::Lib::CustomModule;
 
 use strict;
-use Mouse;
 
-extends 'Lemonldap::NG::Portal::Register::Base';
-
-sub new {
-    my ( $class, $self ) = @_;
-    unless ( $self->{conf}->{customRegister} ) {
-        die 'Custom register module not defined';
-    }
-
-    my $res = $self->{p}->loadModule( $self->{conf}->{customRegister} );
-    unless ($res) {
-        die 'Unable to load register module ' . $self->{conf}->{customRegister};
-    }
-
-    return $res;
-}
+our @ISA = qw(Lemonldap::NG::Portal::Lib::CustomModule);
+use constant {
+    custom_name       => "Register",
+    custom_config_key => "customRegister",
+};
 
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Register/LDAP.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Register/LDAP.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Register/LDAP.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Register/LDAP.pm	2022-02-07 19:06:14.000000000 +0000
@@ -14,7 +14,7 @@ extends qw(
   Lemonldap::NG::Portal::Register::Base
 );
 
-our $VERSION = '2.0.10';
+our $VERSION = '2.0.14';
 
 # RUNNING METHODS
 
@@ -76,7 +76,6 @@ sub createUser {
             "LDAP error " . $mesg->code . ": " . $mesg->error );
 
         $self->ldap->unbind();
-        $self->{flags}->{ldapActive} = 0;
 
         return PE_LDAPERROR;
     }
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/Custom.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/Custom.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/Custom.pm	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/Custom.pm	2022-01-22 14:30:25.000000000 +0000
@@ -1,17 +1,12 @@
 package Lemonldap::NG::Portal::UserDB::Custom;
+use Lemonldap::NG::Portal::Lib::CustomModule;
 
 use strict;
 
-sub new {
-    my ( $class, $self ) = @_;
-    unless ( $self->{conf}->{customUserDB} ) {
-        die 'Custom User DB module not defined';
-    }
-
-    eval $self->{p}->loadModule( $self->{conf}->{customUserDB} );
-    ($@)
-      ? return $self->{p}->loadModule( $self->{conf}->{customUserDB} )
-      : die 'Unable to load UserDB module ' . $self->{conf}->{customUserDB};
-}
+our @ISA = qw(Lemonldap::NG::Portal::Lib::CustomModule);
+use constant {
+    custom_name       => "UserDB",
+    custom_config_key => "customUserDB",
+};
 
 1;
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/Demo.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/Demo.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/Demo.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/Demo.pm	2022-02-19 16:04:21.000000000 +0000
@@ -109,7 +109,7 @@ sub findUser {
         my $uid  = $demoAccounts{$_}->{uid};
         my $cn   = $demoAccounts{$_}->{cn};
         my $mail = $demoAccounts{$_}->{mail};
-        my $guy  = $demoAccounts{$_}->{guy} // 'good';
+        my $guy  = $demoAccounts{$_}->{guy}  // 'good';
         my $type = $demoAccounts{$_}->{type} // 'character';
         eval "($cond)"
           ? $_
@@ -136,8 +136,10 @@ sub findUser {
 sub setSessionInfo {
     my ( $self, $req ) = @_;
 
-    my %vars = ( %{ $self->conf->{exportedVars} },
-        %{ $self->conf->{demoExportedVars} } );
+    my %vars = (
+        %{ $self->conf->{exportedVars} },
+        %{ $self->conf->{demoExportedVars} }
+    );
     while ( my ( $k, $v ) = each %vars ) {
         $req->{sessionInfo}->{$k} = $demoAccounts{ $req->{user} }->{$v};
     }
@@ -151,7 +153,7 @@ sub setSessionInfo {
 sub setGroups {
     my ( $self, $req ) = @_;
     my $user    = $req->user;
-    my $groups  = $req->sessionInfo->{groups} || '';
+    my $groups  = $req->sessionInfo->{groups}  || '';
     my $hGroups = $req->sessionInfo->{hGroups} || {};
     for my $grp ( keys %demoGroups ) {
         if ( grep { $_ eq $user } @{ $demoGroups{$grp} } ) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/LDAP.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/LDAP.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/LDAP.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/LDAP.pm	2022-02-19 16:04:21.000000000 +0000
@@ -36,8 +36,10 @@ sub setSessionInfo {
     my ( $self, $req ) = @_;
     $req->{sessionInfo}->{_dn} = $req->data->{dn};
 
-    my %vars = ( %{ $self->conf->{exportedVars} },
-        %{ $self->conf->{ldapExportedVars} } );
+    my %vars = (
+        %{ $self->conf->{exportedVars} },
+        %{ $self->conf->{ldapExportedVars} }
+    );
     while ( my ( $k, $v ) = each %vars ) {
 
         my $value = $self->ldap->getLdapValue( $req->data->{ldapentry}, $v );
@@ -58,8 +60,8 @@ sub setSessionInfo {
 # @return Lemonldap::NG::Portal constant
 sub setGroups {
     my ( $self, $req ) = @_;
-    my $groups  = $req->{sessionInfo}->{groups};
-    my $hGroups = $req->{sessionInfo}->{hGroups};
+    my $groups  = $req->{sessionInfo}->{groups}  || '';
+    my $hGroups = $req->{sessionInfo}->{hGroups} || {};
 
     if ( $self->conf->{ldapGroupBase} ) {
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/Remote.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/Remote.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/Remote.pm	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/UserDB/Remote.pm	2022-02-19 16:04:21.000000000 +0000
@@ -25,7 +25,7 @@ sub setSessionInfo {
     my ( $self, $req ) = @_;
     delete $req->data->{rSessionInfo}->{_session_id};
     $req->{sessionInfo} = $req->data->{rSessionInfo};
-    
+
     return PE_OK;
 }
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal.pm 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal.pm
--- 2.0.13+ds-3/lemonldap-ng-portal/lib/Lemonldap/NG/Portal.pm	2021-08-20 16:29:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/lib/Lemonldap/NG/Portal.pm	2022-02-19 16:43:01.000000000 +0000
@@ -1,7 +1,7 @@
 # Alias for Lemonldap::NG::Portal::Main
 package Lemonldap::NG::Portal;
 
-our $VERSION = '2.0.13';
+our $VERSION = '2.0.14';
 use Lemonldap::NG::Portal::Main;
 use base 'Lemonldap::NG::Portal::Main';
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/Makefile.PL 2.0.14+ds-1/lemonldap-ng-portal/Makefile.PL
--- 2.0.13+ds-3/lemonldap-ng-portal/Makefile.PL	2021-08-20 16:29:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/Makefile.PL	2022-02-21 12:04:41.000000000 +0000
@@ -9,6 +9,8 @@ WriteMakefile(
     LICENSE      => 'gpl',
     META_MERGE   => {
         'recommends' => {
+            'Authen::Radius'        => 0,
+            'Authen::WebAuthn'      => 0,
             'Convert::Base32'       => 0,
             'DBI'                   => 0,
             'Email::Sender'         => 1.300027,
@@ -16,8 +18,10 @@ WriteMakefile(
             'Glib'                  => 0,
             'HTTP::Message'         => 0,
             'Image::Magick'         => 0,
+            'IO::Socket::Timeout'   => 0,
             'IPC::Run'              => 0,
             'Lasso'                 => '2.3.0',
+            'List::MoreUtils'       => 0,
             'LWP::UserAgent'        => 0,
             'LWP::Protocol::https'  => 0,
             'MIME::Entity'          => 0,
@@ -39,8 +43,8 @@ WriteMakefile(
             },
             MailingList => 'mailto:lemonldap-ng-dev@ow2.org',
             license     => 'http://opensource.org/licenses/GPL-2.0',
-            homepage    => 'http://lemonldap-ng.org/',
-            bugtracker =>
+            homepage    => 'https://lemonldap-ng.org/',
+            bugtracker  =>
               'https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues',
             x_twitter => 'https://twitter.com/lemonldapng',
         },
@@ -61,7 +65,7 @@ WriteMakefile(
     },
     PREREQ_PM => {
         'Clone'                  => 0,
-        'Lemonldap::NG::Handler' => '2.0.13',
+        'Lemonldap::NG::Handler' => '2.0.14',
         'Regexp::Assemble'       => 0,
     },
     (
@@ -71,7 +75,7 @@ WriteMakefile(
             ABSTRACT_FROM =>
               'lib/Lemonldap/NG/Portal.pm',    # retrieve abstract from module
             AUTHOR =>
-'Xavier Guimard <x.guimard@free.fr>, Clément Oudot <clement@oodo.net>'
+'Xavier Guimard <x.guimard@free.fr>, Clement Oudot <clement@oodo.net>, Christophe Maudoux <chrmdx@gmail.com>, Maxime Besson <maxime.besson@worteks.com>'
           )
         : ()
     ),
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/MANIFEST 2.0.14+ds-1/lemonldap-ng-portal/MANIFEST
--- 2.0.13+ds-3/lemonldap-ng-portal/MANIFEST	2021-08-21 17:42:59.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/MANIFEST	2022-02-19 16:04:21.000000000 +0000
@@ -11,11 +11,13 @@ lib/Lemonldap/NG/Portal/2F/Mail2F.pm
 lib/Lemonldap/NG/Portal/2F/Radius.pm
 lib/Lemonldap/NG/Portal/2F/Register/TOTP.pm
 lib/Lemonldap/NG/Portal/2F/Register/U2F.pm
+lib/Lemonldap/NG/Portal/2F/Register/WebAuthn.pm
 lib/Lemonldap/NG/Portal/2F/Register/Yubikey.pm
 lib/Lemonldap/NG/Portal/2F/REST.pm
 lib/Lemonldap/NG/Portal/2F/TOTP.pm
 lib/Lemonldap/NG/Portal/2F/U2F.pm
 lib/Lemonldap/NG/Portal/2F/UTOTP.pm
+lib/Lemonldap/NG/Portal/2F/WebAuthn.pm
 lib/Lemonldap/NG/Portal/2F/Yubikey.pm
 lib/Lemonldap/NG/Portal/Auth.pod
 lib/Lemonldap/NG/Portal/Auth/_WebForm.pm
@@ -59,6 +61,7 @@ lib/Lemonldap/NG/Portal/Lib/_tokenRule.p
 lib/Lemonldap/NG/Portal/Lib/Captcha.pm
 lib/Lemonldap/NG/Portal/Lib/CAS.pm
 lib/Lemonldap/NG/Portal/Lib/Choice.pm
+lib/Lemonldap/NG/Portal/Lib/CustomModule.pm
 lib/Lemonldap/NG/Portal/Lib/DBI.pm
 lib/Lemonldap/NG/Portal/Lib/LDAP.pm
 lib/Lemonldap/NG/Portal/Lib/Net/LDAP.pm
@@ -78,6 +81,7 @@ lib/Lemonldap/NG/Portal/Lib/Slave.pm
 lib/Lemonldap/NG/Portal/Lib/SMTP.pm
 lib/Lemonldap/NG/Portal/Lib/SOAPProxy.pm
 lib/Lemonldap/NG/Portal/Lib/U2F.pm
+lib/Lemonldap/NG/Portal/Lib/WebAuthn.pm
 lib/Lemonldap/NG/Portal/Lib/Wrapper.pm
 lib/Lemonldap/NG/Portal/Main.pm
 lib/Lemonldap/NG/Portal/Main/Auth.pm
@@ -120,6 +124,7 @@ lib/Lemonldap/NG/Portal/Plugins/GrantSes
 lib/Lemonldap/NG/Portal/Plugins/History.pm
 lib/Lemonldap/NG/Portal/Plugins/Impersonation.pm
 lib/Lemonldap/NG/Portal/Plugins/MailPasswordReset.pm
+lib/Lemonldap/NG/Portal/Plugins/NewLocationWarning.pm
 lib/Lemonldap/NG/Portal/Plugins/Notifications.pm
 lib/Lemonldap/NG/Portal/Plugins/PublicPages.pm
 lib/Lemonldap/NG/Portal/Plugins/Refresh.pm
@@ -180,6 +185,8 @@ site/coffee/sslChoice.coffee
 site/coffee/totpregistration.coffee
 site/coffee/u2fcheck.coffee
 site/coffee/u2fregistration.coffee
+site/coffee/webauthncheck.coffee
+site/coffee/webauthnregistration.coffee
 site/cron/purgeCentralCache
 site/cron/purgeCentralCache.cron.d
 site/htdocs/index.fcgi
@@ -193,6 +200,7 @@ site/htdocs/static/bootstrap/js/skin.min
 site/htdocs/static/bootstrap/totp.png
 site/htdocs/static/bootstrap/u2f.png
 site/htdocs/static/bootstrap/utotp.png
+site/htdocs/static/bootstrap/webauthn.png
 site/htdocs/static/bootstrap/yubikey.png
 site/htdocs/static/bwr/bootstrap/dist/css/bootstrap-grid.css
 site/htdocs/static/bwr/bootstrap/dist/css/bootstrap-grid.css.map
@@ -227,6 +235,7 @@ site/htdocs/static/bwr/font-awesome/font
 site/htdocs/static/bwr/font-awesome/fonts/fontawesome-webfont.woff2
 site/htdocs/static/bwr/jquery-ui/jquery-ui.js
 site/htdocs/static/bwr/jquery-ui/jquery-ui.min.js
+site/htdocs/static/bwr/jquery-ui/jquery-ui.min.js.map
 site/htdocs/static/bwr/jquery.cookie/jquery.cookie.js
 site/htdocs/static/bwr/jquery.cookie/jquery.cookie.min.js
 site/htdocs/static/bwr/jquery.cookie/jquery.cookie.min.js.map
@@ -272,6 +281,7 @@ site/htdocs/static/common/favicon.ico
 site/htdocs/static/common/fi.png
 site/htdocs/static/common/fonts/password.ttf
 site/htdocs/static/common/fr.png
+site/htdocs/static/common/he.png
 site/htdocs/static/common/icons/application_cascade.png
 site/htdocs/static/common/icons/arrow_refresh.png
 site/htdocs/static/common/icons/calendar.png
@@ -347,6 +357,14 @@ site/htdocs/static/common/js/u2fcheck.mi
 site/htdocs/static/common/js/u2fregistration.js
 site/htdocs/static/common/js/u2fregistration.min.js
 site/htdocs/static/common/js/u2fregistration.min.js.map
+site/htdocs/static/common/js/webauthn-ui.js
+site/htdocs/static/common/js/webauthn-ui.min.js
+site/htdocs/static/common/js/webauthncheck.js
+site/htdocs/static/common/js/webauthncheck.min.js
+site/htdocs/static/common/js/webauthncheck.min.js.map
+site/htdocs/static/common/js/webauthnregistration.js
+site/htdocs/static/common/js/webauthnregistration.min.js
+site/htdocs/static/common/js/webauthnregistration.min.js.map
 site/htdocs/static/common/logos/logo_llng_400px.png
 site/htdocs/static/common/logos/logo_llng_old.png
 site/htdocs/static/common/modules/Apache.png
@@ -366,6 +384,7 @@ site/htdocs/static/common/modules/WebID.
 site/htdocs/static/common/nl.png
 site/htdocs/static/common/pl.png
 site/htdocs/static/common/pt.png
+site/htdocs/static/common/pt_BR.png
 site/htdocs/static/common/ro.png
 site/htdocs/static/common/tr.png
 site/htdocs/static/common/vi.png
@@ -377,10 +396,12 @@ site/htdocs/static/languages/en.json
 site/htdocs/static/languages/es.json
 site/htdocs/static/languages/fi.json
 site/htdocs/static/languages/fr.json
+site/htdocs/static/languages/he.json
 site/htdocs/static/languages/it.json
 site/htdocs/static/languages/nl.json
 site/htdocs/static/languages/pl.json
 site/htdocs/static/languages/pt.json
+site/htdocs/static/languages/pt_BR.json
 site/htdocs/static/languages/ro.json
 site/htdocs/static/languages/tr.json
 site/htdocs/static/languages/vi.json
@@ -449,6 +470,8 @@ site/templates/bootstrap/u2fcheck.tpl
 site/templates/bootstrap/u2fregister.tpl
 site/templates/bootstrap/upgradesession.tpl
 site/templates/bootstrap/utotp2fcheck.tpl
+site/templates/bootstrap/webauthn2fcheck.tpl
+site/templates/bootstrap/webauthn2fregister.tpl
 site/templates/bootstrap/yubikey2fregister.tpl
 site/templates/bootstrap/yubikeyform.tpl
 site/templates/common/bullet_go.png
@@ -458,8 +481,10 @@ site/templates/common/mail/en.json
 site/templates/common/mail/es.json
 site/templates/common/mail/fi.json
 site/templates/common/mail/fr.json
+site/templates/common/mail/he.json
 site/templates/common/mail/it.json
 site/templates/common/mail/ms.json
+site/templates/common/mail/pt_BR.json
 site/templates/common/mail/tr.json
 site/templates/common/mail/vi.json
 site/templates/common/mail/zh_CN.json
@@ -470,6 +495,7 @@ site/templates/common/mail_certificateRe
 site/templates/common/mail_confirm.tpl
 site/templates/common/mail_footer.tpl
 site/templates/common/mail_header.tpl
+site/templates/common/mail_new_location_warning.tpl
 site/templates/common/mail_password.tpl
 site/templates/common/mail_register_confirm.tpl
 site/templates/common/mail_register_done.tpl
@@ -479,12 +505,15 @@ site/templates/common/oidc_checksession.
 site/templates/common/registerBrowser.tpl
 site/templates/common/script.tpl
 t/01-AuthDemo.t
+t/01-BuildUrl.t
 t/01-CSP-and-CORS-headers.t
 t/01-EnablePasswordDisplay.t
 t/01-Handler-redirection-and-URL-check-by-portal.t
 t/01-pdata.t
 t/01-Reject-Hashes-in-URL.t
 t/01-Unauth-Logout.t
+t/01-WebAuthn-Registration.t
+t/01-WebAuthn.t
 t/02-Password-Demo-Hook.t
 t/02-Password-Demo-Local-noPpolicy.t
 t/02-Password-Demo-Local-Ppolicy.t
@@ -494,12 +523,14 @@ t/03-ConfTimeout.t
 t/03-SessionTimeout.t
 t/03-XSS-protection.t
 t/04-language-selection.t
+t/10-AuthCustom.t
 t/19-Auth-Null.t
 t/20-Auth-and-password-DBI-dynamic-hash.t
 t/20-Auth-and-password-DBI.t
 t/20-Auth-DBI-utf8.t
 t/21-Auth-and-password-LDAP.t
 t/21-Auth-LDAP-attributes.t
+t/21-Auth-LDAP-Policy-Combination.t
 t/21-Auth-LDAP-Policy-only.t
 t/21-Auth-LDAP-Policy.t
 t/22-Auth-and-password-AD.t
@@ -508,9 +539,12 @@ t/24-AuthApache.t
 t/24-AuthKerberos.t
 t/25-AuthSlave-with-Choice.t
 t/25-AuthSlave-with-Credentials.t
+t/26-AuthRadius.t
 t/26-AuthRemote.t
+t/27-AuthProxy-with-choice.t
 t/27-AuthProxy.t
 t/28-AuthChoice-and-password.t
+t/28-AuthChoice-Custom.t
 t/28-AuthChoice-with-captcha.t
 t/28-AuthChoice-with-info.t
 t/28-AuthChoice-with-over.t
@@ -542,12 +576,13 @@ t/30-SAML-POST-with-2F-UpgradeOnly.t
 t/30-SAML-POST-with-Notification.t
 t/30-SAML-ReAuth-with-choice.t
 t/30-SAML-ReAuth.t
+t/30-SAML-RelayState.t
 t/30-SAML-SP-rule.t
+t/31-Auth-and-issuer-CAS-declared-app-multiple-urls.t
 t/31-Auth-and-issuer-CAS-declared-app-userattr.t
 t/31-Auth-and-issuer-CAS-declared-app.t
 t/31-Auth-and-issuer-CAS-declared-apps.t
 t/31-Auth-and-issuer-CAS-default.t
-t/31-Auth-and-issuer-CAS-gateway.t
 t/31-Auth-and-issuer-CAS-Logout-20.t
 t/31-Auth-and-issuer-CAS-Logout-30.t
 t/31-Auth-and-issuer-CAS-proxied.t
@@ -566,13 +601,18 @@ t/32-Auth-and-issuer-OIDC-implicit-no-to
 t/32-Auth-and-issuer-OIDC-implicit.t
 t/32-Auth-and-issuer-OIDC-sorted.t
 t/32-CAS-10.t
+t/32-CAS-Gateway.t
 t/32-CAS-Hooks.t
 t/32-CAS-Macros.t
 t/32-CAS-Prefix.t
+t/32-CAS-Proxy.t
+t/32-CAS-Security.t
 t/32-OIDC-ClaimTypes.t
 t/32-OIDC-ClientCredentials-Grant.t
 t/32-OIDC-Code-Flow-with-2F-UpgradeOnly.t
 t/32-OIDC-Code-Flow-with-2F.t
+t/32-OIDC-Grant-Type-OAuth2-Handler-Rules.t
+t/32-OIDC-Grant-Type-Rules.t
 t/32-OIDC-Hooks.t
 t/32-OIDC-Macro.t
 t/32-OIDC-Offline-Session.t
@@ -588,12 +628,14 @@ t/34-Auth-Proxy-and-REST-Server.t
 t/34-Auth-Proxy-and-REST-sessions.t
 t/34-Auth-Proxy-and-SOAP-Server.t
 t/35-My-session.t
+t/35-REST-auth-password-server.t
 t/35-REST-config-backend.t
 t/35-REST-export-password.t
 t/35-REST-sessions-with-AuthBasic-handler.t
 t/35-REST-sessions-with-REST-server.t
 t/35-SOAP-config-backend.t
 t/35-SOAP-sessions-with-SOAP-server.t
+t/36-Combination-Custom.t
 t/36-Combination-Kerberos-or-Demo.t
 t/36-Combination-Password.t
 t/36-Combination-with-Choice.t
@@ -630,6 +672,7 @@ t/40-Notifications-XML-Server.t
 t/41-Captcha.t
 t/41-Token-with-global-storage.t
 t/41-Token.t
+t/42-Register-Custom.t
 t/42-Register-Demo-with-captcha.t
 t/42-Register-Demo-with-CustomBody.t
 t/42-Register-Demo-with-token.t
@@ -671,6 +714,8 @@ t/61-CrowdSec-warn.t
 t/61-CrowdSec.t
 t/61-ForceAuthn.t
 t/61-GrantSession.t
+t/61-NewLocationWarning-Custom.t
+t/61-NewLocationWarning.t
 t/61-Session-ActivityTimeout.t
 t/61-Session-Timeout.t
 t/62-Refresh-plugin.t
@@ -682,6 +727,8 @@ t/62-UpgradeSession.t
 t/63-History.t
 t/64-StayConnected-with-2F-and-History.t
 t/64-StayConnected-with-History.t
+t/64-StayConnected-with-rule.t
+t/64-StayConnected-without-fingerprint-checking.t
 t/65-AutoSignin.t
 t/65-CheckState.t
 t/66-CDA-already-auth.t
@@ -692,6 +739,8 @@ t/66-CDA-with-REST.t
 t/66-CDA-with-SOAP.t
 t/66-CDA.t
 t/67-CheckUser-with-Global-token.t
+t/67-CheckUser-with-hidden-attributes.t
+t/67-CheckUser-with-history.t
 t/67-CheckUser-with-Impersonation-and-Macros.t
 t/67-CheckUser-with-issuer-SAML-POST.t
 t/67-CheckUser-with-rules.t
@@ -719,6 +768,7 @@ t/68-FindUser-with-UpgradeSession.t
 t/68-FindUser-without-attribute.t
 t/68-FindUser-without-Impersonation.t
 t/68-Impersonation-with-2F.t
+t/68-Impersonation-with-Custom-Plugin.t
 t/68-Impersonation-with-doubleCookies.t
 t/68-Impersonation-with-filtered-merge.t
 t/68-Impersonation-with-History.t
@@ -728,6 +778,7 @@ t/68-Impersonation-with-UnrestrictedUser
 t/68-Impersonation.t
 t/70-2F-TOTP-8-with-global-storage.t
 t/70-2F-TOTP-and-U2F-with-TTL-and-JSON.t
+t/70-2F-TOTP-encryption.t
 t/70-2F-TOTP-with-History-and-Refresh.t
 t/70-2F-TOTP-with-Range.t
 t/70-2F-TOTP-with-TTL-and-JSON.t
@@ -762,17 +813,20 @@ t/79-2F-Yubikey.t
 t/90-Translations.t
 t/91-Handler-cache-cleaned.t
 t/91-Memory-Leak.t
+t/99-Bad-logLevel.t
 t/99-Dont-load-Dumper.t
 t/99-pod.t
+t/AfterDataCustomPlugin.pm
 t/CasHookPlugin.pm
+t/Custom.pm
 t/gpghome/key.asc
 t/gpghome/openpgp-revocs.d/9482CEFB055809CBAFE6D71AAB2D5542891D1677.rev
 t/gpghome/private-keys-v1.d/A076B0E7DB141A919271EE8B581CDFA8DA42F333.key
 t/gpghome/private-keys-v1.d/B7219440BCCD85200121CFB89F94C8D98C0397B3.key
 t/gpghome/pubring.kbx
-t/gpghome/pubring.kbx~
 t/gpghome/tofu.db
 t/gpghome/trustdb.gpg
+t/HistoryPlugin.pm
 t/lib/Apache/Session/Timeout.pm
 t/lib/Lemonldap/NG/Common/Conf/Backends/Timeout.pm
 t/lib/Lemonldap/NG/Handler/Test.pm
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/META.json 2.0.14+ds-1/lemonldap-ng-portal/META.json
--- 2.0.13+ds-3/lemonldap-ng-portal/META.json	2021-08-20 16:29:36.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/META.json	2022-02-21 12:04:41.000000000 +0000
@@ -1,7 +1,7 @@
 {
    "abstract" : "The authentication portal part of Lemonldap::NG Web-SSO system.",
    "author" : [
-      "Xavier Guimard <x.guimard@free.fr>, ClÃ©ment Oudot <clement@oodo.net>"
+      "Xavier Guimard <x.guimard@free.fr>, Clement Oudot <clement@oodo.net>, Christophe Maudoux <chrmdx@gmail.com>, Maxime Besson <maxime.besson@worteks.com>"
    ],
    "dynamic_config" : 1,
    "generated_by" : "ExtUtils::MakeMaker version 7.34, CPAN::Meta::Converter version 2.150010",
@@ -43,17 +43,21 @@
       },
       "runtime" : {
          "recommends" : {
+            "Authen::Radius" : "0",
+            "Authen::WebAuthn" : "0",
             "Convert::Base32" : "0",
             "DBI" : "0",
             "Email::Sender" : "1.300027",
             "GD::SecurityImage" : "0",
             "Glib" : "0",
             "HTTP::Message" : "0",
+            "IO::Socket::Timeout" : "0",
             "IPC::Run" : "0",
             "Image::Magick" : "0",
             "LWP::Protocol::https" : "0",
             "LWP::UserAgent" : "0",
             "Lasso" : "v2.3.0",
+            "List::MoreUtils" : "0",
             "MIME::Entity" : "0",
             "Net::Facebook::Oauth2" : "0",
             "Net::LDAP" : "0.38",
@@ -67,7 +71,7 @@
          },
          "requires" : {
             "Clone" : "0",
-            "Lemonldap::NG::Handler" : "v2.0.13",
+            "Lemonldap::NG::Handler" : "v2.0.14",
             "Regexp::Assemble" : "0"
          }
       }
@@ -78,12 +82,12 @@
       "bugtracker" : {
          "web" : "https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues"
       },
-      "homepage" : "http://lemonldap-ng.org/",
+      "homepage" : "https://lemonldap-ng.org/",
       "license" : [
          "http://opensource.org/licenses/GPL-2.0"
       ],
       "x_MailingList" : "mailto:lemonldap-ng-dev@ow2.org"
    },
-   "version" : "v2.0.13",
+   "version" : "v2.0.14",
    "x_serialization_backend" : "JSON::PP version 4.04"
 }
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/META.yml 2.0.14+ds-1/lemonldap-ng-portal/META.yml
--- 2.0.13+ds-3/lemonldap-ng-portal/META.yml	2021-08-20 16:29:36.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/META.yml	2022-02-21 12:04:41.000000000 +0000
@@ -1,7 +1,7 @@
 ---
 abstract: 'The authentication portal part of Lemonldap::NG Web-SSO system.'
 author:
-  - 'Xavier Guimard <x.guimard@free.fr>, ClÃ©ment Oudot <clement@oodo.net>'
+  - 'Xavier Guimard <x.guimard@free.fr>, Clement Oudot <clement@oodo.net>, Christophe Maudoux <chrmdx@gmail.com>, Maxime Besson <maxime.besson@worteks.com>'
 build_requires:
   Convert::Base32: '0'
   Email::Sender: '0'
@@ -29,17 +29,21 @@ no_index:
     - t
     - inc
 recommends:
+  Authen::Radius: '0'
+  Authen::WebAuthn: '0'
   Convert::Base32: '0'
   DBI: '0'
   Email::Sender: '1.300027'
   GD::SecurityImage: '0'
   Glib: '0'
   HTTP::Message: '0'
+  IO::Socket::Timeout: '0'
   IPC::Run: '0'
   Image::Magick: '0'
   LWP::Protocol::https: '0'
   LWP::UserAgent: '0'
   Lasso: v2.3.0
+  List::MoreUtils: '0'
   MIME::Entity: '0'
   Net::Facebook::Oauth2: '0'
   Net::LDAP: '0.38'
@@ -52,13 +56,13 @@ recommends:
   Web::ID: '0'
 requires:
   Clone: '0'
-  Lemonldap::NG::Handler: v2.0.13
+  Lemonldap::NG::Handler: v2.0.14
   Regexp::Assemble: '0'
 resources:
   MailingList: mailto:lemonldap-ng-dev@ow2.org
   X_twitter: https://twitter.com/lemonldapng
   bugtracker: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues
-  homepage: http://lemonldap-ng.org/
+  homepage: https://lemonldap-ng.org/
   license: http://opensource.org/licenses/GPL-2.0
-version: v2.0.13
+version: v2.0.14
 x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/README 2.0.14+ds-1/lemonldap-ng-portal/README
--- 2.0.13+ds-3/lemonldap-ng-portal/README	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/README	2022-02-21 12:04:41.000000000 +0000
@@ -3,7 +3,7 @@ LemonLDAP::NG
 
 LemonLDAP::NG is a modular Web-SSO based on Apache::Session modules.
 This is the portal part of it. You can find documentation here:
- * for administrators: http://lemonldap-ng.org/
+ * for administrators: https://lemonldap-ng.org/
  * for developers: see embedded perldoc
 
 LemonLDAP::NG is a free software; you can redistribute it and/or modify
@@ -20,7 +20,9 @@ You should have received a copy of the G
 along with this program.  If not, see L<http://www.gnu.org/licenses/>.
 
 Copyright:
- * 2005-2015 by Xavier Guimard and Clément Oudot
+ * 2005-2022 by Xavier Guimard and Clément Oudot
+ * 2018-2022 by Christophe Maudoux
+ * 2019-2022 by Maxime Besson
  * 2008-2011 by Thomas Chemineau
  * 2012-2015 by François-Xavier Deltombe and Sandro Cazzaniga
 
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/2fregistration.coffee 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/2fregistration.coffee
--- 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/2fregistration.coffee	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/2fregistration.coffee	2022-02-19 16:04:21.000000000 +0000
@@ -9,6 +9,7 @@ setMsg = (msg, level) ->
 	$('#color').addClass "message-#{level}"
 	level = 'success' if level == 'positive'
 	$('#color').addClass "alert-#{level}"
+	$('#color').attr "role", "status"
 
 displayError = (j, status, err) ->
 	console.log 'Error', err
@@ -27,9 +28,11 @@ delete2F = (device, epoch) ->
 			device = 'u'
 		else if device == 'UBK'
 				device = 'yubikey'
-			else if device == 'TOTP'
-					device = 'totp'
-				else setMsg 'u2fFailed', 'warning'
+		else if device == 'TOTP'
+				device = 'totp'
+		else if device == 'WebAuthn'
+				device = 'webauthn'
+		else setMsg 'u2fFailed', 'warning'
 		$.ajax
 			type: "POST"
 			url: "#{portal}2fregisters/#{device}/delete"
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/notifications.coffee 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/notifications.coffee
--- 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/notifications.coffee	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/notifications.coffee	2022-02-07 19:06:14.000000000 +0000
@@ -10,6 +10,7 @@ setMsg = (msg, level) ->
 	$('#color').addClass "message-#{level}"
 	level = 'success' if level == 'positive'
 	$('#color').addClass "alert-#{level}"
+	$('#color').attr "role", "status"
 
 displayError = (j, status, err) ->
 	setMsg 'notificationRetrieveFailed', 'warning'
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/portal.coffee 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/portal.coffee
--- 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/portal.coffee	2021-08-21 17:42:59.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/portal.coffee	2022-02-07 19:06:14.000000000 +0000
@@ -13,6 +13,20 @@ translationFields = {}
 #                   content
 #  - trplaceholder: set result in "placeholder" attribute
 #  - localtime    : transform time (in ms)ing translate()
+
+setDanger = (cond, field) ->
+	result = false
+	if cond
+		$("##{field}").addClass 'fa-check text-success'
+		$("##{field}").removeClass 'fa-times text-danger'
+		$("##{field}").attr 'role', 'status'
+	else
+		$("##{field}").addClass 'fa-times text-danger'
+		$("##{field}").removeClass 'fa-check text-success'
+		$("##{field}").attr 'role', 'alert'
+		result = true
+	result
+
 translatePage = (lang) ->
 	$.getJSON "#{window.staticPrefix}languages/#{lang}.json", (data) ->
 		translationFields = data
@@ -33,7 +47,9 @@ translatePage = (lang) ->
 			if msg.match /_hide_/
 				$(this).parent().hide()
 		$("[trplaceholder]").each ->
-			$(this).attr 'placeholder', translate($(this).attr('trplaceholder'))
+			tmp = translate($(this).attr('trplaceholder'))
+			$(this).attr 'placeholder', tmp
+			$(this).attr 'aria-label', tmp
 		$("[localtime]").each ->
 			d = new Date $(this).attr('localtime') * 1000
 			$(this).text d.toLocaleString()
@@ -208,10 +224,10 @@ getCookie = (cname) ->
 			return c
 	return ''
 
-setCookie = (name, value, exdays) ->
+setCookie = (name, value, samesite, exdays) ->
 	d = new Date()
 	d.setTime d.getTime() + exdays*86400000
-	document.cookie = "#{name}=#{value}; expires=#{d.toUTCString()}; path=/"
+	document.cookie = "#{name}=#{value}; expires=#{d.toUTCString()}; path=/; SameSite=#{samesite}"
 
 # Function to change password using Ajax (instead of POST)
 # NOT USED FOR NOW
@@ -364,11 +380,11 @@ $(window).on 'load', () ->
 		console.log 'Selected lang ->', queryLang
 		if setCookieLang
 			console.log 'Set cookie lang ->', queryLang
-			setCookie 'llnglanguage', queryLang
+			setCookie 'llnglanguage', queryLang, datas['sameSite']
 		translatePage(queryLang)
 	else
 		console.log 'Selected lang ->', lang
-		setCookie 'llnglanguage', lang
+		setCookie 'llnglanguage', lang, datas['sameSite']
 		translatePage(lang)
 
 	# Build language icons
@@ -378,7 +394,7 @@ $(window).on 'load', () ->
 	$('#languages').html langdiv
 	$('.langicon').on 'click', () ->
 		lang = $(this).attr 'title'
-		setCookie 'llnglanguage', lang
+		setCookie 'llnglanguage', lang, datas['sameSite']
 		translatePage lang
 
 	isAlphaNumeric = (chr) ->
@@ -391,40 +407,16 @@ $(window).on 'load', () ->
 	checkpassword = (password) ->
 		result = true
 		if window.datas.ppolicy.minsize > 0
-			if password.length >= window.datas.ppolicy.minsize
-				$('#ppolicy-minsize-feedback').addClass 'fa-check text-success'
-				$('#ppolicy-minsize-feedback').removeClass 'fa-times text-danger'
-			else
-				$('#ppolicy-minsize-feedback').removeClass 'fa-check text-success'
-				$('#ppolicy-minsize-feedback').addClass 'fa-times text-danger'
-				result = false
+			result = false if setDanger( password.length >= window.datas.ppolicy.minsize, 'ppolicy-minsize-feedback' )
 		if window.datas.ppolicy.minupper > 0
 			upper = password.match(/[A-Z]/g)
-			if upper and upper.length >= window.datas.ppolicy.minupper
-				$('#ppolicy-minupper-feedback').addClass 'fa-check text-success'
-				$('#ppolicy-minupper-feedback').removeClass 'fa-times text-danger'
-			else
-				$('#ppolicy-minupper-feedback').removeClass 'fa-check text-success'
-				$('#ppolicy-minupper-feedback').addClass 'fa-times text-danger'
-				result = false
+			result = false if setDanger( upper and upper.length >= window.datas.ppolicy.minupper, 'ppolicy-minupper-feedback' )
 		if window.datas.ppolicy.minlower > 0
 			lower = password.match(/[a-z]/g)
-			if lower and lower.length >= window.datas.ppolicy.minlower
-				$('#ppolicy-minlower-feedback').addClass 'fa-check text-success'
-				$('#ppolicy-minlower-feedback').removeClass 'fa-times text-danger'
-			else
-				$('#ppolicy-minlower-feedback').removeClass 'fa-check text-success'
-				$('#ppolicy-minlower-feedback').addClass 'fa-times text-danger'
-				result = false
+			result = false if setDanger( lower and lower.length >= window.datas.ppolicy.minlower, 'ppolicy-minlower-feedback')
 		if window.datas.ppolicy.mindigit > 0
 			digit = password.match(/[0-9]/g)
-			if digit and digit.length >= window.datas.ppolicy.mindigit
-				$('#ppolicy-mindigit-feedback').addClass 'fa-check text-success'
-				$('#ppolicy-mindigit-feedback').removeClass 'fa-times text-danger'
-			else
-				$('#ppolicy-mindigit-feedback').removeClass 'fa-check text-success'
-				$('#ppolicy-mindigit-feedback').addClass 'fa-times text-danger'
-				result = false
+			result = false if setDanger( digit and digit.length >= window.datas.ppolicy.mindigit, 'ppolicy-mindigit-feedback')
 
 		if window.datas.ppolicy.allowedspechar
 			nonwhitespechar = window.datas.ppolicy.allowedspechar.replace(/\s/g, '')
@@ -436,13 +428,7 @@ $(window).on 'load', () ->
 					if nonwhitespechar.indexOf(password.charAt(i)) < 0
 						hasforbidden = true
 				i++
-			if hasforbidden == false
-				$('#ppolicy-allowedspechar-feedback').addClass 'fa-check text-success'
-				$('#ppolicy-allowedspechar-feedback').removeClass 'fa-times text-danger'
-			else
-				$('#ppolicy-allowedspechar-feedback').removeClass 'fa-check text-success'
-				$('#ppolicy-allowedspechar-feedback').addClass 'fa-times text-danger'
-				result = false
+			result = false if setDanger( hasforbidden == false, 'ppolicy-allowedspechar-feedback' )
 
 		if window.datas.ppolicy.minspechar > 0 and window.datas.ppolicy.allowedspechar
 			numspechar = 0
@@ -452,13 +438,7 @@ $(window).on 'load', () ->
 				if nonwhitespechar.indexOf(password.charAt(i)) >= 0
 					numspechar++
 				i++
-			if numspechar >= window.datas.ppolicy.minspechar
-				$('#ppolicy-minspechar-feedback').addClass 'fa-check text-success'
-				$('#ppolicy-minspechar-feedback').removeClass 'fa-times text-danger'
-			else
-				$('#ppolicy-minspechar-feedback').removeClass 'fa-check text-success'
-				$('#ppolicy-minspechar-feedback').addClass 'fa-times text-danger'
-				result = false
+			result = false if setDanger( numspechar >= window.datas.ppolicy.minspechar, 'ppolicy-minspechar-feedback')
 
 		if window.datas.ppolicy.minspechar > 0 and !window.datas.ppolicy.allowedspechar
 			numspechar = 0
@@ -466,13 +446,7 @@ $(window).on 'load', () ->
 			while i < password.length
 				numspechar++ if !isAlphaNumeric(password.charAt(i))
 				i++
-			if numspechar >= window.datas.ppolicy.minspechar
-				$('#ppolicy-minspechar-feedback').addClass 'fa-check text-success'
-				$('#ppolicy-minspechar-feedback').removeClass 'fa-times text-danger'
-			else
-				$('#ppolicy-minspechar-feedback').removeClass 'fa-check text-success'
-				$('#ppolicy-minspechar-feedback').addClass 'fa-times text-danger'
-				result = false
+			result = false if setDanger( numspechar >= window.datas.ppolicy.minspechar, 'ppolicy-minspechar-feedback')
 
 		if result
 			$('.ppolicy').removeClass('border-danger').addClass 'border-success'
@@ -618,11 +592,11 @@ $(window).on 'load', () ->
 				user = data.user
 				console.log 'Suggested spoofId=', user
 				$("input[name=spoofId]").each ->
-					$(this).attr 'value', user
+					$(this).val user
 				$('#captcha').attr 'src', data.captcha if data.captcha
 				if data.token
-					$('#finduserToken').attr 'value', data.token 
-					$('#token').attr 'value', data.token
+					$('#finduserToken').val data.token 
+					$('#token').val data.token
 			error: (j, status, err) ->
 				document.body.style.cursor = 'default'
 				console.log 'Error', err  if err
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/totpregistration.coffee 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/totpregistration.coffee
--- 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/totpregistration.coffee	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/totpregistration.coffee	2022-02-07 19:06:14.000000000 +0000
@@ -9,6 +9,7 @@ setMsg = (msg, level) ->
 	$('#color').addClass "message-#{level}"
 	level = 'success' if level == 'positive'
 	$('#color').addClass "alert-#{level}"
+	$('#msg').attr 'role', (if level == 'danger' then 'alert' else 'status')
 
 displayError = (j, status, err) ->
 	console.log 'Error', err
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/u2fregistration.coffee 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/u2fregistration.coffee
--- 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/u2fregistration.coffee	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/u2fregistration.coffee	2022-02-07 19:06:14.000000000 +0000
@@ -9,6 +9,7 @@ setMsg = (msg, level) ->
 	$('#color').addClass "message-#{level}"
 	level = 'success' if level == 'positive'
 	$('#color').addClass "alert-#{level}"
+	$('#msg').attr 'role', (if level == 'danger' then 'alert' else 'status')
 
 displayError = (j, status, err) ->
 	console.log 'Error', err
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/webauthncheck.coffee 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/webauthncheck.coffee
--- 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/webauthncheck.coffee	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/webauthncheck.coffee	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,30 @@
+###
+LemonLDAP::NG WebAuthn verify script
+###
+
+setMsg = (msg, level) ->
+	$('#msg').attr 'trspan', msg
+	$('#msg').html window.translate msg
+	$('#color').removeClass 'message-positive message-warning message-danger alert-success alert-warning alert-danger'
+	$('#color').addClass "message-#{level}"
+	level = 'success' if level == 'positive'
+	$('#color').addClass "alert-#{level}"
+
+webAuthnError = (error) ->
+	switch (error.name)
+		when 'unsupported' then setMsg 'webAuthnUnsupported', 'warning'
+		else setMsg 'webAuthnBrowserFailed', 'danger'
+
+check = ->
+	setMsg 'webAuthnBrowserInProgress', 'warning'
+	request = window.datas.request
+	WebAuthnUI.WebAuthnUI.getCredential request
+	. then (response) ->
+		$('#credential').val JSON.stringify response
+		$('#verify-form').submit()
+	. catch (error) ->
+		webAuthnError(error)
+
+$(document).ready ->
+	setTimeout check, 1000
+	$('#retrybutton').on 'click', check
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/webauthnregistration.coffee 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/webauthnregistration.coffee
--- 2.0.13+ds-3/lemonldap-ng-portal/site/coffee/webauthnregistration.coffee	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/coffee/webauthnregistration.coffee	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,97 @@
+###
+LemonLDAP::NG WebAuthn registration script
+###
+
+setMsg = (msg, level) ->
+	$('#msg').attr 'trspan', msg
+	$('#msg').html window.translate msg
+	$('#color').removeClass 'message-positive message-warning message-danger alert-success alert-warning alert-danger'
+	$('#color').addClass "message-#{level}"
+	level = 'success' if level == 'positive'
+	$('#color').addClass "alert-#{level}"
+
+displayError = (j, status, err) ->
+	console.log 'Error', err
+	res = JSON.parse j.responseText
+	if res and res.error
+		res = res.error.replace(/.* /, '')
+		console.log 'Returned error', res
+		setMsg res, 'danger'
+
+webAuthnError = (error) ->
+	switch (error.name)
+		when 'unsupported' then setMsg 'webAuthnUnsupported', 'warning'
+		else setMsg 'webAuthnBrowserFailed', 'danger'
+
+# Registration function (launched by "register" button)
+register = ->
+	# 1 get registration token
+	$.ajax
+		type: "POST",
+		url: "#{portal}2fregisters/webauthn/registrationchallenge"
+		data: {}
+		dataType: 'json'
+		error: displayError
+		success: (ch) ->
+			# 2 build response
+			request = ch.request 
+			setMsg 'webAuthnRegisterInProgress', 'warning'
+			$('#u2fPermission').show()
+			WebAuthnUI.WebAuthnUI.createCredential request
+			. then (response) ->
+				$.ajax
+					type: "POST"
+					url: "#{portal}2fregisters/webauthn/registration"
+					data:
+						state_id: ch.state_id
+						credential: JSON.stringify response
+						keyName: $('#keyName').val()
+					dataType: 'json'
+					success: (resp) ->
+						if resp.error
+							if resp.error.match /badName/
+								setMsg resp.error, 'danger'
+							else setMsg 'webAuthnRegisterFailed', 'danger'
+						else if resp.result
+							setMsg 'yourKeyIsRegistered', 'positive'
+					error: displayError
+			. catch (error) ->
+				webAuthnError(error)
+
+# Verification function (launched by "verify" button)
+verify = ->
+	# 1 get challenge
+	$.ajax
+		type: "POST",
+		url: "#{portal}2fregisters/webauthn/verificationchallenge"
+		data: {}
+		dataType: 'json'
+		error: displayError
+		success: (ch) ->
+			# 2 build response
+			request = ch.request
+			setMsg 'webAuthnBrowserInProgress', 'warning'
+			WebAuthnUI.WebAuthnUI.getCredential request
+			. then (response) ->
+				$.ajax
+					type: "POST"
+					url: "#{portal}2fregisters/webauthn/verification"
+					data:
+						state_id: ch.state_id
+						credential: JSON.stringify response
+					dataType: 'json'
+					success: (resp) ->
+						if resp.error
+							setMsg 'webAuthnFailed', 'danger'
+						else if resp.result
+							setMsg 'yourKeyIsVerified', 'positive'
+					error: displayError
+			. catch (error) ->
+				webAuthnError(error)
+
+# Register "click" events
+$(document).ready ->
+	$('#u2fPermission').hide()
+	$('#register').on 'click', register
+	$('#verify').on 'click', verify
+	$('#goback').attr 'href', portal
Binary files 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/bootstrap/webauthn.png and 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/bootstrap/webauthn.png differ
Binary files 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/he.png and 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/he.png differ
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/2fregistration.js 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/2fregistration.js
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/2fregistration.js	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/2fregistration.js	2022-02-19 16:04:21.000000000 +0000
@@ -15,7 +15,8 @@ LemonLDAP::NG 2F registration script
     if (level === 'positive') {
       level = 'success';
     }
-    return $('#color').addClass("alert-" + level);
+    $('#color').addClass("alert-" + level);
+    return $('#color').attr("role", "status");
   };
 
   displayError = function(j, status, err) {
@@ -40,6 +41,8 @@ LemonLDAP::NG 2F registration script
       device = 'yubikey';
     } else if (device === 'TOTP') {
       device = 'totp';
+    } else if (device === 'WebAuthn') {
+      device = 'webauthn';
     } else {
       setMsg('u2fFailed', 'warning');
     }
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/notifications.js 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/notifications.js
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/notifications.js	2021-07-13 08:54:55.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/notifications.js	2022-02-07 19:06:14.000000000 +0000
@@ -16,7 +16,8 @@ LemonLDAP::NG Notifications script
     if (level === 'positive') {
       level = 'success';
     }
-    return $('#color').addClass("alert-" + level);
+    $('#color').addClass("alert-" + level);
+    return $('#color').attr("role", "status");
   };
 
   displayError = function(j, status, err) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/portal.js 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/portal.js
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/portal.js	2021-08-21 17:42:59.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/portal.js	2022-02-07 19:06:14.000000000 +0000
@@ -5,11 +5,27 @@ LemonLDAP::NG Portal jQuery scripts
  */
 
 (function() {
-  var datas, delKey, getCookie, getQueryParam, getValues, isHiddenFormValueSet, ping, removeOidcConsent, restoreOrder, setCookie, setKey, setOrder, setSelector, translate, translatePage, translationFields,
+  var datas, delKey, getCookie, getQueryParam, getValues, isHiddenFormValueSet, ping, removeOidcConsent, restoreOrder, setCookie, setDanger, setKey, setOrder, setSelector, translate, translatePage, translationFields,
     indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
 
   translationFields = {};
 
+  setDanger = function(cond, field) {
+    var result;
+    result = false;
+    if (cond) {
+      $("#" + field).addClass('fa-check text-success');
+      $("#" + field).removeClass('fa-times text-danger');
+      $("#" + field).attr('role', 'status');
+    } else {
+      $("#" + field).addClass('fa-times text-danger');
+      $("#" + field).removeClass('fa-check text-success');
+      $("#" + field).attr('role', 'alert');
+      result = true;
+    }
+    return result;
+  };
+
   translatePage = function(lang) {
     return $.getJSON(window.staticPrefix + "languages/" + lang + ".json", function(data) {
       var k, ref, ref1, v;
@@ -45,7 +61,10 @@ LemonLDAP::NG Portal jQuery scripts
         }
       });
       $("[trplaceholder]").each(function() {
-        return $(this).attr('placeholder', translate($(this).attr('trplaceholder')));
+        var tmp;
+        tmp = translate($(this).attr('trplaceholder'));
+        $(this).attr('placeholder', tmp);
+        return $(this).attr('aria-label', tmp);
       });
       return $("[localtime]").each(function() {
         var d;
@@ -223,11 +242,11 @@ LemonLDAP::NG Portal jQuery scripts
     return '';
   };
 
-  setCookie = function(name, value, exdays) {
+  setCookie = function(name, value, samesite, exdays) {
     var d;
     d = new Date();
     d.setTime(d.getTime() + exdays * 86400000);
-    return document.cookie = name + "=" + value + "; expires=" + (d.toUTCString()) + "; path=/";
+    return document.cookie = name + "=" + value + "; expires=" + (d.toUTCString()) + "; path=/; SameSite=" + samesite;
   };
 
   datas = {};
@@ -379,12 +398,12 @@ LemonLDAP::NG Portal jQuery scripts
       console.log('Selected lang ->', queryLang);
       if (setCookieLang) {
         console.log('Set cookie lang ->', queryLang);
-        setCookie('llnglanguage', queryLang);
+        setCookie('llnglanguage', queryLang, datas['sameSite']);
       }
       translatePage(queryLang);
     } else {
       console.log('Selected lang ->', lang);
-      setCookie('llnglanguage', lang);
+      setCookie('llnglanguage', lang, datas['sameSite']);
       translatePage(lang);
     }
     langdiv = '';
@@ -396,7 +415,7 @@ LemonLDAP::NG Portal jQuery scripts
     $('#languages').html(langdiv);
     $('.langicon').on('click', function() {
       lang = $(this).attr('title');
-      setCookie('llnglanguage', lang);
+      setCookie('llnglanguage', lang, datas['sameSite']);
       return translatePage(lang);
     });
     isAlphaNumeric = function(chr) {
@@ -411,45 +430,25 @@ LemonLDAP::NG Portal jQuery scripts
       var digit, hasforbidden, i, len, lower, nonwhitespechar, numspechar, ref3, ref4, result, upper;
       result = true;
       if (window.datas.ppolicy.minsize > 0) {
-        if (password.length >= window.datas.ppolicy.minsize) {
-          $('#ppolicy-minsize-feedback').addClass('fa-check text-success');
-          $('#ppolicy-minsize-feedback').removeClass('fa-times text-danger');
-        } else {
-          $('#ppolicy-minsize-feedback').removeClass('fa-check text-success');
-          $('#ppolicy-minsize-feedback').addClass('fa-times text-danger');
+        if (setDanger(password.length >= window.datas.ppolicy.minsize, 'ppolicy-minsize-feedback')) {
           result = false;
         }
       }
       if (window.datas.ppolicy.minupper > 0) {
         upper = password.match(/[A-Z]/g);
-        if (upper && upper.length >= window.datas.ppolicy.minupper) {
-          $('#ppolicy-minupper-feedback').addClass('fa-check text-success');
-          $('#ppolicy-minupper-feedback').removeClass('fa-times text-danger');
-        } else {
-          $('#ppolicy-minupper-feedback').removeClass('fa-check text-success');
-          $('#ppolicy-minupper-feedback').addClass('fa-times text-danger');
+        if (setDanger(upper && upper.length >= window.datas.ppolicy.minupper, 'ppolicy-minupper-feedback')) {
           result = false;
         }
       }
       if (window.datas.ppolicy.minlower > 0) {
         lower = password.match(/[a-z]/g);
-        if (lower && lower.length >= window.datas.ppolicy.minlower) {
-          $('#ppolicy-minlower-feedback').addClass('fa-check text-success');
-          $('#ppolicy-minlower-feedback').removeClass('fa-times text-danger');
-        } else {
-          $('#ppolicy-minlower-feedback').removeClass('fa-check text-success');
-          $('#ppolicy-minlower-feedback').addClass('fa-times text-danger');
+        if (setDanger(lower && lower.length >= window.datas.ppolicy.minlower, 'ppolicy-minlower-feedback')) {
           result = false;
         }
       }
       if (window.datas.ppolicy.mindigit > 0) {
         digit = password.match(/[0-9]/g);
-        if (digit && digit.length >= window.datas.ppolicy.mindigit) {
-          $('#ppolicy-mindigit-feedback').addClass('fa-check text-success');
-          $('#ppolicy-mindigit-feedback').removeClass('fa-times text-danger');
-        } else {
-          $('#ppolicy-mindigit-feedback').removeClass('fa-check text-success');
-          $('#ppolicy-mindigit-feedback').addClass('fa-times text-danger');
+        if (setDanger(digit && digit.length >= window.datas.ppolicy.mindigit, 'ppolicy-mindigit-feedback')) {
           result = false;
         }
       }
@@ -466,12 +465,7 @@ LemonLDAP::NG Portal jQuery scripts
           }
           i++;
         }
-        if (hasforbidden === false) {
-          $('#ppolicy-allowedspechar-feedback').addClass('fa-check text-success');
-          $('#ppolicy-allowedspechar-feedback').removeClass('fa-times text-danger');
-        } else {
-          $('#ppolicy-allowedspechar-feedback').removeClass('fa-check text-success');
-          $('#ppolicy-allowedspechar-feedback').addClass('fa-times text-danger');
+        if (setDanger(hasforbidden === false, 'ppolicy-allowedspechar-feedback')) {
           result = false;
         }
       }
@@ -485,12 +479,7 @@ LemonLDAP::NG Portal jQuery scripts
           }
           i++;
         }
-        if (numspechar >= window.datas.ppolicy.minspechar) {
-          $('#ppolicy-minspechar-feedback').addClass('fa-check text-success');
-          $('#ppolicy-minspechar-feedback').removeClass('fa-times text-danger');
-        } else {
-          $('#ppolicy-minspechar-feedback').removeClass('fa-check text-success');
-          $('#ppolicy-minspechar-feedback').addClass('fa-times text-danger');
+        if (setDanger(numspechar >= window.datas.ppolicy.minspechar, 'ppolicy-minspechar-feedback')) {
           result = false;
         }
       }
@@ -503,12 +492,7 @@ LemonLDAP::NG Portal jQuery scripts
           }
           i++;
         }
-        if (numspechar >= window.datas.ppolicy.minspechar) {
-          $('#ppolicy-minspechar-feedback').addClass('fa-check text-success');
-          $('#ppolicy-minspechar-feedback').removeClass('fa-times text-danger');
-        } else {
-          $('#ppolicy-minspechar-feedback').removeClass('fa-check text-success');
-          $('#ppolicy-minspechar-feedback').addClass('fa-times text-danger');
+        if (setDanger(numspechar >= window.datas.ppolicy.minspechar, 'ppolicy-minspechar-feedback')) {
           result = false;
         }
       }
@@ -687,14 +671,14 @@ LemonLDAP::NG Portal jQuery scripts
           user = data.user;
           console.log('Suggested spoofId=', user);
           $("input[name=spoofId]").each(function() {
-            return $(this).attr('value', user);
+            return $(this).val(user);
           });
           if (data.captcha) {
             $('#captcha').attr('src', data.captcha);
           }
           if (data.token) {
-            $('#finduserToken').attr('value', data.token);
-            return $('#token').attr('value', data.token);
+            $('#finduserToken').val(data.token);
+            return $('#token').val(data.token);
           }
         },
         error: function(j, status, err) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/totpregistration.js 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/totpregistration.js
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/totpregistration.js	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/totpregistration.js	2022-02-07 19:06:14.000000000 +0000
@@ -15,7 +15,8 @@ LemonLDAP::NG TOTP registration script
     if (level === 'positive') {
       level = 'success';
     }
-    return $('#color').addClass("alert-" + level);
+    $('#color').addClass("alert-" + level);
+    return $('#msg').attr('role', (level === 'danger' ? 'alert' : 'status'));
   };
 
   displayError = function(j, status, err) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/u2fregistration.js 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/u2fregistration.js
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/u2fregistration.js	2021-07-22 10:37:16.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/u2fregistration.js	2022-02-07 19:06:14.000000000 +0000
@@ -15,7 +15,8 @@ LemonLDAP::NG U2F registration script
     if (level === 'positive') {
       level = 'success';
     }
-    return $('#color').addClass("alert-" + level);
+    $('#color').addClass("alert-" + level);
+    return $('#msg').attr('role', (level === 'danger' ? 'alert' : 'status'));
   };
 
   displayError = function(j, status, err) {
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/webauthncheck.js 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/webauthncheck.js
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/webauthncheck.js	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/webauthncheck.js	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,47 @@
+// Generated by CoffeeScript 1.12.8
+
+/*
+LemonLDAP::NG WebAuthn verify script
+ */
+
+(function() {
+  var check, setMsg, webAuthnError;
+
+  setMsg = function(msg, level) {
+    $('#msg').attr('trspan', msg);
+    $('#msg').html(window.translate(msg));
+    $('#color').removeClass('message-positive message-warning message-danger alert-success alert-warning alert-danger');
+    $('#color').addClass("message-" + level);
+    if (level === 'positive') {
+      level = 'success';
+    }
+    return $('#color').addClass("alert-" + level);
+  };
+
+  webAuthnError = function(error) {
+    switch (error.name) {
+      case 'unsupported':
+        return setMsg('webAuthnUnsupported', 'warning');
+      default:
+        return setMsg('webAuthnBrowserFailed', 'danger');
+    }
+  };
+
+  check = function() {
+    var request;
+    setMsg('webAuthnBrowserInProgress', 'warning');
+    request = window.datas.request;
+    return WebAuthnUI.WebAuthnUI.getCredential(request).then(function(response) {
+      $('#credential').val(JSON.stringify(response));
+      return $('#verify-form').submit();
+    })["catch"](function(error) {
+      return webAuthnError(error);
+    });
+  };
+
+  $(document).ready(function() {
+    setTimeout(check, 1000);
+    return $('#retrybutton').on('click', check);
+  });
+
+}).call(this);
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/webauthnregistration.js 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/webauthnregistration.js
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/webauthnregistration.js	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/webauthnregistration.js	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,126 @@
+// Generated by CoffeeScript 1.12.8
+
+/*
+LemonLDAP::NG WebAuthn registration script
+ */
+
+(function() {
+  var displayError, register, setMsg, verify, webAuthnError;
+
+  setMsg = function(msg, level) {
+    $('#msg').attr('trspan', msg);
+    $('#msg').html(window.translate(msg));
+    $('#color').removeClass('message-positive message-warning message-danger alert-success alert-warning alert-danger');
+    $('#color').addClass("message-" + level);
+    if (level === 'positive') {
+      level = 'success';
+    }
+    return $('#color').addClass("alert-" + level);
+  };
+
+  displayError = function(j, status, err) {
+    var res;
+    console.log('Error', err);
+    res = JSON.parse(j.responseText);
+    if (res && res.error) {
+      res = res.error.replace(/.* /, '');
+      console.log('Returned error', res);
+      return setMsg(res, 'danger');
+    }
+  };
+
+  webAuthnError = function(error) {
+    switch (error.name) {
+      case 'unsupported':
+        return setMsg('webAuthnUnsupported', 'warning');
+      default:
+        return setMsg('webAuthnBrowserFailed', 'danger');
+    }
+  };
+
+  register = function() {
+    return $.ajax({
+      type: "POST",
+      url: portal + "2fregisters/webauthn/registrationchallenge",
+      data: {},
+      dataType: 'json',
+      error: displayError,
+      success: function(ch) {
+        var request;
+        request = ch.request;
+        setMsg('webAuthnRegisterInProgress', 'warning');
+        $('#u2fPermission').show();
+        return WebAuthnUI.WebAuthnUI.createCredential(request).then(function(response) {
+          return $.ajax({
+            type: "POST",
+            url: portal + "2fregisters/webauthn/registration",
+            data: {
+              state_id: ch.state_id,
+              credential: JSON.stringify(response),
+              keyName: $('#keyName').val()
+            },
+            dataType: 'json',
+            success: function(resp) {
+              if (resp.error) {
+                if (resp.error.match(/badName/)) {
+                  return setMsg(resp.error, 'danger');
+                } else {
+                  return setMsg('webAuthnRegisterFailed', 'danger');
+                }
+              } else if (resp.result) {
+                return setMsg('yourKeyIsRegistered', 'positive');
+              }
+            },
+            error: displayError
+          });
+        })["catch"](function(error) {
+          return webAuthnError(error);
+        });
+      }
+    });
+  };
+
+  verify = function() {
+    return $.ajax({
+      type: "POST",
+      url: portal + "2fregisters/webauthn/verificationchallenge",
+      data: {},
+      dataType: 'json',
+      error: displayError,
+      success: function(ch) {
+        var request;
+        request = ch.request;
+        setMsg('webAuthnBrowserInProgress', 'warning');
+        return WebAuthnUI.WebAuthnUI.getCredential(request).then(function(response) {
+          return $.ajax({
+            type: "POST",
+            url: portal + "2fregisters/webauthn/verification",
+            data: {
+              state_id: ch.state_id,
+              credential: JSON.stringify(response)
+            },
+            dataType: 'json',
+            success: function(resp) {
+              if (resp.error) {
+                return setMsg('webAuthnFailed', 'danger');
+              } else if (resp.result) {
+                return setMsg('yourKeyIsVerified', 'positive');
+              }
+            },
+            error: displayError
+          });
+        })["catch"](function(error) {
+          return webAuthnError(error);
+        });
+      }
+    });
+  };
+
+  $(document).ready(function() {
+    $('#u2fPermission').hide();
+    $('#register').on('click', register);
+    $('#verify').on('click', verify);
+    return $('#goback').attr('href', portal);
+  });
+
+}).call(this);
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/webauthn-ui.js 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/webauthn-ui.js
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/js/webauthn-ui.js	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/js/webauthn-ui.js	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,561 @@
+/*! webauthn-ui library (C) 2018 - 2020 Thomas Bleeker (www.madwizard.org) - MIT license */
+
+(function (global, factory) {
+    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+    typeof define === 'function' && define.amd ? define(['exports'], factory) :
+    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.WebAuthnUI = {}));
+}(this, (function (exports) { 'use strict';
+
+    /* Microsoft tslib 0BSD licensed */
+    /* global Reflect, Promise */
+
+    var extendStatics = function(d, b) {
+        extendStatics = Object.setPrototypeOf ||
+            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
+        return extendStatics(d, b);
+    };
+
+    function __extends(d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    }
+
+    function __awaiter(thisArg, _arguments, P, generator) {
+        function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+        return new (P || (P = Promise))(function (resolve, reject) {
+            function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+            function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+            function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+            step((generator = generator.apply(thisArg, _arguments || [])).next());
+        });
+    }
+
+    function __generator(thisArg, body) {
+        var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
+        return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
+        function verb(n) { return function (v) { return step([n, v]); }; }
+        function step(op) {
+            if (f) throw new TypeError("Generator is already executing.");
+            while (_) try {
+                if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
+                if (y = 0, t) op = [op[0] & 2, t.value];
+                switch (op[0]) {
+                    case 0: case 1: t = op; break;
+                    case 4: _.label++; return { value: op[1], done: false };
+                    case 5: _.label++; y = op[1]; op = [0]; continue;
+                    case 7: op = _.ops.pop(); _.trys.pop(); continue;
+                    default:
+                        if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
+                        if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
+                        if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
+                        if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
+                        if (t[2]) _.ops.pop();
+                        _.trys.pop(); continue;
+                }
+                op = body.call(thisArg, _);
+            } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
+            if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
+        }
+    }
+
+    function waitReadyState(alreadyDone, eventDispatcher, eventName) {
+        if (alreadyDone) {
+            return Promise.resolve();
+        }
+        return new Promise(function (resolve) {
+            var readyFunc = function () {
+                eventDispatcher.removeEventListener(eventName, readyFunc);
+                resolve();
+            };
+            eventDispatcher.addEventListener(eventName, readyFunc);
+        });
+    }
+    function ready() {
+        return waitReadyState(document.readyState !== 'loading', document, 'DOMContentLoaded');
+    }
+    function loaded() {
+        return waitReadyState(document.readyState === 'complete', window, 'load');
+    }
+
+    var WebAuthnError = /** @class */ (function (_super) {
+        __extends(WebAuthnError, _super);
+        function WebAuthnError(name, message, innerError) {
+            var _newTarget = this.constructor;
+            var _this = _super.call(this, "WebAuthnUI error: " + (message !== undefined ? message : name)) || this;
+            Object.setPrototypeOf(_this, _newTarget.prototype); // restore prototype chain
+            _this.name = name;
+            _this.innerError = innerError;
+            return _this;
+        }
+        WebAuthnError.fromError = function (error) {
+            var type = 'unknown';
+            var message;
+            if (error instanceof DOMException) {
+                var map = {
+                    NotAllowedError: 'dom-not-allowed',
+                    SecurityError: 'dom-security',
+                    NotSupportedError: 'dom-not-supported',
+                    AbortError: 'dom-abort',
+                    InvalidStateError: 'dom-invalid-state',
+                };
+                type = map[error.name] || 'dom-unknown';
+                message = type;
+            }
+            else {
+                message = "unknown (" + error.toString() + ")";
+            }
+            return new WebAuthnError(type, message, error instanceof Error ? error : undefined);
+        };
+        return WebAuthnError;
+    }(Error));
+
+    function encode(arraybuffer) {
+        var buffer = new Uint8Array(arraybuffer);
+        var binary = '';
+        for (var i_1 = 0; i_1 < buffer.length; i_1++) {
+            binary += String.fromCharCode(buffer[i_1]);
+        }
+        var encoded = window.btoa(binary);
+        var i = encoded.length - 1;
+        while (i > 0 && encoded[i] === '=') {
+            i--;
+        }
+        encoded = encoded.substr(0, i + 1);
+        encoded = encoded.replace(/\+/g, '-').replace(/\//g, '_');
+        return encoded;
+    }
+    function decode(base64) {
+        var converted = base64.replace(/-/g, '+').replace(/_/g, '/');
+        switch (converted.length % 4) {
+            case 2:
+                converted += '==';
+                break;
+            case 3:
+                converted += '=';
+                break;
+            case 1:
+                throw new WebAuthnError('parse-error');
+        }
+        var bin = window.atob(converted);
+        var buffer = new Uint8Array(bin.length);
+        for (var i = 0; i < bin.length; i++) {
+            buffer[i] = bin.charCodeAt(i);
+        }
+        return buffer;
+    }
+
+    function map(src, mapper) {
+        var dest = {};
+        var keys = Object.keys(mapper);
+        for (var i = 0; i < keys.length; i++) {
+            var k = keys[i];
+            var action = mapper[k];
+            var val = src[k];
+            if (val !== undefined) {
+                if (action === 0 /* Copy */) {
+                    dest[k] = val;
+                }
+                else if (action === 1 /* Base64Decode */) {
+                    dest[k] = val === null ? null : decode(val);
+                }
+                else if (action === 2 /* Base64Encode */) {
+                    dest[k] = val === null ? null : encode(val);
+                }
+                else if (typeof action === 'object') {
+                    dest[k] = map(val, action);
+                }
+                else {
+                    dest[k] = action(val);
+                }
+            }
+        }
+        return dest;
+    }
+    function arrayMap(mapper) {
+        return function (src) {
+            var dest = [];
+            for (var i = 0; i < src.length; i++) {
+                dest[i] = map(src[i], mapper);
+            }
+            return dest;
+        };
+    }
+    function getCredentialDescListMap() {
+        return arrayMap({
+            type: 0 /* Copy */,
+            id: 1 /* Base64Decode */,
+            transports: 0 /* Copy */,
+        });
+    }
+    function addExtensionOutputs(dest, pkc) {
+        var clientExtensionResults = pkc.getClientExtensionResults();
+        if (Object.keys(clientExtensionResults).length > 0) {
+            dest.clientExtensionResults = map(clientExtensionResults, {
+                appid: 0 /* Copy */,
+            });
+        }
+    }
+    var Converter = /** @class */ (function () {
+        function Converter() {
+        }
+        Converter.convertCreationOptions = function (options) {
+            return map(options, {
+                rp: 0 /* Copy */,
+                user: {
+                    id: 1 /* Base64Decode */,
+                    name: 0 /* Copy */,
+                    displayName: 0 /* Copy */,
+                    icon: 0 /* Copy */,
+                },
+                challenge: 1 /* Base64Decode */,
+                pubKeyCredParams: 0 /* Copy */,
+                timeout: 0 /* Copy */,
+                excludeCredentials: getCredentialDescListMap(),
+                authenticatorSelection: 0 /* Copy */,
+                attestation: 0 /* Copy */,
+                extensions: {
+                    appid: 0 /* Copy */,
+                },
+            });
+        };
+        Converter.convertCreationResponse = function (pkc) {
+            var response = map(pkc, {
+                type: 0 /* Copy */,
+                id: 0 /* Copy */,
+                rawId: 2 /* Base64Encode */,
+                response: {
+                    clientDataJSON: 2 /* Base64Encode */,
+                    attestationObject: 2 /* Base64Encode */,
+                },
+            });
+            addExtensionOutputs(response, pkc);
+            return response;
+        };
+        Converter.convertRequestOptions = function (options) {
+            return map(options, {
+                challenge: 1 /* Base64Decode */,
+                timeout: 0 /* Copy */,
+                rpId: 0 /* Copy */,
+                allowCredentials: getCredentialDescListMap(),
+                userVerification: 0 /* Copy */,
+                extensions: {
+                    appid: 0 /* Copy */,
+                },
+            });
+        };
+        Converter.convertRequestResponse = function (pkc) {
+            var response = map(pkc, {
+                type: 0 /* Copy */,
+                id: 0 /* Copy */,
+                rawId: 2 /* Base64Encode */,
+                response: {
+                    clientDataJSON: 2 /* Base64Encode */,
+                    authenticatorData: 2 /* Base64Encode */,
+                    signature: 2 /* Base64Encode */,
+                    userHandle: 2 /* Base64Encode */,
+                },
+            });
+            addExtensionOutputs(response, pkc);
+            return response;
+        };
+        return Converter;
+    }());
+
+    var loadEvents = { loaded: loaded, ready: ready };
+    function elementSelector(selector) {
+        var items;
+        if (typeof selector === 'string') {
+            items = document.querySelectorAll(selector);
+        }
+        else {
+            items = [selector];
+        }
+        return items;
+    }
+    var WebAuthnUI = /** @class */ (function () {
+        function WebAuthnUI() {
+        }
+        WebAuthnUI.isSupported = function () {
+            return typeof window.PublicKeyCredential !== 'undefined';
+        };
+        WebAuthnUI.isUVPASupported = function () {
+            return __awaiter(this, void 0, void 0, function () {
+                return __generator(this, function (_a) {
+                    if (this.isSupported()) {
+                        return [2 /*return*/, window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()];
+                    }
+                    return [2 /*return*/, false];
+                });
+            });
+        };
+        WebAuthnUI.checkSupport = function () {
+            if (!WebAuthnUI.isSupported()) {
+                throw new WebAuthnError('unsupported');
+            }
+        };
+        WebAuthnUI.createCredential = function (options) {
+            return __awaiter(this, void 0, void 0, function () {
+                var request, credential, e_1;
+                return __generator(this, function (_a) {
+                    switch (_a.label) {
+                        case 0:
+                            WebAuthnUI.checkSupport();
+                            request = {
+                                publicKey: Converter.convertCreationOptions(options),
+                            };
+                            _a.label = 1;
+                        case 1:
+                            _a.trys.push([1, 3, , 4]);
+                            return [4 /*yield*/, navigator.credentials.create(request)];
+                        case 2:
+                            credential = (_a.sent());
+                            return [3 /*break*/, 4];
+                        case 3:
+                            e_1 = _a.sent();
+                            throw WebAuthnError.fromError(e_1);
+                        case 4: return [2 /*return*/, Converter.convertCreationResponse(credential)];
+                    }
+                });
+            });
+        };
+        WebAuthnUI.getCredential = function (options) {
+            return __awaiter(this, void 0, void 0, function () {
+                var request, credential, e_2;
+                return __generator(this, function (_a) {
+                    switch (_a.label) {
+                        case 0:
+                            WebAuthnUI.checkSupport();
+                            request = {
+                                publicKey: Converter.convertRequestOptions(options),
+                            };
+                            _a.label = 1;
+                        case 1:
+                            _a.trys.push([1, 3, , 4]);
+                            return [4 /*yield*/, navigator.credentials.get(request)];
+                        case 2:
+                            credential = (_a.sent());
+                            return [3 /*break*/, 4];
+                        case 3:
+                            e_2 = _a.sent();
+                            throw WebAuthnError.fromError(e_2);
+                        case 4: return [2 /*return*/, Converter.convertRequestResponse(credential)];
+                    }
+                });
+            });
+        };
+        WebAuthnUI.setFeatureCssClasses = function (selector) {
+            return __awaiter(this, void 0, void 0, function () {
+                var items, applyClass;
+                return __generator(this, function (_a) {
+                    items = elementSelector(selector);
+                    applyClass = function (cls) {
+                        for (var i = 0; i < items.length; i++) {
+                            items[i].classList.add(cls);
+                        }
+                    };
+                    applyClass("webauthn-" + (WebAuthnUI.isSupported() ? '' : 'un') + "supported");
+                    return [2 /*return*/, WebAuthnUI.isUVPASupported().then(function (available) {
+                            applyClass("webauthn-uvpa-" + (available ? '' : 'un') + "supported");
+                        })];
+                });
+            });
+        };
+        WebAuthnUI.loadConfig = function (config) {
+            return __awaiter(this, void 0, void 0, function () {
+                var field, el, submit, response, newField;
+                var _this = this;
+                return __generator(this, function (_a) {
+                    switch (_a.label) {
+                        case 0: 
+                        // Wait for DOM ready
+                        return [4 /*yield*/, ready()];
+                        case 1:
+                            // Wait for DOM ready
+                            _a.sent();
+                            field = config.formField;
+                            if (typeof field === 'string') {
+                                el = document.querySelector(field);
+                                if (el === null) {
+                                    throw new WebAuthnError('bad-config', 'Could not find formField.');
+                                }
+                                field = el;
+                            }
+                            if (!(field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement)) {
+                                throw new WebAuthnError('bad-config', 'formField does not refer to an input element.');
+                            }
+                            submit = config.submitForm !== false;
+                            if (!this.isSupported() && config.postUnsupportedImmediately === true) {
+                                response = { status: 'failed', error: 'unsupported' };
+                                this.setForm(field, response, submit);
+                                return [2 /*return*/, response];
+                            }
+                            newField = field;
+                            return [2 /*return*/, new Promise(function (resolve) {
+                                    var trigger = config.trigger;
+                                    var resolved = false;
+                                    if (trigger.event === 'click') {
+                                        var targets = elementSelector(config.trigger.element);
+                                        var handler = function () { return __awaiter(_this, void 0, void 0, function () {
+                                            var response;
+                                            return __generator(this, function (_a) {
+                                                switch (_a.label) {
+                                                    case 0: return [4 /*yield*/, this.runAutoConfig(config)];
+                                                    case 1:
+                                                        response = _a.sent();
+                                                        this.setForm(newField, response, submit);
+                                                        if (!resolved) {
+                                                            resolved = true;
+                                                            resolve(response);
+                                                        }
+                                                        return [2 /*return*/];
+                                                }
+                                            });
+                                        }); };
+                                        for (var i = 0; i < targets.length; i++) {
+                                            targets[i].addEventListener('click', handler);
+                                        }
+                                    }
+                                    else {
+                                        throw new WebAuthnError('bad-config');
+                                    }
+                                })];
+                    }
+                });
+            });
+        };
+        WebAuthnUI.startConfig = function (config) {
+            return __awaiter(this, void 0, void 0, function () {
+                var credential;
+                return __generator(this, function (_a) {
+                    switch (_a.label) {
+                        case 0:
+                            if (!(config.type === 'get')) return [3 /*break*/, 2];
+                            return [4 /*yield*/, this.getCredential(config.request)];
+                        case 1:
+                            credential = _a.sent();
+                            return [3 /*break*/, 5];
+                        case 2:
+                            if (!(config.type === 'create')) return [3 /*break*/, 4];
+                            return [4 /*yield*/, this.createCredential(config.request)];
+                        case 3:
+                            credential = _a.sent();
+                            return [3 /*break*/, 5];
+                        case 4: throw new WebAuthnError('bad-config', "Invalid config.type " + config.type);
+                        case 5: return [2 /*return*/, {
+                                status: 'ok',
+                                credential: credential,
+                            }];
+                    }
+                });
+            });
+        };
+        WebAuthnUI.runAutoConfig = function (config) {
+            return __awaiter(this, void 0, void 0, function () {
+                var response, e_3, waError;
+                return __generator(this, function (_a) {
+                    switch (_a.label) {
+                        case 0:
+                            _a.trys.push([0, 2, , 3]);
+                            return [4 /*yield*/, this.startConfig(config)];
+                        case 1:
+                            response = _a.sent();
+                            return [3 /*break*/, 3];
+                        case 2:
+                            e_3 = _a.sent();
+                            waError = (e_3 instanceof WebAuthnError);
+                            if (config.debug === true) {
+                                console.error(e_3); // eslint-disable-line no-console
+                                if (waError && e_3.innerError) {
+                                    console.error(e_3.innerError); // eslint-disable-line no-console
+                                }
+                            }
+                            response = {
+                                status: 'failed',
+                                error: (waError ? e_3.name : WebAuthnError.fromError(e_3).name),
+                            };
+                            return [3 /*break*/, 3];
+                        case 3: return [2 /*return*/, response];
+                    }
+                });
+            });
+        };
+        WebAuthnUI.setForm = function (field, response, submit) {
+            field.value = JSON.stringify(response);
+            if (submit && field.form) {
+                field.form.submit();
+            }
+        };
+        WebAuthnUI.autoConfig = function () {
+            return __awaiter(this, void 0, void 0, function () {
+                var promises, list, i, el, isScript, rawJson, json;
+                return __generator(this, function (_a) {
+                    switch (_a.label) {
+                        case 0:
+                            promises = [];
+                            list = document.querySelectorAll('input[data-webauthn],textarea[data-webauthn],script[data-webauthn]');
+                            for (i = 0; i < list.length; i++) {
+                                el = list[i];
+                                isScript = el.tagName === 'SCRIPT';
+                                if (isScript && el.type !== 'application/json') {
+                                    throw new WebAuthnError('bad-config', 'Expecting application/json script with data-webauthn');
+                                }
+                                rawJson = isScript ? el.textContent : (el).getAttribute('data-webauthn');
+                                if (rawJson === null) {
+                                    throw new WebAuthnError('bad-config', 'Missing JSON in data-webauthn');
+                                }
+                                json = void 0;
+                                try {
+                                    json = JSON.parse(rawJson);
+                                }
+                                catch (e) {
+                                    throw new WebAuthnError('bad-config', 'invalid JSON in data-webauthn on element');
+                                }
+                                if (!isScript && json.formField === undefined) {
+                                    json.formField = el;
+                                }
+                                promises.push(this.loadConfig(json));
+                            }
+                            return [4 /*yield*/, Promise.all(promises)];
+                        case 1:
+                            _a.sent();
+                            return [2 /*return*/];
+                    }
+                });
+            });
+        };
+        WebAuthnUI.inProgress = false;
+        return WebAuthnUI;
+    }());
+    function auto() {
+        return __awaiter(this, void 0, void 0, function () {
+            var list, i;
+            return __generator(this, function (_a) {
+                switch (_a.label) {
+                    case 0: return [4 /*yield*/, ready()];
+                    case 1:
+                        _a.sent();
+                        list = document.querySelectorAll('.webauthn-detect');
+                        for (i = 0; i < list.length; i++) {
+                            WebAuthnUI.setFeatureCssClasses(list[i]);
+                        }
+                        return [2 /*return*/, WebAuthnUI.autoConfig()];
+                }
+            });
+        });
+    }
+    var autoPromise = auto().catch(function (e) {
+        if (console && console.error) { // eslint-disable-line no-console
+            console.error(e); // eslint-disable-line no-console
+        }
+    });
+
+    exports.WebAuthnError = WebAuthnError;
+    exports.WebAuthnUI = WebAuthnUI;
+    exports.autoPromise = autoPromise;
+    exports.loadEvents = loadEvents;
+
+    Object.defineProperty(exports, '__esModule', { value: true });
+
+})));
Binary files 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/common/pt_BR.png and 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/common/pt_BR.png differ
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/ar.json 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/ar.json
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/ar.json	2021-08-18 13:42:07.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/ar.json	2022-02-19 16:04:21.000000000 +0000
@@ -10,6 +10,7 @@
 "PE103":"No second factors available for your account",
 "PE104":"Bad DevOps handler file",
 "PE105":"File not found",
+"PE106":"Error during authentication with OpenID Provider",
 "PE2":"حقول المستخدم وكلمة المرور يجب ملؤهم",
 "PE20":"No password backend defined",
 "PE21":"حسابك مقفل",
@@ -137,7 +138,6 @@
 "chooseApp":"اختر أحد التطبيقات المسموح لك بالدخول إليها",
 "cipheredValue":"Ciphered value",
 "click2Reset":"انقر هنا لإعادة تعيين كلمة المرور",
-"click2ResetCertificate":"Click here to reset your certificate",
 "clickHere":"الرجاء الضغط هنا",
 "clickOnYubikey":"Click on your Yubikey",
 "close":"Close",
@@ -203,6 +203,7 @@
 "mail2f":"Email code",
 "mailSent2":"تم إرسال رسالة إلى عنوان بريدك الإلكتروني.",
 "maintenanceMode":"هذا التطبيق في صيانة، يرجى محاولة الاتصال في وقت لاحق",
+"mandatoryField":"* Mandatory field",
 "maxNumberof2FDevicesReached":"Maximum number of 2F devices reached!",
 "missingCode":"Code is missing",
 "myNotification":"My accepted notification",
@@ -267,9 +268,9 @@
 "renewSession":"Renew session",
 "resendConfirmMail":"هل تريد إعادة إرسال رسالة التأكيد؟",
 "resentConfirm":"هل تريد إعادة إرسال رسالة التأكيد؟",
-"resetCertificateOK":"Your certificate has been successfully reset!",
 "resetPwd":"إعادة تعيين كلمة المرور الخاصة بي",
 "rest2f":"Verification code",
+"retry":"Retry",
 "rightsReloadNeedsLogout":" إعادة تحميل الحقوق تحتاج إلى تسجيل الخروج وتسجيل الدخول مرة أخرى",
 "rules":"RULES",
 "scope":"نطاق",
@@ -306,6 +307,7 @@
 "u2fWelcome":"U2F device management",
 "unableToGetKey":"تعذر الوصول إلى المفتاح. أعد محاولة الاتصال بالمشرف أو اتصل به",
 "unknownAction":"Unknown action",
+"unknownAttributes":"Unknown attributes",
 "unregister":"Unregister",
 "updateCdc":"تحديث ملف تعريف ارتباط المجال المشترك",
 "updateTime":"تاريخ التحديث",
@@ -319,6 +321,16 @@
 "wait":"انتظر",
 "waitingmessage":"Authentication in progress, please wait",
 "warning":"تحذير",
+"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
+"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
+"webAuthnFailed":"WebAuthn authentication failed",
+"webAuthnRegisterFailed":"WebAuthn registration failed",
+"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
+"webAuthnRequired":"WebAuthn authentication required",
+"webAuthnUnsupported":"Your web browser does not support WebAuthn",
+"webauthn2f":"WebAuthn",
+"webauthn2fWelcome":"Security device registration",
+"webauthnAlreadyRegistered":"This device is already registered",
 "welcomeOnPortal":"مرحبا بك على بوابة إثبات الهوية الآمنة.",
 "yesResendMail":"نعم، أعد إرسال البريد",
 "yourAddress":"Know your address",
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/de.json 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/de.json
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/de.json	2021-07-22 12:32:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/de.json	2022-02-19 16:04:21.000000000 +0000
@@ -10,6 +10,7 @@
 "PE103":"No second factors available for your account",
 "PE104":"Bad DevOps handler file",
 "PE105":"File not found",
+"PE106":"Error during authentication with OpenID Provider",
 "PE2":"Benutzername oder Passwort nicht eingegeben",
 "PE20":"No password backend defined",
 "PE21":"Ihr Konto ist blockiert",
@@ -137,7 +138,6 @@
 "chooseApp":"Wählen Sie eine Anwendung aus, auf die du zugreifen darfst",
 "cipheredValue":"Ciphered value",
 "click2Reset":"Click here to reset your password",
-"click2ResetCertificate":"Click here to reset your certificate",
 "clickHere":"Bitte hier klicken",
 "clickOnYubikey":"Klicke auf deinen Yubikey",
 "close":"Close",
@@ -203,6 +203,7 @@
 "mail2f":"Email code",
 "mailSent2":"Eine Nachricht wurde an deine E-Mail Adresse gesendet.",
 "maintenanceMode":"Diese Anwendung ist in Wartung, bitte versuche später eine Verbindung herzustellen",
+"mandatoryField":"* Mandatory field",
 "maxNumberof2FDevicesReached":"Maximum number of 2F devices reached!",
 "missingCode":"Code fehlt",
 "myNotification":"My accepted notification",
@@ -267,9 +268,9 @@
 "renewSession":"Renew session",
 "resendConfirmMail":"Bestätigungsmail erneuert senden ?",
 "resentConfirm":"Möchtest du, dass die Bestätigungsmail erneut gesendet wird ?",
-"resetCertificateOK":"Your certificate has been successfully reset!",
 "resetPwd":"Mein Passwort zurücksetzen",
 "rest2f":"Verification code",
+"retry":"Retry",
 "rightsReloadNeedsLogout":"Zum Neuladen der Rechte musst du dich ab- und wieder anmelden",
 "rules":"RULES",
 "scope":"Scope",
@@ -306,6 +307,7 @@
 "u2fWelcome":"U2F - Geräteverwaltung",
 "unableToGetKey":"Unable to access to your key. Retry or contact your administrator",
 "unknownAction":"Unknown action",
+"unknownAttributes":"Unknown attributes",
 "unregister":"Abmelden",
 "updateCdc":"Update Common Domain Cookie",
 "updateTime":"Update date",
@@ -319,6 +321,16 @@
 "wait":"Warten",
 "waitingmessage":"Authentication in progress, please wait",
 "warning":"Warnung",
+"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
+"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
+"webAuthnFailed":"WebAuthn authentication failed",
+"webAuthnRegisterFailed":"WebAuthn registration failed",
+"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
+"webAuthnRequired":"WebAuthn authentication required",
+"webAuthnUnsupported":"Your web browser does not support WebAuthn",
+"webauthn2f":"WebAuthn",
+"webauthn2fWelcome":"Security device registration",
+"webauthnAlreadyRegistered":"This device is already registered",
 "welcomeOnPortal":"Willkommen in Ihrem gesicherten Authentifizierungsportal.",
 "yesResendMail":"Ja, Mail erneut senden.",
 "yourAddress":"Know your address",
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/en.json 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/en.json
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/en.json	2021-07-22 12:32:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/en.json	2022-02-19 16:04:21.000000000 +0000
@@ -10,6 +10,7 @@
 "PE103":"No second factors available for your account",
 "PE104":"Bad DevOps handler file",
 "PE105":"File not found",
+"PE106":"Error during authentication with OpenID Provider",
 "PE2":"User and password fields must be filled",
 "PE20":"No password backend defined",
 "PE21":"Your account is locked",
@@ -137,7 +138,6 @@
 "chooseApp":"Choose an application your are allowed to access to",
 "cipheredValue":"Ciphered value",
 "click2Reset":"Click here to reset your password",
-"click2ResetCertificate":"Click here to reset your certificate",
 "clickHere":"Please click here",
 "clickOnYubikey":"Click on your Yubikey",
 "close":"Close",
@@ -203,6 +203,7 @@
 "mail2f":"Email code",
 "mailSent2":"A message has been sent to your mail address.",
 "maintenanceMode":"This application is in maintenance, please try to connect later",
+"mandatoryField":"* Mandatory field",
 "maxNumberof2FDevicesReached":"Maximum number of 2F devices reached!",
 "missingCode":"Code is missing",
 "myNotification":"My accepted notification",
@@ -267,9 +268,9 @@
 "renewSession":"Renew session",
 "resendConfirmMail":"Resend confirmation mail?",
 "resentConfirm":"Do you want the confirmation mail to be resent?",
-"resetCertificateOK":"Your certificate has been successfully reset!",
 "resetPwd":"Reset my password",
 "rest2f":"Verification code",
+"retry":"Retry",
 "rightsReloadNeedsLogout":"Rights reloads need to logout and login again",
 "rules":"RULES",
 "scope":"Scope",
@@ -306,6 +307,7 @@
 "u2fWelcome":"U2F device management",
 "unableToGetKey":"Unable to access to your key. Retry or contact your administrator",
 "unknownAction":"Unknown action",
+"unknownAttributes":"Unknown attributes",
 "unregister":"Unregister",
 "updateCdc":"Update Common Domain Cookie",
 "updateTime":"Update date",
@@ -319,6 +321,16 @@
 "wait":"Wait",
 "waitingmessage":"Authentication in progress, please wait",
 "warning":"Warning",
+"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
+"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
+"webAuthnFailed":"WebAuthn authentication failed",
+"webAuthnRegisterFailed":"WebAuthn registration failed",
+"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
+"webAuthnRequired":"WebAuthn authentication required",
+"webAuthnUnsupported":"Your web browser does not support WebAuthn",
+"webauthn2f":"WebAuthn",
+"webauthn2fWelcome":"Security device registration",
+"webauthnAlreadyRegistered":"This device is already registered",
 "welcomeOnPortal":"Welcome on your secured authentication portal.",
 "yesResendMail":"Yes, resend the mail",
 "yourAddress":"Know your address",
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/es.json 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/es.json
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/es.json	2021-07-22 12:32:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/es.json	2022-02-19 16:04:21.000000000 +0000
@@ -10,6 +10,7 @@
 "PE103":"No second factors available for your account",
 "PE104":"Bad DevOps handler file",
 "PE105":"File not found",
+"PE106":"Error during authentication with OpenID Provider",
 "PE2":"Los campos \"usuario\" y \"password\" deben tener contenido",
 "PE20":"No password backend defined",
 "PE21":"Su cuenta está bloqueada",
@@ -137,7 +138,6 @@
 "chooseApp":"Elija una aplicación a la cual se le está permitido acceder",
 "cipheredValue":"Valor cifrado",
 "click2Reset":"Pulse aquí para restaurar el password",
-"click2ResetCertificate":"Pulse aquí para reiniciar su certificado",
 "clickHere":"Por favor haga clic aquí",
 "clickOnYubikey":"Haga clic en su Yubikey",
 "close":"Close",
@@ -203,6 +203,7 @@
 "mail2f":"Código de e-mail",
 "mailSent2":"Un mensaje ha sido enviado a su dirección de e-mail",
 "maintenanceMode":"Aplicación en mantenimiento, por favor intente conectarse luego",
+"mandatoryField":"* Mandatory field",
 "maxNumberof2FDevicesReached":"¡El límite de dispositivos 2F ha sido alcanzado!",
 "missingCode":"Código faltante",
 "myNotification":"My accepted notification",
@@ -267,9 +268,9 @@
 "renewSession":"Renew session",
 "resendConfirmMail":"¿Reenviar e-mail de confirmación?",
 "resentConfirm":"¿Desea que el e-mail de confirmación sea reenviado?",
-"resetCertificateOK":"Su certificado ha sido reiniciado con éxito",
 "resetPwd":"Reiniciar mi contraseña",
 "rest2f":"Código de verificación",
+"retry":"Retry",
 "rightsReloadNeedsLogout":"La recarga de derechos necesita desconectarse y conectarse de nuevo",
 "rules":"RULES",
 "scope":"Alcance",
@@ -306,6 +307,7 @@
 "u2fWelcome":"Administración de dispositivos U2F",
 "unableToGetKey":"Imposible acceder a su llave. Reintente o póngase en contacto con su administrador",
 "unknownAction":"Acción desconocida",
+"unknownAttributes":"Unknown attributes",
 "unregister":"Suprimir",
 "updateCdc":"Actualizar el cookie de dominio común",
 "updateTime":"Fecha de actualización",
@@ -319,6 +321,16 @@
 "wait":"Esperar",
 "waitingmessage":"Autenticación en progreso, espere por favor",
 "warning":"Precaución",
+"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
+"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
+"webAuthnFailed":"WebAuthn authentication failed",
+"webAuthnRegisterFailed":"WebAuthn registration failed",
+"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
+"webAuthnRequired":"WebAuthn authentication required",
+"webAuthnUnsupported":"Your web browser does not support WebAuthn",
+"webauthn2f":"WebAuthn",
+"webauthn2fWelcome":"Security device registration",
+"webauthnAlreadyRegistered":"This device is already registered",
 "welcomeOnPortal":"Bienvenido a su portal de autenticación.",
 "yesResendMail":"Sí, reenviar el e-mail",
 "yourAddress":"Conozca su dirección",
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/fi.json 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/fi.json
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/fi.json	2021-07-22 12:32:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/fi.json	2022-02-19 16:04:21.000000000 +0000
@@ -10,6 +10,7 @@
 "PE103":"No second factors available for your account",
 "PE104":"Bad DevOps handler file",
 "PE105":"File not found",
+"PE106":"Error during authentication with OpenID Provider",
 "PE2":"Käyttäjänimi ja salasana kenttä pitää olla täytetty",
 "PE20":"No password backend defined",
 "PE21":"Tunnuksesi on lukittu",
@@ -137,7 +138,6 @@
 "chooseApp":"Choose an application your are allowed to access to",
 "cipheredValue":"Ciphered value",
 "click2Reset":"Click here to reset your password",
-"click2ResetCertificate":"Click here to reset your certificate",
 "clickHere":"Please click here",
 "clickOnYubikey":"Click on your Yubikey",
 "close":"Close",
@@ -203,6 +203,7 @@
 "mail2f":"Email code",
 "mailSent2":"Viesti on lähetetty sähköpostiisi.",
 "maintenanceMode":"This application is in maintenance, please try to connect later",
+"mandatoryField":"* Mandatory field",
 "maxNumberof2FDevicesReached":"Maximum number of 2F devices reached!",
 "missingCode":"Code is missing",
 "myNotification":"My accepted notification",
@@ -267,9 +268,9 @@
 "renewSession":"Renew session",
 "resendConfirmMail":"Uudelleen lähetä vahvistus sähköposti?",
 "resentConfirm":"Do you want the confirmation mail to be resent?",
-"resetCertificateOK":"Your certificate has been successfully reset!",
 "resetPwd":"Palauta salasanani?",
 "rest2f":"Verification code",
+"retry":"Retry",
 "rightsReloadNeedsLogout":"Rights reloads need to logout and login again",
 "rules":"RULES",
 "scope":"Scope",
@@ -306,6 +307,7 @@
 "u2fWelcome":"U2F device management",
 "unableToGetKey":"Unable to access to your key. Retry or contact your administrator",
 "unknownAction":"Unknown action",
+"unknownAttributes":"Unknown attributes",
 "unregister":"Unregister",
 "updateCdc":"Update Common Domain Cookie",
 "updateTime":"Update date",
@@ -319,6 +321,16 @@
 "wait":"Odota",
 "waitingmessage":"Authentication in progress, please wait",
 "warning":"Varoitus",
+"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
+"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
+"webAuthnFailed":"WebAuthn authentication failed",
+"webAuthnRegisterFailed":"WebAuthn registration failed",
+"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
+"webAuthnRequired":"WebAuthn authentication required",
+"webAuthnUnsupported":"Your web browser does not support WebAuthn",
+"webauthn2f":"WebAuthn",
+"webauthn2fWelcome":"Security device registration",
+"webauthnAlreadyRegistered":"This device is already registered",
 "welcomeOnPortal":"Welcome on your secured authentication portal.",
 "yesResendMail":"Kyllä, uudelleen lähetä sähköposti",
 "yourAddress":"Know your address",
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/fr.json 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/fr.json
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/fr.json	2021-07-22 12:33:56.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/fr.json	2022-02-19 16:04:21.000000000 +0000
@@ -10,6 +10,7 @@
 "PE103":"Aucun second facteur disponible pour votre compte",
 "PE104":"Fichier DevOps mal formaté",
 "PE105":"Fichier inexistant",
+"PE106":"Erreur pendant l'authentification auprès du fournisseur OpenID",
 "PE2":"Identifiant ou mot de passe non renseigné",
 "PE20":"Base des mots de passe non définie",
 "PE21":"Votre compte est bloqué",
@@ -137,7 +138,6 @@
 "chooseApp":"Choisissez une application à laquelle vous êtes autorisé à accéder",
 "cipheredValue":"Valeur cryptée",
 "click2Reset":"Cliquez içi pour réinitialiser votre mot de passe",
-"click2ResetCertificate":"Cliquez içi pour réinitialiser votre certificat",
 "clickHere":"Cliquez ici",
 "clickOnYubikey":"Cliquez sur votre Yubikey",
 "close":"Fermer",
@@ -203,6 +203,7 @@
 "mail2f":"Code par mail",
 "mailSent2":"Un message a été envoyé à votre adresse mail.",
 "maintenanceMode":"Cette application est en maintenance, merci de réessayer plus tard",
+"mandatoryField":"* Champ obligatoire",
 "maxNumberof2FDevicesReached":"Nombre maximum de seconds facteurs atteint !",
 "missingCode":"Code manquant",
 "myNotification":"Ma notification acceptée",
@@ -267,9 +268,9 @@
 "renewSession":"Renouveller la session",
 "resendConfirmMail":"Renvoyer le mail de confirmation ?",
 "resentConfirm":"Voulez-vous que le message de confirmation soit renvoyé ?",
-"resetCertificateOK":"Votre certificat a bien été réinitialisé!",
 "resetPwd":"Réinitialiser mon mot de passe",
 "rest2f":"Code de vérification",
+"retry":"Réessayer",
 "rightsReloadNeedsLogout":"Le rechargement des droits nécessite une déconnexion",
 "rules":"REGLES",
 "scope":"Informations",
@@ -306,6 +307,7 @@
 "u2fWelcome":"Gestion du périphérique U2F",
 "unableToGetKey":"Impossible d'accéder à la clef. Réessayez ou contactez votre administrateur",
 "unknownAction":"Action inconnue",
+"unknownAttributes":"Attributs inconnus",
 "unregister":"Supprimer",
 "updateCdc":"Mise à jour du cookie de domaine commun",
 "updateTime":"Date de mise à jour",
@@ -319,6 +321,16 @@
 "wait":"Attendre",
 "waitingmessage":"Authentification en cours, merci de patienter",
 "warning":"Attention",
+"webAuthnBrowserFailed":"Le navigateur n'a pas pu obtenir d'assertion WebAuthn",
+"webAuthnBrowserInProgress":"Authentification WebAuthn en cours, veuillez suivre les instructions de votre navigateur",
+"webAuthnFailed":"L'authentification WebAuthn a échoué",
+"webAuthnRegisterFailed":"L'enregistrement WebAuthn a échoué",
+"webAuthnRegisterInProgress":"Enregistrement WebAuthn en cours, veuillez suivre les instructions de votre navigateur",
+"webAuthnRequired":"Authentification WebAuthn nécessaire",
+"webAuthnUnsupported":"Votre navigateur ne supporte pas WebAuthn",
+"webauthn2f":"WebAuthn",
+"webauthn2fWelcome":"Enregistrement d'un périphérique de sécurité",
+"webauthnAlreadyRegistered":"Ce périphérique est déjà enregistré",
 "welcomeOnPortal":"Bienvenue sur votre portail d'authentification sécurisée.",
 "yesResendMail":"Oui, renvoyer le mail",
 "yourAddress":"Connaître votre adresse",
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/he.json 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/he.json
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/he.json	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/he.json	2022-02-19 16:04:21.000000000 +0000
@@ -0,0 +1,351 @@
+{
+"2FDeviceNotFound":"לא נמצא התקן לאימות דו־שלבי",
+"2fRegRequired":"שירות זה דורש אימות דו־שלבי. ניתן לרשום התקן כעת ואז לחזור לשער.",
+"PE0":"המשתמש עבר אימות",
+"PE1":"תוקף החיבור שלך פג, עליך להזדהות פעם נוספת",
+"PE10":"אישור שגוי",
+"PE100":"הסיסמה מכילה תו אסור",
+"PE101":"הסיסמה מכילה תווים אסורים",
+"PE102":"יש לשדרג את ההפעלה",
+"PE103":"אימות דו־שלבי אינו זמין לחשבון שלך",
+"PE104":"קובץ טיפול ב־DevOps פגום",
+"PE105":"קובץ לא נמצא",
+"PE106":"שגיאה במהלך אימות מול ספק OpenID",
+"PE2":"יש למלא את שדות המשתמש והסיסמה",
+"PE20":"לא צוין מנגנון סיסמאות",
+"PE21":"החשבון שלך נעול",
+"PE22":"תוקף הסיסמה שלך פג",
+"PE23":"נדרש אישור",
+"PE24":"שגיאה",
+"PE25":"הסיסמה שלך עברה איפוס ויש להחליף אותה כעת",
+"PE26":"אי אפשר להחליף את הסיסמה",
+"PE27":"יש לספק את הסיסמה הישנה בעת הגדרת סיסמה חדשה",
+"PE28":"איכות הסיסמה ירודה",
+"PE29":"הסיסמה קצרה מדי",
+"PE3":"חשבון או סיסמה שגויים מול מנהל הספרייה",
+"PE30":"הסיסמה צעירה מדי",
+"PE31":"השתמשת בסיסמה ממש לאחרונה",
+"PE32":" ניסיונות אימות נותרו, נא להחליף את הסיסמה שלך!",
+"PE33":"%d ימים, %d שעות, %d דקות ו־%d בטרם פקיעת תוקף הסיסמה, קדימה להחליף אותה!",
+"PE34":"הסיסמאות אינן תואמות",
+"PE35":"הסיסמה הוחלפה בהצלחה",
+"PE36":"יש לך הודעה חדשה",
+"PE37":"כתובת שגויה",
+"PE38":"אין סכמה זמינה",
+"PE39":"סיסמה ישנה שגויה",
+"PE4":"המשתמש לא נמצא בספרייה",
+"PE40":"שם משתמש שגוי",
+"PE41":"אסור לפתוח הפעלות",
+"PE42":"נדרש אישור",
+"PE43":"כתובת הדוא״ל שלך מחייבת",
+"PE44":"מפתח האישור שגוי או ישן מדי",
+"PE45":"אירעה שגיאה בשליחת הודעת דוא״ל",
+"PE46":"נשלחה הודעת דוא״ל",
+"PE47":"התנתקת",
+"PE48":"שגיאת SAML לא מוגדרת",
+"PE49":"לא ניתן לטעון שירות SAML",
+"PE5":"פרטי גישה שגויים",
+"PE50":"תקלה בטעינת ספק הזהות",
+"PE51":"אירעה שגיאה במהלך כניסה אחודה עם SAML",
+"PE52":"יישות ה־SAML אינה ידועה",
+"PE53":"יעד הודעת ה־SAML שגוי",
+"PE54":"תנאי הודעת ה־SAML לא מכובדים",
+"PE55":"ספק הזהות שהחל כניסה אחודה אינו מורשה",
+"PE56":"אירעה שגיאה במהלך יציאה אחודה עם SAML",
+"PE57":"שגיאה בניהול חתימות הודעות SAML",
+"PE58":"אירעה שגיאה במהלך שימוש באמצעי מה־SAML",
+"PE59":"שגיאת תקשורת עם הפעלות SAML",
+"PE6":"לא ניתן להתחבר לשרת ה־LDAP",
+"PE60":"שגיאה בטעינת ספק השירות",
+"PE61":"אירעה שגיאה בהמהלך החלפת מאפייני SAML",
+"PE62":"זה עמוד נקודת גישה של OpenID",
+"PE63":"זהו ניסיון להשתמש בזהות OpenID שאיננה שלך",
+"PE64":"תכונה נדרשת אינה זמינה",
+"PE65":"איחוד אסור לפי מדיניות האבטחה",
+"PE66":"הודעת האימות כבר נשלחה בדוא״ל",
+"PE67":"יש למלא את שדה הסיסמה",
+"PE68":"לא הוענקה גישה בשירות ה־CAS",
+"PE69":"נא לספק את כתובת הדוא״ל שלך",
+"PE7":"שגיאה חריגה משרת ה־LDAP",
+"PE70":"אין משתמש תואם",
+"PE71":"נא לספק את הסיסמה החדשה שלך",
+"PE72":"נשלחה הודעת אימות בדוא״ל",
+"PE73":"חיבור ה־Radius נכשל",
+"PE74":"הסיסמה הישנה נחוצה",
+"PE75":"הגעת מכתובת IP שלא הוסמכה",
+"PE76":"נכשלת בהקלדת הקאפצ׳ה",
+"PE77":"עליך להקליד את הקאפצ׳ה",
+"PE78":"נא למלא את הפרטים שלך",
+"PE79":"חסרים פרטים",
+"PE8":"המודול Apache::Session נכשל",
+"PE80":"כתובת זו כבר בשימוש",
+"PE81":"ניסיון האימות שגוי",
+"PE82":"חרגת מזמן האימות",
+"PE83":"אימות U2F נכשל. נא לנסות שוב או ליצור קשר עם ההנהלה",
+"PE84":"אין לך הרשאה לגשת למארח הזה",
+"PE85":"האתר המאוחר ביקש הפעלה חדשה יותר (והתוסף UpgradeSession לא נטען). יש לצאת ולנסות שוב",
+"PE86":"החשבון שלך נעול. עליך להמתין",
+"PE87":"עליך לעבור אימות שוב כדי לגשת לשער",
+"PE88":"לחשבון שלך חייבת להיות כתובת דוא״ל כדי להשתמש באימות דו־שלבי",
+"PE89":"לא הוענקה גישה בשירות SAML",
+"PE9":"נדרש אימות",
+"PE90":"לא הוענקה גישה בשירות OIDC",
+"PE91":"לא הוענקה גישה בשירות OID",
+"PE92":"לא הוענקה גישה בשירות GET",
+"PE93":"לא הוענקה גישה בשירות IMPERSONATION",
+"PE94":"תכונה נדרשת אינה זמינה",
+"PE95":"לא הוענקה גישה בשירות DECRYPT",
+"PE96":"קוד אימות שגוי",
+"PE97":"האישור שלך שגוי או שתוקפו יפוג בקרוב",
+"PE98":"נא לבחור את האישור החדש שלך",
+"PE99":"נא לבחור את האישור החדש שלך",
+"SSOSessionInactive":"הפעלת ה־SSO אינה פעילה",
+"UA":"סוכן משתמש",
+"VHnotFound":"מארח וירטואלי לא נמצא",
+"accept":"הסכמה",
+"accessDenied":"אין לך אימות גישה ליישום הזה",
+"accountCreated":"החשבון שלך נוצר, הסיסמה הזמנית שלך נשלחה לכתובת הדוא״ל שלך.",
+"accountCreationSuccess":"החשבון שלך נוצר בהצלחה.",
+"action":"פעולה",
+"activeSessions":"הפעלות SSO פעילות",
+"all":"הכול",
+"allowed":"הגישה מותרת",
+"anotherInformation":"מידע נוסף:",
+"areYouSure":"להמשיך?",
+"askToRenew":"ליישום זה נדרש אימות יותר עדכני. לעבור אימות מחדש?",
+"askToUpgrade":"היישום הזה דורש רמת אימות גבוהה יותר. לעבור אימות מחדש?",
+"attributes":"מאפיינים",
+"authLevel":"רמת אימות",
+"authPortal":"שער אימות",
+"authRemaining":"%s ניסיונות אימות נותרו, נא להחליף את הסיסמה שלך!",
+"autoAccept":"לקבל אוטומטית תוך 30 שניות",
+"autoGlobalLogout":"יציאה גלובלית מהמערכת תוך 30 שניות",
+"back2CasUrl":"היישום שיצאת ממנו סיפק קישור שהוא היה רוצה שתיגש אליו",
+"back2Portal":"חזרה לשער",
+"badCode":"קוד שגוי",
+"badName":"שם שגוי",
+"cancel":"ביטול",
+"captcha":"מבחן טיורינג",
+"certificateReset":"איפוס האישור שלי",
+"changePwd":"החלפת הסיסמה שלך",
+"checkDevOps":"בדיקת קובץ טיפול ב־DevOps",
+"checkLastLogins":"בדיקת הכניסות האחרונות שלי",
+"checkUser":"בדיקת פרופיל ה־SSO של המשתמש",
+"checkUserComputedSession":"לא נמצאה הפעלת SSO. הנתונים מחושבים!",
+"checkUserMerged":"נא לבדוק את הפעלת ה־SSO שלך. כמה קבוצות SSO אמתיות ומזויפות ממוזגות!",
+"checkUserNoSessionFound":"לא נמצאה הפעלת SSO",
+"choose2f":"נא לבחור את הגורם השני שלך",
+"chooseApp":"נא לבחור יישום שמותר לך לגשת אליו",
+"cipheredValue":"ערך מוצפן",
+"click2Reset":"לחיצה כאן לאיפוס הסיסמה שלך",
+"clickHere":"נא ללחוץ כאן",
+"clickOnYubikey":"יש ללחוץ על ה־Yubikey שלך",
+"close":"סגירה",
+"closeSSO":"סגירת הפעלת ה־SSO שלך",
+"code":"קוד",
+"confirmLinkSent":"נשלח קישור אימות. הקישור הזה תקף עד",
+"confirmPwd":"אישור סיסמה",
+"confirmation":"אישור",
+"connect":"התחברות",
+"connectedAs":"התחברת בתור",
+"contextSwitching_OFF":"הפסקת התחזות",
+"contextSwitching_ON":"התחזות למשתמש אחר",
+"continue":"להמשיך",
+"createAccount":"יצירת חשבון",
+"current":"נוכחית",
+"currentPwd":"סיסמה נוכחית",
+"date":"תאריך",
+"decryptCipheredValue":"פענוח ערך מוצפן",
+"enterCred":"נא למלא את פרטי הגישה שלך",
+"enterExt2fCode":"נשלח אליך קוד. נא להקליד אותו",
+"enterMail2fCode":"נשלח קוד לכתובת הדוא״ל שלך. נא להקליד אותו",
+"enterOpenIDLogin":"נא למלא את הכניסה שלך ב־OpenID",
+"enterRadius2fCode":"נא להקליד את קוד ה־OTP שלך",
+"enterRest2fCode":"נא להקליד את קוד ה־OTP שלך",
+"enterTotpCode":"נא להקליד קוד TOTP",
+"enterYubikey":"נא להשתמש ב־Yubikey שלך",
+"errorMsg":"הודעת שגיאה",
+"expired2Fremoved":"%s התקני אימות דו־שלבי שתוקפם פג הוסרו (%s)!",
+"explorer":"חוקר",
+"ext2f":"קוד וידוא",
+"firstName":"שם פרטי",
+"forbidden":"הגישה נדחתה",
+"forgotPwd":"שכחת את הסיסמה שלך?",
+"generatePwd":"יצירת סיסמה אוטומטית",
+"globalLogout":"יציאה גלובלית",
+"goToPortal":"מעבר לשער",
+"gotNewMessages":"יש לך כמה הודעות חדשות",
+"gplSoft":"תכנה חופשית שמגובה ברישיון GPL",
+"groups_sso":"קבוצות SSO",
+"headers":"כותרות",
+"hello":"שלום",
+"hide":"הסתרה",
+"id":"מזהה",
+"imSure":"בוודאות",
+"info":"מידע",
+"ipAddr":"כתובת IP",
+"key":"מפתח",
+"lastFailedLogins":"כניסות אחרונות שנכשלו",
+"lastFailedLoginsCaptionLabel":"כניסות אחרונות שנכשלו",
+"lastLogins":"כניסות אחרונות",
+"lastLoginsCaptionLabel":"כניסות אחרונות",
+"lastName":"שם משפחה",
+"linkValidUntil":"הודעה זו מכילה קישור לאיפוס הסיסמה שלך, הקישור הזה תקף עד",
+"linkValidUntilCertif":"ההודעה מכילה קישור לאיפוס האישור שלך, הקישור הזה תקף עד",
+"login":"כניסה",
+"loginHistory":"היסטוריית כניסות",
+"logout":"יציאה",
+"logoutConfirm":"לצאת?",
+"logoutFromOtherApp":"יציאה מיישומים אחרים…",
+"logoutFromSP":"יציאה מספקי שירות…",
+"macros":"תסריטים",
+"mail":"דוא״ל",
+"mail2f":"קוד בדוא״ל",
+"mailSent2":"נשלחה הודעה לכתובת הדוא״ל שלך.",
+"maintenanceMode":"יישום זה מושבת לצורכי תחזוקה, נא לנסות להתחבר מאוחר יותר",
+"mandatoryField":"* שדה חובה",
+"maxNumberof2FDevicesReached":"הגעת לכמות התקני האימות הדו־שלבי המרבית!",
+"missingCode":"חסר קוד",
+"myNotification":"ההתראה שלי שאושרה",
+"myNotifications":"ההתראות שלי שאושרו",
+"name":"שם",
+"newMessages":"הודעות חדשות",
+"newPassword":"סיסמה חדשה",
+"newPwdSentTo":"נשלח אימות לכתובת הדוא״ל שלך.",
+"noHistory":"זה החיבור הראשון שלך, ברוך בואך!",
+"noNotification":"לא נמצאו התראות שאושרו",
+"noTOTPFound":"לא נמצא TOTP",
+"noU2FKeyFound":"לא נמצא מפתח U2F",
+"notAnEncryptedValue":"זה לא ערך מוצפן",
+"notAuthorized":"אסור לך לעשות את זה",
+"notAuthorizedAuthLevel":"הפעולה דורשת רמת אימות גבוהה יותר",
+"notFound":"לא נמצא: ניסית לגשת לעמוד שאינו זמין",
+"notificationNotFound":"לא נמצאו התראות במסד הנתונים",
+"notificationRetrieveFailed":"לא ניתן לקבל התראות",
+"notificationsExplorer":"חוקר התראות",
+"oidcConsent":"היישום %s מעוניין:",
+"oidcConsents":"הסכמות OIDC",
+"oidcConsentsFull":"הסכמות OpenID Connect",
+"oneExpired2Fremoved":"התקן אימות דו־שלבי שתוקפו פג הוסר (%s)!",
+"openIdExample":"למשל:http://myopenid.org/toto",
+"openSSOSession":"פתיחת הפעלת ה־SSO שלך",
+"openSessionSpace":"מרחב זה מאפשר לך לפתוח הפעלת SSO. הוא יאפשר לך לגשת בצורה מאובטחת לכל היישומים שלפרופיל שלך מותר לגשת אליהם.",
+"openidAp":"האם מוסכם עליך לספק את המשתנים הבאים?",
+"openidExchange":"לאמת את עצמך מול %s?",
+"openidPA":"מדיניות השימוש בנתונים זמינה תחת",
+"openidRpns":"המשתנה %s נחוץ שאיחוד אינו זמין",
+"otherSessions":"הפעלות פעילות נוספות",
+"password":"סיסמה",
+"passwordPolicy":"נא לכבד את המדיניות הבאה:",
+"passwordPolicyMinDigit":"כמות מזערית של ספרות:",
+"passwordPolicyMinLower":"כמות מזערית של אותיות קטנות:",
+"passwordPolicyMinSize":"גודל מזערי:",
+"passwordPolicyMinSpeChar":"כמות מזערית של תווים מיוחדים:",
+"passwordPolicyMinUpper":"כמות מזערים של אותיות גדולות:",
+"passwordPolicyNone":"הסיסמה שלך נתונה לבחירתך החופשית!",
+"passwordPolicySpecialChar":"תווים מיוחדים מורשים:",
+"pasteHere":"ניתן להדביק את הקובץ שלך כאן…",
+"ppGrace":" ניסיונות אימות נותרו, נא להחליף את הסיסמה שלך!",
+"proxyError":"שער גישה שגוי: לא ניתן להצטרף לשרת המרוחק",
+"pwd":"סיסמה",
+"pwdChange":"החלפת סיסמה",
+"pwdChanged":"הסיסמה שלך הוחלפה בהצלחה!",
+"pwdResetAlreadyIssued":"כבר הוגשה בקשה לאיפוס סיסמה ב־",
+"pwdWillExpire":"%s ימים, %s שעות, %s דקות ו־%s בטרם פקיעת תוקף הסיסמה, קדימה להחליף אותה!",
+"radius2f":"רדיוס",
+"redirectedFrom":"הופנית דרך",
+"redirectedIn":"תתבצע הפניה בעוד 30 שניות",
+"redirectionInProgress":"מתבצעת הפניה…",
+"redirectionToIdp":"הפניה לספק הזהות שלך",
+"reference":"הפניה",
+"refreshrights":"רענון הזכויות שלי",
+"refuse":"סירוב",
+"register":"הרשמה",
+"registerRequestAlreadyIssued":"כבר הוגשה בקשה לרישום החשבון הזה ב־",
+"rememberChoice":"שמירת הבחירה שלי",
+"remove2fWarning":"פעולה זו אינה הפיכה",
+"removeOtherSessions":"הסרת הפעלות אחרות",
+"renewSession":"חידוש הפעלה",
+"resendConfirmMail":"לשלוח את הודעת האימות שוב?",
+"resentConfirm":"האם לשלוח שוב את הודעת האימות בדוא״ל?",
+"resetPwd":"איפוס הסיסמה שלי",
+"rest2f":"קוד אימות",
+"retry":"Retry",
+"rightsReloadNeedsLogout":"רענוני זכויות דורשים יציאה וכניסה מחדש",
+"rules":"כללים",
+"scope":"היקף",
+"search":"חיפוש",
+"searchAccount":"חיפוש אחר חשבון",
+"searchingForm":"טופס חיפוש",
+"seconds":"שניות",
+"selectIdP":"נא לבחור את ספק הזהות שלך",
+"sendPwd":"לשלוח לי קישור",
+"serverError":"מתרחשת שגיאה בשרת",
+"service":"שירות",
+"serviceProvidedBy":"השירות בחסות",
+"sessionsDeleted":"ההפעלות הבאות נסגרו",
+"sfaManager":"מנהל אימות דו־שלבי",
+"showhidePasswords":"הצגת/הסתרת סיסמאות",
+"spoofId":"מזהה מזויף",
+"startTime":"תאריך יצירה",
+"stayConnected":"לשמור על הקישור במכשיר זה",
+"submit":"הגשה",
+"switchContext":"החלפת הקשר",
+"totp2f":"יישומון סיסמה חד־פעמית",
+"totpExistingKey":"כבר יש התקן TOTP רשום, עליך להסיר אותו בטרם הוספת התקן TOTP חדש",
+"totpMissingCode":"יש למלא את הקוד שסופק על ידי יישומון ה־TOTP שלך",
+"totpQrCode":"יש לסרוק את קוד ה־QR הזה ביישומון ה־TOTP שלך",
+"totpRegisterCode":"יש למלא את הקוד שסופק על ידי היישומון שלך",
+"totpRegisterName":"נא לבחור שם להתקן ה־TOTP הזה",
+"totpSecretKey":"אם יישומון ה־TOTP שלך לא תומך בקודים מסוג QR, יש למלא את המפתח הבא במקום:",
+"touchU2fDevice":"נא לגעת בהתקן ה־U2F המהבהב כעת.",
+"touchU2fDeviceOrEnterTotp":"נא לגעת בהתקן ה־U2F המהבהב או להקליד את קוד ה־TOTP.",
+"type":"סוג",
+"u2f":"מפתח U2F",
+"u2fFailed":"וידוא U2F נכשל. נא לנסות שוב או ליצור קשר עם ההנהלה",
+"u2fPermission":"יכול להיות שתופיע בקשה לאפשר לאתר לגשת למפתחות האבטחה שלך. לאחר הענקת ההרשאה, המכשיר יתחיל להבהב.",
+"u2fWelcome":"ניהול התקני U2F",
+"unableToGetKey":"לא ניתן לגשת למפתח שלך. יש לנסות שוב או ליצור קשר עם ההנהלה שלך",
+"unknownAction":"פעולה לא ידועה",
+"unknownAttributes":"Unknown attributes",
+"unregister":"ביטול רישום",
+"updateCdc":"עדכון עוגיית שם תחום שיתופי",
+"updateTime":"תאריך שדרוג",
+"upgradeSession":"שדרוג הפעלה",
+"useYubikey":"שימוש ב־Yubikey שלך",
+"user":"משתמש",
+"utotp2f":"TOTP-או-U2F",
+"validationDate":"תאריך תיקוף",
+"value":"ערך",
+"verify":"אימות",
+"wait":"המתנה",
+"waitingmessage":"מתבצע אימות, נא להמתין",
+"warning":"אזהרה",
+"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
+"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
+"webAuthnFailed":"WebAuthn authentication failed",
+"webAuthnRegisterFailed":"WebAuthn registration failed",
+"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
+"webAuthnRequired":"WebAuthn authentication required",
+"webAuthnUnsupported":"Your web browser does not support WebAuthn",
+"webauthn2f":"WebAuthn",
+"webauthn2fWelcome":"Security device registration",
+"webauthnAlreadyRegistered":"This device is already registered",
+"welcomeOnPortal":"ברוך בואך לשער האימות המאובטח שלך.",
+"yesResendMail":"כן, לשלוח את ההודעה שוב",
+"yourAddress":"היכרות עם הכתובת שלך",
+"yourApps":"היישומים שלך",
+"yourEmail":"היכרות עם כתובת הדוא״ל שלך",
+"yourIdentity":"היכרות עם הזהות שלך",
+"yourIdentityIs":"הזהות שלך היא",
+"yourKeyIsAlreadyRegistered":"המפתח שלך כבר רשום!",
+"yourKeyIsRegistered":"המפתח שלך רשום",
+"yourKeyIsUnregistered":"רישום המפתח שלך בוטל",
+"yourKeyIsVerified":"המפתח שלך מאומת",
+"yourNewTotpKey":"מפתח ה־TOTP החדש שלך, נא לבדוק אותו ולמלא את הסיסמה",
+"yourOffline":"גישה לחשבון שלך ללא חיבור לאינטרנט",
+"yourPhone":"היכרות עם מספר הטלפון שלך",
+"yourProfile":"היכרות עם הפרופיל שלך",
+"yourTotpKey":"מפתח ה־TOTP שלך",
+"yubikey2f":"Yubikey"
+}
\ No newline at end of file
diff -pruN 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/it.json 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/it.json
--- 2.0.13+ds-3/lemonldap-ng-portal/site/htdocs/static/languages/it.json	2021-07-22 12:32:30.000000000 +0000
+++ 2.0.14+ds-1/lemonldap-ng-portal/site/htdocs/static/languages/it.json	2022-02-19 16:04:21.000000000 +0000
@@ -10,6 +10,7 @@
 "PE103":"No second factors available for your account",
 "PE104":"Bad DevOps handler file",
 "PE105":"File not found",
+"PE106":"Error during authentication with OpenID Provider",
 "PE2":"ID o password mancante",
 "PE20":"No password backend defined",
 "PE21":"Account bloccato",
@@ -137,7 +138,6 @@
 "chooseApp":"Scegli un'applicazione alla quale ti è consentito l'accesso",
 "cipheredValue":"Ciphered value",
 "click2Reset":"Clicca qui per reimpostare la password",
-"click2ResetCertificate":"Click here to reset your certificate",
 "clickHere":"Per favore clicka qui",
 "clickOnYubikey":"Clicca sulla tua Yubikey",
 "close":"Close",
@@ -203,6 +203,7 @@
 "mail2f":"Email code",
 "mailSent2":"Vi é stato inviato un messaggio via mail",
 "maintenanceMode":"Questa applicazione è in manutenzione, prova a connetterti più tardi",
+"mandatoryField":"* Mandatory field",
 "maxNumberof2FDevicesReached":"Raggiunto il numero massimo di dispositivi 2F !",
 "missingCode":"Manca il codice",
 "myNotification":"My accepted notification",
@@ -267,9 +268,9 @@
 "renewSession":"Renew session",
 "resendConfirmMail":"Inviare nuovamente mail di conferma?",
 "resentConfirm":"Vuoi inviare di nuovo la mail di conferma?",
-"resetCertificateOK":"Your certificate has been successfully reset!",
 "resetPwd":"Reimpostare la password",
 "rest2f":"Verification code",
+"retry":"Retry",
 "rightsReloadNeedsLogout":"Le ricariche dei diritti necessitano di disconnettersi e di riconnettersi",
 "rules":"RULES",
 "scope":"Ambito",
@@ -306,6 +307,7 @@
 "u2fWelcome":"Gestione dei dispositivi U2F",
 "unableToGetKey":"Impossibile accedere alla chiave. Riprovare o contattare l'amministratore",
 "unknownAction":"Azione sconosciuta",
+"unknownAttributes":"Unknown attributes",
 "unregister":"Annullare la registrazione",
 "updateCdc":"Aggiorna il Cookie di Common Domain",
 "updateTime":"Aggiorna data",
@@ -319,6 +321,16 @@
 "wait":"Attendere",
 "waitingmessage":"Autenticazione in corso, attendere prego",
 "warning":"Avvertimento",
+"webAuthnBrowserFailed":"Browser failed to obtain WebAuthn credential",
+"webAuthnBrowserInProgress":"WebAuthn authentication in progress. Please follow your browser's instructions",
+"webAuthnFailed":"WebAuthn authentication failed",
+"webAuthnRegisterFailed":"WebAuthn registration failed",
+"webAuthnRegisterInProgress":"WebAuthn registration in progress. Please follow your browser's instructions",
+"webAuthnRequired":"WebAuthn authentication required",
+"webAuthnUnsupported":"Your web browser does not support WebAuthn",
+"webauthn2f":"