diff --git a/dictionaries/privacyidea.definition.json b/dictionaries/privacyidea.definition.json
index 7ec13d06184b24be1a4e5fddbc2c782dd15b14ba..a762ff495955526ff0b62ca16d84667f61e337be 100644
--- a/dictionaries/privacyidea.definition.json
+++ b/dictionaries/privacyidea.definition.json
@@ -17,8 +17,17 @@
   "otp": {
     "en": "OTP"
   },
+  "webauthn": {
+    "en": "WebAuthn"
+  },
+  "push": {
+    "en": "Push"
+  },
+  "u2f": {
+    "en": "U2F"
+  },
   "login_title_challenge": {
-    "en": "Please enter your One Time Password"
+    "en": "Multi-factor authentication"
   },
   "otp_extra_text": {
     "en": "Please enter your Password and One Time Password"
@@ -26,7 +35,7 @@
   "login_text_challenge": {
     "en": "You entered the correct PIN or password. But now you also need to use your second factor."
   },
-  "scanTokenQR": {
+  "scan_token_qr": {
     "en": "Scan this QR code with an app like Google Authenticator or privacyIDEA Authenticator and enter the displayed code below."
   },
   "chal_resp_message": {
@@ -35,37 +44,55 @@
   "enroll_u2f": {
     "en": "Please use your u2f device to finish the enrollment process"
   },
-  "u2fNotWorking": {
-    "en": "U2F is currently not working."
-  },
   "alternate_login_options": {
     "en": "Alternate login options:"
   },
-  "alert_webauthn_insecure_context": {
+  "webauthn_insecure_context": {
     "en": "Unable to proceed with Web Authn because the context is insecure!"
   },
-  "alert_webauthn_unavailable": {
+  "webauthn_library_unavailable": {
     "en": "Could not load WebAuthn, your device probably does not support it. Please try again or use another method."
   },
-  "alert_webAuthnSignRequest_error": {
-    "en": "Error while signing WebAuthnSignRequest:"
+  "webauthn_AbortError": {
+    "en": "You have canceled the authentication."
+  },
+  "webauthn_InvalidStateError": {
+    "en": "You have used an unregistered WebAuthn device. Please use a different one."
+  },
+  "webauthn_NotAllowedError": {
+    "en": "WebAuthn is disabled for this site."
+  },
+  "webauthn_NotSupportedError": {
+    "en": "Your device does not support WebAuthn features that we need."
+  },
+  "webauthn_TypeError": {
+    "en": "There is a problem with the WebAuthn parameters that we use."
+  },
+  "webauthn_other_error": {
+    "en": "There was a problem communicating with your device."
+  },
+  "webauthn_in_progress": {
+    "en": "Trying to communicate with your WebAuthn device. Plug it in and press the button or confirm the system dialog."
+  },
+  "webauthn_success": {
+    "en": "We heard back from your WebAuthn device. You have been authenticated."
   },
-  "alert_u2f_insecure_context": {
+  "u2f_insecure_context": {
     "en": "Unable to proceed with U2F because the context is insecure!"
   },
-  "alert_u2f_unavailable": {
+  "u2f_unavailable": {
     "en": "Could not load U2F, your device probably does not support it. Please try again or use another method."
   },
-  "alert_U2FSignRequest_error": {
+  "u2f_sign_request_error": {
     "en": "Error while signing U2FSignRequest:"
   },
-  "tryAgain": {
+  "try_again": {
     "en": "Try Again"
   },
   "error_message": {
     "en": "Verification was not successful. Please try again."
   },
   "error": {
-    "en": "Error "
+    "en": "Error"
   }
 }
diff --git a/dictionaries/privacyidea.translation.json b/dictionaries/privacyidea.translation.json
index b74df6542a22031bf00514cb8b4b60aad3b55e93..a1d528c67c09c136205230bd81695751bb44b563 100644
--- a/dictionaries/privacyidea.translation.json
+++ b/dictionaries/privacyidea.translation.json
@@ -24,10 +24,25 @@
     "de": "Einmalpasswort",
     "nl": "OTP"
   },
+  "webauthn": {
+    "cs": "WebAuthn",
+    "de": "WebAuthn",
+    "nl": "WebAuthn"
+  },
+  "push": {
+    "cs": "push notifikace",
+    "de": "Push",
+    "nl": "Push"
+  },
+  "u2f": {
+    "cs": "U2F",
+    "de": "U2F",
+    "nl": "U2F"
+  },
   "login_title_challenge": {
-    "cs": "Vložte jednorázový kód",
-    "de": "Bitte geben Sie Ihr Einmalpasswort ein",
-    "nl": "Vul je eenmalige wachtwoord in."
+    "cs": "Vícefázové ověření",
+    "de": "Multi-Faktor-Authentisierung",
+    "nl": "Multifactorauthenticatie"
   },
   "otp_extra_text": {
     "cs": "Vložte své heslo a jednorázový kód",
@@ -39,7 +54,7 @@
     "de": "Sie haben die korrekte PIN, bzw. das korrekte Passwort eingegeben. Aber nun mĂĽssen Sie noch Ihren zweiten Faktor nutzen.",
     "nl": "U heeft het de juiste PIN of wachtwoord ingegegeven. Geef nu ook uw tweede factor in."
   },
-  "scanTokenQR": {
+  "scan_token_qr": {
     "cs": "Naskenujte tento QR kĂłd aplikacĂ­ pro TOTP (napĹ™. Google Authenticator nebo privacyIDEA Authenticator) a zobrazenĂ˝ kĂłd vloĹľte nĂ­Ĺľe.",
     "de": "Scannen Sie den QR-Code mit einer App, wie dem Google Authenticator oder privacyIDEA Authenticator und geben Sie den angezeigten Code unten ein.",
     "nl": "Scan deze QR-code met een app zoals Google Authenticator of privacyIDEA Authenticator en geef de getoonde code in."
@@ -54,34 +69,50 @@
     "de": "Bitte nutzen Sie Ihr U2F-Gerät, um das Ausrollen fertigzustellen.",
     "nl": "Gebruik uw u2f-toestel om de aanmelding verder te zetten."
   },
-  "u2fNotWorking": {
-    "cs": "U2F nenĂ­ k dispozici.",
-    "de": "U2F funktioniert momentan nicht.",
-    "nl": "U2F werkt momenteel niet."
-  },
   "alternate_login_options": {
     "cs": "Další možnosti přihlášení:",
     "de": "Alternative Login Optionen:"
   },
-  "alert_webauthn_insecure_context": {
+  "webauthn_insecure_context": {
     "cs": "Není možné použít autentizaci WebAuthn, protože neodpovídá bezpečnostní kontext."
   },
-  "alert_webauthn_unavailable": {
+  "webauthn_library_unavailable": {
     "cs": "Načtení WebAuthn se nezdařilo, nejspíš ho Vaše zařízení nepodporuje. Zkuste to znovu nebo použijte jinou metodu."
   },
-  "alert_webAuthnSignRequest_error": {
-    "cs": "Chyba při podepisování WebAuthnSignRequest:"
+  "webauthn_AbortError": {
+    "cs": "Ověření WebAuthn bylo zrušeno."
+  },
+  "webauthn_NotAllowedError": {
+    "cs": "WebAuthn není pro tuto stránku povoleno."
+  },
+  "webauthn_InvalidStateError": {
+    "cs": "Použité WebAuthn zařízení nemáte zaregistrované, prosím použijte jiné."
+  },
+  "webauthn_NotSupportedError": {
+    "cs": "Vaše zařízení nepodporuje možnosti WebAuthn, které potřebujeme."
+  },
+  "webauthn_TypeError": {
+    "cs": "WebAuthn parametry jsou nesprávné."
+  },
+  "webauthn_other_error": {
+    "cs": "Komunikace s Vaším WebAuthn zařízením se nezdařila."
+  },
+  "webauthn_in_progress": {
+    "cs": "Probíhá komunikace s Vaším WebAuthn zařízením. Připojte fyzické zařízení a zmáčkněte tlačítko nebo potvrďte systémový dialog."
+  },
+  "webauthn_success": {
+    "cs": "Obdrželi jsme odpověď z Vašeho WebAuthn zařízení. Autentizace byla úspěšná."
   },
-  "alert_u2f_insecure_context": {
+  "u2f_insecure_context": {
     "cs": "Není možné použít autentizaci U2F, protože neodpovídá bezpečnostní kontext."
   },
-  "alert_u2f_unavailable": {
+  "u2f_unavailable": {
     "cs": "Načtení U2F se nezdařilo, nejspíš ho Vaše zařízení nepodporuje. Zkuste to znovu nebo použijte jinou metodu."
   },
-  "alert_U2FSignRequest_error": {
+  "u2f_sign_request_error": {
     "cs": "Chyba při podepisování U2FSignRequest:"
   },
-  "tryAgain": {
+  "try_again": {
     "cs": "Zkusit znovu",
     "de": "Nochmal versuchen",
     "nl": "Probeer opnieuw"
@@ -90,6 +121,6 @@
     "cs": "Ověření nebylo úspěšné. Zkuste to znovu nebo použijte jinou metodu."
   },
   "error": {
-    "cs": "Chyba "
+    "cs": "Chyba"
   }
 }
diff --git a/docs/privacyidea.md b/docs/privacyidea.md
index c6ccc2cd5fb156b2d5027f60a06be288e7ffd1ca..0de4049fcaf36b27fcd9d0e3a052bee64c469656 100644
--- a/docs/privacyidea.md
+++ b/docs/privacyidea.md
@@ -4,10 +4,8 @@ This module is an authentication module for simpleSAMLphp to use with the privac
 
 You can use this plugin in two different ways:
 
-<ol>
-    <li> AuthSource: This module does the complete authentication process against privacyIDEA
-    <li> AuthProc: This module does just one step of the authentication, the second factor against privacyIDEA
-</ol>
+1. AuthSource: This module does the complete authentication process against privacyIDEA
+2. AuthProc: This module does just one step of the authentication, the second factor against privacyIDEA
 
 NOTE: This plugin is enabled by default when installed, you do not need to enable it manually.
 
@@ -31,14 +29,14 @@ You need to add the authentication source 'privacyidea' to
      * The value have to be a string.
      * Optional.
      */
-    'sslVerifyHost' => 'false',
+    'sslVerifyHost' => false,
 
     /*
      * Check if the certificate is valid, signed by a trusted CA.
      * The value have to be a string.
      * Optional.
      */
-    'sslVerifyPeer' => 'false',
+    'sslVerifyPeer' => false,
 
     /*
      * The realm where the user is located in.
@@ -57,21 +55,21 @@ You need to add the authentication source 'privacyidea' to
     'servicePass'       => 'service',
 
     /**
-     *  Set doTriggerChallenge to 'true' to trigger challenges prior to the login
+     *  Set doTriggerChallenge to true to trigger challenges prior to the login
      *  using the configured service account.
      *  This setting takes precedence over 'doSendPassword'.
      *  The value have to be a string.
      */
-    'doTriggerChallenge' => 'true',
+    'doTriggerChallenge' => true,
 
     /**
-     *  Set doSendPassword to 'true' to send a request to validate/check with the username
+     *  Set doSendPassword to true to send a request to validate/check with the username
      *  and an empty pass prior to the login.
      *  This can be used to trigger challenges depending on the configuration in privacyIDEA
      *  and requires no service account. If 'doTriggerChallenge' is enabled, this setting has no effect.
      *  The value have to be a string.
      */
-    'doSendPassword' => 'true',
+    'doSendPassword' => true,
 
     /**
      * Set custom hints for the OTP and password fields
@@ -80,11 +78,11 @@ You need to add the authentication source 'privacyidea' to
     'passFieldHint' => 'Password',
 
     /**
-     * Set SSO to 'true' if you want to use single sign on.
+     * Set SSO to true if you want to use single sign on.
      * All information required for SSO will be saved in the session.
      * After logging out, the SSO data will be removed from the session.
      */
-    'SSO' => 'false',
+    'SSO' => false,
 
     /**
      * Set preferredTokenType to your favourite token type.
@@ -128,6 +126,14 @@ You need to add the authentication source 'privacyidea' to
         'serial' => 'otpSerial',
         'otplen' => 'otpLength'
     ],
+
+    /*
+     * Override (string) or reformat (callable) messages from privacyIDEA.
+     * When using callable, HTML is not escaped.
+     * Optional.
+     */
+    //'messageOverride' => 'Use any of your tokens.',
+    //'messageOverride' => function($messages){return htmlspecialchars(current(explode(',', $messages)));},
 ],
 ```
 
@@ -180,13 +186,13 @@ If you want to use privacyIDEA as an auth process filter, add the configuration
          *  Check if the hostname matches the name in the certificate.
          *  The value have to be a string.
          */
-        'sslVerifyHost'     => 'true',
+        'sslVerifyHost'     => true,
 
         /**
          *  Check if the certificate is valid, signed by a trusted CA
          *  The value have to be a string.
          */
-        'sslVerifyPeer'     => 'true',
+        'sslVerifyPeer'     => true,
 
         /**
          *  Here you need to enter the username of your service account
@@ -202,10 +208,10 @@ If you want to use privacyIDEA as an auth process filter, add the configuration
          *  You can add this option, if you want to enroll tokens for users, who do not have one yet.
          *  The value have to be a string.
          */
-        'doEnrollToken'     => 'true',
+        'doEnrollToken'     => true,
 
         /**
-         *  You can select a time based otp (totp), an event based otp (hotp) or an u2f (u2f)
+         *  You can select the token type for enrollment: a time based otp (totp), an event based otp (hotp) or an u2f (u2f)
          */
         'tokenType'         => 'totp',
 
@@ -213,14 +219,14 @@ If you want to use privacyIDEA as an auth process filter, add the configuration
          *  You can enable or disable trigger challenge.
          *  The value have to be a string.
          */
-        'doTriggerChallenge' => 'true',
+        'doTriggerChallenge' => true,
 
         /**
-         * Set this to 'true' if you want to use single sign on.
+         * Set this to true if you want to use single sign on.
          * All information required for SSO will be saved in the session.
          * After logging out, the SSO data will be removed from the session.
          */
-        'SSO' => 'false',
+        'SSO' => false,
 
         /**
          * Set preferredTokenType to your favourite token type.
@@ -252,7 +258,7 @@ If you want to use privacyIDEA as an auth process filter, add the configuration
          *  privacyIDEA. If passOnNoToken is activated and the user does not have a token, he will be passed by privacyIDEA.
          *  NOTE: Do not use it with privacyidea:tokenEnrollment.
          */
-        'tryFirstAuthentication' => 'true',
+        'tryFirstAuthentication' => true,
 
         /**
          *  You can decide, which password should be used for tryFirstAuthentication
@@ -273,7 +279,7 @@ If you want to use privacyIDEA as an auth process filter, add the configuration
          *  the entityID and/or SAML attributes, you may enable this filter.
          *  Value have to be string.
          */
-        'checkEntityID'        => 'true',
+        'checkEntityID'        => true,
 
         /**
          *  Depending on excludeEntityIDs and includeAttributes the filter will set the state variable
@@ -317,6 +323,14 @@ If you want to use privacyIDEA as an auth process filter, add the configuration
          * Optional, default to true.
          */
         'showLogout' => false,
+
+        /**
+         * Override (string) or reformat (callable) messages from privacyIDEA.
+         * When using callable, HTML is not escaped.
+         * Optional.
+         */
+        //'messageOverride' => 'Use any of your tokens.',
+        //'messageOverride' => function($messages){return htmlspecialchars(current(explode(',', $messages)));},
     ],
 ]
 ```
diff --git a/lib/Auth/Process/PrivacyideaAuthProc.php b/lib/Auth/Process/PrivacyideaAuthProc.php
index 94ed5f52c9ec26390dc86343500ba24c9b5788e3..861e6906144ff6d851ca6a61de70b4023b964ec1 100644
--- a/lib/Auth/Process/PrivacyideaAuthProc.php
+++ b/lib/Auth/Process/PrivacyideaAuthProc.php
@@ -56,6 +56,7 @@ class PrivacyideaAuthProc extends ProcessingFilter
         $state['privacyidea:privacyidea'] = $this->authProcConfig;
         $state['privacyidea:privacyidea']['authenticationMethod'] = 'authprocess';
         $state['privacyidea:privacyidea:ui']['showLogout'] = $this->authProcConfig['showLogout'] ?? true;
+        $state['privacyidea:privacyidea:ui']['messageOverride'] = $this->authProcConfig['messageOverride'] ?? null;
 
         // If set in config, allow to check the IP of the client and to control the 2FA depending on the client IP.
         // It can be used to configure that a user does not need to provide a second factor when logging in from the local network.
@@ -67,9 +68,9 @@ class PrivacyideaAuthProc extends ProcessingFilter
             }
         }
 
-        // If set to "true" in config, selectively disable the privacyIDEA authentication using the entityID and/or SAML attributes.
+        // If set to true in config, selectively disable the privacyIDEA authentication using the entityID and/or SAML attributes.
         // The skipping will be done in self::isPrivacyIDEADisabled
-        if (!empty($this->authProcConfig['checkEntityID']) && 'true' === $this->authProcConfig['checkEntityID']) {
+        if (!empty($this->authProcConfig['checkEntityID']) && true === $this->authProcConfig['checkEntityID']) {
             $stateId = State::saveState($state, 'privacyidea:privacyidea');
             $stateId = $this->checkEntityID($this->authProcConfig, $stateId);
             $state = State::loadState($stateId, 'privacyidea:privacyidea');
@@ -83,7 +84,7 @@ class PrivacyideaAuthProc extends ProcessingFilter
 
         // SSO check if authentication should be skipped
         if (array_key_exists('SSO', $this->authProcConfig)
-            && 'true' === $this->authProcConfig['SSO']) {
+            && true === $this->authProcConfig['SSO']) {
             if (Utils::checkForValidSSO($state)) {
                 Logger::debug('privacyIDEA: SSO data valid - logging in..');
                 ProcessingChain::resumeProcessing($state);
@@ -96,12 +97,12 @@ class PrivacyideaAuthProc extends ProcessingFilter
         $stateId = State::saveState($state, 'privacyidea:privacyidea');
 
         // Check if it should be controlled that user has no tokens and a new token should be enrolled.
-        if (!empty($this->authProcConfig['doEnrollToken']) && 'true' === $this->authProcConfig['doEnrollToken']) {
+        if (!empty($this->authProcConfig['doEnrollToken']) && true === $this->authProcConfig['doEnrollToken']) {
             $stateId = $this->enrollToken($stateId, $username);
         }
 
         // Check if triggerChallenge or a call with a static pass to /validate/check should be done
-        if (!empty($this->authProcConfig['doTriggerChallenge']) && 'true' === $this->authProcConfig['doTriggerChallenge']) {
+        if (!empty($this->authProcConfig['doTriggerChallenge']) && true === $this->authProcConfig['doTriggerChallenge']) {
             // Call /validate/triggerchallenge with the service account from the configuration to trigger all token of the user
             $stateId = State::saveState($state, 'privacyidea:privacyidea');
             if (!$this->pi->serviceAccountAvailable()) {
@@ -120,7 +121,7 @@ class PrivacyideaAuthProc extends ProcessingFilter
                     $stateId = Utils::processPIResponse($stateId, $response);
                 }
             }
-        } elseif (!empty($this->authProcConfig['tryFirstAuthentication']) && 'true' === $this->authProcConfig['tryFirstAuthentication']) {
+        } elseif (!empty($this->authProcConfig['tryFirstAuthentication']) && true === $this->authProcConfig['tryFirstAuthentication']) {
             // Call /validate/check with a static pass from the configuration
             // This could already end the authentication with the "passOnNoToken" policy, or it could trigger challenges
             $response = Utils::authenticatePI($state, [
diff --git a/lib/Auth/Source/PrivacyideaAuthSource.php b/lib/Auth/Source/PrivacyideaAuthSource.php
index 4dc73af18eddfe559ba70726a02f14613885e59c..70198eeffb9ba4d9ad2a34da940f2c1464c4e035 100644
--- a/lib/Auth/Source/PrivacyideaAuthSource.php
+++ b/lib/Auth/Source/PrivacyideaAuthSource.php
@@ -103,6 +103,7 @@ class PrivacyideaAuthSource extends UserPassBase
         $state['privacyidea:privacyidea:ui']['otpFieldHint'] = $this->authSourceConfig['otpFieldHint'] ?? '';
         $state['privacyidea:privacyidea:ui']['passFieldHint'] = $this->authSourceConfig['passFieldHint'] ?? '';
         $state['privacyidea:privacyidea:ui']['loadCounter'] = '1';
+        $state['privacyidea:privacyidea:ui']['messageOverride'] = $this->authSourceConfig['messageOverride'] ?? null;
 
         $stateId = State::saveState($state, 'privacyidea:privacyidea');
 
@@ -144,7 +145,7 @@ class PrivacyideaAuthSource extends UserPassBase
             $stateId = State::saveState($state, 'privacyidea:privacyidea');
 
             if (array_key_exists('doTriggerChallenge', $source->authSourceConfig)
-                && 'true' === $source->authSourceConfig['doTriggerChallenge']) {
+                && true === $source->authSourceConfig['doTriggerChallenge']) {
                 if (!empty($username) && $source->pi->serviceAccountAvailable()) {
                     try {
                         $response = $source->pi->triggerChallenge($username);
@@ -153,7 +154,7 @@ class PrivacyideaAuthSource extends UserPassBase
                     }
                 }
             } elseif (array_key_exists('doSendPassword', $source->authSourceConfig)
-                && 'true' === $source->authSourceConfig['doSendPassword']) {
+                && true === $source->authSourceConfig['doSendPassword']) {
                 if (!empty($username)) {
                     try {
                         $response = $source->pi->validateCheck($username, $password);
diff --git a/lib/Auth/Utils.php b/lib/Auth/Utils.php
index 99ff885ac485db6b4bb40d2329bb74404cbf7af1..e4a294f45d4878cbc6688e2c677c6bbb9c9dec8c 100644
--- a/lib/Auth/Utils.php
+++ b/lib/Auth/Utils.php
@@ -32,7 +32,7 @@ class Utils
         $state['privacyidea:privacyidea:ui']['mode'] = $formParams['mode'];
 
         // If the mode was changed, do not make any requests
-        if ('true' === $formParams['modeChanged']) {
+        if (true === $formParams['modeChanged']) {
             $state['privacyidea:privacyidea:ui']['loadCounter'] = 1;
 
             return null;
@@ -202,11 +202,11 @@ class Utils
             $pi->logger = new PILogger();
 
             if (array_key_exists('sslVerifyHost', $config) && !empty($config['sslVerifyHost'])) {
-                $pi->sslVerifyHost = 'false' !== $config['sslVerifyHost'];
+                $pi->sslVerifyHost = false !== $config['sslVerifyHost'];
             }
 
             if (array_key_exists('sslVerifyPeer', $config) && !empty($config['sslVerifyPeer'])) {
-                $pi->sslVerifyPeer = 'false' !== $config['sslVerifyPeer'];
+                $pi->sslVerifyPeer = false !== $config['sslVerifyPeer'];
             }
 
             if (array_key_exists('serviceAccount', $config) && !empty($config['serviceAccount'])) {
diff --git a/templates/LoginForm.php b/templates/LoginForm.php
index 0ba5d5bc233071f2c60d691b97c8f7f55cc3d7a1..93528e049d521a69a0bf0a70ed4f86d082bb6b46 100644
--- a/templates/LoginForm.php
+++ b/templates/LoginForm.php
@@ -2,6 +2,17 @@
 
 use SimpleSAML\Module;
 
+$this->data['u2fAvailable'] = !empty($this->data['u2fSignRequest']);
+$this->data['webauthnAvailable'] = !empty($this->data['webAuthnSignRequest']);
+$this->data['mode'] = ($this->data['mode'] ?? null) ?: 'otp';
+$this->data['noAlternatives'] = true;
+foreach (['otp', 'push', 'u2f', 'webauthn'] as $mode) {
+    if ($mode !== $this->data['mode'] && $this->data[$mode . 'Available']) {
+        $this->data['noAlternatives'] = false;
+        break;
+    }
+}
+
 // Set default scenario if isn't set
 if (!empty($this->data['authProcFilterScenario'])) {
     if (empty($this->data['username'])) {
@@ -43,16 +54,22 @@ if (null !== $this->data['errorCode']) {
     ?>
 
     <div class="error-dialog">
-        <img src="/<?php echo htmlspecialchars(
+        <img src="/<?php
+    echo htmlspecialchars(
         $this->data['baseurlpath'],
         ENT_QUOTES
     ); ?>resources/icons/experience/gtk-dialog-error.48x48.png"
              class="float-l erroricon" alt="gtk-dialog-error"/>
         <h2><?php echo $this->t('{login:error_header}'); ?></h2>
         <p>
-            <strong><?php echo htmlspecialchars(
-        $this->t('{privacyidea:privacyidea:error}') . $this->data['errorCode'] . ': ' . $this->data['errorMessage']
-    ); ?></strong>
+            <strong>
+    <?php
+    echo htmlspecialchars(
+        sprintf('%s%s: %s', $this->t(
+            '{privacyidea:privacyidea:error}'
+        ), $this->data['errorCode'] ? (' ' . $this->data['errorCode']) : '', $this->data['errorMessage'])
+    ); ?>
+            </strong>
         </p>
     </div>
 
@@ -62,15 +79,11 @@ if (null !== $this->data['errorCode']) {
 
     <div class="container">
         <div class="login">
-            <div class="loginlogo"></div>
-
             <?php
             if ($this->data['authProcFilterScenario']) {
                 echo '<h2>' . htmlspecialchars($this->t('{privacyidea:privacyidea:login_title_challenge}')) . '</h2>';
-            } else {
-                if ($this->data['step'] < 2) {
-                    echo '<h2>' . htmlspecialchars($this->t('{privacyidea:privacyidea:login_title}')) . '</h2>';
-                }
+            } elseif ($this->data['step'] < 2) {
+                echo '<h2>' . htmlspecialchars($this->t('{privacyidea:privacyidea:login_title}')) . '</h2>';
             }
             ?>
 
@@ -89,16 +102,17 @@ if (null !== $this->data['errorCode']) {
                                     <input type="hidden" id="username" name="username"
                                            value="<?php echo htmlspecialchars($this->data['username'] ?? '', ENT_QUOTES); ?>"/>
                                     <?php
-                                } else {
+                                } elseif ($this->data['step'] <= 1) {
                                     ?>
-                                    <label for="username" class="sr-only">
-                                        <?php echo $this->t('{login:username}'); ?>
-                                    </label>
-                                    <input type="text" id="username" tabindex="1" name="username" autofocus
-                                           value="<?php echo htmlspecialchars($this->data['username'], ENT_QUOTES); ?>"
-                                           placeholder="<?php echo htmlspecialchars($this->t('{login:username}'), ENT_QUOTES); ?>"
-                                    />
-                                    <br>
+                                    <p>
+                                        <label for="username" class="sr-only">
+                                            <?php echo $this->t('{login:username}'); ?>
+                                        </label>
+                                        <input type="text" id="username" tabindex="1" name="username" autofocus
+                                               value="<?php echo htmlspecialchars($this->data['username'], ENT_QUOTES); ?>"
+                                               placeholder="<?php echo htmlspecialchars($this->t('{login:username}'), ENT_QUOTES); ?>"
+                                        />
+                                    </p>
                                     <?php
                                 }
 
@@ -129,28 +143,46 @@ if (null !== $this->data['errorCode']) {
                                 } ?>
 
                                 <!-- Pass and OTP fields -->
+                                <?php if (!$this->data['authProcFilterScenario']) { ?>
                                 <label for="password" class="sr-only">
                                     <?php echo $this->t('{privacyidea:privacyidea:password}'); ?>
                                 </label>
                                 <input id="password" name="password" tabindex="1" type="password" value="" class="text"
                                        placeholder="<?php echo htmlspecialchars($passHint, ENT_QUOTES); ?>"/>
+                                <?php } ?>
+
+                                <?php if ($this->data['step'] > 1) { ?>
+                                <p id="message" role="alert"><?php
+                                    $messageOverride = $this->data['messageOverride'] ?? null;
+                                    if (null === $messageOverride || is_string($messageOverride)) {
+                                        echo htmlspecialchars(
+                                            $messageOverride ?? $this->data['message'] ?? '',
+                                            ENT_QUOTES
+                                        );
+                                    } elseif (is_callable($messageOverride)) {
+                                        echo call_user_func($messageOverride, $this->data['message'] ?? '');
+                                    }
+                                ?></p>
+                                <?php } ?>
 
-                                <strong id="message"><?php echo htmlspecialchars($this->data['message'] ?? '', ENT_QUOTES); ?></strong>
-                                <br><br>
-                                <label for="otp" class="sr-only">
-                                    <?php echo $this->t('{privacyidea:privacyidea:otp}'); ?>
-                                </label>
-                                <input id="otp" name="otp" type="password"
-                                       placeholder="<?php echo htmlspecialchars($otpHint, ENT_QUOTES); ?>">
-                                <br><br>
-                                <input id="submitButton" tabindex="1" class="rc-button rc-button-submit" type="submit"
-                                       name="Submit"
-                                       value="<?php echo htmlspecialchars($this->t('{login:login_button}'), ENT_QUOTES); ?>"/>
-                                <br><br>
+                                <?php if ($this->data['step'] > 1) { ?>
+                                <p>
+                                    <label for="otp" class="sr-only">
+                                        <?php echo $this->t('{privacyidea:privacyidea:otp}'); ?>
+                                    </label>
+                                    <input id="otp" name="otp" type="password" placeholder="<?php echo htmlspecialchars($otpHint, ENT_QUOTES); ?>">
+                                </p>
+                                <?php } ?>
+
+                                <p>
+                                    <button id="submitButton" tabindex="1" class="rc-button rc-button-submit" type="submit" name="Submit" value="1">
+                                        <?php echo htmlspecialchars($this->t('{login:login_button}'), ENT_QUOTES); ?>
+                                    </button>
+                                </p>
 
                                 <!-- Undefined index is suppressed and the default is used for these values -->
-                                <input id="mode" type="hidden" name="mode"
-                                       value="<?php echo htmlspecialchars(($this->data['mode'] ?? null) ?: 'otp', ENT_QUOTES); ?>"/>
+                                <input id="mode" type="hidden" name="mode" value="otp"
+                                       data-preferred="<?php echo htmlspecialchars($this->data['mode'], ENT_QUOTES); ?>"/>
 
                                 <input id="pushAvailable" type="hidden" name="pushAvailable"
                                        value="<?php echo ($this->data['pushAvailable'] ?? false) ? 'true' : ''; ?>"/>
@@ -181,7 +213,7 @@ if (null !== $this->data['errorCode']) {
                                 <?php
                                 // If enrollToken load QR Code
                                 if (isset($this->data['tokenQR'])) {
-                                    echo htmlspecialchars($this->t('{privacyidea:privacyidea:scanTokenQR}')); ?>
+                                    echo htmlspecialchars($this->t('{privacyidea:privacyidea:scan_token_qr}')); ?>
                                     <div class="tokenQR">
                                         <?php echo '<img src="' . $this->data['tokenQR'] . '" />'; ?>
                                     </div>
@@ -228,20 +260,37 @@ if (null !== $this->data['errorCode']) {
                     </div> <!-- slide-out-->
                 </div> <!-- form-panel -->
 
-                <div id="AlternateLoginOptions" class="groupMargin">
+                <?php if (!$this->data['noAlternatives'] && $this->data['step'] > 1) { ?>
+                <div id="AlternateLoginOptions" class="groupMargin hidden js-show">
                     <h3><?php echo $this->t('{privacyidea:privacyidea:alternate_login_options}'); ?></h3>
                     <!-- Alternate Login Options-->
-                    <input id="useWebAuthnButton" name="useWebAuthnButton" type="button" value="WebAuthn"/>
-                    <input id="usePushButton" name="usePushButton" type="button" value="Push"/>
-                    <input id="useOTPButton" name="useOTPButton" type="button" value="OTP"/>
-                    <input id="useU2FButton" name="useU2FButton" type="button" value="U2F"/>
+                    <?php if (($this->data['webauthnAvailable'] ?? false) && 'webauthn' !== $this->data['mode']) { ?>
+                    <button id="useWebAuthnButton" name="useWebAuthnButton" type="button">
+                        <span><?php echo $this->t('{privacyidea:privacyidea:webauthn}'); ?></span>
+                    </button>
+                    <?php } ?>
+                    <?php if (($this->data['pushAvailable'] ?? false) && 'push' !== $this->data['mode']) { ?>
+                    <button id="usePushButton" name="usePushButton" type="button">
+                        <span><?php echo $this->t('{privacyidea:privacyidea:push}'); ?></span>
+                    </button>
+                    <?php } ?>
+                    <?php if (($this->data['otpAvailable'] ?? true) && 'otp' !== $this->data['mode']) { ?>
+                    <button id="useOTPButton" name="useOTPButton" type="button">
+                        <span><?php echo $this->t('{privacyidea:privacyidea:otp}'); ?></span>
+                    </button>
+                    <?php } ?>
+                    <?php if (($this->data['u2fAvailable'] ?? false) && 'u2f' !== $this->data['mode']) { ?>
+                    <button id="useU2FButton" name="useU2FButton" type="button">
+                        <span><?php echo $this->t('{privacyidea:privacyidea:u2f}'); ?></span>
+                    </button>
+                    <?php } ?>
                 </div>
-                <br>
+                <?php } ?>
             </form>
 
             <?php
             // Logout
-            if ($this->data['showLogout'] ?? true && isset($this->data['LogoutURL'])) { ?>
+            if (($this->data['showLogout'] ?? true) && isset($this->data['LogoutURL'])) { ?>
                 <p>
                     <a href="<?php echo htmlspecialchars($this->data['LogoutURL']); ?>"><?php echo $this->t('{status:logout}'); ?></a>
                 </p>
@@ -268,17 +317,24 @@ if (!empty($this->data['links'])) {
     </script>
 
     <meta id="privacyidea-step" name="privacyidea-step" content="<?php echo $this->data['step']; ?>">
-    <meta id="privacyidea-hide-alternate" name="privacyidea-hide-alternate" content="<?php echo (
-        !$this->data['pushAvailable']
-        && (!isset($this->data['u2fSignRequest']) || ($this->data['u2fSignRequest']) === '')
-        && (!isset($this->data['webAuthnSignRequest']) || ($this->data['webAuthnSignRequest']) === '')
-    ) ? 'true' : ''; ?>">
 
     <meta id="privacyidea-translations" name="privacyidea-translations" content="<?php
     $translations = [];
     $translation_keys = [
-        'alert_webauthn_insecure_context', 'alert_webauthn_unavailable', 'alert_webAuthnSignRequest_error',
-        'alert_u2f_insecure_context', 'alert_u2f_unavailable', 'alert_U2FSignRequest_error',
+        'webauthn_insecure_context',
+        'webauthn_library_unavailable',
+        'webauthn_AbortError',
+        'webauthn_InvalidStateError',
+        'webauthn_NotAllowedError',
+        'webauthn_NotSupportedError',
+        'webauthn_TypeError',
+        'webauthn_other_error',
+        'webauthn_in_progress',
+        'webauthn_success',
+        'u2f_insecure_context',
+        'u2f_unavailable',
+        'u2f_sign_request_error',
+        'try_again',
     ];
     foreach ($translation_keys as $translation_key) {
         $translations[$translation_key] = $this->t(sprintf('{privacyidea:privacyidea:%s}', $translation_key));
diff --git a/www/js/loginform.js b/www/js/loginform.js
index 4f8f1362dbc2c2b7dc87d2b3bc3e18cacabe17f3..3ac9a0839bb477d457d56baca320dcf8df2e04e0 100644
--- a/www/js/loginform.js
+++ b/www/js/loginform.js
@@ -24,119 +24,111 @@ function set(id, value) {
   }
 }
 
-function disable(id) {
+function hide(id) {
   const element = getElement(id);
   if (element != null) {
     element.classList.add("hidden");
   }
 }
 
-function enable(id) {
+function show(id) {
   const element = getElement(id);
   if (element != null) {
     element.classList.remove("hidden");
   }
 }
 
-function changeMode(newMode) {
-  set("mode", newMode);
-  set("modeChanged", "true");
-  document.forms["piLoginForm"].submit();
-}
-
-function t(key) {
-  return JSON.parse(getContent("privacyidea-translations"))[key];
-}
-
-const step = getContent("privacyidea-step");
-
-if (step > "1") {
-  disable("username");
-  disable("password");
-} else {
-  disable("otp");
-  disable("message");
-  disable("AlternateLoginOptions");
-}
-
-// Set alternate token button visibility
-if (value("webAuthnSignRequest") === "") {
-  disable("useWebAuthnButton");
-}
-
-if (value("u2fSignRequest") === "") {
-  disable("useU2FButton");
+function disable(id) {
+  const element = getElement(id);
+  if (element != null) {
+    element.disabled = true;
+    element.classList.add("disabled");
+  }
 }
 
-if (!booleanValue("pushAvailable")) {
-  disable("usePushButton");
+function enable(id) {
+  const element = getElement(id);
+  if (element != null) {
+    element.disabled = false;
+    element.classList.remove("disabled");
+  }
 }
 
-if (!booleanValue("otpAvailable")) {
-  disable("useOTPButton");
+function changeMode(newMode) {
+  set("mode", newMode);
+  set("modeChanged", "true");
+  document.forms["piLoginForm"].submit();
 }
 
-if (
-  !booleanValue("pushAvailable") &&
-  value("webAuthnSignRequest") === "" &&
-  value("u2fSignRequest") === ""
-) {
-  disable("alternateTokenDiv");
+function fallbackToOTP() {
+  if (value("mode") !== "otp") {
+    setTimeout(() => {
+      changeMode("otp");
+    }, 3000);
+  }
 }
 
-if (value("mode") === "otp") {
-  disable("useOTPButton");
+function setMessage(newMessage) {
+  getElement("message").innerText = newMessage;
 }
 
-if (value("mode") === "webauthn") {
-  disable("otp");
-  disable("submitButton");
-  doWebAuthn();
+function t(key) {
+  return JSON.parse(getContent("privacyidea-translations"))[key];
 }
 
-if (value("mode") === "u2f") {
-  disable("otp");
-  disable("submitButton");
-  doU2F();
+function getWebAuthnErrorMessage(err) {
+  switch (err.name) {
+    case "AbortError":
+    case "InvalidStateError":
+    case "NotAllowedError":
+    case "NotSupportedError":
+    case "TypeError":
+      return t("webauthn_" + err.name);
+    default:
+      return t("webauthn_other_error") + " (" + err.name + ")";
+  }
 }
 
-if (value("mode") === "push") {
-  const pollingIntervals = [4, 3, 2, 1];
-
-  disable("otp");
-  disable("usePushButton");
-  disable("submitButton");
-
-  if (value("loadCounter") > pollingIntervals.length - 1) {
-    refreshTime = pollingIntervals[pollingIntervals.length - 1];
+function webAuthnError(err) {
+  const errorMessage = getWebAuthnErrorMessage(err);
+  console.log("Error while signing WebAuthnSignRequest: " + err);
+  enable("useWebAuthnButton");
+  setMessage(errorMessage);
+  if (getElement("retryWebAuthnButton")) {
+    show("retryWebAuthnButton");
   } else {
-    refreshTime = pollingIntervals[Number(value("loadCounter") - 1)];
+    const retryWebAuthnButton = getElement("useWebAuthnButton").cloneNode();
+    retryWebAuthnButton.addEventListener("click", doWebAuthn);
+    retryWebAuthnButton.id = "retryWebAuthnButton";
+    retryWebAuthnButton.innerHTML = "<span>" + t("try_again") + "</span>";
+    getElement("message").innerHTML += " ";
+    getElement("message").appendChild(retryWebAuthnButton);
   }
-
-  refreshTime *= 1000;
-  setTimeout(() => {
-    document.forms["piLoginForm"].submit();
-  }, refreshTime);
 }
 
 function doWebAuthn() {
+  disable("useWebAuthnButton");
+  hide("retryWebAuthnButton");
+  setMessage(t("webauthn_in_progress"));
   // If mode is push, we have to change it, otherwise the site will refresh while doing webauthn
   if (value("mode") === "push") {
     changeMode("webauthn");
   }
 
   if (!window.isSecureContext) {
-    alert(t("alert_webauthn_insecure_context"));
+    enable("useWebAuthnButton");
+    setMessage(t("webauthn_insecure_context"));
     console.log(
       "Insecure context detected: Aborting Web Authn authentication!"
     );
-    changeMode("otp");
+    fallbackToOTP();
     return;
   }
 
   if (!window.pi_webauthn) {
-    alert(t("alert_webauthn_unavailable"));
-    changeMode("otp");
+    enable("useWebAuthnButton");
+    setMessage(t("webauthn_library_unavailable"));
+    fallbackToOTP();
     return;
   }
 
@@ -156,15 +148,17 @@ function doWebAuthn() {
     const requestjson = JSON.parse(requestStr);
 
     const webAuthnSignResponse = window.pi_webauthn.sign(requestjson);
-    webAuthnSignResponse.then((webauthnresponse) => {
-      const response = JSON.stringify(webauthnresponse);
-      set("webAuthnSignResponse", response);
-      set("mode", "webauthn");
-      document.forms["piLoginForm"].submit();
-    });
+    webAuthnSignResponse
+      .then((webauthnresponse) => {
+        const response = JSON.stringify(webauthnresponse);
+        set("webAuthnSignResponse", response);
+        set("mode", "webauthn");
+        setMessage(t("webauthn_success"));
+        document.forms["piLoginForm"].submit();
+      })
+      .catch(webAuthnError);
   } catch (err) {
-    console.log("Error while signing WebAuthnSignRequest: " + err);
-    alert(t("alert_webAuthnSignRequest_error") + " " + err);
+    webAuthnError(err);
   }
 }
 
@@ -175,17 +169,17 @@ function doU2F() {
   }
 
   if (!window.isSecureContext) {
-    alert(t("alert_u2f_insecure_context"));
+    setMessage(t("u2f_insecure_context"));
     console.log("Insecure context detected: Aborting U2F authentication!");
-    changeMode("otp");
+    fallbackToOTP();
     return;
   }
 
   const requestStr = value("u2fSignRequest");
 
   if (requestStr === null) {
-    alert(t("alert_u2f_unavailable"));
-    changeMode("otp");
+    setMessage(t("u2f_unavailable"));
+    fallbackToOTP();
     return;
   }
 
@@ -194,7 +188,7 @@ function doU2F() {
     sign_u2f_request(requestjson);
   } catch (err) {
     console.log("Error while signing U2FSignRequest: " + err);
-    alert(t("alert_U2FSignRequest_error") + " " + err);
+    setMessage(t("u2f_sign_request_error") + " " + err);
   }
 }
 
@@ -221,17 +215,65 @@ function sign_u2f_request(signRequest) {
   });
 }
 
-if (getContent("privacyidea-hide-alternate") === "true") {
-  disable("AlternateLoginOptions");
+function addClickListener(id, listener) {
+  const el = getElement(id);
+  if (el !== null) {
+    el.addEventListener("click", listener);
+  }
 }
 
-document.addEventListener("DOMContentLoaded", (event) => {
-  getElement("useWebAuthnButton").addEventListener("click", doWebAuthn);
-  getElement("usePushButton").addEventListener("click", function () {
-    changeMode("push");
-  });
-  getElement("useOTPButton").addEventListener("click", function () {
-    changeMode("otp");
+function initPrivacyIDEA() {
+  // set preferred mode by JavaScript so that users without it always have "otp"
+  const preferredMode = getElement("mode").dataset.preferred;
+  if (preferredMode) {
+    set("mode", preferredMode);
+  }
+  document.querySelectorAll(".js-show").forEach((el) => {
+    el.classList.remove("hidden");
   });
-  getElement("useU2FButton").addEventListener("click", doU2F);
-});
+
+  const step = getContent("privacyidea-step");
+
+  if (step > 1) {
+    hide("username");
+    hide("password");
+  } else {
+    hide("otp");
+    hide("message");
+  }
+
+  if (step > 1 && value("mode") !== "otp") {
+    hide("otp");
+    hide("submitButton");
+  }
+
+  if (value("mode") === "webauthn") {
+    doWebAuthn();
+  }
+
+  if (value("mode") === "u2f") {
+    doU2F();
+  }
+
+  if (value("mode") === "push") {
+    const pollingIntervals = [4, 3, 2, 1];
+
+    if (value("loadCounter") > pollingIntervals.length - 1) {
+      refreshTime = pollingIntervals[pollingIntervals.length - 1];
+    } else {
+      refreshTime = pollingIntervals[Number(value("loadCounter") - 1)];
+    }
+
+    refreshTime *= 1000;
+    setTimeout(() => {
+      document.forms["piLoginForm"].submit();
+    }, refreshTime);
+  }
+
+  addClickListener("useWebAuthnButton", doWebAuthn);
+  addClickListener("usePushButton", () => changeMode("push"));
+  addClickListener("useOTPButton", () => changeMode("otp"));
+  addClickListener("useU2FButton", doU2F);
+}
+
+document.addEventListener("DOMContentLoaded", initPrivacyIDEA);