More work on SMS integration, added Plivo support.

This commit is contained in:
Ylian Saint-Hilaire 2020-04-22 15:29:26 -07:00
parent 2b6925205f
commit cefd6c98b3
8 changed files with 1877 additions and 1630 deletions

View File

@ -2571,6 +2571,7 @@ function mainStart() {
// SMS support // SMS support
if ((config.sms != null) && (config.sms.provider == 'twilio')) { modules.push('twilio'); } if ((config.sms != null) && (config.sms.provider == 'twilio')) { modules.push('twilio'); }
if ((config.sms != null) && (config.sms.provider == 'plivo')) { modules.push('plivo'); }
// Syslog support // Syslog support
if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog'); } if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog'); }

View File

@ -14,6 +14,24 @@
/*jshint esversion: 6 */ /*jshint esversion: 6 */
"use strict"; "use strict";
/*
// For Twilio, add this in config.json
"sms": {
"provider": "twilio",
"sid": "ACxxxxxxxxx",
"auth": "xxxxxxx",
"from": "+15555555555"
},
// For Plivo, add this in config.json
"sms": {
"provider": "plivo",
"id": "xxxxxxx",
"token": "xxxxxxx",
"from": "15555555555"
}
*/
// Construct a MeshAgent object, called upon connection // Construct a MeshAgent object, called upon connection
module.exports.CreateMeshSMS = function (parent) { module.exports.CreateMeshSMS = function (parent) {
var obj = {}; var obj = {};
@ -33,6 +51,17 @@ module.exports.CreateMeshSMS = function (parent) {
obj.provider = new Twilio(parent.config.sms.sid, parent.config.sms.auth); obj.provider = new Twilio(parent.config.sms.sid, parent.config.sms.auth);
break; break;
} }
case 'plivo': {
// Validate Plivo configuration values
if (typeof parent.config.sms.id != 'string') { console.log('Invalid or missing SMS gateway provider id.'); return null; }
if (typeof parent.config.sms.token != 'string') { console.log('Invalid or missing SMS gateway provider token.'); return null; }
if (typeof parent.config.sms.from != 'string') { console.log('Invalid or missing SMS gateway provider from.'); return null; }
// Setup Twilio
var plivo = require('plivo');
obj.provider = new plivo.Client(parent.config.sms.id, parent.config.sms.token);
break;
}
default: { default: {
// Unknown SMS gateway provider // Unknown SMS gateway provider
console.log('Unknown SMS gateway provider: ' + parent.config.sms.provider); console.log('Unknown SMS gateway provider: ' + parent.config.sms.provider);
@ -43,15 +72,32 @@ module.exports.CreateMeshSMS = function (parent) {
// Send an SMS message // Send an SMS message
obj.sendSMS = function (to, msg, func) { obj.sendSMS = function (to, msg, func) {
parent.debug('email', 'Sending SMS to: ' + to + ': ' + msg); parent.debug('email', 'Sending SMS to: ' + to + ': ' + msg);
if (parent.config.sms.provider == 'twilio') { if (parent.config.sms.provider == 'twilio') { // Twilio
obj.provider.messages.create({ obj.provider.messages.create({
from: parent.config.sms.from, from: parent.config.sms.from,
to: to, to: to,
body: msg body: msg
}, function (err, result) { }, function (err, result) {
if (err != null) { parent.debug('email', 'SMS error: ' + JSON.stringify(err)); } else { parent.debug('email', 'SMS result: ' + JSON.stringify(result)); } if (err != null) { parent.debug('email', 'SMS error: ' + err.message); } else { parent.debug('email', 'SMS result: ' + JSON.stringify(result)); }
if (func != null) { func((err == null) && (result.status == 'queued'), err, result); } if (func != null) { func((err == null) && (result.status == 'queued'), err ? err.message : null, result); }
}); });
} else if (parent.config.sms.provider == 'plivo') { // Plivo
if (to.split('-').join('').split(' ').join('').split('+').join('').length == 10) { to = '1' + to; } // If we only have 10 digits, add a 1 in front.
obj.provider.messages.create(
parent.config.sms.from,
to,
msg
).then(function (result) {
parent.debug('email', 'SMS result: ' + JSON.stringify(result));
if (func != null) { func((result != null) && (result.messageUuid != null), null, result); }
}
).catch(function (err) {
var msg = null;
if ((err != null) && err.message) { msg = JSON.parse(err.message).error; }
parent.debug('email', 'SMS error: ' + msg);
if (func != null) { func(false, msg, null); }
}
);
} }
} }
@ -109,5 +155,20 @@ module.exports.CreateMeshSMS = function (parent) {
obj.sendSMS(phoneNumber, sms, func); obj.sendSMS(phoneNumber, sms, func);
}; };
// Send phone number verification SMS
obj.sendToken = function (domain, phoneNumber, verificationCode, language, func) {
parent.debug('email', "Sending login token SMS to " + phoneNumber);
var sms = getTemplate(1, domain, language);
if (sms == null) { parent.debug('email', "Error: Failed to get SMS template"); return; } // No SMS template found
// Setup the template
sms = sms.split('[[0]]').join(domain.title ? domain.title : 'MeshCentral');
sms = sms.split('[[1]]').join(verificationCode);
// Send the SMS
obj.sendSMS(phoneNumber, sms, func);
};
return obj; return obj;
}; };

View File

@ -762,8 +762,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if (cmdargs['_'].length != 2) { if (cmdargs['_'].length != 2) {
r = "Usage: SMS \"PhoneNumber\" \"Message\"."; r = "Usage: SMS \"PhoneNumber\" \"Message\".";
} else { } else {
parent.parent.smsserver.sendSMS(cmdargs['_'][0], cmdargs['_'][1], function (status) { parent.parent.smsserver.sendSMS(cmdargs['_'][0], cmdargs['_'][1], function (status, msg) {
try { ws.send(JSON.stringify({ action: 'serverconsole', value: status?'Success':'Failed', tag: command.tag })); } catch (ex) { } if (typeof msg == 'string') {
try { ws.send(JSON.stringify({ action: 'serverconsole', value: status ? ('Success: ' + msg) : ('Failed: ' + msg), tag: command.tag })); } catch (ex) { }
} else {
try { ws.send(JSON.stringify({ action: 'serverconsole', value: status ? 'Success' : 'Failed', tag: command.tag })); } catch (ex) { }
}
}); });
} }
} }
@ -3713,9 +3717,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if (parent.parent.smsserver == null) return; if (parent.parent.smsserver == null) return;
if (common.validateString(command.phone, 1, 18) == false) break; // Check phone length if (common.validateString(command.phone, 1, 18) == false) break; // Check phone length
if (command.phone.match(/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/) == false) break; // Check phone if (command.phone.match(/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/) == false) break; // Check phone
const code = getRandomEightDigitInteger(); const code = common.zeroPad(getRandomSixDigitInteger(), 6)
// TODO: We need limit how many times we can guess the code
const phoneCookie = parent.parent.encodeCookie({ a: 'verifyPhone', c: code, p: command.phone, s: ws.sessionId }); const phoneCookie = parent.parent.encodeCookie({ a: 'verifyPhone', c: code, p: command.phone, s: ws.sessionId });
parent.parent.smsserver.sendPhoneCheck(domain, command.phone, code, parent.getLanguageCodes(req), function (success) { parent.parent.smsserver.sendPhoneCheck(domain, command.phone, code, parent.getLanguageCodes(req), function (success) {
ws.send(JSON.stringify({ action: 'verifyPhone', cookie: phoneCookie, success: success })); ws.send(JSON.stringify({ action: 'verifyPhone', cookie: phoneCookie, success: success }));
@ -3723,11 +3725,19 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
break; break;
} }
case 'confirmPhone': { case 'confirmPhone': {
if ((parent.parent.smsserver == null) || (typeof command.cookie != 'string') || (typeof command.code != 'number')) break; // Input checks if ((parent.parent.smsserver == null) || (typeof command.cookie != 'string') || (typeof command.code != 'string') || (obj.failedSmsCookieCheck == 1)) break; // Input checks
var cookie = parent.parent.decodeCookie(command.cookie); var cookie = parent.parent.decodeCookie(command.cookie);
if (cookie == null) break; // Invalid cookie if (cookie == null) break; // Invalid cookie
if (cookie.s != ws.sessionId) break; // Invalid session if (cookie.s != ws.sessionId) break; // Invalid session
if (cookie.c != command.code) { ws.send(JSON.stringify({ action: 'verifyPhone', cookie: command.cookie, success: true })); break; } // Code does not match if (cookie.c != command.code) {
obj.failedSmsCookieCheck = 1;
// Code does not match, delay the response to limit how many guesses we can make and don't allow more than 1 guess at any given time.
setTimeout(function () {
ws.send(JSON.stringify({ action: 'verifyPhone', cookie: command.cookie, success: true }));
delete obj.failedSmsCookieCheck;
}, 2000 + (parent.crypto.randomBytes(2).readUInt16BE(0) % 4095));
break;
}
// Set the user's phone // Set the user's phone
user.phone = cookie.p; user.phone = cookie.p;
@ -3755,14 +3765,25 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
break; break;
} }
case 'smsuser': { // Send a SMS message to a user case 'smsuser': { // Send a SMS message to a user
if (parent.parent.smsserver == null) break; var errMsg = null, smsuser = null;
if ((user.siteadmin & 2) == 0) break; if (parent.parent.smsserver == null) { errMsg = 'SMS gateway not enabled'; }
if (common.validateString(command.userid, 1, 2048) == false) break; else if ((user.siteadmin & 2) == 0) { errMsg = 'No user management rights'; }
if (common.validateString(command.msg, 1, 160) == false) break; else if (common.validateString(command.userid, 1, 2048) == false) { errMsg = 'Invalid userid'; }
var smsuser = parent.users[command.userid]; else if (common.validateString(command.msg, 1, 160) == false) { errMsg = 'Invalid SMS message'; }
if ((smsuser == null) || (smsuser.phone == null)) break; else {
parent.parent.smsserver.sendSMS(smsuser.phone, command.msg, function (success) { smsuser = parent.users[command.userid];
// TODO if (smsuser == null) { errMsg = 'Invalid userid'; }
else if (smsuser.phone == null) { errMsg = 'No phone number for this user'; }
}
if (errMsg != null) { displayNotificationMessage(errMsg); break; }
parent.parent.smsserver.sendSMS(smsuser.phone, command.msg, function (success, msg) {
if (success) {
displayNotificationMessage('SMS succesfuly sent.');
} else {
if (typeof msg == 'string') { displayNotificationMessage('SMS error: ' + msg); } else { displayNotificationMessage('SMS error'); }
}
}); });
break; break;
} }
@ -4179,11 +4200,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
} }
// Generate a 8 digit integer with even random probability for each value. // Generate a 8 digit integer with even random probability for each value.
function getRandomEightDigitInteger() { function getRandomEightDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 100000000; }
var bigInt; function getRandomSixDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 1000000; }
do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000);
return bigInt % 100000000;
}
// Parse arguments string array into an object // Parse arguments string array into an object
function parseArgs(argv) { function parseArgs(argv) {

View File

@ -178,5 +178,17 @@
"_tlscertcheck": false, "_tlscertcheck": false,
"__tlsstrict__": "When set to true, TLS cypher setup is more limited, SSLv2 and SSLv3 are not allowed.", "__tlsstrict__": "When set to true, TLS cypher setup is more limited, SSLv2 and SSLv3 are not allowed.",
"_tlsstrict": true "_tlsstrict": true
},
"_sms": {
"provider": "twilio",
"sid": "ACxxxxxxxxx",
"auth": "xxxxxxx",
"from": "+1-555-555-5555"
},
"__sms": {
"provider": "plivo",
"id": "xxxxxxx",
"token": "xxxxxxx",
"from": "1-555-555-5555"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -306,7 +306,7 @@
<div id="p2AccountSecurity" style="display:none"> <div id="p2AccountSecurity" style="display:none">
<p><strong>Account security</strong></p> <p><strong>Account security</strong></p>
<div style="margin-left:25px"> <div style="margin-left:25px">
<div id="managePhoneNumber"><div class="p2AccountActions"><span id="authPhoneNumberCheck"><strong>&#x2713;</strong></span></div><span><a href=# onclick="return account_managePhone()">Manage phone number</a><br /></span></div> <div id="managePhoneNumber1"><div class="p2AccountActions"><span id="authPhoneNumberCheck"><strong>&#x2713;</strong></span></div><span><a href=# onclick="return account_managePhone()">Manage phone number</a><br /></span></div>
<div id="manageEmail2FA"><div class="p2AccountActions"><span id="authEmailSetupCheck"><strong>&#x2713;</strong></span></div><span><a href=# onclick="return account_manageAuthEmail()">Manage email authentication</a><br /></span></div> <div id="manageEmail2FA"><div class="p2AccountActions"><span id="authEmailSetupCheck"><strong>&#x2713;</strong></span></div><span><a href=# onclick="return account_manageAuthEmail()">Manage email authentication</a><br /></span></div>
<div id="manageAuthApp"><div class="p2AccountActions"><span id="authAppSetupCheck"><strong>&#x2713;</strong></span></div><span><a href=# onclick="return account_manageAuthApp()">Manage authenticator app</a><br /></span></div> <div id="manageAuthApp"><div class="p2AccountActions"><span id="authAppSetupCheck"><strong>&#x2713;</strong></span></div><span><a href=# onclick="return account_manageAuthApp()">Manage authenticator app</a><br /></span></div>
<div id="manageHardwareOtp"><div class="p2AccountActions"><span id="authKeySetupCheck"><strong>&#x2713;</strong></span></div><span><a href=# onclick="return account_manageHardwareOtp(0)">Manage security keys</a><br /></span></div> <div id="manageHardwareOtp"><div class="p2AccountActions"><span id="authKeySetupCheck"><strong>&#x2713;</strong></span></div><span><a href=# onclick="return account_manageHardwareOtp(0)">Manage security keys</a><br /></span></div>
@ -316,6 +316,7 @@
<div id="p2AccountActions"> <div id="p2AccountActions">
<p><strong>Account actions</strong></p> <p><strong>Account actions</strong></p>
<p class="mL"> <p class="mL">
<span id="managePhoneNumber2" style="display:none"><a href=# onclick="return account_managePhone()">Manage phone number</a><br /></span>
<span id="verifyEmailId" style="display:none"><a href=# onclick="return account_showVerifyEmail()">Verify email</a><br /></span> <span id="verifyEmailId" style="display:none"><a href=# onclick="return account_showVerifyEmail()">Verify email</a><br /></span>
<span id="accountEnableNotificationsSpan" style="display:none"><a href=# onclick="return account_enableNotifications()">Enable web notifications</a><br /></span> <span id="accountEnableNotificationsSpan" style="display:none"><a href=# onclick="return account_enableNotifications()">Enable web notifications</a><br /></span>
<a href=# onclick="return account_showLocalizationSettings()">Localization Settings</a><br /> <a href=# onclick="return account_showLocalizationSettings()">Localization Settings</a><br />
@ -1609,7 +1610,8 @@
// Update account actions // Update account actions
QV('p2AccountSecurity', ((features & 4) == 0) && (serverinfo.domainauth == false) && ((features & 4096) != 0)); // Hide Account Security if in single user mode, domain authentication to 2 factor auth not supported. QV('p2AccountSecurity', ((features & 4) == 0) && (serverinfo.domainauth == false) && ((features & 4096) != 0)); // Hide Account Security if in single user mode, domain authentication to 2 factor auth not supported.
QV('managePhoneNumber', features & 0x02000000); QV('managePhoneNumber1', (features & 0x02000000) && (features & 0x04000000));
QV('managePhoneNumber2', (features & 0x02000000) && !(features & 0x04000000));
QV('manageEmail2FA', features & 0x00800000); QV('manageEmail2FA', features & 0x00800000);
QV('p2AccountPassActions', ((features & 4) == 0) && (serverinfo.domainauth == false)); // Hide Account Actions if in single user mode or domain authentication QV('p2AccountPassActions', ((features & 4) == 0) && (serverinfo.domainauth == false)); // Hide Account Actions if in single user mode or domain authentication
//QV('p2AccountImage', ((features & 4) == 0) && (serverinfo.domainauth == false)); // If account actions are not visible, also remove the image on that panel //QV('p2AccountImage', ((features & 4) == 0) && (serverinfo.domainauth == false)); // If account actions are not visible, also remove the image on that panel
@ -1691,6 +1693,7 @@
// Check if none or at least 2 factors are enabled. // Check if none or at least 2 factors are enabled.
var authFactorCount = 0; var authFactorCount = 0;
if ((features & 0x00800000) && (userinfo.otpekey == 1)) { authFactorCount += 1; } if ((features & 0x00800000) && (userinfo.otpekey == 1)) { authFactorCount += 1; }
if ((features & 0x02000000) && (features & 0x04000000) && (userinfo.phone != null)) { authFactorCount += 1; }
if (userinfo.otpkeys == 1) { authFactorCount += 1; } if (userinfo.otpkeys == 1) { authFactorCount += 1; }
if (userinfo.otpsecret == 1) { authFactorCount += 1; } if (userinfo.otpsecret == 1) { authFactorCount += 1; }
if (userinfo.otphkeys != null) { authFactorCount += userinfo.otphkeys; } if (userinfo.otphkeys != null) { authFactorCount += userinfo.otphkeys; }
@ -2189,7 +2192,7 @@
if (xxdialogMode && (xxdialogTag != 'verifyPhone')) return; if (xxdialogMode && (xxdialogTag != 'verifyPhone')) return;
var x = '<table><tr><td><img src="images/phone80.png" style=padding:8px>'; var x = '<table><tr><td><img src="images/phone80.png" style=padding:8px>';
x += '<td>Check your phone and enter the verification code.'; x += '<td>Check your phone and enter the verification code.';
x += '<br /><br /><div style=width:100%;text-align:center>' + "Verification code:" + ' <input type=tel pattern="[0-9]" inputmode="number" maxlength=8 id=d2phoneCodeInput onKeyUp=account_managePhoneCodeValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneCodeValidate(1)"></div></table>'; x += '<br /><br /><div style=width:100%;text-align:center>' + "Verification code:" + ' <input type=tel pattern="[0-9]" inputmode="number" maxlength=6 id=d2phoneCodeInput onKeyUp=account_managePhoneCodeValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneCodeValidate(1)"></div></table>';
setDialogMode(2, "Phone Notifications", 3, account_managePhoneConfirm, x, message.cookie); setDialogMode(2, "Phone Notifications", 3, account_managePhoneConfirm, x, message.cookie);
Q('d2phoneCodeInput').focus(); Q('d2phoneCodeInput').focus();
account_managePhoneCodeValidate(); account_managePhoneCodeValidate();
@ -7896,17 +7899,17 @@
} else { } else {
x = '<table style=width:100%><tr><td style=width:56px><img src="images/phone80.png" style=padding:8px>'; x = '<table style=width:100%><tr><td style=width:56px><img src="images/phone80.png" style=padding:8px>';
x += '<td>Enter your SMS capable phone number. Once verified, the number may be used for login verification and other notifications.'; x += '<td>Enter your SMS capable phone number. Once verified, the number may be used for login verification and other notifications.';
x += '<br /><br /><div style=width:100%;text-align:center>' + "Phone number:" + ' <input type=tel pattern="[0-9]{9}" autocomplete="tel" inputmode="tel" maxlength=18 id=d2phoneinput onKeyUp=account_managePhoneValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneValidate(1)"></div></table>'; x += '<br /><br /><div style=width:100%;text-align:center>' + "Phone number:" + ' <input type=tel pattern="[0-9]" autocomplete="tel" inputmode="tel" maxlength=18 id=d2phoneinput onKeyUp=account_managePhoneValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneValidate(1)"></div></table>';
setDialogMode(2, "Phone Notifications", 3, account_managePhoneAdd, x, 'verifyPhone'); setDialogMode(2, "Phone Notifications", 3, account_managePhoneAdd, x, 'verifyPhone');
Q('d2phoneinput').focus(); Q('d2phoneinput').focus();
account_managePhoneValidate(); account_managePhoneValidate();
} }
} }
function isPhoneNumber(x) { return x.match(/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/) } function isPhoneNumber(x) { return x.match(/^\(?([0-9]{3,4})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/) }
function account_managePhoneValidate(x) { var ok = isPhoneNumber(Q('d2phoneinput').value); QE('idx_dlgOkButton', ok); if ((x == 1) && ok) { dialogclose(1); } } function account_managePhoneValidate(x) { var ok = isPhoneNumber(Q('d2phoneinput').value); QE('idx_dlgOkButton', ok); if ((x == 1) && ok) { dialogclose(1); } }
function account_managePhoneCodeValidate(x) { var ok = Q('d2phoneCodeInput').value.match(/[0-9]/); QE('idx_dlgOkButton', ok); if ((x == 1) && ok) { dialogclose(1); } } function account_managePhoneCodeValidate(x) { var ok = (Q('d2phoneCodeInput').value.length == 6) && Q('d2phoneCodeInput').value.match(/[0-9]/); QE('idx_dlgOkButton', ok); if ((x == 1) && ok) { dialogclose(1); } }
function account_managePhoneConfirm(b, tag) { meshserver.send({ action: 'confirmPhone', code: parseInt(Q('d2phoneCodeInput').value), cookie: tag }); } function account_managePhoneConfirm(b, tag) { meshserver.send({ action: 'confirmPhone', code: Q('d2phoneCodeInput').value, cookie: tag }); }
function account_managePhoneAdd() { if (isPhoneNumber(Q('d2phoneinput').value) == false) return; QE('d2phoneinput', false); meshserver.send({ action: 'verifyPhone', phone: Q('d2phoneinput').value }); } function account_managePhoneAdd() { if (isPhoneNumber(Q('d2phoneinput').value) == false) return; QE('d2phoneinput', false); meshserver.send({ action: 'verifyPhone', phone: Q('d2phoneinput').value }); }
function account_managePhoneRemove() { if (Q('d2delPhone').checked) { meshserver.send({ action: 'removePhone' }); } } function account_managePhoneRemove() { if (Q('d2delPhone').checked) { meshserver.send({ action: 'removePhone' }); } }
function account_managePhoneRemoveValidate() { QE('idx_dlgOkButton', Q('d2delPhone').checked); } function account_managePhoneRemoveValidate() { QE('idx_dlgOkButton', Q('d2delPhone').checked); }
@ -8159,7 +8162,7 @@
if ((userinfo.emailVerified !== true) && (serverinfo.emailcheck == true) && (userinfo.siteadmin != 0xFFFFFFFF)) { setDialogMode(2, "Account Security", 1, null, "Unable to access a device until a email address is verified. This is required for password recovery. Go to the \"My Account\" tab to change and verify an email address."); return false; } if ((userinfo.emailVerified !== true) && (serverinfo.emailcheck == true) && (userinfo.siteadmin != 0xFFFFFFFF)) { setDialogMode(2, "Account Security", 1, null, "Unable to access a device until a email address is verified. This is required for password recovery. Go to the \"My Account\" tab to change and verify an email address."); return false; }
// Remind the user to add two factor authentication // Remind the user to add two factor authentication
if ((features & 0x00040000) && !((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0) || (userinfo.otpkeys > 0) || ((features & 0x00800000) && (userinfo.otpekey == 1)))) { setDialogMode(2, "Account Security", 1, null, "Unable to access a device until two-factor authentication is enabled. This is required for extra security. Go to the \"My Account\" tab and look at the \"Account Security\" section."); return false; } if ((features & 0x00040000) && !((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0) || (userinfo.otpkeys > 0) || ((features & 0x02000000) && (features & 0x04000000) && (userinfo.phone != null)) || ((features & 0x00800000) && (userinfo.otpekey == 1)))) { setDialogMode(2, "Account Security", 1, null, "Unable to access a device until two-factor authentication is enabled. This is required for extra security. Go to the \"My Account\" tab and look at the \"Account Security\" section."); return false; }
// We are allowed, let's prompt to information // We are allowed, let's prompt to information
var x = "Create a new device group using the options below." + '<br /><br />'; var x = "Create a new device group using the options below." + '<br /><br />';
@ -9798,10 +9801,9 @@
} }
} }
if ((user.otpsecret > 0) || (user.otphkeys > 0)) { username += ' <img src="images/key12.png" height=12 width=11 title="' + "2nd factor authentication enabled" + '" style="margin-top:2px" />'; } if ((user.otpsecret > 0) || (user.otphkeys > 0) || ((user.otpekey == 1) && (features & 0x00800000)) || ((user.phone != null) && (features & 0x04000000))) { username += ' <img src="images/key12.png" height=12 width=11 title="' + "2nd factor authentication enabled" + '" style="margin-top:2px" />'; }
if (user.phone != null) { username += ' <img src="images/phone12.png" height=12 width=7 title="' + "Verified phone number" + '" style="margin-top:2px" />'; } if (user.phone != null) { username += ' <img src="images/phone12.png" height=12 width=7 title="' + "Verified phone number" + '" style="margin-top:2px" />'; }
if ((user.siteadmin != null) && ((user.siteadmin & 32) != 0) && (user.siteadmin != 0xFFFFFFFF)) { username += ' <img src="images/padlock12.png" height=12 width=8 title="' + "Account is locked" + '" style="margin-top:2px" />'; } if ((user.siteadmin != null) && ((user.siteadmin & 32) != 0) && (user.siteadmin != 0xFFFFFFFF)) { username += ' <img src="images/padlock12.png" height=12 width=8 title="' + "Account is locked" + '" style="margin-top:2px" />'; }
x += '<tr tabindex=0 onmouseover=userMouseHover(this,1) onmouseout=userMouseHover(this,0) onkeypress="if (event.key==\'Enter\') gotoUser(\'' + encodeURIComponent(user._id) + '\')"><td>'; x += '<tr tabindex=0 onmouseover=userMouseHover(this,1) onmouseout=userMouseHover(this,0) onkeypress="if (event.key==\'Enter\') gotoUser(\'' + encodeURIComponent(user._id) + '\')"><td>';
x += '<div class=bar>'; x += '<div class=bar>';
x += '<div class=baricon><input class=UserCheckbox value=' + encodeURIComponent(user._id) + ' onclick=p3updateInfo() type=checkbox' + ((user._id == userinfo._id)?' disabled':'') + '></div><div style=cursor:pointer onclick=gotoUser(\"' + encodeURIComponent(user._id) + '\")>'; x += '<div class=baricon><input class=UserCheckbox value=' + encodeURIComponent(user._id) + ' onclick=p3updateInfo() type=checkbox' + ((user._id == userinfo._id)?' disabled':'') + '></div><div style=cursor:pointer onclick=gotoUser(\"' + encodeURIComponent(user._id) + '\")>';
@ -9910,7 +9912,7 @@
function showSendSMS(userid) { function showSendSMS(userid) {
if (xxdialogMode) return; if (xxdialogMode) return;
setDialogMode(2, "Send SMS", 3, showSendSMSEx, '<textarea id=d2smsText maxlength=160 style=background-color:#fcf3cf;width:100%;height:100px;resize:none onKeyUp=showSendSMSValidate()></textarea><span style=font-size:10px><span>', decodeURIComponent(userid)); setDialogMode(2, "Send SMS", 3, showSendSMSEx, '<textarea id=d2smsText maxlength=160 style=background-color:#fcf3cf;width:100%;height:100px;resize:none onKeyUp=showSendSMSValidate()></textarea><span style=font-size:10px><span>', userid);
Q('d2smsText').focus(); Q('d2smsText').focus();
showSendSMSValidate(); showSendSMSValidate();
} }
@ -10661,6 +10663,7 @@
if (user.otpsecret > 0) { factors.push("Authentication App"); } if (user.otpsecret > 0) { factors.push("Authentication App"); }
if (user.otphkeys > 0) { factors.push("Security Key"); } if (user.otphkeys > 0) { factors.push("Security Key"); }
if (user.otpkeys > 0) { factors.push("Backup Codes"); } if (user.otpkeys > 0) { factors.push("Backup Codes"); }
if ((user.phone != null) && (features & 0x04000000)) { factors.push("SMS"); }
x += addDeviceAttribute("Security", '<img src="images/key12.png" height=12 width=11 title=\"' + "2nd factor authentication enabled" + '\" style="margin-top:2px" /> ' + factors.join(', ')); x += addDeviceAttribute("Security", '<img src="images/key12.png" height=12 width=11 title=\"' + "2nd factor authentication enabled" + '\" style="margin-top:2px" /> ' + factors.join(', '));
} }
@ -10668,7 +10671,7 @@
// Add action buttons // Add action buttons
x += '<input type=button value=\"' + "Notes" + '\" title=\"' + "View notes about this user" + '\" onclick=showNotes(false,"' + userid + '") />'; x += '<input type=button value=\"' + "Notes" + '\" title=\"' + "View notes about this user" + '\" onclick=showNotes(false,"' + userid + '") />';
if (user.phone && (features & 0x02000000)) { x += '<input type=button value=\"' + "SMS" + '\" title=\"' + "Send a SMS message to this user" + '\" onclick=showSendSMS("' + encodeURIComponent(userid) + '") />'; } if (user.phone && (features & 0x02000000)) { x += '<input type=button value=\"' + "SMS" + '\" title=\"' + "Send a SMS message to this user" + '\" onclick=showSendSMS("' + userid + '") />'; }
if (!self && (activeSessions > 0)) { x += '<input type=button value=\"' + "Notify" + '\" title=\"' + "Send user notification" + '\" onclick=showUserAlertDialog(event,"' + userid + '") />'; } if (!self && (activeSessions > 0)) { x += '<input type=button value=\"' + "Notify" + '\" title=\"' + "Send user notification" + '\" onclick=showUserAlertDialog(event,"' + userid + '") />'; }
// Setup the panel // Setup the panel
@ -11459,7 +11462,7 @@
x += '<div><label><input type=checkbox id=p41c14 ' + ((serverTraceSources.indexOf('agentupdate') >= 0) ? 'checked' : '') + '>' + "MeshAgent update" + '</label></div>'; x += '<div><label><input type=checkbox id=p41c14 ' + ((serverTraceSources.indexOf('agentupdate') >= 0) ? 'checked' : '') + '>' + "MeshAgent update" + '</label></div>';
x += '<div><label><input type=checkbox id=p41c16 ' + ((serverTraceSources.indexOf('cert') >= 0) ? 'checked' : '') + '>' + "Server Certificate" + '</label></div>'; x += '<div><label><input type=checkbox id=p41c16 ' + ((serverTraceSources.indexOf('cert') >= 0) ? 'checked' : '') + '>' + "Server Certificate" + '</label></div>';
x += '<div><label><input type=checkbox id=p41c17 ' + ((serverTraceSources.indexOf('db') >= 0) ? 'checked' : '') + '>' + "Server Database" + '</label></div>'; x += '<div><label><input type=checkbox id=p41c17 ' + ((serverTraceSources.indexOf('db') >= 0) ? 'checked' : '') + '>' + "Server Database" + '</label></div>';
x += '<div><label><input type=checkbox id=p41c18 ' + ((serverTraceSources.indexOf('email') >= 0) ? 'checked' : '') + '>' + "Email Traffic" + '</label></div>'; x += '<div><label><input type=checkbox id=p41c18 ' + ((serverTraceSources.indexOf('email') >= 0) ? 'checked' : '') + '>' + "Email/SMS Traffic" + '</label></div>';
x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:5px"><b>' + "Web Server" + '</b></div>'; x += '<div style="width:100%;border-bottom:1px solid gray;margin-bottom:5px;margin-top:5px"><b>' + "Web Server" + '</b></div>';
x += '<div><label><input type=checkbox id=p41c5 ' + ((serverTraceSources.indexOf('web') >= 0) ? 'checked' : '') + '>' + "Web Server" + '</label></div>'; x += '<div><label><input type=checkbox id=p41c5 ' + ((serverTraceSources.indexOf('web') >= 0) ? 'checked' : '') + '>' + "Web Server" + '</label></div>';
x += '<div><label><input type=checkbox id=p41c6 ' + ((serverTraceSources.indexOf('webrequest') >= 0) ? 'checked' : '') + '>' + "Web Server Requests" + '</label></div>'; x += '<div><label><input type=checkbox id=p41c6 ' + ((serverTraceSources.indexOf('webrequest') >= 0) ? 'checked' : '') + '>' + "Web Server Requests" + '</label></div>';

View File

@ -163,6 +163,7 @@
<div style=float:right> <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=securityKeyButton type=button value="Use Security Key" onclick="useSecurityKey()" />
<input style="display:none;float:right" id=emailKeyButton type=button value="Email" onclick="useEmailToken()" /> <input style="display:none;float:right" id=emailKeyButton type=button value="Email" onclick="useEmailToken()" />
<input style="display:none;float:right" id=smsKeyButton type=button value="SMS" onclick="useSMSToken()" />
</div> </div>
</td> </td>
</tr> </tr>
@ -299,10 +300,11 @@
var nightMode = (getstore('_nightMode', '0') == '1'); var nightMode = (getstore('_nightMode', '0') == '1');
var publicKeyCredentialRequestOptions = null; var publicKeyCredentialRequestOptions = null;
var otpemail = ('{{{otpemail}}}' === 'true'); var otpemail = ('{{{otpemail}}}' === 'true');
var otpsms = ('{{{otpsms}}}' === '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.", "Email sent.", "Email verification required, check your mailbox and click the confirmation link."]; var okmessages = ['', "Hold on, reset mail sent.", "Email sent.", "Email verification required, check your mailbox and click the confirmation link.", "SMS 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 = '';
@ -380,6 +382,7 @@
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)); QV('emailKeyButton', otpemail && (messageid != 2));
QV('smsKeyButton', otpsms && (messageid != 2));
} }
if (loginMode == '5') { if (loginMode == '5') {
@ -459,6 +462,17 @@
Q('tokenOkButton').click(); Q('tokenOkButton').click();
} }
function useSMSToken() {
if (otpsms != true) return;
setDialogMode(1, "Secure Login", 3, useSMSTokenEx, "Send token to registed phone number?");
}
function useSMSTokenEx() {
Q('hwtokenInput').value = '**sms**';
QE('tokenOkButton', true);
Q('tokenOkButton').click();
}
function showPassHint(e) { function showPassHint(e) {
messagebox("Password Hint", passhint); messagebox("Password Hint", passhint);
haltEvent(e); haltEvent(e);

View File

@ -598,8 +598,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
} }
} }
// See if SMS 2FA is available
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
// Check if a 2nd factor is present // Check if a 2nd factor is present
return ((parent.config.settings.no2factorauth !== true) && ((user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (parent.mailserver != null) && (user.otpekey != null)) || ((user.otphkeys != null) && (user.otphkeys.length > 0)))); return ((parent.config.settings.no2factorauth !== true) && (sms2fa || (user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (parent.mailserver != null) && (user.otpekey != null)) || ((user.otphkeys != null) && (user.otphkeys.length > 0))));
} }
// Check the 2-step auth token // Check the 2-step auth token
@ -611,6 +614,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// Check if we can use OTP tokens with email // Check if we can use OTP tokens with email
var otpemail = (parent.mailserver != null); var otpemail = (parent.mailserver != null);
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; } if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
var otpsms = (parent.smsserver != null);
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
// Check email key // Check email key
if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) { if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) {
@ -624,6 +629,18 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
} }
} }
// Check sms key
if ((otpsms) && (user.phone != null) && (user.otpsms != null) && (user.otpsms.d != null) && (user.otpsms.k === token)) {
var deltaTime = (Date.now() - user.otpsms.d);
if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the SMS token (10000 * 60 * 5).
delete user.otpsms;
obj.db.SetUser(user);
parent.debug('web', 'checkUserOneTimePassword: success (SMS).');
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;
@ -776,9 +793,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if (userid) { if (userid) {
var user = obj.users[userid]; var user = obj.users[userid];
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null));
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
// 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)) { if ((req.body.hwtoken == '**email**') && email2fa) {
user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() }; user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
obj.db.SetUser(user); obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA email to: ' + user.email); parent.debug('web', 'Sending 2FA email to: ' + user.email);
@ -789,6 +809,19 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
return; return;
} }
if ((req.body.hwtoken == '**sms**') && sms2fa) {
// Cause a token to be sent to the user's phone number
user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm sms was sent
req.session.messageid = 4; // "SMS 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;
@ -809,6 +842,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
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.tokenemail = ((user.email != null) && (user.emailVerified == true) && (parent.mailserver != null) && (user.otpekey != null));
req.session.tokensms = ((user.phone != null) && (parent.smsserver != 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)); }
@ -920,7 +954,6 @@ 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;
@ -1140,7 +1173,6 @@ 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;
@ -1839,6 +1871,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed
if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes
if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported
if ((parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))) { features += 0x04000000; } // SMS 2FA is allowed
// Create a authentication cookie // Create a authentication cookie
const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: cleanRemoteAddr(req.ip) }, obj.parent.loginCookieEncryptionKey); const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: cleanRemoteAddr(req.ip) }, obj.parent.loginCookieEncryptionKey);
@ -1936,9 +1969,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// Check if we can use OTP tokens with email // Check if we can use OTP tokens with email
var otpemail = (parent.mailserver != null) && (req.session != null) && (req.session.tokenemail != null); var otpemail = (parent.mailserver != null) && (req.session != null) && (req.session.tokenemail != null);
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; } if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tokensms != null);
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
// Render the login page // Render the login page
render(req, res, getRenderPage('login', req, domain), 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)); render(req, res, getRenderPage('login', req, domain), 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, otpsms: otpsms }, domain));
} }
// Handle a post request on the root // Handle a post request on the root
@ -4014,7 +4049,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if (checkUserOneTimePasswordRequired(domain, user, req) == true) { if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
// Figure out if email 2FA is allowed // Figure out if email 2FA is allowed
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null) && (user.otpekey != null)); var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null) && (user.otpekey != null));
if ((typeof req.query.token != 'string') || (req.query.token == '**email**')) { var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
if ((typeof req.query.token != 'string') || (req.query.token == '**email**') || (req.query.token == '**sms**')) {
if ((req.query.token == '**email**') && (email2fa == true)) { if ((req.query.token == '**email**') && (email2fa == true)) {
// Cause a token to be sent to the user's registered email // Cause a token to be sent to the user's registered email
user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() }; user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
@ -4023,6 +4059,14 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
parent.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req)); parent.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm email was sent // Ask for a login token & confirm email was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, email2fasent: true })); ws.close(); } catch (e) { } try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, email2fasent: true })); ws.close(); } catch (e) { }
} else if ((req.query.token == '**sms**') && (sms2fa == true)) {
// Cause a token to be sent to the user's phone number
user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm sms was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', sms2fa: sms2fa, sms2fasent: true })); ws.close(); } catch (e) { }
} else { } else {
// Ask for a login token // Ask for a login token
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa })); ws.close(); } catch (e) { } try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa })); ws.close(); } catch (e) { }
@ -4097,6 +4141,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if (checkUserOneTimePasswordRequired(domain, user, req) == true) { if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
// Figure out if email 2FA is allowed // Figure out if email 2FA is allowed
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null) && (user.otpekey != null)); var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null) && (user.otpekey != null));
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
if (s.length != 3) { if (s.length != 3) {
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa })); ws.close(); } catch (e) { } try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa })); ws.close(); } catch (e) { }
} else { } else {
@ -4110,6 +4155,14 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
parent.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req)); parent.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm email was sent // Ask for a login token & confirm email was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, email2fasent: true })); ws.close(); } catch (e) { } try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, email2fasent: true })); ws.close(); } catch (e) { }
} else if ((s[2] == '**sms**') && (sms2fa == true)) {
// Cause a token to be sent to the user's phone number
user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm sms was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', sms2fa: sms2fa, sms2fasent: true })); ws.close(); } catch (e) { }
} else { } else {
// Ask for a login token // Ask for a login token
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa })); ws.close(); } catch (e) { } try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa })); ws.close(); } catch (e) { }
@ -4621,6 +4674,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;
delete user2.otpsms;
if ((typeof user2.otpekey == 'object') && (user2.otpekey != null)) { user2.otpekey = 1; } // Indicates that email 2FA is enabled. if ((typeof user2.otpekey == 'object') && (user2.otpekey != 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.
@ -4974,6 +5028,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// Generate a 8 digit integer with even random probability for each value. // 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; } function getRandomEightDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 100000000; }
function getRandomSixDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 1000000; }
// 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; } }