diff --git a/dictionaries/privacyidea.definition.json b/dictionaries/privacyidea.definition.json index df58b6f5cc0baea5780351678b1941db17b573a0..a762ff495955526ff0b62ca16d84667f61e337be 100644 --- a/dictionaries/privacyidea.definition.json +++ b/dictionaries/privacyidea.definition.json @@ -35,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": { @@ -44,31 +44,49 @@ "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": { diff --git a/dictionaries/privacyidea.translation.json b/dictionaries/privacyidea.translation.json index cebf65a2aaf45f78b43a5073376ebb368cd373b7..a1d528c67c09c136205230bd81695751bb44b563 100644 --- a/dictionaries/privacyidea.translation.json +++ b/dictionaries/privacyidea.translation.json @@ -54,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." @@ -69,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" diff --git a/templates/LoginForm.php b/templates/LoginForm.php index 81dd63ef29200fa83776b3d26332172163fe1e22..988be5682e64509895a8e7caa52a71998473764d 100644 --- a/templates/LoginForm.php +++ b/templates/LoginForm.php @@ -93,14 +93,15 @@ if (null !== $this->data['errorCode']) { <?php } else { ?> - <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 } @@ -137,7 +138,7 @@ if (null !== $this->data['errorCode']) { <input id="password" name="password" tabindex="1" type="password" value="" class="text" placeholder="<?php echo htmlspecialchars($passHint, ENT_QUOTES); ?>"/> - <strong id="message"><?php + <p id="message" role="alert"><?php $messageOverride = $this->data['messageOverride'] ?? null; if (null === $messageOverride || is_string($messageOverride)) { echo htmlspecialchars( @@ -147,18 +148,20 @@ if (null !== $this->data['errorCode']) { } elseif (is_callable($messageOverride)) { echo call_user_func($messageOverride, $this->data['message'] ?? ''); } - ?></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> + ?></p> + + <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> + + <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" @@ -193,7 +196,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> @@ -297,8 +300,20 @@ if (!empty($this->data['links'])) { <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..4d55780b321f4402f439cb126a5ebbce2d4821dc 100644 --- a/www/js/loginform.js +++ b/www/js/loginform.js @@ -24,109 +24,92 @@ 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"); -} - -if (!booleanValue("pushAvailable")) { - disable("usePushButton"); +function disable(id) { + const element = getElement(id); + if (element != null) { + element.disabled = true; + element.classList.add("disabled"); + } } -if (!booleanValue("otpAvailable")) { - disable("useOTPButton"); +function enable(id) { + const element = getElement(id); + if (element != null) { + element.disabled = false; + element.classList.remove("disabled"); + } } -if ( - !booleanValue("pushAvailable") && - value("webAuthnSignRequest") === "" && - value("u2fSignRequest") === "" -) { - disable("alternateTokenDiv"); +function changeMode(newMode) { + set("mode", newMode); + set("modeChanged", "true"); + document.forms["piLoginForm"].submit(); } -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!" ); @@ -135,7 +118,8 @@ function doWebAuthn() { } if (!window.pi_webauthn) { - alert(t("alert_webauthn_unavailable")); + enable("useWebAuthnButton"); + setMessage(t("webauthn_library_unavailable")); changeMode("otp"); return; } @@ -156,15 +140,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,7 +161,7 @@ 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"); return; @@ -184,7 +170,7 @@ function doU2F() { const requestStr = value("u2fSignRequest"); if (requestStr === null) { - alert(t("alert_u2f_unavailable")); + setMessage(t("u2f_unavailable")); changeMode("otp"); return; } @@ -194,7 +180,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 +207,93 @@ 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"); - }); - getElement("useU2FButton").addEventListener("click", doU2F); -}); +function initPrivacyIDEA() { + const step = getContent("privacyidea-step"); + + 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 (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 { + refreshTime = pollingIntervals[Number(value("loadCounter") - 1)]; + } + + refreshTime *= 1000; + setTimeout(() => { + document.forms["piLoginForm"].submit(); + }, refreshTime); + } + + if (getContent("privacyidea-hide-alternate") === "true") { + hide("AlternateLoginOptions"); + } + + addClickListener("useWebAuthnButton", doWebAuthn); + addClickListener("usePushButton", () => changeMode("push")); + addClickListener("useOTPButton", () => changeMode("otp")); + addClickListener("useU2FButton", doU2F); +} + +document.addEventListener("DOMContentLoaded", initPrivacyIDEA);