sftpgo/templates/webclient/mfa.html

691 lines
28 KiB
HTML
Raw Normal View History

<!--
Copyright (C) 2023 Nicola Murino
This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
https://keenthemes.com/products/templates-mega-bundle
KeenThemes HTML/CSS/JS components are allowed for use only within the
SFTPGo product and restricted to be used in a resealable HTML template
that can compete with KeenThemes products anyhow.
This WebUI is allowed for use only within the SFTPGo product and
therefore cannot be used in derivative works/products without an
explicit grant from the SFTPGo Team (support@sftpgo.com).
-->
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{- define "page_body"}}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 class="card-title text-primary">Two-factor authentication using Authenticator apps</h3>
</div>
<div class="card-body">
<div class="notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5">
<i class="ki-duotone ki-shield-tick fs-2tx text-primary me-4">
<span class="path1"></span>
<span class="path2"></span>
</i>
<div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap">
<div class="mb-3 mb-md-0 fw-semibold">
<h4 class="text-gray-900 fw-bold">
{{- if .TOTPConfig.Enabled}}
Two-factor authentication is enabled {{- if gt (len .TOTPConfigs) 1 }}. Configuration: "{{$.TOTPConfig.ConfigName}}" {{- end}}
{{- else}}
Secure Your Account
{{- end}}
</h4>
<div class="fs-6 text-gray-800 pe-7">
Two-factor authentication adds an extra layer of security to your account. To log in you'll need
to provide an additional authentication code.
</div>
</div>
{{- if .TOTPConfig.Enabled}}
<button type="button" id="disable_btn" class="btn btn-danger ms-4 px-6 align-self-center text-nowrap">
<span class="indicator-label">
Disable
</span>
<span class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
{{- end}}
</div>
</div>
{{- template "errmsg" ""}}
<div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Configuration</label>
<div class="col-md-9">
<select id="id_config" name="config_name" onchange="onConfigChanged();" class="form-select" data-control="select2" data-hide-search="true">
<option value="">None</option>
{{range .TOTPConfigs}}
<option value="{{.}}" {{if eq . $.TOTPConfig.ConfigName}}selected{{end}}>{{.}}</option>
{{end}}
</select>
</div>
</div>
<div class="form-group row mt-10">
<label class="col-md-3 col-form-label required">
Require 2FA for
</label>
<div class="col-md-9">
<select id="id_protocols" name="multi_factor_protocols" class="form-select" data-control="select2" data-hide-search="true" data-close-on-select="false" multiple="multiple">
<option></option>
{{range $protocol := .Protocols}}
<option value="{{$protocol}}" {{range $p :=$.TOTPConfig.Protocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}
</option>
{{end}}
</select>
</div>
</div>
<div class="d-flex justify-content-end mt-15">
{{- if .TOTPConfig.Enabled }}
<button type="button" id="generate_secret_btn" class="btn btn-light-primary px-10 me-10 d-none">
<span class="indicator-label">
Generate new secret
</span>
<span class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
{{- end}}
<button type="button" id="save_btn" class="btn btn-primary px-10 d-none">
<span id="save_label" class="indicator-label"></span>
<span class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
</div>
</div>
</div>
{{- if .TOTPConfig.Enabled}}
<div class="notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 my-10">
<i class="ki-duotone ki-shield-tick fs-2tx text-primary me-4">
<span class="path1"></span>
<span class="path2"></span>
</i>
<div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap">
<div class="mb-3 mb-md-0 fw-semibold">
<h4 class="text-gray-900 fw-bold">
Recovery codes
</h4>
<div class="fs-6 text-gray-800">
<p>Recovery codes are a set of one time use codes that can be used in place of the authentication code to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate two-factor configuration.</p>
<p>To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p>
<p>If you generate new recovery codes, you automatically invalidate old ones.</p>
</div>
<div class="d-flex justify-content-center mt-10">
<button type="button" id="generate_recovery_code_btn" class="btn btn-primary px-10 me-10">
<span class="indicator-label">
Generate
</span>
<span class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
<button type="button" id="view_recovery_code_btn" class="btn btn-primary px-10">
<span id="save_label" class="indicator-label">View</span>
<span class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
</div>
</div>
</div>
</div>
{{- end}}
{{- end}}
{{- define "additionalnavitems"}}
<div class="d-flex align-items-center ms-2 ms-lg-3" id="kt_header_user_menu_toggle">
<div class="btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px" data-bs-toggle="modal" data-bs-target="#info_modal">
<i class="ki-duotone ki-information-2 fs-2">
<i class="path1"></i>
<i class="path2"></i>
<i class="path3"></i>
</i>
</div>
</div>
{{- end}}
{{- define "modals"}}
<div class="modal fade" id="recovery_codes_modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 id="idRecoveryCodesTitle" class="modal-title"></h3>
<div class="btn btn-icon btn-sm btn-active-color-primary ms-2" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div>
</div>
<div class="modal-body">
<div id="idRecoveryCodesList" class="d-flex flex-column">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="button" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="info_modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Learn about two-factor authentication</h3>
<div class="btn btn-icon btn-sm btn-active-color-primary ms-2" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div>
</div>
<div class="modal-body fw-semibold fs-6">
<p>SSH protocol (SFTP/SCP/SSH commands) will ask for the passcode if the client uses keyboard interactive
authentication.</p>
<p>HTTP protocol means Web UI and REST APIs. Web UI will ask for the passcode using a specific page. For REST API
you have to add the passcode using an HTTP header.</p>
<p>FTP has no standard way to support two factor authentication, if you enable the FTP support, you have to add the
TOTP passcode after the password. For example if your password is "password" and your one time passcode is
"123456" you have to use "password123456" as password.</p>
<p>WebDAV is not supported since each single request must be authenticated and a passcode cannot be reused.</p>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="button" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="qrcode_modal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Set up two-factor authentication</h3>
<div class="btn btn-icon btn-sm btn-active-color-primary ms-2" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div>
</div>
<div class="modal-body scroll-y pt-10 pb-15 px-lg-17">
<div class="text-gray-700 fw-semibold fs-6 mb-10">
Use your preferred Authenticator App (e.g. Microsoft Authenticator, Google Authenticator, Authy, 1Password etc. ) to scan the QR code. It will generate an authentication code for you to enter below.
</div>
<div class="pt-5 text-center">
<img id="id_qr_code" src="data:image/gif;base64, R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="QR code" class="mw-150px"/>
</div>
<div class="notice d-flex bg-light-warning rounded border-warning border border-dashed my-10 p-6">
<i class="ki-duotone ki-information fs-2tx text-warning me-4">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
</i>
<div class="d-flex flex-stack flex-grow-1">
<div class="fw-semibold">
<div class="fs-6 text-gray-800">If you have trouble using the QR code, select manual entry on your app, and enter the code:
<span id="id_secret" class="fw-bold text-gray-900 pt-2"></span>
</div>
</div>
</div>
</div>
<div id="errorModalMsg" class="d-none alert alert-dismissible bg-light-warning d-flex align-items-center p-5 mb-10">
<i class="ki-duotone ki-information-5 fs-3x text-warning me-5"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i>
<div class="text-gray-700 fw-bold fs-5 d-flex flex-column pe-0 pe-sm-10">
<span id="errorModalTxt"></span>
</div>
<button id="id_dismiss_error_modal_msg" type="button" class="position-absolute position-sm-relative m-2 m-sm-0 top-0 end-0 btn btn-icon btn-sm btn-active-light-primary ms-sm-auto">
<i class="ki-duotone ki-cross fs-2x text-primary"><span class="path1"></span><span class="path2"></span></i>
</button>
</div>
<div class="fv-row">
<input type="text" id="id_passcode" name="passcode" class="form-control form-control-lg form-control-solid" placeholder="Enter authentication code" spellcheck="false" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary ms-6" id="passcode_btn">
<span class="indicator-label">
Submit
</span>
<span class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
</div>
</div>
</div>
</div>
{{- end}}
{{- define "extra_js"}}
<script type="text/javascript">
const qrModal = new bootstrap.Modal('#qrcode_modal');
const recCodesModal = new bootstrap.Modal('#recovery_codes_modal');
function onConfigChanged() {
let selectedConfig = $('#id_config option:selected').val();
if (selectedConfig == ""){
$('#save_btn').addClass("d-none");
$('#generate_secret_btn').addClass("d-none");
} else {
//{{- if .TOTPConfig.Enabled }}
if (selectedConfig == "{{.TOTPConfig.ConfigName}}"){
$('#save_label').text("Save");
} else {
$('#save_label').text("Enable");
}
$('#save_btn').removeClass("d-none");
$('#generate_secret_btn').removeClass("d-none");
//{{- else}}
$('#save_btn').removeClass("d-none");
$('#save_label').text("Enable");
$('#generate_secret_btn').addClass("d-none");
//{{- end}}
}
}
function generateRecoveryCodes() {
el = document.querySelector('#generate_recovery_code_btn');
el.setAttribute('data-kt-indicator', 'on');
el.disabled = true;
axios.post('{{.RecCodesURL}}', null, {
timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 200;
}
}).then(function (response){
el.removeAttribute('data-kt-indicator');
el.disabled = false;
$('#idRecoveryCodesTitle').text("New recovery codes");
let recList = $('#idRecoveryCodesList');
recList.empty();
$.each(response.data, function(key, item) {
itemCode = escapeHTML(item);
recList.append(`<li class="d-flex align-items-center py-2 fw-semibold fs-5 text-gray-800"><span class="bullet bullet-dot me-5"></span>${itemCode}</li>`);
});
recCodesModal.show();
}).catch(function (error){
el.removeAttribute('data-kt-indicator');
el.disabled = false;
let errorMessage = "Failed to generate new recovery codes";
if (error && error.response) {
if (error.response.data.message) {
errorMessage = error.response.data.message;
}
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
}
}
ModalAlert.fire({
text: errorMessage,
icon: "warning",
confirmButtonText: "OK",
customClass: {
confirmButton: "btn btn-danger"
}
});
});
}
function viewRecoveryCodes() {
el = document.querySelector('#view_recovery_code_btn');
el.setAttribute('data-kt-indicator', 'on');
el.disabled = true;
axios.get('{{.RecCodesURL}}',{
timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 200;
}
}).then(function (response){
el.removeAttribute('data-kt-indicator');
el.disabled = false;
$('#idRecoveryCodesTitle').text("Recovery codes");
let recList = $('#idRecoveryCodesList');
recList.empty();
$.each(response.data, function(key, item) {
itemCode = escapeHTML(item.code);
let txtStyleClass = "";
if (item.used) {
txtStyleClass = "line-through";
}
recList.append(`<li class="d-flex align-items-center py-2 fw-semibold fs-5 text-gray-800 ${txtStyleClass}"><span class="bullet bullet-dot me-5"></span>${itemCode}</li>`);
});
recCodesModal.show();
}).catch(function (error){
el.removeAttribute('data-kt-indicator');
el.disabled = false;
let errorMessage = "Failed to get your recovery codes";
if (error && error.response) {
if (error.response.data.message) {
errorMessage = error.response.data.message;
}
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
}
}
ModalAlert.fire({
text: errorMessage,
icon: "warning",
confirmButtonText: "OK",
customClass: {
confirmButton: "btn btn-danger"
}
});
});
}
function disableConfig() {
ModalAlert.fire({
text: `Do you want to disable two-factor authentication?`,
icon: "warning",
confirmButtonText: "Confirm",
cancelButtonText: 'Cancel',
customClass: {
confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary'
}
}).then((result) => {
if (result.isConfirmed){
doSaveConfig(document.querySelector('#disable_btn'), null, false, true);
}
});
}
function validatePasscode() {
el = document.querySelector('#passcode_btn');
let errDivEl = $('#errorModalMsg');
let errTxtEl = $('#errorModalTxt');
let errorMessage = "Failed to validate the provided authentication code";
let passcode = $('#id_passcode').val();
errDivEl.addClass("d-none");
if (passcode == "") {
errTxtEl.text("The authentication code is required");
errDivEl.removeClass("d-none");
return;
}
el.setAttribute('data-kt-indicator', 'on');
el.disabled = true;
axios.post("{{.ValidateTOTPURL}}", {
passcode: passcode,
config_name: $('#id_config option:selected').val(),
secret: $('#id_secret').text()
}, {
headers: {
timeout: 15000,
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 200;
}
}).then(function (response){
el.removeAttribute('data-kt-indicator');
el.disabled = false;
if (!response.data.message) {
errTxtEl.text(errorMessage);
errDivEl.removeClass("d-none");
return;
}
qrModal.hide();
doSaveConfig(null, null, true, false);
}).catch(function (error){
el.removeAttribute('data-kt-indicator');
el.disabled = false;
if (error && error.response) {
if (error.response.data.message) {
errorMessage = error.response.data.message;
}
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
}
}
errTxtEl.text(errorMessage);
errDivEl.removeClass("d-none");
});
}
function confirmGenerateSecret() {
ModalAlert.fire({
text: `Do you want to generate a new secret and invalidate the previous one? Any registered Authenticator App will stop working`,
icon: "warning",
confirmButtonText: "Confirm",
cancelButtonText: 'Cancel',
customClass: {
confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary'
}
}).then((result) => {
if (result.isConfirmed){
el = document.querySelector('#generate_secret_btn');
configName = $('#id_config option:selected').val();
generateSecret(el,configName);
}
});
}
function generateSecret(el, configName) {
if (!el || !configName){
confirmGenerateSecret();
return;
}
let errDivEl = $('#errorMsg');
let errTxtEl = $('#errorTxt');
if ($('#id_protocols').find('option:selected').length == 0){
errTxtEl.text("Please select at least a protocol");
errDivEl.removeClass("d-none");
return;
}
let errorMessage = "Failed to generate authentication secret";
$('#id_secret').text("");
errDivEl.addClass("d-none");
el.setAttribute('data-kt-indicator', 'on');
el.disabled = true;
axios.post("{{.GenerateTOTPURL}}", {
config_name: configName
}, {
headers: {
timeout: 15000,
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 200;
}
}).then(function (response){
el.removeAttribute('data-kt-indicator');
el.disabled = false;
if (!response.data.secret) {
errTxtEl.text(errorMessage);
errDivEl.removeClass("d-none");
return;
}
$('#id_secret').text(response.data.secret);
$('#id_qr_code').attr('src', 'data:image/png;base64, '+ response.data.qr_code);
$('#errorModalMsg').addClass("d-none");
$('#id_passcode').val("");
qrModal.show();
}).catch(function (error){
el.removeAttribute('data-kt-indicator');
el.disabled = false;
if (error && error.response) {
if (error.response.data.message) {
errorMessage = error.response.data.message;
}
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
}
}
errTxtEl.text(errorMessage);
errDivEl.removeClass("d-none");
});
}
function doSaveConfig(el, configName, full, disabled) {
if (!el){
el = document.querySelector('#save_btn');
}
if (!configName) {
configName = $('#id_config option:selected').val();
}
let errDivEl = $('#errorMsg');
let errTxtEl = $('#errorTxt');
let errorMessage = "Failed to save configuration";
let protocolsArray = [];
$('#id_protocols').find('option:selected').each(function(){
protocolsArray.push($(this).val());
});
if (protocolsArray.length == 0){
errTxtEl.text("Please select at least a protocol");
errDivEl.removeClass("d-none");
return;
}
let postData = {
protocols: protocolsArray
}
if (full){
postData.config_name = configName;
postData.secret = {
status: "Plain",
payload: $('#id_secret').text()
}
}
if (disabled){
postData.enabled = false;
} else {
postData.enabled = true;
}
errDivEl.addClass("d-none");
el.setAttribute('data-kt-indicator', 'on');
el.disabled = true;
axios.post("{{.SaveTOTPURL}}", postData, {
headers: {
timeout: 15000,
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 200;
}
}).then(function (response){
el.removeAttribute('data-kt-indicator');
el.disabled = false;
if (!response.data.message) {
errTxtEl.text(errorMessage);
errDivEl.removeClass("d-none");
return;
}
ModalAlert.fire({
text: `Configuration saved`,
icon: "success",
confirmButtonText: "OK",
customClass: {
confirmButton: 'btn btn-primary'
}
}).then((result) => {
if (result.isConfirmed){
location.reload();
}
});
}).catch(function (error) {
el.removeAttribute('data-kt-indicator');
el.disabled = false;
if (error && error.response) {
if (error.response.data.message) {
errorMessage = error.response.data.message;
}
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
}
}
errTxtEl.text(errorMessage);
errDivEl.removeClass("d-none");
});
}
function saveConfig() {
let selectedConfig = $('#id_config option:selected').val();
let saveBtn = document.querySelector('#save_btn');
if (selectedConfig == "{{.TOTPConfig.ConfigName}}"){
doSaveConfig(saveBtn, selectedConfig, false, false);
return;
}
generateSecret(saveBtn, selectedConfig);
}
KTUtil.onDOMContentLoaded(function () {
onConfigChanged();
var dismissErrorModalBtn = $('#id_dismiss_error_modal_msg');
if (dismissErrorModalBtn){
dismissErrorModalBtn.on("click",function(){
$('#errorModalMsg').addClass("d-none");
});
}
var disableBtn = $('#disable_btn');
if (disableBtn){
disableBtn.on("click", function(){
disableConfig();
});
}
var generateSecretBtn = $('#generate_secret_btn');
if (generateSecretBtn){
generateSecretBtn.on("click", function(){
generateSecret();
});
}
var saveBtn = $('#save_btn');
if (saveBtn){
saveBtn.on("click", function(){
saveConfig();
});
}
var generateRecoveryCodeBtn = $('#generate_recovery_code_btn');
if (generateRecoveryCodeBtn){
generateRecoveryCodeBtn.on("click", function(){
generateRecoveryCodes();
});
}
var viewRecoveryCodesBtn = $('#view_recovery_code_btn');
if (viewRecoveryCodesBtn){
viewRecoveryCodesBtn.on("click", function(){
viewRecoveryCodes();
});
}
var passcodeBtn = $('#passcode_btn');
if (passcodeBtn){
passcodeBtn.on("click", function() {
validatePasscode();
});
}
});
</script>
{{- end}}