From 8e8192a8cd35b34f8f1973f36c4f14d6b94a7ff9 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Tue, 1 Feb 2022 09:11:11 -0800 Subject: [PATCH] Added 2FA lock feature. --- meshcentral-config-schema.json | 1 + meshuser.js | 58 +++++++++++++++++++++++++++++++++ views/default-mobile.handlebars | 14 +++++--- views/default.handlebars | 17 ++++++---- webserver.js | 2 +- 5 files changed, 80 insertions(+), 12 deletions(-) diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index 5f061635..f1cd5a05 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -442,6 +442,7 @@ "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." }, "otp2factor": { "type": "boolean", "default": true, "description": "Set to false to disable one-time-password 2FA." }, + "lock2factor": { "type": "boolean", "default": false, "description": "When set to true, prevents any changes to 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." }, diff --git a/meshuser.js b/meshuser.js index 8756ed82..75dfa284 100644 --- a/meshuser.js +++ b/meshuser.js @@ -548,6 +548,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (parent.parent.webpush != null) { serverinfo.vapidpublickey = parent.parent.webpush.vapidPublicKey; } // Web push public key if (parent.parent.amtProvisioningServer != null) { serverinfo.amtProvServerMeshId = parent.parent.amtProvisioningServer.meshid; } // Device group that allows for bare-metal Intel AMT activation if ((typeof domain.autoremoveinactivedevices == 'number') && (domain.autoremoveinactivedevices > 0)) { serverinfo.autoremoveinactivedevices = domain.autoremoveinactivedevices; } // Default number of days before inactive devices are removed + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) { serverinfo.lock2factor = true; } // Indicate 2FA change are not allowed // Build the mobile agent URL, this is used to connect mobile devices var agentServerName = parent.getWebServerName(domain); @@ -3146,6 +3147,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'otpemail': { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3173,6 +3177,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'otpauth-request': { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3198,6 +3205,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'otpauth-setup': { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3234,6 +3244,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'otpauth-clear': { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3262,6 +3275,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'otpauth-getpasswords': { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3310,6 +3326,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'otp-hkey-get': { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3326,6 +3345,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'otp-hkey-remove': { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3353,6 +3375,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'otp-hkey-yubikey-add': { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3411,6 +3436,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'otpdev-clear': { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3431,6 +3459,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'otpdev-set': { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3460,6 +3491,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'webauthn-startregister': { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3477,6 +3511,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'webauthn-endregister': { + // Do not allow this command if 2FA's are locked + if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -4775,6 +4812,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use }; const serverUserCommands = { + '2falock': [serverUserCommand2faLock, "Shows and changes the 2FA lock state"], 'acceleratorsstats': [serverUserCommandAcceleratorsStats, "Show data on work being offloaded to other CPU's"], 'agentissues': [serverUserCommandAgentIssues, ""], 'agentstats': [serverUserCommandAgentStats, ""], @@ -6387,6 +6425,26 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } } + function serverUserCommand2faLock(cmdData) { + var arg = null; + if (cmdData.cmdargs['_'].length > 0) { arg = cmdData.cmdargs['_'][0]; } + if (domain.passwordrequirements == null) { domain.passwordrequirements = {}; } + if (arg == 'set') { + // TODO: Change 2FA lock for peer servers + domain.passwordrequirements.lock2factor = true; + cmdData.result = "2FA lock is set"; + parent.parent.DispatchEvent(['server-allusers'], obj, { action: 'serverinfochange', lock2factor: true, nolog: 1, domain: domain.id }); + } else if (arg == 'clear') { + // TODO: Change 2FA lock for peer servers + delete domain.passwordrequirements.lock2factor; + cmdData.result = "2FA lock is cleared"; + parent.parent.DispatchEvent(['server-allusers'], obj, { action: 'serverinfochange', lock2factor: false, nolog: 1, domain: domain.id }); + } else { + cmdData.result = (domain.passwordrequirements.lock2factor == true) ? "2FA lock is set" : "2FA lock is cleared"; + cmdData.result += ", use '2falock [set/clear]' to change the lock state." + } + } + function serverUserCommandAcceleratorsStats(cmdData) { var stats = parent.parent.certificateOperations.getAcceleratorStats(); for (var i in stats) { diff --git a/views/default-mobile.handlebars b/views/default-mobile.handlebars index 0e4a4840..8e56f2b1 100644 --- a/views/default-mobile.handlebars +++ b/views/default-mobile.handlebars @@ -1299,9 +1299,9 @@ applyDesktopSettings(); // Arrange the user interface - QV('manageEmail2FA', features & 0x00800000); - QV('managePhoneNumber1', (features & 0x02000000) && (features & 0x04000000)); - QV('managePhoneNumber2', (features & 0x02000000) && !(features & 0x04000000)); + QV('manageEmail2FA', (features & 0x00800000) && (serverinfo.lock2factor != true)); + QV('managePhoneNumber1', (features & 0x02000000) && (features & 0x04000000) && (serverinfo.lock2factor != true)); + QV('managePhoneNumber2', (features & 0x02000000) && !(features & 0x04000000) && (serverinfo.lock2factor != true)); //attemptWebRTC = false; // For now, default WebRTC off unless we set it in the URL. if (args.webrtc != null) { attemptWebRTC = (args.webrtc == 1); } @@ -1371,8 +1371,8 @@ QV('p2AccountSecurity', ((features & 4) == 0) && (serverinfo.domainauth == false) && ((features & 4096) != 0) && (accountSettingsLocked == false)); // Hide Account Security if in single user mode or domain authentication, 2 factor auth not supported. QV('p2AccountImage', !accountSettingsLocked); QV('verifyEmailId', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true)); - QV('manageAuthApp', (features & 4096) && ((userinfo.otpsecret == 1) || ((features2 & 0x00020000) == 0))); - QV('manageOtp', (features & 4096) && ((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0))); + QV('manageAuthApp', (serverinfo.lock2factor != true) && (features & 4096) && ((userinfo.otpsecret == 1) || ((features2 & 0x00020000) == 0))); + QV('manageOtp', (serverinfo.lock2factor != true) && (features & 4096) && ((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0))); QV('authPhoneNumberCheck', (userinfo.phone != null)); QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true)); QV('authAppSetupCheck', userinfo.otpsecret == 1); @@ -1685,6 +1685,10 @@ */ if (message.event.noact) break; // Take no action on this event switch (message.event.action) { + case 'serverinfochange': { + if (message.event.lock2factor != null) { serverinfo.lock2factor = message.event.lock2factor; updateSelf(); } + break; + } case 'userWebState': { // New user web state, update the web page as needed if (localStorage != null) { diff --git a/views/default.handlebars b/views/default.handlebars index 46a957a6..fffcee26 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -2027,9 +2027,9 @@ // Update account actions QV('p2AccountSecurity', ((features & 4) == 0) && (serverinfo.domainauth == false) && ((features & 4096) != 0) && (accountSettingsLocked == false)); // Hide Account Security if in single user mode or domain authentication, 2 factor auth not supported. QV('p2AccountActions', !accountSettingsLocked) - QV('managePhoneNumber1', (features & 0x02000000) && (features & 0x04000000)); - QV('managePhoneNumber2', (features & 0x02000000) && !(features & 0x04000000)); - QV('manageEmail2FA', features & 0x00800000); + QV('managePhoneNumber1', (features & 0x02000000) && (features & 0x04000000) && (serverinfo.lock2factor != true)); + QV('managePhoneNumber2', (features & 0x02000000) && !(features & 0x04000000) && (serverinfo.lock2factor != true)); + QV('manageEmail2FA', (features & 0x00800000) && (serverinfo.lock2factor != true)); QV('p2AccountPassActions', ((features & 4) == 0) && (serverinfo.domainauth == false) && (userinfo != null) && (userinfo._id.split('/')[2].startsWith('~') == false)); // Hide Account Actions if in single user mode or domain authentication //QV('p2AccountImage', ((features & 4) == 0) && (serverinfo.domainauth == false)); // If account actions are not visible, also remove the image on that panel QV('accountCreateLoginTokenSpan', features2 & 0x00000080); @@ -2126,15 +2126,16 @@ var accountSettingsLocked = ((userinfo.siteadmin != 0xFFFFFFFF) && ((userinfo.siteadmin & 1024) != 0)); QV('verifyEmailId', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true)); QV('verifyEmailId2', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true) && (accountSettingsLocked == false)); - QV('manageOtp', authFactorCount > 0); + QV('manageOtp', (serverinfo.lock2factor != true) && (authFactorCount > 0)); QV('authPhoneNumberCheck', (userinfo.phone != null)); QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true)); QV('authAppSetupCheck', userinfo.otpsecret == 1); - QV('manageAuthApp', (userinfo.otpsecret == 1) || ((features2 & 0x00020000) == 0)); + QV('manageAuthApp', (serverinfo.lock2factor != true) && ((userinfo.otpsecret == 1) || ((features2 & 0x00020000) == 0))); QV('authKeySetupCheck', userinfo.otphkeys > 0); QV('authPushAuthDevCheck', (userinfo.otpdev > 0) && ((features2 & 0x40) != 0)); QV('authCodesSetupCheck', userinfo.otpkeys > 0); - QV('managePushAuthDev', (features2 & 0x40) && (authFactorCount > 0)); + QV('managePushAuthDev', (serverinfo.lock2factor != true) && (features2 & 0x40) && (authFactorCount > 0)); + QV('manageHardwareOtp', (serverinfo.lock2factor != true)); mainUpdate(4 + 128 + 4096); // Check if none or at least 2 factors are enabled. @@ -2838,6 +2839,10 @@ if (message.event.noact) break; // Take no action on this event switch (message.event.action) { + case 'serverinfochange': { + if (message.event.lock2factor != null) { serverinfo.lock2factor = message.event.lock2factor; updateSelf(); updateSiteAdmin(); } + break; + } case 'deviceShareUpdate': { if (message.event.nodeid != deviceSharesReq) break; deviceSharesNode = message.event.nodeid; diff --git a/webserver.js b/webserver.js index eb9f1b8b..441065ff 100644 --- a/webserver.js +++ b/webserver.js @@ -4059,7 +4059,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // Subscribe to all events we are allowed to receive obj.subscribe = function (userid, target) { const user = obj.users[userid]; - const subscriptions = [userid, 'server-global']; + const subscriptions = [userid, 'server-allusers']; if (user.siteadmin != null) { // Allow full site administrators of users with all events rights to see all events. if ((user.siteadmin == 0xFFFFFFFF) || ((user.siteadmin & 2048) != 0)) { subscriptions.push('*'); }