More work on mobile 2FA.

This commit is contained in:
Ylian Saint-Hilaire 2021-04-13 23:23:09 -07:00
parent da33f1dfb1
commit c74a207606
11 changed files with 133 additions and 10 deletions

View File

@ -99,6 +99,7 @@
<Compile Include="amt\amt.js" />
<Compile Include="exeHandler.js" />
<Compile Include="amtprovisioningserver.js" />
<Compile Include="firebase.js" />
<Compile Include="letsencrypt.js" />
<Compile Include="mcrec.js" />
<Compile Include="meshaccelerator.js" />

View File

@ -90,8 +90,16 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) {
//var payload = { notification: { title: command.title, body: command.msg }, data: { url: obj.msgurl } };
//var options = { priority: 'High', timeToLive: 5 * 60 }; // TTL: 5 minutes, priority 'Normal' or 'High'
// Send an outbound push notification
obj.sendToDevice = function (node, payload, options, func) {
if (typeof node == 'string') {
parent.db.Get(node, function (err, docs) { if ((err == null) && (docs != null) && (docs.length == 1)) { obj.sendToDeviceEx(docs[0], payload, options, func); } else { func(0, 'error'); } })
} else {
obj.sendToDeviceEx(node, payload, options, func);
}
}
// Send an outbound push notification
obj.sendToDeviceEx = function (node, payload, options, func) {
parent.debug('email', 'Firebase-sendToDevice');
if ((node == null) || (typeof node.pmt != 'string')) return;
obj.log('sendToDevice, node:' + node._id + ', payload: ' + JSON.stringify(payload) + ', options: ' + JSON.stringify(options));
@ -270,6 +278,14 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
}
obj.sendToDevice = function (node, payload, options, func) {
if (typeof node == 'string') {
parent.db.Get(node, function (err, docs) { if ((err == null) && (docs != null) && (docs.length == 1)) { obj.sendToDeviceEx(docs[0], payload, options, func); } else { func(0, 'error'); } })
} else {
obj.sendToDeviceEx(node, payload, options, func);
}
}
obj.sendToDeviceEx = function (node, payload, options, func) {
parent.debug('email', 'Firebase-sendToDevice-webSocket');
if ((node == null) || (typeof node.pmt != 'string')) { func(0, 'error'); return; }
obj.log('sendToDevice, node:' + node._id + ', payload: ' + JSON.stringify(payload) + ', options: ' + JSON.stringify(options));
@ -298,7 +314,16 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
} else if (relayUrl.protocol == 'https:') {
// Send an outbound push notification using an HTTPS POST
obj.pushOnly = true;
obj.sendToDevice = function (node, payload, options, func) {
if (typeof node == 'string') {
parent.db.Get(node, function (err, docs) { if ((err == null) && (docs != null) && (docs.length == 1)) { obj.sendToDeviceEx(docs[0], payload, options, func); } else { func(0, 'error'); } })
} else {
obj.sendToDeviceEx(node, payload, options, func);
}
}
obj.sendToDeviceEx = function (node, payload, options, func) {
parent.debug('email', 'Firebase-sendToDevice-httpPost');
if ((node == null) || (typeof node.pmt != 'string')) return;

View File

@ -1556,6 +1556,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
// Complete 2FA checking
if (authCookie.a == 'checkAuth') {
// TODO
console.log(authCookie);
}
break;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -394,7 +394,7 @@ body {
color: blue;
}
#loginpanel, #createpanel, #resetpanel, #tokenpanel, #resettokenpanel, #resetpasswordpanel, #resetpasswordpanel, #checkemailpanel {
#loginpanel, #createpanel, #resetpanel, #tokenpanel, #resettokenpanel, #resetpasswordpanel, #resetpasswordpanel, #checkemailpanel, #waitpushpanel {
display: inline-block;
margin: 0;
background-color: #979797;

View File

@ -2032,6 +2032,7 @@
QV('authKeySetupCheck', userinfo.otphkeys > 0);
QV('authPushAuthDevCheck', (userinfo.otpdev > 0) && ((features2 & 2) != 0));
QV('authCodesSetupCheck', userinfo.otpkeys > 0);
QV('managePushAuthDev', (features2 & 2) && (count2factoraAuths() > 0));
mainUpdate(4 + 128 + 4096);
// Check if none or at least 2 factors are enabled.

View File

@ -186,6 +186,7 @@
<img id=securityKeyButton src="images/login/2fa-key-48.png" srcset="images/login/2fa-key-96.png 2x" title="Use Security Key" loading="lazy" width="48" height="48" style="display:none;margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer;background-color:#FFF" onclick="useSecurityKey(1)" />
<img id=smsKeyButton src="images/login/2fa-sms-48.png" srcset="images/login/2fa-sms-96.png 2x" title="SMS" loading="lazy" width="48" height="48" style="display:none;margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer;background-color:#FFF" onclick="useSMSToken(1)" />
<img id=emailKeyButton src="images/login/2fa-mail-48.png" srcset="images/login/2fa-mail-96.png 2x" title="Email" loading="lazy" width="48" height="48" style="display:none;margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer;background-color:#FFF" onclick="useEmailToken(1)" />
<img id=pushKeyButton src="images/login/2fa-push-48.png" srcset="images/login/2fa-push-96.png 2x" title="Device Authentication" loading="lazy" width="48" height="48" style="display:none;margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer;background-color:#FFF" onclick="usePushToken(1)" />
</div>
</td>
</tr>
@ -219,6 +220,7 @@
<img id=securityKeyButton2 src="images/login/2fa-key-48.png" srcset="images/login/2fa-key-96.png 2x" title="Use Security Key" loading="lazy" width="48" height="48" style="display:none;margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer;background-color:#FFF" onclick="useSecurityKey(2)" />
<img id=smsKeyButton2 src="images/login/2fa-sms-48.png" srcset="images/login/2fa-sms-96.png 2x" title="SMS" loading="lazy" width="48" height="48" style="display:none;margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer;background-color:#FFF" onclick="useSMSToken(2)" />
<img id=emailKeyButton2 src="images/login/2fa-mail-48.png" srcset="images/login/2fa-mail-96.png 2x" title="Email" loading="lazy" width="48" height="48" style="display:none;margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer;background-color:#FFF" onclick="useEmailToken(2)" />
<img id=pushKeyButton2 src="images/login/2fa-push-48.png" srcset="images/login/2fa-push-96.png 2x" title="Device Authentication" loading="lazy" width="48" height="48" style="display:none;margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer;background-color:#FFF" onclick="usePushToken(2)" />
</div>
</td>
</tr>
@ -281,6 +283,22 @@
<input id=checkemailformargs name="urlargs" type="hidden" value="" />
</form>
</div>
<div id=waitpushpanel style="display:none">
<div id=message8></div>
<table style="width:100%">
<tr>
<td style="align-content:center;padding-top:10px">
<img src="images/login/push-150.png" srcset="images/login/push-300.png 2x" loading="lazy" width="265" height="150" />
</td>
</tr>
<tr>
<td style="align-content:center;padding-top:10px">
<label id=tokenInputRememberLabel2><input id=tokenInputRemember2 name=remembertoken type=checkbox /><span id=tokenInputRememberSpan2></span></label>
</td>
</tr>
</table>
<hr /><a onclick="return xgo(1,event);" href="#" style=cursor:pointer>Back to login</a>
</div>
</td>
</tr>
<tr id="welcomeTextRow"><td><div id="welcomeText" style="color:white;text-align:center;margin-left:20px;margin-right:20px"></div></td></tr>
@ -330,6 +348,7 @@
var publicKeyCredentialRequestOptions = null;
var otpemail = (decodeURIComponent('{{{otpemail}}}') === 'true');
var otpsms = (decodeURIComponent('{{{otpsms}}}') === 'true');
var otppush = (decodeURIComponent('{{{otppush}}}') === 'true');
var twoFactorCookieDays = parseInt('{{{twoFactorCookieDays}}}');
var authStrategies = '{{{authStrategies}}}'.split(',');
@ -342,15 +361,16 @@
// Display the right server message
var i;
var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "If valid, 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.", "Server under maintenance."];
var okmessages = ['', "If valid, reset mail sent.", "Email sent.", "Email verification required, check your mailbox and click the confirmation link.", "SMS sent.", "Notification sent, {0}."];
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.", "Server under maintenance.", "Unable to send device notification."];
if (messageid > 0) {
var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }
else if ((messageid >= 100) && ((messageid - 100) < failmessages.length)) { msg = failmessages[messageid - 100]; }
if (messageid == 5) { msg = format(msg, passhint); }
if (msg != '') {
if (messageid >= 100) { msg = ('<span class="msg error"><b style=color:#8C001A>' + msg + '<b></span><br /><br />'); } else { msg = ('<span class="msg success"><b>' + msg + '</b></span><br /><br />'); }
for (i = 1; i < 8; i++) { QH('message' + i, msg); }
for (i = 1; i < 9; i++) { QH('message' + i, msg); }
}
}
@ -369,8 +389,11 @@
if (twoFactorCookieDays > 0) {
QV('tokenInputRememberLabel', true);
QH('tokenInputRememberSpan', format("Remember this device for {0} days.", twoFactorCookieDays));
QV('tokenInputRememberLabel2', true);
QH('tokenInputRememberSpan2', format("Remember this device for {0} days.", twoFactorCookieDays));
} else {
QV('tokenInputRememberLabel', false);
QV('tokenInputRememberLabel2', false);
}
// If URL arguments are provided, add them to form posts
@ -443,10 +466,12 @@
var twofakey = (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn');
var emailkey = otpemail && (messageid != 2) && (messageid != 4);
var smskey = otpsms && (messageid != 2) && (messageid != 4);
var pushkey = otppush && (messageid != 2) && (messageid != 4);
QV('securityKeyButton', twofakey);
QV('emailKeyButton', emailkey);
QV('smsKeyButton', smskey);
QV('2farow', twofakey || emailkey || smskey);
QV('pushKeyButton', pushkey);
QV('2farow', twofakey || emailkey || smskey || pushkey);
}
if (loginMode == '5') {
@ -454,10 +479,12 @@
var twofakey = (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn');
var emailkey = otpemail && (messageid != 2) && (messageid != 4);
var smskey = otpsms && (messageid != 2) && (messageid != 4);
var pushkey = otppush && (messageid != 2) && (messageid != 4);
QV('securityKeyButton2', twofakey);
QV('emailKeyButton2', emailkey);
QV('smsKeyButton2', smskey);
QV('2farow2', twofakey || emailkey || smskey);
QV('pushKeyButton', pushkey);
QV('2farow2', twofakey || emailkey || smskey || pushkey);
}
/*
@ -565,6 +592,18 @@
}
}
function usePushToken(panelAction) {
if (panelAction == 1) {
Q('hwtokenInput').value = '**push**';
QE('tokenOkButton', true);
Q('tokenOkButton').click();
} else if (panelAction == 2) {
Q('resetHwtokenInput').value = '**push**';
QE('resetTokenOkButton', true);
Q('resetTokenOkButton').click();
}
}
function showPassHint(e) {
messagebox("Password Hint", passhint);
haltEvent(e);
@ -595,6 +634,7 @@
QV('resettokenpanel', x == 5);
QV('resetpasswordpanel', x == 6);
QV('checkemailpanel', x == 7);
QV('waitpushpanel', x == 8);
if (x == 1) { Q('username').focus(); }
if (x == 2) { if (features & 0x200000) { Q('aemail').focus(); } else { Q('ausername').focus(); } } // Email is username
if (x == 3) { Q('remail').focus(); }

View File

@ -924,6 +924,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.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));
var push2fa = ((parent.firebase != null) && (user.otpdev != null));
// Check if this user has 2-step login active
if ((req.session.loginmode != '6') && checkUserOneTimePasswordRequired(domain, user, req)) {
@ -951,6 +952,29 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
return;
}
if ((req.body.hwtoken == '**push**') && push2fa) {
// Cause push notification to device
const logincode = obj.common.zeroPad(getRandomSixDigitInteger(), 6);
const code = Buffer.from(logincode).toString('base64');
const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev });
var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
if (err == null) {
// Request that the login page wait for device auth
req.session.messageid = 5; // "Notification sent." message
req.session.passhint = logincode;
req.session.loginmode = '8';
} else {
// Indicate the push notification failed
req.session.messageid = 116; // "Unable to send device notification." 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) {
if (result == false) {
var randomWaitTime = 0;
@ -973,6 +997,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
req.session.loginmode = '4';
req.session.tokenemail = ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null));
req.session.tokensms = ((user.phone != null) && (parent.smsserver != null));
req.session.tokenpush = ((user.otpdev != null) && (parent.firebase != null));
req.session.tokenuserid = userid;
req.session.tokenusername = xusername;
req.session.tokenpassword = xpassword;
@ -1097,6 +1122,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
delete req.session.tokenpassword;
delete req.session.tokenemail;
delete req.session.tokensms;
delete req.session.tokenpush;
delete req.session.messageid;
delete req.session.passhint;
delete req.session.cuserid;
@ -1301,6 +1327,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
delete req.session.resettokenpassword;
delete req.session.tokenemail;
delete req.session.tokensms;
delete req.session.tokenpush;
delete req.session.messageid;
delete req.session.passhint;
delete req.session.cuserid;
@ -1382,6 +1409,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
delete req.session.resettokenpassword;
delete req.session.tokenemail;
delete req.session.tokensms;
delete req.session.tokenpush;
delete req.session.messageid;
delete req.session.passhint;
delete req.session.cuserid;
@ -2638,7 +2666,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
var passhint = null, msgid = 0;
if (req.session != null) {
msgid = req.session.messageid;
if ((loginmode == '7') || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); }
if ((msgid == 5) || (loginmode == '7') || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); }
delete req.session.messageid;
delete req.session.passhint;
}
@ -2658,6 +2686,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tokensms == true);
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tokenpush == true);
//if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; }
// See if we support two-factor trusted cookies
var twoFactorCookieDays = 30;
@ -2704,6 +2734,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
hwstate: hwstate,
otpemail: otpemail,
otpsms: otpsms,
otppush: otppush,
twoFactorCookieDays: twoFactorCookieDays,
authStrategies: authStrategies.join(','),
loginpicture: (typeof domain.loginpicture == 'string')
@ -5844,7 +5875,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// Figure out if email 2FA is allowed
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
if ((typeof command.token != 'string') || (command.token == '**email**') || (command.token == '**sms**')) {
//var push2fa = ((parent.firebase != null) && (user.otpdev != null));
if ((typeof command.token != 'string') || (command.token == '**email**') || (command.token == '**sms**')/* || (command.token == '**push**')*/) {
if ((command.token == '**email**') && (email2fa == true)) {
// Cause a token to be sent to the user's registered email
user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
@ -5861,6 +5893,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
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', email2fa: email2fa, sms2fa: sms2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
/*
} else if ((command.token == '**push**') && (push2fa == true)) {
// Cause push notification to device
const code = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev });
var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
if (err == null) { parent.debug('email', 'Successfully auth check send push message to device'); } else { parent.debug('email', 'Failed auth check push message to device, error: ' + errdesc); }
});
*/
} else {
// Ask for a login token
parent.debug('web', 'Asking for login token');
@ -5965,7 +6008,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// Figure out if email 2FA is allowed
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
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**')) {
//var push2fa = ((parent.firebase != null) && (user.otpdev != null));
if ((typeof req.query.token != 'string') || (req.query.token == '**email**') || (req.query.token == '**sms**')/* || (req.query.token == '**push**')*/) {
if ((req.query.token == '**email**') && (email2fa == true)) {
// Cause a token to be sent to the user's registered email
user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
@ -5982,6 +6026,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
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', email2fa: email2fa, sms2fa: sms2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
/*
} else if ((command.token == '**push**') && (push2fa == true)) {
// Cause push notification to device
const code = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev });
var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
if (err == null) { parent.debug('email', 'Successfully auth check send push message to device'); } else { parent.debug('email', 'Failed auth check push message to device, error: ' + errdesc); }
});
*/
} else {
// Ask for a login token
parent.debug('web', 'Asking for login token');