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>&nbsp;&nbsp;&nbsp;Username: <b>[[[ACCOUNTNAME]]]</b><br />&nbsp;&nbsp;&nbsp;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