From 21aabc676d2358968beaddb20f9af0408eba0e4f Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sat, 24 Jul 2021 15:14:21 -0700 Subject: [PATCH] Added 2FA timeout on login screen, default is 5 minutes. --- meshcentral-config-schema.json | 3 ++- sample-config-advanced.json | 3 ++- views/login-mobile.handlebars | 3 +++ views/login.handlebars | 3 +++ views/login2.handlebars | 3 +++ webserver.js | 15 ++++++++++++++- 6 files changed, 27 insertions(+), 3 deletions(-) diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index 4dd09257..126bf430 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -384,7 +384,8 @@ "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", "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." } + "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." } } }, "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/sample-config-advanced.json b/sample-config-advanced.json index f6f60180..33ec59a3 100644 --- a/sample-config-advanced.json +++ b/sample-config-advanced.json @@ -196,7 +196,8 @@ "force2factor": true, "skip2factor": "127.0.0.1,192.168.2.0/24", "oldPasswordBan": 5, - "banCommonPasswords": false + "banCommonPasswords": false, + "twoFactorTimeout": 300 }, "_twoFactorCookieDurationDays": 30, "_agentInviteCodes": true, diff --git a/views/login-mobile.handlebars b/views/login-mobile.handlebars index e12f7316..8ce0cbef 100644 --- a/views/login-mobile.handlebars +++ b/views/login-mobile.handlebars @@ -326,6 +326,7 @@ var otpsms = ('{{{otpsms}}}' === 'true'); var twoFactorCookieDays = parseInt('{{{twoFactorCookieDays}}}'); var authStrategies = '{{{authStrategies}}}'.split(','); + var tokenTimeout = parseInt('{{{tokenTimeout}}}'); // Display the right server message var messageid = parseInt('{{{messageid}}}'); @@ -402,6 +403,7 @@ QV('hrAccountDiv', (emailCheck == 'true') || (newAccountPass == 1)); if (loginMode == '4') { + if (tokenTimeout > 0) { setTimeout(function () { Q('hwtokenInput').value = '**timeout**'; QE('tokenOkButton', true); Q('tokenOkButton').click(); }, tokenTimeout); } try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null } QV('securityKeyButton', (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')); QV('emailKeyButton', otpemail && (messageid != 2) && (messageid != 4)); @@ -409,6 +411,7 @@ } if (loginMode == '5') { + if (tokenTimeout > 0) { setTimeout(function () { Q('hwtokenInput').value = '**timeout**'; QE('tokenOkButton', true); Q('tokenOkButton').click(); }, tokenTimeout); } try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null } if ((hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')) { if (typeof hardwareKeyChallenge.challenge == 'string') { hardwareKeyChallenge.challenge = Uint8Array.from(atob(hardwareKeyChallenge.challenge), function (c) { return c.charCodeAt(0) }).buffer; } diff --git a/views/login.handlebars b/views/login.handlebars index af253c65..bba9a00d 100644 --- a/views/login.handlebars +++ b/views/login.handlebars @@ -325,6 +325,7 @@ var otpsms = (decodeURIComponent('{{{otpsms}}}') === 'true'); var twoFactorCookieDays = parseInt('{{{twoFactorCookieDays}}}'); var authStrategies = '{{{authStrategies}}}'.split(','); + var tokenTimeout = parseInt('{{{tokenTimeout}}}'); function startup() { // Display the right server message @@ -432,6 +433,7 @@ QV('hrAccountDiv', (emailCheck == 'true') || (newAccountPass == 1)); if (loginMode == '4') { + if (tokenTimeout > 0) { setTimeout(function () { Q('hwtokenInput').value = '**timeout**'; QE('tokenOkButton', true); Q('tokenOkButton').click(); }, tokenTimeout); } try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null } QV('securityKeyButton', (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')); QV('emailKeyButton', otpemail && (messageid != 2) && (messageid != 4)); @@ -439,6 +441,7 @@ } if (loginMode == '5') { + if (tokenTimeout > 0) { setTimeout(function () { Q('hwtokenInput').value = '**timeout**'; QE('tokenOkButton', true); Q('tokenOkButton').click(); }, tokenTimeout); } try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null } QV('securityKeyButton2', (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')); QV('emailKeyButton2', otpemail && (messageid != 2) && (messageid != 4)); diff --git a/views/login2.handlebars b/views/login2.handlebars index d0105e11..416cc7cb 100644 --- a/views/login2.handlebars +++ b/views/login2.handlebars @@ -358,6 +358,7 @@ var otppush = (decodeURIComponent('{{{otppush}}}') === 'true'); var twoFactorCookieDays = parseInt('{{{twoFactorCookieDays}}}'); var authStrategies = '{{{authStrategies}}}'.split(','); + var tokenTimeout = parseInt('{{{tokenTimeout}}}'); var websocket = null; function startup() { @@ -463,6 +464,7 @@ QV('hrAccountDiv', (emailCheck == 'true') || (newAccountPass == 1)); if (loginMode == '4') { + if (tokenTimeout > 0) { setTimeout(function () { Q('hwtokenInput').value = '**timeout**'; QE('tokenOkButton', true); Q('tokenOkButton').click(); }, tokenTimeout); } try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null } var twofakey = (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn'); var emailkey = otpemail && (messageid != 2) && (messageid != 4); @@ -476,6 +478,7 @@ } if (loginMode == '5') { + if (tokenTimeout > 0) { setTimeout(function () { Q('hwtokenInput').value = '**timeout**'; QE('tokenOkButton', true); Q('tokenOkButton').click(); }, tokenTimeout); } try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null } var twofakey = (hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn'); var emailkey = otpemail && (messageid != 2) && (messageid != 4); diff --git a/webserver.js b/webserver.js index c2a35e16..a1b0727d 100644 --- a/webserver.js +++ b/webserver.js @@ -1018,6 +1018,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Check if this user has 2-step login active if ((req.session.loginmode != 6) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions)) { + if ((req.body.hwtoken == '**timeout**')) { + delete req.session; // Clear the session + res.redirect(domain.url + getQueryPortion(req)); + return; + } + if ((req.body.hwtoken == '**email**') && email2fa) { user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() }; obj.db.SetUser(user); @@ -2879,6 +2885,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { var customui = ''; if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); } + // Get two-factor screen timeout + var twoFactorTimeout = 300000; // Default is 5 minutes, 0 for no timeout. + if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.twofactortimeout == 'number')) { + twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000; + } + // Render the login page render(req, res, getRenderPage((domain.sitestyle == 2) ? 'login2' : 'login', req, domain), @@ -2907,7 +2919,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { otppush: otppush, twoFactorCookieDays: twoFactorCookieDays, authStrategies: authStrategies.join(','), - loginpicture: (typeof domain.loginpicture == 'string') + loginpicture: (typeof domain.loginpicture == 'string'), + tokenTimeout: twoFactorTimeout // Two-factor authentication screen timeout in milliseconds }, req, domain, (domain.sitestyle == 2) ? 'login2' : 'login')); }