More work on device 2FA.

This commit is contained in:
Ylian Saint-Hilaire 2021-04-14 14:06:31 -07:00
parent 48d5abca40
commit 69fd9dffe2
3 changed files with 26 additions and 46 deletions

View File

@ -339,10 +339,13 @@
"numeric": { "type": "integer", "description": "Minimum number of numeric characters required in the password." }, "numeric": { "type": "integer", "description": "Minimum number of numeric characters required in the password." },
"nonalpha": { "type": "integer", "description": "Minimum number of non-alpha-numeric characters required in the password." }, "nonalpha": { "type": "integer", "description": "Minimum number of non-alpha-numeric characters required in the password." },
"reset": { "type": "integer", "description": "Number of days after which the user is required to change the account password." }, "reset": { "type": "integer", "description": "Number of days after which the user is required to change the account password." },
"force2factor": { "type": "boolean", "description": "Requires that all accounts setup 2FA." }, "email2factor": { "type": "boolean", "default": true, "description": "Set to false to disable email 2FA." },
"sms2factor": { "type": "boolean", "default": true, "description": "Set to false to disable SMS 2FA." },
"push2factor": { "type": "boolean", "default": true, "description": "Set to false to disable push notification 2FA." },
"force2factor": { "type": "boolean", "default": false, "description": "Requires that all accounts setup 2FA." },
"skip2factor": { "type": "string", "description": "IP addresses where 2FA login is skipped, for example: 127.0.0.1,192.168.2.0/24" }, "skip2factor": { "type": "string", "description": "IP addresses where 2FA login is skipped, for example: 127.0.0.1,192.168.2.0/24" },
"oldPasswordBan": { "type": "integer", "description": "Number of old passwords the server should remember and not allow the user to switch back to." }, "oldPasswordBan": { "type": "integer", "description": "Number of old passwords the server should remember and not allow the user to switch back to." },
"banCommonPasswords": { "type": "boolean", "description": "Uses WildLeek to block use of the 10000 most commonly used passwords." } "banCommonPasswords": { "type": "boolean", "default": false, "description": "Uses WildLeek to block use of the 10000 most commonly used passwords." }
} }
}, },
"twoFactorCookieDurationDays": { "type": "integer", "default": 30, "description": "Number of days that a user is allowed to remember this device for when completing 2FA. Set this to 0 to remove this option." }, "twoFactorCookieDurationDays": { "type": "integer", "default": 30, "description": "Number of days that a user is allowed to remember this device for when completing 2FA. Set this to 0 to remove this option." },

View File

@ -2030,9 +2030,9 @@
QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true)); QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true));
QV('authAppSetupCheck', userinfo.otpsecret == 1); QV('authAppSetupCheck', userinfo.otpsecret == 1);
QV('authKeySetupCheck', userinfo.otphkeys > 0); QV('authKeySetupCheck', userinfo.otphkeys > 0);
QV('authPushAuthDevCheck', (userinfo.otpdev > 0) && ((features2 & 2) != 0)); QV('authPushAuthDevCheck', (userinfo.otpdev > 0) && ((features2 & 0x40) != 0));
QV('authCodesSetupCheck', userinfo.otpkeys > 0); QV('authCodesSetupCheck', userinfo.otpkeys > 0);
QV('managePushAuthDev', (features2 & 2) && (count2factoraAuths() > 0)); QV('managePushAuthDev', (features2 & 0x40) && (count2factoraAuths() > 0));
mainUpdate(4 + 128 + 4096); mainUpdate(4 + 128 + 4096);
// Check if none or at least 2 factors are enabled. // Check if none or at least 2 factors are enabled.
@ -10056,7 +10056,7 @@
} }
function account_managePushAuthDev() { function account_managePushAuthDev() {
if (xxdialogMode || ((features2 & 2) == 0)) return; if (xxdialogMode || ((features2 & 0x40) == 0)) return;
if (userinfo.otpdev == 1) { if (userinfo.otpdev == 1) {
// Remove the 2FA device // Remove the 2FA device
setDialogMode(2, "Authentication Device", 3, function () { meshserver.send({ action: 'otpdev-clear' }); }, "Confirm removal of push authentication device?"); setDialogMode(2, "Authentication Device", 3, function () { meshserver.send({ action: 'otpdev-clear' }); }, "Confirm removal of push authentication device?");

View File

@ -952,7 +952,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
return; return;
} }
if ((req.body.hwtoken == '**push**') && push2fa) { // Handle device push notification 2FA request
// We create a browser cookie, send it back and when the browser connects it's web socket, it will trigger the push notification.
if ((req.body.hwtoken == '**push**') && push2fa && ((domain.passwordrequirements == null) || (domain.passwordrequirements.push2factor != false))) {
const logincodeb64 = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64'); const logincodeb64 = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
const sessioncode = obj.crypto.randomBytes(24).toString('base64'); const sessioncode = obj.crypto.randomBytes(24).toString('base64');
@ -978,43 +980,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
req.session.passhint = url; req.session.passhint = url;
req.session.loginmode = '8'; req.session.loginmode = '8';
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)); }
/*
// Perform push notification to device
const deviceCookie = parent.encodeCookie({ a: 'checkAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode });
var payload = { notification: { title: "MeshCentral", body: "Authentication - " + logincode }, data: { url: '2fa://auth?code=' + logincodeb64 + '&c=' + deviceCookie } };
var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
if (err == null) {
// Create a browser cookie so the browser can connect using websocket and wait for device accept/reject.
const browserCookie = parent.encodeCookie({ a: 'waitAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode, d: domain.id });
// Get the HTTPS port
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
// Get the agent connection server name
var serverName = obj.getWebServerName(domain);
if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
// Build the connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += '/';
var url = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + '2fahold.ashx?c=' + browserCookie;
// Request that the login page wait for device auth
req.session.messageid = 5; // "Notification sent." message
req.session.passhint = logincode + '|' + url;
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; return;
} }
@ -2581,6 +2546,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if (obj.parent.webpush != null) { features2 += 0x00000008; } // Indicates web push is enabled if (obj.parent.webpush != null) { features2 += 0x00000008; } // Indicates web push is enabled
if (((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update if (((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update
if (parent.amtProvisioningServer != null) { features2 += 0x00000020; } // Intel AMT LAN provisioning server if (parent.amtProvisioningServer != null) { features2 += 0x00000020; } // Intel AMT LAN provisioning server
if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.push2factor != false)) && (obj.parent.firebase != null)) { features2 += 0x00000040; } // Indicates device push notification 2FA is enabled
// Create a authentication cookie // Create a authentication cookie
const authCookie = obj.parent.encodeCookie({ userid: dbGetFunc.user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey); const authCookie = obj.parent.encodeCookie({ userid: dbGetFunc.user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
@ -2733,7 +2699,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tokensms == true); var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tokensms == true);
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; } if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tokenpush == true); var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tokenpush == true);
//if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; } if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; }
// See if we support two-factor trusted cookies // See if we support two-factor trusted cookies
var twoFactorCookieDays = 30; var twoFactorCookieDays = 30;
@ -2807,7 +2773,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if (req.body.hwstate) { if (req.body.hwstate) {
var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 1); var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 1);
if ((cookie != null) && (typeof cookie.u == 'string') && (cookie.d == domain.id) && (cookie.a == 'pushAuth')) { if ((cookie != null) && (typeof cookie.u == 'string') && (cookie.d == domain.id) && (cookie.a == 'pushAuth')) {
req.session = { userid: cookie.u, domainid: cookie.d } // Push authentication is a success, login the user // Push authentication is a success, login the user
req.session = { userid: cookie.u, domainid: cookie.d }
// Check if we need to remember this device
if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
var maxCookieAge = domain.twofactorcookiedurationdays;
if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: 'strict', secure: true });
}
handleRootRequestEx(req, res, domain); handleRootRequestEx(req, res, domain);
return; return;
} }
@ -4295,12 +4271,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
function handle2faHoldWebSocket(ws, req) { function handle2faHoldWebSocket(ws, req) {
const domain = checkUserIpAddress(ws, req); const domain = checkUserIpAddress(ws, req);
if (domain == null) { return; } if (domain == null) { return; }
ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { ws.close(); return; } // Push 2FA is disabled
if (typeof req.query.c !== 'string') { ws.close(); return; } if (typeof req.query.c !== 'string') { ws.close(); return; }
const cookie = parent.decodeCookie(req.query.c, null, 1); const cookie = parent.decodeCookie(req.query.c, null, 1);
if ((cookie == null) || (cookie.d != domain.id)) { ws.close(); return; } if ((cookie == null) || (cookie.d != domain.id)) { ws.close(); return; }
var user = obj.users[cookie.u]; var user = obj.users[cookie.u];
if ((user == null) || (typeof user.otpdev != 'string')) { ws.close(); return; } if ((user == null) || (typeof user.otpdev != 'string')) { ws.close(); return; }
ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
// 2FA event subscription // 2FA event subscription
obj.parent.AddEventDispatch(['2fadev-' + cookie.s], ws); obj.parent.AddEventDispatch(['2fadev-' + cookie.s], ws);