From c92b88a37451e1394e0019dadae49bee5ad22c17 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sun, 22 Dec 2024 19:10:35 -0800 Subject: [PATCH] Duo changes, but not yet fully tested. --- meshcentral-config-schema.json | 41 +++++++++++++++++++--------------- meshcentral.js | 2 +- meshuser.js | 6 +++++ package.json | 14 ++++++++++++ sample-config-advanced.json | 5 +++++ views/default.handlebars | 8 ++++--- webserver.js | 37 +++++++++++++++--------------- 7 files changed, 73 insertions(+), 40 deletions(-) diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index fc761195..462895b3 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -1665,24 +1665,9 @@ "description": "Set to false to disable SMS 2FA." }, "duo2factor": { - "type": "object", - "properties": { - "integrationkey": { - "type": "string", - "default": "", - "description": "Integration key from Duo" - }, - "secretkey": { - "type": "string", - "default": "", - "description": "Secret key from Duo" - }, - "apihostname": { - "type": "string", - "default": "", - "description": "API Hostname from Duo" - } - } + "type": "boolean", + "default": true, + "description": "Set to false to disable Duo 2FA." }, "push2factor": { "type": "boolean", @@ -2704,6 +2689,26 @@ }, "description": "This is used to create HTTP redirections. For example setting \"redirects\": { \"example\":\"https://example.com\" } will make it so that anyone accessing /example on the server will get redirected to the specified URL." }, + "duo2factor": { + "type": "object", + "properties": { + "integrationkey": { + "type": "string", + "default": "", + "description": "Integration key from Duo" + }, + "secretkey": { + "type": "string", + "default": "", + "description": "Secret key from Duo" + }, + "apihostname": { + "type": "string", + "default": "", + "description": "API Hostname from Duo" + } + } + }, "yubikey": { "type": "object", "properties": { diff --git a/meshcentral.js b/meshcentral.js index 249311ee..fe6c3548 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -4230,7 +4230,7 @@ function mainStart() { if (config.domains[i].sessionrecording != null) { sessionRecording = true; } if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; } if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; } - if ((config.domains[i].passwordrequirements != null) && (typeof config.domains[i].passwordrequirements.duo2factor == 'object') && (passport.indexOf('@duosecurity/duo_universal') == -1)) { passport.push('@duosecurity/duo_universal'); } + if ((typeof config.domains[i].duo2factor == 'object') && (passport.indexOf('@duosecurity/duo_universal') == -1)) { passport.push('@duosecurity/duo_universal'); } } // Build the list of required modules diff --git a/meshuser.js b/meshuser.js index fb726fb4..5c80ec85 100644 --- a/meshuser.js +++ b/meshuser.js @@ -3633,6 +3633,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Do not allow this command if 2FA's are locked if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return; + // Do not allow if Duo is not supported + if ((typeof domain.duo2factor != 'object') || (typeof domain.duo2factor.integrationkey != 'string') || (typeof domain.duo2factor.secretkey != 'string') || (typeof domain.duo2factor.apihostname != 'string')) return; + + // Do not allow if Duo is disabled + if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) return; + // Do not allow this command when logged in using a login token if (req.session.loginToken != null) break; diff --git a/package.json b/package.json index e8551d52..10745781 100644 --- a/package.json +++ b/package.json @@ -37,19 +37,33 @@ "sample-config-advanced.json" ], "dependencies": { + "@duosecurity/duo_universal": "2.0.3", "@seald-io/nedb": "4.0.4", "archiver": "7.0.1", "body-parser": "1.20.3", "cbor": "5.2.0", "compression": "1.7.5", + "connect-flash": "0.1.1", "cookie-session": "2.1.0", "express": "4.21.2", "express-handlebars": "7.1.3", "express-ws": "5.0.2", + "firebase-admin": "12.7.0", "ipcheck": "0.1.0", + "jwt-simple": "0.5.6", + "loadavg-windows": "1.1.1", "minimist": "1.2.8", "multiparty": "4.2.3", "node-forge": "1.3.1", + "node-windows": "0.1.14", + "otplib": "10.2.3", + "passport": "0.7.0", + "passport-azure-oauth2": "0.1.0", + "passport-github2": "0.1.12", + "passport-google-oauth20": "2.0.0", + "passport-saml": "3.2.4", + "passport-twitter": "1.0.4", + "ssh2": "1.16.0", "ua-parser-js": "1.0.39", "ws": "8.18.0", "yauzl": "2.10.0" diff --git a/sample-config-advanced.json b/sample-config-advanced.json index c744f7ff..b865691c 100644 --- a/sample-config-advanced.json +++ b/sample-config-advanced.json @@ -454,6 +454,11 @@ "_redirects": { "meshcommander": "https://www.meshcommander.com/" }, + "_duo2factor": { + "integrationkey": "mykey", + "secretkey": "mysecret", + "apihostname": "api-xxxxxxxxxxx.duosecurity.com" + }, "_yubikey": { "id": "0000", "secret": "xxxxxxxxxxxxxxxxxxxxx", diff --git a/views/default.handlebars b/views/default.handlebars index 2cee33ac..3d1139b4 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -2358,9 +2358,11 @@ QV('authPhoneNumberCheck', (userinfo.phone != null)); QV('authMessagingCheck', (userinfo.msghandle != null)); QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true)); - QV('authDuoSetupCheck', (userinfo.otpduo == 1)); + QV('authDuoSetupCheck', (userinfo.otpduo == 1) && ((features2 & 0x20000000) != 0)); QV('authAppSetupCheck', userinfo.otpsecret == 1); QV('manageAuthApp', (serverinfo.lock2factor != true) && ((userinfo.otpsecret == 1) || ((features2 & 0x00020000) == 0))); + QV('manageDuoApp', (serverinfo.lock2factor != true) && ((features2 & 0x20000000) != 0)); + console.log('duo', (serverinfo.lock2factor != true) && ((features2 & 0x20000000) != 0)); QV('authKeySetupCheck', userinfo.otphkeys > 0); QV('authPushAuthDevCheck', (userinfo.otpdev > 0) && ((features2 & 0x40) != 0)); QV('authCodesSetupCheck', userinfo.otpkeys > 0); @@ -12884,11 +12886,11 @@ } function account_manageAuthDuo() { - if (xxdialogMode || ((features & 0x00800000) == 0)) return; + if (xxdialogMode || ((features2 & 0x20000000) == 0)) return; var duoU2Fenabled = ((userinfo.otpduo == 1)); setDialogMode(2, "Duo Authentication", 1, function () { if (duoU2Fenabled != Q('duo2facheck').checked) { meshserver.send({ action: 'otpduo', enabled: Q('duo2facheck').checked }); } - }, "When enabled, on each login, you will be given the option to login using Duo for added security." + '

'); + }, "When enabled, on each login, you will be given the option to use Duo for added security." + '

'); } function account_manageAuthApp() { diff --git a/webserver.js b/webserver.js index 321bed91..fee6f03c 100644 --- a/webserver.js +++ b/webserver.js @@ -1162,7 +1162,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null)); var push2fa = ((parent.firebase != null) && (user.otpdev != null)); - var duo2fa = (((typeof domain.passwordrequirements != 'object') || (typeof domain.passwordrequirements.duo2factor == 'object')) && (user.otpduo != null)); + var duo2fa = ((((typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) || ((typeof domain.passwordrequirements != 'object') && (typeof domain.passwordrequirements.duo2factor != false))) && (user.otpduo != null)); // Check if two factor can be skipped const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions); @@ -1212,13 +1212,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF return; } - if ((req.body.hwtoken == '**duo**') && duo2fa) { + if ((req.body.hwtoken == '**duo**') && duo2fa && (typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) { // Redirect to duo here const duo = require('@duosecurity/duo_universal'); const client = new duo.Client({ - clientId: domain.passwordrequirements.duo2factor.integrationkey, - clientSecret: domain.passwordrequirements.duo2factor.secretkey, - apiHost: domain.passwordrequirements.duo2factor.apihostname, + clientId: domain.duo2factor.integrationkey, + clientSecret: domain.duo2factor.secretkey, + apiHost: domain.duo2factor.apihostname, redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('?key=' + domain.loginkey) : '') }); // Decrypt any session data @@ -3302,6 +3302,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if ((parent.msgserver != null) && (parent.msgserver.providers != 0) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false))) { features2 += 0x04000000; } // User messaging 2FA is allowed if (domain.scrolltotop == true) { features2 += 0x08000000; } // Show the "Scroll to top" button if (domain.devicesearchbargroupname === true) { features2 += 0x10000000; } // Search bar will find by group name too + if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.duo2factor != false)) && (typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) { features2 += 0x20000000; } // using Duo for 2FA is allowed return { features: features, features2: features2 }; } @@ -3341,7 +3342,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string')); if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; } var otpduo = (req.session != null) && (req.session.tduo === 1); - if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) { otpduo = false; } + if (((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) || (typeof domain.duo2factor != 'object')) { otpduo = false; } var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tsms === 1); if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; } var otpmsg = (parent.msgserver != null) && (req.session != null) && (req.session.tmsg === 1); @@ -6950,14 +6951,14 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF } } - // // Setup Duo callback if needed - if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.duo2factor == 'object')) { + // Setup Duo callback if needed + if ((typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) { obj.app.get(url + 'auth-duo', function (req, res){ var domain = getDomain(req); const sec = parent.decryptSessionData(req.session.e); if (req.query.state !== sec.duostate) { - // the state returned from Duo IS NOT the same as what was in the session, so must fail! - parent.debug('web', 'handleRootRequest: duo 2fa state failed!'); + // The state returned from Duo IS NOT the same as what was in the session, so must fail + parent.debug('web', 'handleRootRequest: Duo 2FA state failed.'); req.session.loginmode = 1; req.session.messageid = 117; // Invalid security check res.redirect(domain.url + getQueryPortion(req)); // redirect back to main page @@ -6966,26 +6967,26 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // User credentials are stored in session, just check again and get userid obj.authenticate(sec.tuser, sec.tpass, domain, function (err, userid, passhint, loginOptions) { if ((userid != null) && (err == null)) { - // Login data correct, now exchange authorization code for 2fa + // Login data correct, now exchange authorization code for 2FA const duo = require('@duosecurity/duo_universal'); const client = new duo.Client({ - clientId: domain.passwordrequirements.duo2factor.integrationkey, - clientSecret: domain.passwordrequirements.duo2factor.secretkey, - apiHost: domain.passwordrequirements.duo2factor.apihostname, + clientId: domain.duo2factor.integrationkey, + clientSecret: domain.duo2factor.secretkey, + apiHost: domain.duo2factor.apihostname, redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('?key=' + domain.loginkey) : '') }); client.exchangeAuthorizationCodeFor2FAResult(req.query.duo_code, userid).then(function (data) { - parent.debug('web', 'handleRootRequest: duo 2fa auth ok.'); + parent.debug('web', 'handleRootRequest: Duo 2FA auth ok.'); req.session.userid = userid; delete req.session.currentNode; req.session.ip = req.clientIp; // Bind this session to the IP address of the request setSessionRandom(req); - obj.parent.authLog('https', 'Accepted duo authentication for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x }); + obj.parent.authLog('https', 'Accepted Duo authentication for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x }); res.redirect(domain.url + getQueryPortion(req)); }).catch(function (err) { - // Duo 2FA exchange failed, so must fail! + // Duo 2FA exchange failed console.log('err',err); - parent.debug('web', 'handleRootRequest: duo 2fa exchange authorization code failed!.'); + parent.debug('web', 'handleRootRequest: Duo 2FA exchange authorization code failed.'); req.session.loginmode = 1; req.session.messageid = 117; // Invalid security check res.redirect(domain.url + getQueryPortion(req));