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);