From 9f5589996062f21fcc3eb75aa9144463be55e2e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pavel=20B=C5=99ou=C5=A1ek?= <brousek@ics.muni.cz>
Date: Thu, 24 Mar 2022 19:29:09 +0100
Subject: [PATCH] fix: support OTP for users without JavaScript

hide elements by PHP instead of JavaScript,
set preferred mode by JavaScript so that users without it always have "otp"
---
 templates/LoginForm.php | 38 ++++++++++++++++++++---------
 www/js/loginform.js     | 54 ++++++++++-------------------------------
 2 files changed, 40 insertions(+), 52 deletions(-)

diff --git a/templates/LoginForm.php b/templates/LoginForm.php
index 988be56..e3083c6 100644
--- a/templates/LoginForm.php
+++ b/templates/LoginForm.php
@@ -2,6 +2,12 @@
 
 use SimpleSAML\Module;
 
+$this->data['u2fAvailable'] = !empty($this->data['u2fSignRequest']);
+$this->data['webAuthnAvailable'] = !empty($this->data['webAuthnSignRequest']);
+$this->data['noAlternatives'] = !$this->data['pushAvailable'] && (!$this->data['u2fAvailable']) && (!$this->data['webAuthnAvailable']);
+
+$this->data['mode'] = ($this->data['mode'] ?? null) ?: 'otp';
+
 // Set default scenario if isn't set
 if (!empty($this->data['authProcFilterScenario'])) {
     if (empty($this->data['username'])) {
@@ -91,7 +97,7 @@ 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) {
                                     ?>
                                     <p>
                                         <label for="username" class="sr-only">
@@ -132,12 +138,15 @@ 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)) {
@@ -149,13 +158,16 @@ if (null !== $this->data['errorCode']) {
                                         echo call_user_func($messageOverride, $this->data['message'] ?? '');
                                     }
                                 ?></p>
+                                <?php } ?>
 
+                                <?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">
@@ -164,8 +176,8 @@ if (null !== $this->data['errorCode']) {
                                 </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' : ''; ?>"/>
@@ -243,28 +255,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-->
+                    <?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>
@@ -291,11 +312,6 @@ 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 = [];
diff --git a/www/js/loginform.js b/www/js/loginform.js
index e5e2b71..3ac9a08 100644
--- a/www/js/loginform.js
+++ b/www/js/loginform.js
@@ -223,65 +223,41 @@ function addClickListener(id, listener) {
 }
 
 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");
+  });
+
   const step = getContent("privacyidea-step");
 
-  if (step > "1") {
+  if (step > 1) {
     hide("username");
     hide("password");
   } else {
     hide("otp");
     hide("message");
-    hide("AlternateLoginOptions");
-  }
-
-  // Set alternate token button visibility
-  if (value("webAuthnSignRequest") === "") {
-    hide("useWebAuthnButton");
-  }
-
-  if (value("u2fSignRequest") === "") {
-    hide("useU2FButton");
-  }
-
-  if (!booleanValue("pushAvailable")) {
-    hide("usePushButton");
-  }
-
-  if (!booleanValue("otpAvailable")) {
-    hide("useOTPButton");
-  }
-
-  if (
-    !booleanValue("pushAvailable") &&
-    value("webAuthnSignRequest") === "" &&
-    value("u2fSignRequest") === ""
-  ) {
-    hide("AlternateLoginOptions");
   }
 
-  if (value("mode") === "otp") {
-    hide("useOTPButton");
+  if (step > 1 && value("mode") !== "otp") {
+    hide("otp");
+    hide("submitButton");
   }
 
   if (value("mode") === "webauthn") {
-    hide("otp");
-    hide("submitButton");
     doWebAuthn();
   }
 
   if (value("mode") === "u2f") {
-    hide("otp");
-    hide("submitButton");
     doU2F();
   }
 
   if (value("mode") === "push") {
     const pollingIntervals = [4, 3, 2, 1];
 
-    hide("otp");
-    hide("usePushButton");
-    hide("submitButton");
-
     if (value("loadCounter") > pollingIntervals.length - 1) {
       refreshTime = pollingIntervals[pollingIntervals.length - 1];
     } else {
@@ -294,10 +270,6 @@ function initPrivacyIDEA() {
     }, refreshTime);
   }
 
-  if (getContent("privacyidea-hide-alternate") === "true") {
-    hide("AlternateLoginOptions");
-  }
-
   addClickListener("useWebAuthnButton", doWebAuthn);
   addClickListener("usePushButton", () => changeMode("push"));
   addClickListener("useOTPButton", () => changeMode("otp"));
-- 
GitLab