Email based 2FA almost completed.
Ylian Saint-Hilaire committed
Mar 13, 2020 at 20:39 UTC
70e93f0c0fcb9adde188c13f69edde3cf301980d
6 files changed
+121
-17
meshmail.js
+16
@@ -43,11 +43,13 @@ module.exports.CreateMeshMail = function (parent) {
43
// Set default mail templates
44
// You can override these by placing a file with the same name in "meshcentral-data/mail"
45
// If the server hash many domains, just add the domainid to the file like this: 'account-check-customer1.html', 'mesh-invite-customer1.txt'.
46
+ 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>';
47
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>';
48
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>';
49
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>';
50
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>';
51
52
+ 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.';
53
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]]]';
54
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';
55
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) {
135
sendNextMail();
136
};
137
138
+ // Send account login mail / 2 factor token
139
+ obj.sendAccountLoginMail = function (domain, email, token) {
140
+ var template = getTemplateEx('account-login', domain);
141
+ 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.
142
+
143
+ // Set all the options.
144
+ var options = { email: email, servername: domain.title ? domain.title : 'MeshCentral', token: token };
145
+
146
+ // Send the email
147
+ console.log(options);
148
+ 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) });
149
+ sendNextMail();
150
+ };
151
+
152
// Send account invitation mail
153
obj.sendAccountInviteMail = function (domain, username, accountname, email, password) {
154
var template = getTemplateEx('account-invite', domain);
package.json
+1
-1
@@ -1,6 +1,6 @@
1
{
2
"name": "meshcentral",
3
- "version": "0.5.0-e",
3
+ "version": "0.5.0-f",
4
"keywords": [
5
"Remote Management",
6
"Intel AMT",
pluginHandler.js
+2
-3
@@ -507,10 +507,9 @@ module.exports.pluginHandler = function (parent) {
507
obj.removePlugin = function (id, func) {
508
parent.db.getPlugin(id, function (err, docs) {
509
var plugin = docs[0];
510
- var rimraf = null;
511
- try { rimraf = require('rimraf'); } catch (ex) { }
510
+ var rimraf = require('rimraf');
511
let pluginPath = obj.parent.path.join(obj.pluginPath, plugin.shortName);
513
- if (rimraf) rimraf.sync(pluginPath);
512
+ rimraf.sync(pluginPath);
513
parent.db.deletePlugin(id, func);
514
delete obj.plugins[plugin.shortName];
515
});
views/login-mobile.handlebars
+22
-4
@@ -164,7 +164,10 @@
164
<tr>
165
<td colspan=2>
166
<div style=float:right><input id=tokenOkButton type=submit value="Login" disabled="disabled" /></div>
167
- <div style=float:right><input style="display:none;float:right" id=securityKeyButton type=button value="Use Security Key" onclick="useSecurityKey()" /></div>
167
+ <div style=float:right>
168
+ <input style="display:none;float:right" id=securityKeyButton type=button value="Use Security Key" onclick="useSecurityKey()" />
169
+ <input style="display:none;float:right" id=emailKeyButton type=button value="Email" onclick="useEmailToken()" />
170
+ </div>
171
</td>
172
</tr>
173
</table>
@@ -238,7 +241,7 @@
241
</table>
242
</div>
243
</div>
241
- <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">
244
+ <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">
245
<div style="width:100%;background-color:#003366;color:#FFF;border-radius:5px 5px 0 0">
246
<div id=id_dialogclose style=float:right;padding:5px;cursor:pointer onclick=setDialogMode()><b>X</b></div>
247
<div id=id_dialogtitle style=padding:5px></div>
@@ -271,10 +274,11 @@
274
var hardwareKeyChallenge = decodeURIComponent('{{{hkey}}}');
275
var publicKeyCredentialRequestOptions = null;
276
var currentpanel = 0;
277
+ var otpemail = ('{{{otpemail}}}' === 'true');
278
279
// Display the right server message
280
var messageid = parseInt('{{{messageid}}}');
277
- var okmessages = ['', "Hold on, reset mail sent."];
281
+ var okmessages = ['', "Hold on, reset mail sent.", "Email sent."];
282
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."];
283
if (messageid > 0) {
284
var msg = '';
@@ -328,6 +332,7 @@
332
if (loginMode == '4') {
333
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
334
QV('securityKeyButton', (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn'));
335
+ QV('emailKeyButton', otpemail && (messageid != 2)); // TODO
336
}
337
338
if (loginMode == '5') {
@@ -364,6 +369,7 @@
369
370
// Use a hardware security key
371
function useSecurityKey() {
372
+ if (xxdialogMode) return;
373
if ((hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')) {
374
if (typeof hardwareKeyChallenge.challenge == 'string') { hardwareKeyChallenge.challenge = Uint8Array.from(atob(hardwareKeyChallenge.challenge), function (c) { return c.charCodeAt(0) }).buffer; }
375
@@ -393,6 +399,18 @@
399
}
400
}
401
402
+ function useEmailToken() {
403
+ if (xxdialogMode) return;
404
+ if (otpemail != true) return;
405
+ setDialogMode(1, "Secure Login", 3, useEmailKeyEx, "Send token to registed email address?");
406
+ }
407
+
408
+ function useEmailKeyEx() {
409
+ Q('hwtokenInput').value = '**email**';
410
+ QE('tokenOkButton', true);
411
+ Q('tokenOkButton').click();
412
+ }
413
+
414
function showPassHint() {
415
if (passRequirements.hint === true) { messagebox("Password Hint", passhint); }
416
}
@@ -633,7 +651,7 @@
651
if (((b & 8) || x) && f) f(x, t);
652
}
653
636
- function center() { QS('dialog').left = ((((getDocWidth() - 400) / 2)) + 'px'); }
654
+ function center() { QS('dialog').left = ((((getDocWidth() - 300) / 2)) + 'px'); }
655
function messagebox(t, m) { QH('id_dialogMessage', m); setDialogMode(1, t, 1); }
656
function statusbox(t, m) { QH('id_dialogMessage', m); setDialogMode(1, t); }
657
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; }
views/login.handlebars
+18
-2
@@ -160,7 +160,10 @@
160
<tr>
161
<td colspan=2>
162
<div style=float:right><input id=tokenOkButton type=submit value="Login" disabled="disabled" /></div>
163
- <div style=float:right><input style="display:none;float:right" id=securityKeyButton type=button value="Use Security Key" onclick="useSecurityKey()" /></div>
163
+ <div style=float:right>
164
+ <input style="display:none;float:right" id=securityKeyButton type=button value="Use Security Key" onclick="useSecurityKey()" />
165
+ <input style="display:none;float:right" id=emailKeyButton type=button value="Email" onclick="useEmailToken()" />
166
+ </div>
167
</td>
168
</tr>
169
</table>
@@ -270,10 +273,11 @@
273
var webPageFullScreen = true;
274
var nightMode = (getstore('_nightMode', '0') == '1');
275
var publicKeyCredentialRequestOptions = null;
276
+ var otpemail = ('{{{otpemail}}}' === 'true');
277
278
// Display the right server message
279
var messageid = parseInt('{{{messageid}}}');
276
- var okmessages = ['', "Hold on, reset mail sent."];
280
+ var okmessages = ['', "Hold on, reset mail sent.", "Email sent."];
281
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."];
282
if (messageid > 0) {
283
var msg = '';
@@ -349,6 +353,7 @@
353
if (loginMode == '4') {
354
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
355
QV('securityKeyButton', (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn'));
356
+ QV('emailKeyButton', otpemail && (messageid != 2));
357
}
358
359
if (loginMode == '5') {
@@ -417,6 +422,17 @@
422
}
423
}
424
425
+ function useEmailToken() {
426
+ if (otpemail != true) return;
427
+ setDialogMode(1, "Secure Login", 3, useEmailKeyEx, "Send token to registed email address?");
428
+ }
429
+
430
+ function useEmailKeyEx() {
431
+ Q('hwtokenInput').value = '**email**';
432
+ QE('tokenOkButton', true);
433
+ Q('tokenOkButton').click();
434
+ }
435
+
436
function showPassHint(e) {
437
messagebox("Password Hint", passhint);
438
haltEvent(e);
webserver.js
+62
-7
@@ -541,7 +541,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
541
}
542
543
// Check if a 2nd factor is present
544
- return ((parent.config.settings.no2factorauth !== true) && ((user.otpsecret != null) || ((user.otphkeys != null) && (user.otphkeys.length > 0))));
544
+ return ((parent.config.settings.no2factorauth !== true) && ((user.otpsecret != null) || (user.otpekey != null) || ((user.otphkeys != null) && (user.otphkeys.length > 0))));
545
}
546
547
// Check the 2-step auth token
@@ -550,6 +550,22 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
550
const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (parent.config.settings.no2factorauth !== true));
551
if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; };
552
553
+ // Check if we can use OTP tokens with email
554
+ var otpemail = (parent.mailserver != null);
555
+ if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.email2factor == false)) { otpemail = false; }
556
+
557
+ // Check email key
558
+ if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) {
559
+ var deltaTime = (Date.now() - user.otpekey.d);
560
+ if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the email token (10000 * 60 * 5).
561
+ user.otpekey = {};
562
+ obj.db.SetUser(user);
563
+ parent.debug('web', 'checkUserOneTimePassword: success (email).');
564
+ func(true);
565
+ return;
566
+ }
567
+ }
568
+
569
// Check hardware key
570
if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
571
var authResponse = null;
@@ -595,10 +611,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
611
// Update the hardware key counter and accept the 2nd factor
612
webAuthnKey.counter = webauthnResponse.counter;
613
obj.db.SetUser(user);
598
- parent.debug('web', 'checkUserOneTimePassword: success.');
614
+ parent.debug('web', 'checkUserOneTimePassword: success (hardware).');
615
func(true);
616
} else {
601
- parent.debug('web', 'checkUserOneTimePassword: fail.');
617
+ parent.debug('web', 'checkUserOneTimePassword: fail (hardware).');
618
func(false);
619
}
620
return;
@@ -610,12 +626,21 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
626
// Check Google Authenticator
627
const otplib = require('otplib')
628
otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
613
- if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6) && (otplib.authenticator.check(token, user.otpsecret) == true)) { func(true); return; };
629
+ if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6) && (otplib.authenticator.check(token, user.otpsecret) == true)) {
630
+ parent.debug('web', 'checkUserOneTimePassword: success (authenticator).');
631
+ func(true);
632
+ return;
633
+ };
634
635
// Check written down keys
636
if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
637
var tokenNumber = parseInt(token);
618
- 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; } }
638
+ for (var i = 0; i < user.otpkeys.keys.length; i++) {
639
+ if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) {
640
+ parent.debug('web', 'checkUserOneTimePassword: success (one-time).');
641
+ user.otpkeys.keys[i].u = false; func(true); return;
642
+ }
643
+ }
644
}
645
646
// Check OTP hardware key
@@ -631,7 +656,15 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
656
var yubikeyotp = require('yubikeyotp');
657
var request = { otp: token, id: domain.yubikey.id, key: domain.yubikey.secret, timestamp: true }
658
if (domain.yubikey.proxy) { request.requestParams = { proxy: domain.yubikey.proxy }; }
634
- yubikeyotp.verifyOTP(request, function (err, results) { func((results != null) && (results.status == 'OK')); });
659
+ yubikeyotp.verifyOTP(request, function (err, results) {
660
+ if ((results != null) && (results.status == 'OK')) {
661
+ parent.debug('web', 'checkUserOneTimePassword: success (Yubikey).');
662
+ func(true);
663
+ } else {
664
+ parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).');
665
+ func(false);
666
+ }
667
+ });
668
return;
669
}
670
}
@@ -687,6 +720,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
720
721
// Check if this user has 2-step login active
722
if ((req.session.loginmode != '6') && checkUserOneTimePasswordRequired(domain, user, req)) {
723
+ if ((req.body.hwtoken == '**email**') && (user.email != null) && (user.emailVerified == true) && (parent.mailserver != null) && (user.otpekey != null)) {
724
+ user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
725
+ obj.db.SetUser(user);
726
+ parent.debug('web', 'Sending 2FA email to: ' + user.email);
727
+ parent.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k);
728
+ req.session.messageid = 2; // "Email sent" message
729
+ req.session.loginmode = '4';
730
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
731
+ return;
732
+ }
733
+
734
checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result) {
735
if (result == false) {
736
var randomWaitTime = 0;
@@ -706,6 +750,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
750
// Wait and redirect the user
751
setTimeout(function () {
752
req.session.loginmode = '4';
753
+ req.session.tokenemail = ((user.email != null) && (user.emailVerified == true) && (parent.mailserver != null) && (user.otpekey != null));
754
req.session.tokenusername = xusername;
755
req.session.tokenpassword = xpassword;
756
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) {
838
//req.session.regenerate(function () {
839
// Store the user's primary key in the session store to be retrieved, or in this case the entire user object
840
delete req.session.loginmode;
841
+ delete req.session.tokenemail;
842
delete req.session.tokenusername;
843
delete req.session.tokenpassword;
844
delete req.session.tokenemail;
@@ -1008,6 +1054,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1054
// Failed, error out.
1055
parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
1056
delete req.session.loginmode;
1057
+ delete req.session.tokenemail;
1058
delete req.session.tokenusername;
1059
delete req.session.tokenpassword;
1060
delete req.session.resettokenusername;
@@ -1672,8 +1719,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1719
var hwstate = null;
1720
if (hardwareKeyChallenge) { hwstate = obj.parent.encodeCookie({ u: req.session.tokenusername, p: req.session.tokenpassword, c: req.session.u2fchallenge }, obj.parent.loginCookieEncryptionKey) }
1721
1722
+ // Check if we can use OTP tokens with email
1723
+ var otpemail = (parent.mailserver != null) && (req.session.tokenemail);
1724
+ if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.email2factor == false)) { otpemail = false; }
1725
+
1726
// Render the login page
1676
- 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));
1727
+ 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));
1728
}
1729
1730
// Handle a post request on the root
@@ -4223,6 +4274,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4274
delete user2.domain;
4275
delete user2.subscriptions;
4276
delete user2.passtype;
4277
+ if ((typeof user2.otpkeys == 'object') && (user2.otpkeys != null)) { user2.otpekey = 1; } // Indicates that email 2FA is enabled.
4278
if ((typeof user2.otpsecret == 'string') && (user2.otpsecret != null)) { user2.otpsecret = 1; } // Indicates a time secret is present.
4279
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.
4280
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) {
4562
function getRandomPassword() { return Buffer.from(obj.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); }
4563
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; }
4564
4565
+ // Generate a 8 digit integer with even random probability for each value.
4566
+ function getRandomEightDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 100000000; }
4567
+
4568
// Clean a IPv6 address that encodes a IPv4 address
4569
function cleanRemoteAddr(addr) { if (typeof addr != 'string') { return null; } if (addr.indexOf('::ffff:') == 0) { return addr.substring(7); } else { return addr; } }
4570