Duo changes, but not yet fully tested.

Ylian Saint-Hilaire committed Dec 22, 2024 at 19:10 UTC c92b88a37451e1394e0019dadae49bee5ad22c17
7 files changed +73 -40
meshcentral-config-schema.json
+23 -18
@@ -1665,24 +1665,9 @@
1665 "description": "Set to false to disable SMS 2FA."
1666 },
1667 "duo2factor": {
1668 - "type": "object",
1669 - "properties": {
1670 - "integrationkey": {
1671 - "type": "string",
1672 - "default": "",
1673 - "description": "Integration key from Duo"
1674 - },
1675 - "secretkey": {
1676 - "type": "string",
1677 - "default": "",
1678 - "description": "Secret key from Duo"
1679 - },
1680 - "apihostname": {
1681 - "type": "string",
1682 - "default": "",
1683 - "description": "API Hostname from Duo"
1684 - }
1685 - }
1668 + "type": "boolean",
1669 + "default": true,
1670 + "description": "Set to false to disable Duo 2FA."
1671 },
1672 "push2factor": {
1673 "type": "boolean",
@@ -2704,6 +2689,26 @@
2689 },
2690 "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."
2691 },
2692 + "duo2factor": {
2693 + "type": "object",
2694 + "properties": {
2695 + "integrationkey": {
2696 + "type": "string",
2697 + "default": "",
2698 + "description": "Integration key from Duo"
2699 + },
2700 + "secretkey": {
2701 + "type": "string",
2702 + "default": "",
2703 + "description": "Secret key from Duo"
2704 + },
2705 + "apihostname": {
2706 + "type": "string",
2707 + "default": "",
2708 + "description": "API Hostname from Duo"
2709 + }
2710 + }
2711 + },
2712 "yubikey": {
2713 "type": "object",
2714 "properties": {
meshcentral.js
+1 -1
@@ -4230,7 +4230,7 @@ function mainStart() {
4230 if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
4231 if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
4232 if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
4233 - if ((config.domains[i].passwordrequirements != null) && (typeof config.domains[i].passwordrequirements.duo2factor == 'object') && (passport.indexOf('@duosecurity/duo_universal') == -1)) { passport.push('@duosecurity/duo_universal'); }
4233 + if ((typeof config.domains[i].duo2factor == 'object') && (passport.indexOf('@duosecurity/duo_universal') == -1)) { passport.push('@duosecurity/duo_universal'); }
4234 }
4235
4236 // Build the list of required modules
meshuser.js
+6
@@ -3633,6 +3633,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
3633 // Do not allow this command if 2FA's are locked
3634 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3635
3636 + // Do not allow if Duo is not supported
3637 + if ((typeof domain.duo2factor != 'object') || (typeof domain.duo2factor.integrationkey != 'string') || (typeof domain.duo2factor.secretkey != 'string') || (typeof domain.duo2factor.apihostname != 'string')) return;
3638 +
3639 + // Do not allow if Duo is disabled
3640 + if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) return;
3641 +
3642 // Do not allow this command when logged in using a login token
3643 if (req.session.loginToken != null) break;
3644
package.json
+14
@@ -37,19 +37,33 @@
37 "sample-config-advanced.json"
38 ],
39 "dependencies": {
40 + "@duosecurity/duo_universal": "2.0.3",
41 "@seald-io/nedb": "4.0.4",
42 "archiver": "7.0.1",
43 "body-parser": "1.20.3",
44 "cbor": "5.2.0",
45 "compression": "1.7.5",
46 + "connect-flash": "0.1.1",
47 "cookie-session": "2.1.0",
48 "express": "4.21.2",
49 "express-handlebars": "7.1.3",
50 "express-ws": "5.0.2",
51 + "firebase-admin": "12.7.0",
52 "ipcheck": "0.1.0",
53 + "jwt-simple": "0.5.6",
54 + "loadavg-windows": "1.1.1",
55 "minimist": "1.2.8",
56 "multiparty": "4.2.3",
57 "node-forge": "1.3.1",
58 + "node-windows": "0.1.14",
59 + "otplib": "10.2.3",
60 + "passport": "0.7.0",
61 + "passport-azure-oauth2": "0.1.0",
62 + "passport-github2": "0.1.12",
63 + "passport-google-oauth20": "2.0.0",
64 + "passport-saml": "3.2.4",
65 + "passport-twitter": "1.0.4",
66 + "ssh2": "1.16.0",
67 "ua-parser-js": "1.0.39",
68 "ws": "8.18.0",
69 "yauzl": "2.10.0"
sample-config-advanced.json
+5
@@ -454,6 +454,11 @@
454 "_redirects": {
455 "meshcommander": "https://www.meshcommander.com/"
456 },
457 + "_duo2factor": {
458 + "integrationkey": "mykey",
459 + "secretkey": "mysecret",
460 + "apihostname": "api-xxxxxxxxxxx.duosecurity.com"
461 + },
462 "_yubikey": {
463 "id": "0000",
464 "secret": "xxxxxxxxxxxxxxxxxxxxx",
views/default.handlebars
+5 -3
@@ -2358,9 +2358,11 @@
2358 QV('authPhoneNumberCheck', (userinfo.phone != null));
2359 QV('authMessagingCheck', (userinfo.msghandle != null));
2360 QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true));
2361 - QV('authDuoSetupCheck', (userinfo.otpduo == 1));
2361 + QV('authDuoSetupCheck', (userinfo.otpduo == 1) && ((features2 & 0x20000000) != 0));
2362 QV('authAppSetupCheck', userinfo.otpsecret == 1);
2363 QV('manageAuthApp', (serverinfo.lock2factor != true) && ((userinfo.otpsecret == 1) || ((features2 & 0x00020000) == 0)));
2364 + QV('manageDuoApp', (serverinfo.lock2factor != true) && ((features2 & 0x20000000) != 0));
2365 + console.log('duo', (serverinfo.lock2factor != true) && ((features2 & 0x20000000) != 0));
2366 QV('authKeySetupCheck', userinfo.otphkeys > 0);
2367 QV('authPushAuthDevCheck', (userinfo.otpdev > 0) && ((features2 & 0x40) != 0));
2368 QV('authCodesSetupCheck', userinfo.otpkeys > 0);
@@ -12884,11 +12886,11 @@
12886 }
12887
12888 function account_manageAuthDuo() {
12887 - if (xxdialogMode || ((features & 0x00800000) == 0)) return;
12889 + if (xxdialogMode || ((features2 & 0x20000000) == 0)) return;
12890 var duoU2Fenabled = ((userinfo.otpduo == 1));
12891 setDialogMode(2, "Duo Authentication", 1, function () {
12892 if (duoU2Fenabled != Q('duo2facheck').checked) { meshserver.send({ action: 'otpduo', enabled: Q('duo2facheck').checked }); }
12891 - }, "When enabled, on each login, you will be given the option to login using Duo for added security." + '<br /><br /><label><input id=duo2facheck type=checkbox ' + (duoU2Fenabled?'checked':'') + '/>' + "Enable Duo two-factor authentication." + '</label>');
12893 + }, "When enabled, on each login, you will be given the option to use Duo for added security." + '<br /><br /><label><input id=duo2facheck type=checkbox ' + (duoU2Fenabled?'checked':'') + '/>' + "Enable Duo two-factor authentication." + '</label>');
12894 }
12895
12896 function account_manageAuthApp() {
webserver.js
+19 -18
@@ -1162,7 +1162,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1162 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
1163 var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
1164 var push2fa = ((parent.firebase != null) && (user.otpdev != null));
1165 - var duo2fa = (((typeof domain.passwordrequirements != 'object') || (typeof domain.passwordrequirements.duo2factor == 'object')) && (user.otpduo != null));
1165 + 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));
1166
1167 // Check if two factor can be skipped
1168 const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
@@ -1212,13 +1212,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1212 return;
1213 }
1214
1215 - if ((req.body.hwtoken == '**duo**') && duo2fa) {
1215 + 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')) {
1216 // Redirect to duo here
1217 const duo = require('@duosecurity/duo_universal');
1218 const client = new duo.Client({
1219 - clientId: domain.passwordrequirements.duo2factor.integrationkey,
1220 - clientSecret: domain.passwordrequirements.duo2factor.secretkey,
1221 - apiHost: domain.passwordrequirements.duo2factor.apihostname,
1219 + clientId: domain.duo2factor.integrationkey,
1220 + clientSecret: domain.duo2factor.secretkey,
1221 + apiHost: domain.duo2factor.apihostname,
1222 redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('?key=' + domain.loginkey) : '')
1223 });
1224 // Decrypt any session data
@@ -3302,6 +3302,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3302 if ((parent.msgserver != null) && (parent.msgserver.providers != 0) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false))) { features2 += 0x04000000; } // User messaging 2FA is allowed
3303 if (domain.scrolltotop == true) { features2 += 0x08000000; } // Show the "Scroll to top" button
3304 if (domain.devicesearchbargroupname === true) { features2 += 0x10000000; } // Search bar will find by group name too
3305 + 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
3306 return { features: features, features2: features2 };
3307 }
3308
@@ -3341,7 +3342,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3342 var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string'));
3343 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
3344 var otpduo = (req.session != null) && (req.session.tduo === 1);
3344 - if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) { otpduo = false; }
3345 + if (((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) || (typeof domain.duo2factor != 'object')) { otpduo = false; }
3346 var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tsms === 1);
3347 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
3348 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
6951 }
6952 }
6953
6953 - // // Setup Duo callback if needed
6954 - if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.duo2factor == 'object')) {
6954 + // Setup Duo callback if needed
6955 + if ((typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) {
6956 obj.app.get(url + 'auth-duo', function (req, res){
6957 var domain = getDomain(req);
6958 const sec = parent.decryptSessionData(req.session.e);
6959 if (req.query.state !== sec.duostate) {
6959 - // the state returned from Duo IS NOT the same as what was in the session, so must fail!
6960 - parent.debug('web', 'handleRootRequest: duo 2fa state failed!');
6960 + // The state returned from Duo IS NOT the same as what was in the session, so must fail
6961 + parent.debug('web', 'handleRootRequest: Duo 2FA state failed.');
6962 req.session.loginmode = 1;
6963 req.session.messageid = 117; // Invalid security check
6964 res.redirect(domain.url + getQueryPortion(req)); // redirect back to main page
@@ -6966,26 +6967,26 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6967 // User credentials are stored in session, just check again and get userid
6968 obj.authenticate(sec.tuser, sec.tpass, domain, function (err, userid, passhint, loginOptions) {
6969 if ((userid != null) && (err == null)) {
6969 - // Login data correct, now exchange authorization code for 2fa
6970 + // Login data correct, now exchange authorization code for 2FA
6971 const duo = require('@duosecurity/duo_universal');
6972 const client = new duo.Client({
6972 - clientId: domain.passwordrequirements.duo2factor.integrationkey,
6973 - clientSecret: domain.passwordrequirements.duo2factor.secretkey,
6974 - apiHost: domain.passwordrequirements.duo2factor.apihostname,
6973 + clientId: domain.duo2factor.integrationkey,
6974 + clientSecret: domain.duo2factor.secretkey,
6975 + apiHost: domain.duo2factor.apihostname,
6976 redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('?key=' + domain.loginkey) : '')
6977 });
6978 client.exchangeAuthorizationCodeFor2FAResult(req.query.duo_code, userid).then(function (data) {
6978 - parent.debug('web', 'handleRootRequest: duo 2fa auth ok.');
6979 + parent.debug('web', 'handleRootRequest: Duo 2FA auth ok.');
6980 req.session.userid = userid;
6981 delete req.session.currentNode;
6982 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
6983 setSessionRandom(req);
6983 - 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 });
6984 + 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 });
6985 res.redirect(domain.url + getQueryPortion(req));
6986 }).catch(function (err) {
6986 - // Duo 2FA exchange failed, so must fail!
6987 + // Duo 2FA exchange failed
6988 console.log('err',err);
6988 - parent.debug('web', 'handleRootRequest: duo 2fa exchange authorization code failed!.');
6989 + parent.debug('web', 'handleRootRequest: Duo 2FA exchange authorization code failed.');
6990 req.session.loginmode = 1;
6991 req.session.messageid = 117; // Invalid security check
6992 res.redirect(domain.url + getQueryPortion(req));