Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve TOTP field #2795

Merged
merged 11 commits into from
Nov 10, 2023
45 changes: 32 additions & 13 deletions login.lp
Original file line number Diff line number Diff line change
Expand Up @@ -37,32 +37,51 @@ mg.include('scripts/pi-hole/lua/header.lp','r')
</div>
</div>
<div class="form-group has-error login-box-msg" id="error-label" style="display: none;">
<label class="control-label"><i class="fa fa-times-circle"></i> Wrong password!</label>
<label class="control-label"><i class="fa fa-times-circle"></i> <span id="error-message"></span><br><span id="error-hint" style="display: none;"></span></label>
</div>

<form id="loginform">
<div class="login-options has-feedback" id="pw-field">
<input type="text" id="username" value="pi.hole" autocomplete="username" hidden>
<div class="pwd-field form-group">
<div class="input-group pwd-field form-group">
<!-- hidden username input field to help password managers to autfill the password -->
<input type="password" id="loginpw" class="form-control" placeholder="Password" value="" spellcheck="false" autocomplete="current-password" autofocus>
<span class="fa fa-key pwd-field form-control-feedback"></span>
</div>
<div class="form-group hidden" id="totp_input">
<input type="text" id="totp" size="6" maxlen="6" class="form-control totp_token" placeholder="" value="" spellcheck="false" autocomplete="current-password" autofocus>
<span class="fa-solid fa-clock-rotate-left pwd-field form-control-feedback"></span>
<input type="password" class="form-control" placeholder="Password" value="" spellcheck="false" autocomplete="current-password" id="current-password" autofocus required>
<span class="input-group-btn">
<button class="btn btn-default" id="toggle-password" type="button" title="Show password as plain text. Warning: this will display your password on the screen.">
<span class="fa fa-fw fa-eye field-icon"></span>
</button>
</span>
</div>
</div>
<!--
<div class="form-group" title="Pi-hole has to set a cookie for the login session to be successful. The cookie will not contain your password nor is it used anywhere outside of you local Pi-hole.">
<input type="checkbox" id="logincookie" checked>
<label for="logincookie">Keep me logged in (uses cookie)</label>
</div> -->
<div class="form-group has-feedback hidden" id="totp_input">
<input type="text" id="totp" size="6" maxlen="6" class="form-control totp_token" placeholder="123456" value="" spellcheck="false" autofocus autocomplete="off">
<span class="fa fa-clock-rotate-left pwd-field form-control-feedback"></span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary form-control"><i class="fas fa-sign-in-alt"></i>&nbsp;&nbsp;&nbsp;Log in (uses cookie)</button>
</div>
</form>
<br>
<div class="row">
<div class="col-xs-12">
<div class="box box-danger hidden" id="invalid2fa-box">
<div class="box-header with-border pointer no-user-select" data-widget="collapse">
<h3 class="box-title">Wrong 2FA token</h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool">
<i class="fa fa-minus"></i>
</button>
</div>
</div>
<div class="box-body">
<p>Your Pi-hole has two-factor authentication enabled. You have to
enter a valid <a href="https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm" target="_blank">TOTP</a>
token in addition to your password. You see this message because your
token was incorrect or has already expired.</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box box-info collapsed-box" id="forgot-pw-box">
Expand Down
116 changes: 91 additions & 25 deletions scripts/pi-hole/js/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,42 +37,84 @@ function redirect() {
window.location.replace(target);
}

function wrongPassword(isError = false, isSuccess = false) {
function wrongPassword(isError = false, isSuccess = false, data = null) {
if (isError) {
$("#pw-field").addClass("has-error");
let isErrorResponse = false,
isInvalidTOTP = false;

// Reset hint and error message
$("#error-message").text("");
$("#error-hint").hide();
$("#error-hint").text("");
if (data !== null && "error" in data.responseJSON && "message" in data.responseJSON.error) {
// This is an error, highlight both the password and the TOTP field
isErrorResponse = true;
// Check if the error is caused by an invalid TOTP token
isInvalidTOTP = data.responseJSON.error.message === "Invalid 2FA token";
$("#error-message").text(data.responseJSON.error.message);
if ("hint" in data.responseJSON.error && data.responseJSON.error.hint !== null) {
$("#error-hint").text(data.responseJSON.error.hint);
$("#error-hint").show();
}
} else {
$("#error-message").text("Wrong password!");
}

$("#error-label").show();
$("#forgot-pw-box").removeClass("box-info").removeClass("collapsed-box").addClass("box-danger");
$("#forgot-pw-box .box-body").show();
$("#forgot-pw-toggle-icon").removeClass("fa-plus").addClass("fa-minus");

// Always highlight the TOTP field on error
if (isErrorResponse) $("#totp_input").addClass("has-error");

// Only show the invalid 2FA box if the error is caused by an invalid TOTP
// token
if (isInvalidTOTP) $("#invalid2fa-box").removeClass("hidden");

// Only highlight the password field if the error is NOT caused by an
// invalid TOTP token
if (!isInvalidTOTP) $("#pw-field").addClass("has-error");

// Only show the forgot password box if the error is NOT caused by an
// invalid TOTP token and this is no error response (= password is wrong)
if (!isErrorResponse && !isInvalidTOTP) {
$("#forgot-pw-box")
.removeClass("box-info")
.removeClass("collapsed-box")
.addClass("box-danger");
$("#forgot-pw-box .box-body").show();
$("#forgot-pw-toggle-icon").removeClass("fa-plus").addClass("fa-minus");
}

return;
} else if (isSuccess) {
$("#pw-field").addClass("has-success");
$("#totp_input").addClass("has-success");
} else {
$("#pw-field").removeClass("has-error");
$("#totp_input").removeClass("has-error");
$("#error-label").hide();
$("#forgot-pw-box").addClass("box-info").addClass("collapsed-box").removeClass("box-danger");
$("#forgot-pw-box .box-body").hide();
$("#forgot-pw-toggle-icon").removeClass("fa-minus").addClass("fa-plus");
}

$("#invalid2fa-box").addClass("hidden");
$("#forgot-pw-box").addClass("box-info").addClass("collapsed-box").removeClass("box-danger");
$("#forgot-pw-box .box-body").hide();
$("#forgot-pw-toggle-icon").removeClass("fa-minus").addClass("fa-plus");
}

function doLogin(password) {
wrongPassword(false, false);
wrongPassword(false, false, null);
$.ajax({
url: "/api/auth",
method: "POST",
dataType: "json",
processData: false,
data: JSON.stringify({ password: password, totp: parseInt($("#totp").val(), 10) }),
})
.done(function () {
wrongPassword(false, true);
.done(function (data) {
wrongPassword(false, true, data);
redirect();
})
.fail(function (data) {
if (data.status === 401) {
// Login failed, show error message
wrongPassword(true, false);
}
wrongPassword(true, false, data);
});
}

Expand All @@ -87,19 +129,38 @@ $("#loginform").submit(function (e) {
return;
}*/

doLogin($("#loginpw").val());
doLogin($("#current-password").val());
});

// Trigger keyup event when pasting into the TOTP code input field
$("#totp").on("paste", function (e) {
$(e.target).keyup();
// Submit form when TOTP code is entered and password is already filled
$("#totp").on("input", function () {
const code = $(this).val();
const password = $("#current-password").val();
if (code.length === 6 && password.length > 0) {
$("#loginform").submit();
}
});

$("#totp").on("keyup", function () {
var code = $(this).val();
if (code.length === 6) {
$("#loginform").submit();
// Toggle password visibility button
$("#toggle-password").on("click", function () {
// Toggle font-awesome classes to change the svg icon on the button
$("svg", this).toggleClass("fa-eye fa-eye-slash");

// Password field
var $pwd = $("#current-password");
if ($pwd.attr("type") === "password") {
$pwd.attr("type", "text");
$pwd.attr("title", "Hide password");
} else {
$pwd.attr("type", "password");
$pwd.attr(
"title",
"Show password as plain text. Warning: this will display your password on the screen"
);
}

// move the focus to password field after the click
$pwd.trigger("focus");
});

function showDNSfailure() {
Expand All @@ -118,8 +179,13 @@ $(function () {
})
.fail(function (xhr) {
const session = xhr.responseJSON.session;
// If TOPT is enabled, show the input field
if (session.totp === true) $("#totp_input").removeClass("hidden");
// If TOPT is enabled, show the input field and add the required attribute
if (session.totp === true) {
$("#totp_input").removeClass("hidden");
$("#totp").attr("required", "required");
$("#totp-forgotten-title").removeClass("hidden");
$("#totp-forgotten-body").removeClass("hidden");
}
});

// Get information about HTTPS port and DNS status
Expand Down