diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index f1cd5a05..8ad547fe 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -449,7 +449,8 @@ "banCommonPasswords": { "type": "boolean", "default": false, "description": "Uses WildLeek to block use of the 10000 most commonly used passwords." }, "loginTokens": { "type": "boolean", "default": true, "description": "Allows users to create alternative username/passwords for their account." }, "twoFactorTimeout": { "type": "integer", "default": 300, "description": "Maximum about of time the to wait for a 2FA token on the login page in seconds." }, - "autofido2fa": { "type": "boolean", "default": false, "description": "If true and user account has FIDO key setup, 2FA login screen will automatically request FIDO 2FA." } + "autofido2fa": { "type": "boolean", "default": false, "description": "If true and user account has FIDO key setup, 2FA login screen will automatically request FIDO 2FA." }, + "maxfidokeys": { "type": "integer", "default": null, "description": "Maximum number of FIDO/YubikeyOTP hardware 2FA keys that can be setup in a user account." } } }, "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/meshuser.js b/meshuser.js index 75dfa284..eb095f7e 100644 --- a/meshuser.js +++ b/meshuser.js @@ -548,7 +548,10 @@ 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 + if (domain.passwordrequirements) { + if (domain.passwordrequirements.lock2factor == true) { serverinfo.lock2factor = true; } // Indicate 2FA change are not allowed + if (typeof domain.passwordrequirements.maxfidokeys == 'number') { serverinfo.maxfidokeys = domain.passwordrequirements.maxfidokeys; } + } // Build the mobile agent URL, this is used to connect mobile devices var agentServerName = parent.getWebServerName(domain); @@ -3375,8 +3378,11 @@ 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 if 2FA's are locked or max keys reached + if (domain.passwordrequirements) { + if (domain.passwordrequirements.lock2factor == true) return; + if ((typeof domain.passwordrequirements.maxfidokeys == 'number') && (user.otphkeys) && (user.otphkeys.length >= domain.passwordrequirements.maxfidokeys)) return; + } // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3491,8 +3497,11 @@ 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 if 2FA's are locked or max keys reached + if (domain.passwordrequirements) { + if (domain.passwordrequirements.lock2factor == true) return; + if ((typeof domain.passwordrequirements.maxfidokeys == 'number') && (user.otphkeys) && (user.otphkeys.length >= domain.passwordrequirements.maxfidokeys)) return; + } // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; @@ -3511,8 +3520,11 @@ 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 if 2FA's are locked or max keys reached + if (domain.passwordrequirements) { + if (domain.passwordrequirements.lock2factor == true) return; + if ((typeof domain.passwordrequirements.maxfidokeys == 'number') && (user.otphkeys) && (user.otphkeys.length >= domain.passwordrequirements.maxfidokeys)) return; + } // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; diff --git a/views/default.handlebars b/views/default.handlebars index fffcee26..81a75a94 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -2756,8 +2756,13 @@ } x += ''; x += '