diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index e5312668..20becaed 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -339,10 +339,13 @@ "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." }, "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" }, "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." }, diff --git a/views/default.handlebars b/views/default.handlebars index 7f67ab08..ba5abe2e 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -2030,9 +2030,9 @@ QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true)); QV('authAppSetupCheck', userinfo.otpsecret == 1); 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('managePushAuthDev', (features2 & 2) && (count2factoraAuths() > 0)); + QV('managePushAuthDev', (features2 & 0x40) && (count2factoraAuths() > 0)); mainUpdate(4 + 128 + 4096); // Check if none or at least 2 factors are enabled. @@ -10056,7 +10056,7 @@ } function account_managePushAuthDev() { - if (xxdialogMode || ((features2 & 2) == 0)) return; + if (xxdialogMode || ((features2 & 0x40) == 0)) return; if (userinfo.otpdev == 1) { // Remove the 2FA device setDialogMode(2, "Authentication Device", 3, function () { meshserver.send({ action: 'otpdev-clear' }); }, "Confirm removal of push authentication device?"); diff --git a/webserver.js b/webserver.js index 3278d7ab..44975137 100644 --- a/webserver.js +++ b/webserver.js @@ -952,7 +952,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { 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 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.loginmode = '8'; 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; } @@ -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.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update 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 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); 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; } + if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; } // See if we support two-factor trusted cookies var twoFactorCookieDays = 30; @@ -2807,7 +2773,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (req.body.hwstate) { 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')) { - 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); return; } @@ -4295,12 +4271,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { function handle2faHoldWebSocket(ws, req) { const domain = checkUserIpAddress(ws, req); 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; } const cookie = parent.decodeCookie(req.query.c, null, 1); if ((cookie == null) || (cookie.d != domain.id)) { ws.close(); return; } var user = obj.users[cookie.u]; if ((user == null) || (typeof user.otpdev != 'string')) { ws.close(); return; } + ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive // 2FA event subscription obj.parent.AddEventDispatch(['2fadev-' + cookie.s], ws);