Email based 2FA almost completed.
This commit is contained in:
parent
cee4e44c18
commit
59f1463a3f
16
meshmail.js
16
meshmail.js
|
@ -43,11 +43,13 @@ module.exports.CreateMeshMail = function (parent) {
|
||||||
// Set default mail templates
|
// Set default mail templates
|
||||||
// You can override these by placing a file with the same name in "meshcentral-data/mail"
|
// You can override these by placing a file with the same name in "meshcentral-data/mail"
|
||||||
// If the server hash many domains, just add the domainid to the file like this: 'account-check-customer1.html', 'mesh-invite-customer1.txt'.
|
// If the server hash many domains, just add the domainid to the file like this: 'account-check-customer1.html', 'mesh-invite-customer1.txt'.
|
||||||
|
obj.mailTemplates['account-login.html'] = '<title>[[[SERVERNAME]]] - Account Login</title>\r\n<div style="font-family:Arial,Helvetica,sans-serif"><table style="background-color:#003366;color:lightgray;width:100%" cellpadding=8><tr><td><b style="font-size:20px;font-family:Arial,Helvetica,sans-serif">[[[SERVERNAME]]] - Account Login</b></td></tr></table><p>Your login token is: [[[TOKEN]]]</p><p>This token can only be used once and is valid for 5 minutes.</p></div>';
|
||||||
obj.mailTemplates['account-invite.html'] = '<title>[[[SERVERNAME]]] - Account Invitation</title>\r\n<div style="font-family:Arial,Helvetica,sans-serif"><table style="background-color:#003366;color:lightgray;width:100%" cellpadding=8><tr><td><b style="font-size:20px;font-family:Arial,Helvetica,sans-serif">[[[SERVERNAME]]] - Account Invitation</b></td></tr></table><p>An account was created for you on server <a href="[[[SERVERURL]]]">[[[SERVERNAME]]]</a>, you can access it now with:</p><p> Username: <b>[[[ACCOUNTNAME]]]</b><br /> Password: <b>[[[PASSWORD]]]</b></p>Best regards,<br>[[[USERNAME]]]<br></div>';
|
obj.mailTemplates['account-invite.html'] = '<title>[[[SERVERNAME]]] - Account Invitation</title>\r\n<div style="font-family:Arial,Helvetica,sans-serif"><table style="background-color:#003366;color:lightgray;width:100%" cellpadding=8><tr><td><b style="font-size:20px;font-family:Arial,Helvetica,sans-serif">[[[SERVERNAME]]] - Account Invitation</b></td></tr></table><p>An account was created for you on server <a href="[[[SERVERURL]]]">[[[SERVERNAME]]]</a>, you can access it now with:</p><p> Username: <b>[[[ACCOUNTNAME]]]</b><br /> Password: <b>[[[PASSWORD]]]</b></p>Best regards,<br>[[[USERNAME]]]<br></div>';
|
||||||
obj.mailTemplates['account-check.html'] = '<title>[[[SERVERNAME]]] - Email Verification</title>\r\n<div style="font-family:Arial,Helvetica,sans-serif"><table style="background-color:#003366;color:lightgray;width:100%" cellpadding=8><tr><td><b style="font-size:20px;font-family:Arial,Helvetica,sans-serif">[[[SERVERNAME]]] - Verification</b></td></tr></table><p>Hi [[[USERNAME]]], <a href="[[[SERVERURL]]]">[[[SERVERNAME]]]</a> is requesting email verification, click on the following link to complete the process.</p><p style="margin-left:30px"><a href="[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]]">Click here to verify your e-mail address.</a></p>If you did not initiate this request, please ignore this mail.</div>';
|
obj.mailTemplates['account-check.html'] = '<title>[[[SERVERNAME]]] - Email Verification</title>\r\n<div style="font-family:Arial,Helvetica,sans-serif"><table style="background-color:#003366;color:lightgray;width:100%" cellpadding=8><tr><td><b style="font-size:20px;font-family:Arial,Helvetica,sans-serif">[[[SERVERNAME]]] - Verification</b></td></tr></table><p>Hi [[[USERNAME]]], <a href="[[[SERVERURL]]]">[[[SERVERNAME]]]</a> is requesting email verification, click on the following link to complete the process.</p><p style="margin-left:30px"><a href="[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]]">Click here to verify your e-mail address.</a></p>If you did not initiate this request, please ignore this mail.</div>';
|
||||||
obj.mailTemplates['account-reset.html'] = '<title>[[[SERVERNAME]]] - Account Reset</title>\r\n<div style="font-family:Arial,Helvetica,sans-serif"><table style="background-color:#003366;color:lightgray;width:100%" cellpadding=8><tr><td><b style="font-size:20px;font-family:Arial,Helvetica,sans-serif">[[[SERVERNAME]]] - Verification</b></td></tr></table><p>Hi [[[USERNAME]]], <a href="[[[SERVERURL]]]">[[[SERVERNAME]]]</a> is requesting an account password reset, click on the following link to complete the process.</p><p style="margin-left:30px"><a href="[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]]">Click here to reset your account password.</a></p>If you did not initiate this request, please ignore this mail.</div>';
|
obj.mailTemplates['account-reset.html'] = '<title>[[[SERVERNAME]]] - Account Reset</title>\r\n<div style="font-family:Arial,Helvetica,sans-serif"><table style="background-color:#003366;color:lightgray;width:100%" cellpadding=8><tr><td><b style="font-size:20px;font-family:Arial,Helvetica,sans-serif">[[[SERVERNAME]]] - Verification</b></td></tr></table><p>Hi [[[USERNAME]]], <a href="[[[SERVERURL]]]">[[[SERVERNAME]]]</a> is requesting an account password reset, click on the following link to complete the process.</p><p style="margin-left:30px"><a href="[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]]">Click here to reset your account password.</a></p>If you did not initiate this request, please ignore this mail.</div>';
|
||||||
obj.mailTemplates['mesh-invite.html'] = '<title>[[[SERVERNAME]]] - Invitation</title>\r\n<div style="font-family:Arial,Helvetica,sans-serif"><table style="background-color:#003366;color:lightgray;width:100%" cellpadding=8><tr><td><b style="font-size:20px;font-family:Arial,Helvetica,sans-serif">[[[SERVERNAME]]] - Agent Installation</b></td></tr></table>[[[AREA-NAME]]]<p>Hello [[[NAME]]],</p>[[[/AREA-NAME]]]<p>User [[[USERNAME]]] on server <a href="[[[SERVERURL]]]">[[[SERVERNAME]]]</a> is requesting you to install software to start a remote control session.</p>[[[AREA-MSG]]]<p>Message: <b>[[[MSG]]]</b></p>[[[/AREA-MSG]]][[[AREA-WINDOWS]]]<p style="margin-left:30px"><a href="[[[SERVERURL]]]/meshagents?id=3&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]">Click here to download the MeshAgent for Windows.</a></p>[[[/AREA-WINDOWS]]][[[AREA-OSX]]]<p style="margin-left:30px"><a href="[[[SERVERURL]]]/meshagents?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]">Click here to download the MeshAgent for Apple OSX.</a></p>[[[/AREA-OSX]]][[[AREA-LINUX]]]<p>For Linux, cut & paste the following in a terminal to install the agent:<br /><pre style="margin-left:30px">wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'</pre></p>[[[/AREA-LINUX]]][[[AREA-LINK]]]<p>To install the software, <a href="[[[SERVERURL]]][[[LINKURL]]]">click here</a> and follow the instructions.</p>[[[/AREA-LINK]]]<p>If you did not initiate this request, please ignore this mail.</p>Best regards,<br>[[[USERNAME]]]<br></div>';
|
obj.mailTemplates['mesh-invite.html'] = '<title>[[[SERVERNAME]]] - Invitation</title>\r\n<div style="font-family:Arial,Helvetica,sans-serif"><table style="background-color:#003366;color:lightgray;width:100%" cellpadding=8><tr><td><b style="font-size:20px;font-family:Arial,Helvetica,sans-serif">[[[SERVERNAME]]] - Agent Installation</b></td></tr></table>[[[AREA-NAME]]]<p>Hello [[[NAME]]],</p>[[[/AREA-NAME]]]<p>User [[[USERNAME]]] on server <a href="[[[SERVERURL]]]">[[[SERVERNAME]]]</a> is requesting you to install software to start a remote control session.</p>[[[AREA-MSG]]]<p>Message: <b>[[[MSG]]]</b></p>[[[/AREA-MSG]]][[[AREA-WINDOWS]]]<p style="margin-left:30px"><a href="[[[SERVERURL]]]/meshagents?id=3&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]">Click here to download the MeshAgent for Windows.</a></p>[[[/AREA-WINDOWS]]][[[AREA-OSX]]]<p style="margin-left:30px"><a href="[[[SERVERURL]]]/meshagents?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]">Click here to download the MeshAgent for Apple OSX.</a></p>[[[/AREA-OSX]]][[[AREA-LINUX]]]<p>For Linux, cut & paste the following in a terminal to install the agent:<br /><pre style="margin-left:30px">wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'</pre></p>[[[/AREA-LINUX]]][[[AREA-LINK]]]<p>To install the software, <a href="[[[SERVERURL]]][[[LINKURL]]]">click here</a> and follow the instructions.</p>[[[/AREA-LINK]]]<p>If you did not initiate this request, please ignore this mail.</p>Best regards,<br>[[[USERNAME]]]<br></div>';
|
||||||
|
|
||||||
|
obj.mailTemplates['account-login.txt'] = '[[[SERVERNAME]]] - Account Login\r\nYour login token is: [[[TOKEN]]]\r\n\r\nThis token can only be used once and is valid for 5 minutes.';
|
||||||
obj.mailTemplates['account-invite.txt'] = '[[[SERVERNAME]]] - Account Invitation\r\nAn account was created for you on server [[[SERVERNAME]]] ([[[SERVERURL]]]/), you can access it now with username \"[[[ACCOUNTNAME]]]\" and password \"[[[PASSWORD]]]\".\r\n\r\nBest regards,\r\n[[[USERNAME]]]';
|
obj.mailTemplates['account-invite.txt'] = '[[[SERVERNAME]]] - Account Invitation\r\nAn account was created for you on server [[[SERVERNAME]]] ([[[SERVERURL]]]/), you can access it now with username \"[[[ACCOUNTNAME]]]\" and password \"[[[PASSWORD]]]\".\r\n\r\nBest regards,\r\n[[[USERNAME]]]';
|
||||||
obj.mailTemplates['account-check.txt'] = '[[[SERVERNAME]]] - Email Verification\r\nHi [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]]) is performing an e-mail verification. Nagivate to the following link to complete the process:\r\n\r\n[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]]\r\n\r\nIf you did not initiate this request, please ignore this mail.\r\n';
|
obj.mailTemplates['account-check.txt'] = '[[[SERVERNAME]]] - Email Verification\r\nHi [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]]) is performing an e-mail verification. Nagivate to the following link to complete the process:\r\n\r\n[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]]\r\n\r\nIf you did not initiate this request, please ignore this mail.\r\n';
|
||||||
obj.mailTemplates['account-reset.txt'] = '[[[SERVERNAME]]] - Account Reset\r\nHi [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]]) is requesting an account password reset. Nagivate to the following link to complete the process:\r\n\r\n[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]]\r\n\r\nIf you did not initiate this request, please ignore this mail.';
|
obj.mailTemplates['account-reset.txt'] = '[[[SERVERNAME]]] - Account Reset\r\nHi [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]]) is requesting an account password reset. Nagivate to the following link to complete the process:\r\n\r\n[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]]\r\n\r\nIf you did not initiate this request, please ignore this mail.';
|
||||||
|
@ -133,6 +135,20 @@ module.exports.CreateMeshMail = function (parent) {
|
||||||
sendNextMail();
|
sendNextMail();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Send account login mail / 2 factor token
|
||||||
|
obj.sendAccountLoginMail = function (domain, email, token) {
|
||||||
|
var template = getTemplateEx('account-login', domain);
|
||||||
|
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null) || (parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) return; // If the server name is not set, invitation not possible.
|
||||||
|
|
||||||
|
// Set all the options.
|
||||||
|
var options = { email: email, servername: domain.title ? domain.title : 'MeshCentral', token: token };
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
console.log(options);
|
||||||
|
obj.pendingMails.push({ to: email, from: parent.config.smtp.from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) });
|
||||||
|
sendNextMail();
|
||||||
|
};
|
||||||
|
|
||||||
// Send account invitation mail
|
// Send account invitation mail
|
||||||
obj.sendAccountInviteMail = function (domain, username, accountname, email, password) {
|
obj.sendAccountInviteMail = function (domain, username, accountname, email, password) {
|
||||||
var template = getTemplateEx('account-invite', domain);
|
var template = getTemplateEx('account-invite', domain);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "meshcentral",
|
"name": "meshcentral",
|
||||||
"version": "0.5.0-e",
|
"version": "0.5.0-f",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Remote Management",
|
"Remote Management",
|
||||||
"Intel AMT",
|
"Intel AMT",
|
||||||
|
|
|
@ -507,10 +507,9 @@ module.exports.pluginHandler = function (parent) {
|
||||||
obj.removePlugin = function (id, func) {
|
obj.removePlugin = function (id, func) {
|
||||||
parent.db.getPlugin(id, function (err, docs) {
|
parent.db.getPlugin(id, function (err, docs) {
|
||||||
var plugin = docs[0];
|
var plugin = docs[0];
|
||||||
var rimraf = null;
|
var rimraf = require('rimraf');
|
||||||
try { rimraf = require('rimraf'); } catch (ex) { }
|
|
||||||
let pluginPath = obj.parent.path.join(obj.pluginPath, plugin.shortName);
|
let pluginPath = obj.parent.path.join(obj.pluginPath, plugin.shortName);
|
||||||
if (rimraf) rimraf.sync(pluginPath);
|
rimraf.sync(pluginPath);
|
||||||
parent.db.deletePlugin(id, func);
|
parent.db.deletePlugin(id, func);
|
||||||
delete obj.plugins[plugin.shortName];
|
delete obj.plugins[plugin.shortName];
|
||||||
});
|
});
|
||||||
|
|
|
@ -164,7 +164,10 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan=2>
|
<td colspan=2>
|
||||||
<div style=float:right><input id=tokenOkButton type=submit value="Login" disabled="disabled" /></div>
|
<div style=float:right><input id=tokenOkButton type=submit value="Login" disabled="disabled" /></div>
|
||||||
<div style=float:right><input style="display:none;float:right" id=securityKeyButton type=button value="Use Security Key" onclick="useSecurityKey()" /></div>
|
<div style=float:right>
|
||||||
|
<input style="display:none;float:right" id=securityKeyButton type=button value="Use Security Key" onclick="useSecurityKey()" />
|
||||||
|
<input style="display:none;float:right" id=emailKeyButton type=button value="Email" onclick="useEmailToken()" />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -238,7 +241,7 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id=dialog style="z-index:1000;background-color:#EEE;box-shadow:0px 0px 15px #666;font-family:Arial,Helvetica,sans-serif;border-radius:5px;position:fixed;top:180px;width:400px;display:none">
|
<div id=dialog style="z-index:1000;background-color:#EEE;box-shadow:0px 0px 15px #666;font-family:Arial,Helvetica,sans-serif;border-radius:5px;position:fixed;top:180px;width:300px;display:none">
|
||||||
<div style="width:100%;background-color:#003366;color:#FFF;border-radius:5px 5px 0 0">
|
<div style="width:100%;background-color:#003366;color:#FFF;border-radius:5px 5px 0 0">
|
||||||
<div id=id_dialogclose style=float:right;padding:5px;cursor:pointer onclick=setDialogMode()><b>X</b></div>
|
<div id=id_dialogclose style=float:right;padding:5px;cursor:pointer onclick=setDialogMode()><b>X</b></div>
|
||||||
<div id=id_dialogtitle style=padding:5px></div>
|
<div id=id_dialogtitle style=padding:5px></div>
|
||||||
|
@ -271,10 +274,11 @@
|
||||||
var hardwareKeyChallenge = decodeURIComponent('{{{hkey}}}');
|
var hardwareKeyChallenge = decodeURIComponent('{{{hkey}}}');
|
||||||
var publicKeyCredentialRequestOptions = null;
|
var publicKeyCredentialRequestOptions = null;
|
||||||
var currentpanel = 0;
|
var currentpanel = 0;
|
||||||
|
var otpemail = ('{{{otpemail}}}' === 'true');
|
||||||
|
|
||||||
// Display the right server message
|
// Display the right server message
|
||||||
var messageid = parseInt('{{{messageid}}}');
|
var messageid = parseInt('{{{messageid}}}');
|
||||||
var okmessages = ['', "Hold on, reset mail sent."];
|
var okmessages = ['', "Hold on, reset mail sent.", "Email sent."];
|
||||||
var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later."];
|
var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later."];
|
||||||
if (messageid > 0) {
|
if (messageid > 0) {
|
||||||
var msg = '';
|
var msg = '';
|
||||||
|
@ -328,6 +332,7 @@
|
||||||
if (loginMode == '4') {
|
if (loginMode == '4') {
|
||||||
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
|
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
|
||||||
QV('securityKeyButton', (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn'));
|
QV('securityKeyButton', (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn'));
|
||||||
|
QV('emailKeyButton', otpemail && (messageid != 2)); // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginMode == '5') {
|
if (loginMode == '5') {
|
||||||
|
@ -364,6 +369,7 @@
|
||||||
|
|
||||||
// Use a hardware security key
|
// Use a hardware security key
|
||||||
function useSecurityKey() {
|
function useSecurityKey() {
|
||||||
|
if (xxdialogMode) return;
|
||||||
if ((hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')) {
|
if ((hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')) {
|
||||||
if (typeof hardwareKeyChallenge.challenge == 'string') { hardwareKeyChallenge.challenge = Uint8Array.from(atob(hardwareKeyChallenge.challenge), function (c) { return c.charCodeAt(0) }).buffer; }
|
if (typeof hardwareKeyChallenge.challenge == 'string') { hardwareKeyChallenge.challenge = Uint8Array.from(atob(hardwareKeyChallenge.challenge), function (c) { return c.charCodeAt(0) }).buffer; }
|
||||||
|
|
||||||
|
@ -393,6 +399,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useEmailToken() {
|
||||||
|
if (xxdialogMode) return;
|
||||||
|
if (otpemail != true) return;
|
||||||
|
setDialogMode(1, "Secure Login", 3, useEmailKeyEx, "Send token to registed email address?");
|
||||||
|
}
|
||||||
|
|
||||||
|
function useEmailKeyEx() {
|
||||||
|
Q('hwtokenInput').value = '**email**';
|
||||||
|
QE('tokenOkButton', true);
|
||||||
|
Q('tokenOkButton').click();
|
||||||
|
}
|
||||||
|
|
||||||
function showPassHint() {
|
function showPassHint() {
|
||||||
if (passRequirements.hint === true) { messagebox("Password Hint", passhint); }
|
if (passRequirements.hint === true) { messagebox("Password Hint", passhint); }
|
||||||
}
|
}
|
||||||
|
@ -633,7 +651,7 @@
|
||||||
if (((b & 8) || x) && f) f(x, t);
|
if (((b & 8) || x) && f) f(x, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
function center() { QS('dialog').left = ((((getDocWidth() - 400) / 2)) + 'px'); }
|
function center() { QS('dialog').left = ((((getDocWidth() - 300) / 2)) + 'px'); }
|
||||||
function messagebox(t, m) { QH('id_dialogMessage', m); setDialogMode(1, t, 1); }
|
function messagebox(t, m) { QH('id_dialogMessage', m); setDialogMode(1, t, 1); }
|
||||||
function statusbox(t, m) { QH('id_dialogMessage', m); setDialogMode(1, t); }
|
function statusbox(t, m) { QH('id_dialogMessage', m); setDialogMode(1, t); }
|
||||||
function getDocWidth() { if (window.innerWidth) return window.innerWidth; if (document.documentElement && document.documentElement.clientWidth && document.documentElement.clientWidth != 0) return document.documentElement.clientWidth; return document.getElementsByTagName('body')[0].clientWidth; }
|
function getDocWidth() { if (window.innerWidth) return window.innerWidth; if (document.documentElement && document.documentElement.clientWidth && document.documentElement.clientWidth != 0) return document.documentElement.clientWidth; return document.getElementsByTagName('body')[0].clientWidth; }
|
||||||
|
|
|
@ -160,7 +160,10 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan=2>
|
<td colspan=2>
|
||||||
<div style=float:right><input id=tokenOkButton type=submit value="Login" disabled="disabled" /></div>
|
<div style=float:right><input id=tokenOkButton type=submit value="Login" disabled="disabled" /></div>
|
||||||
<div style=float:right><input style="display:none;float:right" id=securityKeyButton type=button value="Use Security Key" onclick="useSecurityKey()" /></div>
|
<div style=float:right>
|
||||||
|
<input style="display:none;float:right" id=securityKeyButton type=button value="Use Security Key" onclick="useSecurityKey()" />
|
||||||
|
<input style="display:none;float:right" id=emailKeyButton type=button value="Email" onclick="useEmailToken()" />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -270,10 +273,11 @@
|
||||||
var webPageFullScreen = true;
|
var webPageFullScreen = true;
|
||||||
var nightMode = (getstore('_nightMode', '0') == '1');
|
var nightMode = (getstore('_nightMode', '0') == '1');
|
||||||
var publicKeyCredentialRequestOptions = null;
|
var publicKeyCredentialRequestOptions = null;
|
||||||
|
var otpemail = ('{{{otpemail}}}' === 'true');
|
||||||
|
|
||||||
// Display the right server message
|
// Display the right server message
|
||||||
var messageid = parseInt('{{{messageid}}}');
|
var messageid = parseInt('{{{messageid}}}');
|
||||||
var okmessages = ['', "Hold on, reset mail sent."];
|
var okmessages = ['', "Hold on, reset mail sent.", "Email sent."];
|
||||||
var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later."];
|
var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later."];
|
||||||
if (messageid > 0) {
|
if (messageid > 0) {
|
||||||
var msg = '';
|
var msg = '';
|
||||||
|
@ -349,6 +353,7 @@
|
||||||
if (loginMode == '4') {
|
if (loginMode == '4') {
|
||||||
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
|
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
|
||||||
QV('securityKeyButton', (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn'));
|
QV('securityKeyButton', (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn'));
|
||||||
|
QV('emailKeyButton', otpemail && (messageid != 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginMode == '5') {
|
if (loginMode == '5') {
|
||||||
|
@ -417,6 +422,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useEmailToken() {
|
||||||
|
if (otpemail != true) return;
|
||||||
|
setDialogMode(1, "Secure Login", 3, useEmailKeyEx, "Send token to registed email address?");
|
||||||
|
}
|
||||||
|
|
||||||
|
function useEmailKeyEx() {
|
||||||
|
Q('hwtokenInput').value = '**email**';
|
||||||
|
QE('tokenOkButton', true);
|
||||||
|
Q('tokenOkButton').click();
|
||||||
|
}
|
||||||
|
|
||||||
function showPassHint(e) {
|
function showPassHint(e) {
|
||||||
messagebox("Password Hint", passhint);
|
messagebox("Password Hint", passhint);
|
||||||
haltEvent(e);
|
haltEvent(e);
|
||||||
|
|
69
webserver.js
69
webserver.js
|
@ -541,7 +541,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a 2nd factor is present
|
// Check if a 2nd factor is present
|
||||||
return ((parent.config.settings.no2factorauth !== true) && ((user.otpsecret != null) || ((user.otphkeys != null) && (user.otphkeys.length > 0))));
|
return ((parent.config.settings.no2factorauth !== true) && ((user.otpsecret != null) || (user.otpekey != null) || ((user.otphkeys != null) && (user.otphkeys.length > 0))));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the 2-step auth token
|
// Check the 2-step auth token
|
||||||
|
@ -550,6 +550,22 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (parent.config.settings.no2factorauth !== true));
|
const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (parent.config.settings.no2factorauth !== true));
|
||||||
if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; };
|
if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; };
|
||||||
|
|
||||||
|
// Check if we can use OTP tokens with email
|
||||||
|
var otpemail = (parent.mailserver != null);
|
||||||
|
if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.email2factor == false)) { otpemail = false; }
|
||||||
|
|
||||||
|
// Check email key
|
||||||
|
if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) {
|
||||||
|
var deltaTime = (Date.now() - user.otpekey.d);
|
||||||
|
if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the email token (10000 * 60 * 5).
|
||||||
|
user.otpekey = {};
|
||||||
|
obj.db.SetUser(user);
|
||||||
|
parent.debug('web', 'checkUserOneTimePassword: success (email).');
|
||||||
|
func(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check hardware key
|
// Check hardware key
|
||||||
if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
|
if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
|
||||||
var authResponse = null;
|
var authResponse = null;
|
||||||
|
@ -595,10 +611,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
// Update the hardware key counter and accept the 2nd factor
|
// Update the hardware key counter and accept the 2nd factor
|
||||||
webAuthnKey.counter = webauthnResponse.counter;
|
webAuthnKey.counter = webauthnResponse.counter;
|
||||||
obj.db.SetUser(user);
|
obj.db.SetUser(user);
|
||||||
parent.debug('web', 'checkUserOneTimePassword: success.');
|
parent.debug('web', 'checkUserOneTimePassword: success (hardware).');
|
||||||
func(true);
|
func(true);
|
||||||
} else {
|
} else {
|
||||||
parent.debug('web', 'checkUserOneTimePassword: fail.');
|
parent.debug('web', 'checkUserOneTimePassword: fail (hardware).');
|
||||||
func(false);
|
func(false);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -610,12 +626,21 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
// Check Google Authenticator
|
// Check Google Authenticator
|
||||||
const otplib = require('otplib')
|
const otplib = require('otplib')
|
||||||
otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
|
otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
|
||||||
if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6) && (otplib.authenticator.check(token, user.otpsecret) == true)) { func(true); return; };
|
if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6) && (otplib.authenticator.check(token, user.otpsecret) == true)) {
|
||||||
|
parent.debug('web', 'checkUserOneTimePassword: success (authenticator).');
|
||||||
|
func(true);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
// Check written down keys
|
// Check written down keys
|
||||||
if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
|
if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
|
||||||
var tokenNumber = parseInt(token);
|
var tokenNumber = parseInt(token);
|
||||||
for (var i = 0; i < user.otpkeys.keys.length; i++) { if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) { user.otpkeys.keys[i].u = false; func(true); return; } }
|
for (var i = 0; i < user.otpkeys.keys.length; i++) {
|
||||||
|
if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) {
|
||||||
|
parent.debug('web', 'checkUserOneTimePassword: success (one-time).');
|
||||||
|
user.otpkeys.keys[i].u = false; func(true); return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check OTP hardware key
|
// Check OTP hardware key
|
||||||
|
@ -631,7 +656,15 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
var yubikeyotp = require('yubikeyotp');
|
var yubikeyotp = require('yubikeyotp');
|
||||||
var request = { otp: token, id: domain.yubikey.id, key: domain.yubikey.secret, timestamp: true }
|
var request = { otp: token, id: domain.yubikey.id, key: domain.yubikey.secret, timestamp: true }
|
||||||
if (domain.yubikey.proxy) { request.requestParams = { proxy: domain.yubikey.proxy }; }
|
if (domain.yubikey.proxy) { request.requestParams = { proxy: domain.yubikey.proxy }; }
|
||||||
yubikeyotp.verifyOTP(request, function (err, results) { func((results != null) && (results.status == 'OK')); });
|
yubikeyotp.verifyOTP(request, function (err, results) {
|
||||||
|
if ((results != null) && (results.status == 'OK')) {
|
||||||
|
parent.debug('web', 'checkUserOneTimePassword: success (Yubikey).');
|
||||||
|
func(true);
|
||||||
|
} else {
|
||||||
|
parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).');
|
||||||
|
func(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -687,6 +720,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
|
|
||||||
// Check if this user has 2-step login active
|
// Check if this user has 2-step login active
|
||||||
if ((req.session.loginmode != '6') && checkUserOneTimePasswordRequired(domain, user, req)) {
|
if ((req.session.loginmode != '6') && checkUserOneTimePasswordRequired(domain, user, req)) {
|
||||||
|
if ((req.body.hwtoken == '**email**') && (user.email != null) && (user.emailVerified == true) && (parent.mailserver != null) && (user.otpekey != null)) {
|
||||||
|
user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
|
||||||
|
obj.db.SetUser(user);
|
||||||
|
parent.debug('web', 'Sending 2FA email to: ' + user.email);
|
||||||
|
parent.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k);
|
||||||
|
req.session.messageid = 2; // "Email sent" message
|
||||||
|
req.session.loginmode = '4';
|
||||||
|
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result) {
|
checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result) {
|
||||||
if (result == false) {
|
if (result == false) {
|
||||||
var randomWaitTime = 0;
|
var randomWaitTime = 0;
|
||||||
|
@ -706,6 +750,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
// Wait and redirect the user
|
// Wait and redirect the user
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
req.session.loginmode = '4';
|
req.session.loginmode = '4';
|
||||||
|
req.session.tokenemail = ((user.email != null) && (user.emailVerified == true) && (parent.mailserver != null) && (user.otpekey != null));
|
||||||
req.session.tokenusername = xusername;
|
req.session.tokenusername = xusername;
|
||||||
req.session.tokenpassword = xpassword;
|
req.session.tokenpassword = xpassword;
|
||||||
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
|
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
|
||||||
|
@ -793,6 +838,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
//req.session.regenerate(function () {
|
//req.session.regenerate(function () {
|
||||||
// Store the user's primary key in the session store to be retrieved, or in this case the entire user object
|
// Store the user's primary key in the session store to be retrieved, or in this case the entire user object
|
||||||
delete req.session.loginmode;
|
delete req.session.loginmode;
|
||||||
|
delete req.session.tokenemail;
|
||||||
delete req.session.tokenusername;
|
delete req.session.tokenusername;
|
||||||
delete req.session.tokenpassword;
|
delete req.session.tokenpassword;
|
||||||
delete req.session.tokenemail;
|
delete req.session.tokenemail;
|
||||||
|
@ -1008,6 +1054,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
// Failed, error out.
|
// Failed, error out.
|
||||||
parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
|
parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
|
||||||
delete req.session.loginmode;
|
delete req.session.loginmode;
|
||||||
|
delete req.session.tokenemail;
|
||||||
delete req.session.tokenusername;
|
delete req.session.tokenusername;
|
||||||
delete req.session.tokenpassword;
|
delete req.session.tokenpassword;
|
||||||
delete req.session.resettokenusername;
|
delete req.session.resettokenusername;
|
||||||
|
@ -1672,8 +1719,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
var hwstate = null;
|
var hwstate = null;
|
||||||
if (hardwareKeyChallenge) { hwstate = obj.parent.encodeCookie({ u: req.session.tokenusername, p: req.session.tokenpassword, c: req.session.u2fchallenge }, obj.parent.loginCookieEncryptionKey) }
|
if (hardwareKeyChallenge) { hwstate = obj.parent.encodeCookie({ u: req.session.tokenusername, p: req.session.tokenpassword, c: req.session.u2fchallenge }, obj.parent.loginCookieEncryptionKey) }
|
||||||
|
|
||||||
|
// Check if we can use OTP tokens with email
|
||||||
|
var otpemail = (parent.mailserver != null) && (req.session.tokenemail);
|
||||||
|
if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.email2factor == false)) { otpemail = false; }
|
||||||
|
|
||||||
// Render the login page
|
// Render the login page
|
||||||
render(req, res, getRenderPage('login', req), getRenderArgs({ loginmode: loginmode, rootCertLink: getRootCertLink(), newAccount: newAccountsAllowed, newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), serverDnsName: obj.getWebServerName(domain), serverPublicPort: httpsPort, emailcheck: emailcheck, features: features, sessiontime: args.sessiontime, passRequirements: passRequirements, footer: (domain.footer == null) ? '' : domain.footer, hkey: encodeURIComponent(hardwareKeyChallenge), messageid: msgid, passhint: passhint, welcometext: domain.welcometext ? encodeURIComponent(domain.welcometext).split('\'').join('\\\'') : null, hwstate: hwstate }, domain));
|
render(req, res, getRenderPage('login', req), getRenderArgs({ loginmode: loginmode, rootCertLink: getRootCertLink(), newAccount: newAccountsAllowed, newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), serverDnsName: obj.getWebServerName(domain), serverPublicPort: httpsPort, emailcheck: emailcheck, features: features, sessiontime: args.sessiontime, passRequirements: passRequirements, footer: (domain.footer == null) ? '' : domain.footer, hkey: encodeURIComponent(hardwareKeyChallenge), messageid: msgid, passhint: passhint, welcometext: domain.welcometext ? encodeURIComponent(domain.welcometext).split('\'').join('\\\'') : null, hwstate: hwstate, otpemail: otpemail }, domain));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle a post request on the root
|
// Handle a post request on the root
|
||||||
|
@ -4223,6 +4274,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
delete user2.domain;
|
delete user2.domain;
|
||||||
delete user2.subscriptions;
|
delete user2.subscriptions;
|
||||||
delete user2.passtype;
|
delete user2.passtype;
|
||||||
|
if ((typeof user2.otpkeys == 'object') && (user2.otpkeys != null)) { user2.otpekey = 1; } // Indicates that email 2FA is enabled.
|
||||||
if ((typeof user2.otpsecret == 'string') && (user2.otpsecret != null)) { user2.otpsecret = 1; } // Indicates a time secret is present.
|
if ((typeof user2.otpsecret == 'string') && (user2.otpsecret != null)) { user2.otpsecret = 1; } // Indicates a time secret is present.
|
||||||
if ((typeof user2.otpkeys == 'object') && (user2.otpkeys != null)) { user2.otpkeys = 0; if (user.otpkeys != null) { for (var i = 0; i < user.otpkeys.keys.length; i++) { if (user.otpkeys.keys[i].u == true) { user2.otpkeys = 1; } } } } // Indicates the number of one time backup codes that are active.
|
if ((typeof user2.otpkeys == 'object') && (user2.otpkeys != null)) { user2.otpkeys = 0; if (user.otpkeys != null) { for (var i = 0; i < user.otpkeys.keys.length; i++) { if (user.otpkeys.keys[i].u == true) { user2.otpkeys = 1; } } } } // Indicates the number of one time backup codes that are active.
|
||||||
if ((typeof user2.otphkeys == 'object') && (user2.otphkeys != null)) { user2.otphkeys = user2.otphkeys.length; } // Indicates the number of hardware keys setup
|
if ((typeof user2.otphkeys == 'object') && (user2.otphkeys != null)) { user2.otphkeys = user2.otphkeys.length; } // Indicates the number of hardware keys setup
|
||||||
|
@ -4510,6 +4562,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
function getRandomPassword() { return Buffer.from(obj.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); }
|
function getRandomPassword() { return Buffer.from(obj.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); }
|
||||||
function getRandomLowerCase(len) { var r = '', random = obj.crypto.randomBytes(len); for (var i = 0; i < len; i++) { r += String.fromCharCode(97 + (random[i] % 26)); } return r; }
|
function getRandomLowerCase(len) { var r = '', random = obj.crypto.randomBytes(len); for (var i = 0; i < len; i++) { r += String.fromCharCode(97 + (random[i] % 26)); } return r; }
|
||||||
|
|
||||||
|
// Generate a 8 digit integer with even random probability for each value.
|
||||||
|
function getRandomEightDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 100000000; }
|
||||||
|
|
||||||
// Clean a IPv6 address that encodes a IPv4 address
|
// Clean a IPv6 address that encodes a IPv4 address
|
||||||
function cleanRemoteAddr(addr) { if (typeof addr != 'string') { return null; } if (addr.indexOf('::ffff:') == 0) { return addr.substring(7); } else { return addr; } }
|
function cleanRemoteAddr(addr) { if (typeof addr != 'string') { return null; } if (addr.indexOf('::ffff:') == 0) { return addr.substring(7); } else { return addr; } }
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue