More work on device 2FA.

Ylian Saint-Hilaire committed Apr 14, 2021 at 14:06 UTC 69fd9dffe2a692c8ad6b53fbdd0f0910c5d5bf3e
3 files changed +26 -46
meshcentral-config-schema.json
+5 -2
@@ -339,10 +339,13 @@
339 "numeric": { "type": "integer", "description": "Minimum number of numeric characters required in the password." },
340 "nonalpha": { "type": "integer", "description": "Minimum number of non-alpha-numeric characters required in the password." },
341 "reset": { "type": "integer", "description": "Number of days after which the user is required to change the account password." },
342 - "force2factor": { "type": "boolean", "description": "Requires that all accounts setup 2FA." },
342 + "email2factor": { "type": "boolean", "default": true, "description": "Set to false to disable email 2FA." },
343 + "sms2factor": { "type": "boolean", "default": true, "description": "Set to false to disable SMS 2FA." },
344 + "push2factor": { "type": "boolean", "default": true, "description": "Set to false to disable push notification 2FA." },
345 + "force2factor": { "type": "boolean", "default": false, "description": "Requires that all accounts setup 2FA." },
346 "skip2factor": { "type": "string", "description": "IP addresses where 2FA login is skipped, for example: 127.0.0.1,192.168.2.0/24" },
347 "oldPasswordBan": { "type": "integer", "description": "Number of old passwords the server should remember and not allow the user to switch back to." },
345 - "banCommonPasswords": { "type": "boolean", "description": "Uses WildLeek to block use of the 10000 most commonly used passwords." }
348 + "banCommonPasswords": { "type": "boolean", "default": false, "description": "Uses WildLeek to block use of the 10000 most commonly used passwords." }
349 }
350 },
351 "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." },
views/default.handlebars
+3 -3
@@ -2030,9 +2030,9 @@
2030 QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true));
2031 QV('authAppSetupCheck', userinfo.otpsecret == 1);
2032 QV('authKeySetupCheck', userinfo.otphkeys > 0);
2033 - QV('authPushAuthDevCheck', (userinfo.otpdev > 0) && ((features2 & 2) != 0));
2033 + QV('authPushAuthDevCheck', (userinfo.otpdev > 0) && ((features2 & 0x40) != 0));
2034 QV('authCodesSetupCheck', userinfo.otpkeys > 0);
2035 - QV('managePushAuthDev', (features2 & 2) && (count2factoraAuths() > 0));
2035 + QV('managePushAuthDev', (features2 & 0x40) && (count2factoraAuths() > 0));
2036 mainUpdate(4 + 128 + 4096);
2037
2038 // Check if none or at least 2 factors are enabled.
@@ -10056,7 +10056,7 @@
10056 }
10057
10058 function account_managePushAuthDev() {
10059 - if (xxdialogMode || ((features2 & 2) == 0)) return;
10059 + if (xxdialogMode || ((features2 & 0x40) == 0)) return;
10060 if (userinfo.otpdev == 1) {
10061 // Remove the 2FA device
10062 setDialogMode(2, "Authentication Device", 3, function () { meshserver.send({ action: 'otpdev-clear' }); }, "Confirm removal of push authentication device?");
webserver.js
+18 -41
@@ -952,7 +952,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
952 return;
953 }
954
955 - if ((req.body.hwtoken == '**push**') && push2fa) {
955 + // Handle device push notification 2FA request
956 + // We create a browser cookie, send it back and when the browser connects it's web socket, it will trigger the push notification.
957 + if ((req.body.hwtoken == '**push**') && push2fa && ((domain.passwordrequirements == null) || (domain.passwordrequirements.push2factor != false))) {
958 const logincodeb64 = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
959 const sessioncode = obj.crypto.randomBytes(24).toString('base64');
960
@@ -978,43 +980,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
980 req.session.passhint = url;
981 req.session.loginmode = '8';
982 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
981 -
982 - /*
983 - // Perform push notification to device
984 - const deviceCookie = parent.encodeCookie({ a: 'checkAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode });
985 - var payload = { notification: { title: "MeshCentral", body: "Authentication - " + logincode }, data: { url: '2fa://auth?code=' + logincodeb64 + '&c=' + deviceCookie } };
986 - var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
987 - parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
988 - if (err == null) {
989 - // Create a browser cookie so the browser can connect using websocket and wait for device accept/reject.
990 - const browserCookie = parent.encodeCookie({ a: 'waitAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode, d: domain.id });
991 -
992 - // Get the HTTPS port
993 - var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
994 - if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
995 - if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
996 -
997 - // Get the agent connection server name
998 - var serverName = obj.getWebServerName(domain);
999 - if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
1000 -
1001 - // Build the connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
1002 - var xdomain = (domain.dns == null) ? domain.id : '';
1003 - if (xdomain != '') xdomain += '/';
1004 - var url = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + '2fahold.ashx?c=' + browserCookie;
1005 -
1006 - // Request that the login page wait for device auth
1007 - req.session.messageid = 5; // "Notification sent." message
1008 - req.session.passhint = logincode + '|' + url;
1009 - req.session.loginmode = '8';
1010 - } else {
1011 - // Indicate the push notification failed
1012 - req.session.messageid = 116; // "Unable to send device notification." message
1013 - req.session.loginmode = '4';
1014 - }
1015 - if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1016 - });
1017 - */
983 return;
984 }
985
@@ -2581,6 +2546,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
2546 if (obj.parent.webpush != null) { features2 += 0x00000008; } // Indicates web push is enabled
2547 if (((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update
2548 if (parent.amtProvisioningServer != null) { features2 += 0x00000020; } // Intel AMT LAN provisioning server
2549 + if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.push2factor != false)) && (obj.parent.firebase != null)) { features2 += 0x00000040; } // Indicates device push notification 2FA is enabled
2550
2551 // Create a authentication cookie
2552 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) {
2699 var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tokensms == true);
2700 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
2701 var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tokenpush == true);
2736 - //if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; }
2702 + if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; }
2703
2704 // See if we support two-factor trusted cookies
2705 var twoFactorCookieDays = 30;
@@ -2807,7 +2773,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
2773 if (req.body.hwstate) {
2774 var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 1);
2775 if ((cookie != null) && (typeof cookie.u == 'string') && (cookie.d == domain.id) && (cookie.a == 'pushAuth')) {
2810 - req.session = { userid: cookie.u, domainid: cookie.d } // Push authentication is a success, login the user
2776 + // Push authentication is a success, login the user
2777 + req.session = { userid: cookie.u, domainid: cookie.d }
2778 +
2779 + // Check if we need to remember this device
2780 + if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
2781 + var maxCookieAge = domain.twofactorcookiedurationdays;
2782 + if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
2783 + const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
2784 + res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: 'strict', secure: true });
2785 + }
2786 +
2787 handleRootRequestEx(req, res, domain);
2788 return;
2789 }
@@ -4295,12 +4271,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4271 function handle2faHoldWebSocket(ws, req) {
4272 const domain = checkUserIpAddress(ws, req);
4273 if (domain == null) { return; }
4298 - ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
4274 + if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { ws.close(); return; } // Push 2FA is disabled
4275 if (typeof req.query.c !== 'string') { ws.close(); return; }
4276 const cookie = parent.decodeCookie(req.query.c, null, 1);
4277 if ((cookie == null) || (cookie.d != domain.id)) { ws.close(); return; }
4278 var user = obj.users[cookie.u];
4279 if ((user == null) || (typeof user.otpdev != 'string')) { ws.close(); return; }
4280 + ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
4281
4282 // 2FA event subscription
4283 obj.parent.AddEventDispatch(['2fadev-' + cookie.s], ws);