Fixed Duo 2FA security.

Ylian Saint-Hilaire committed Dec 31, 2024 at 10:37 UTC f80ba62cfc8a8f717a3bfc591fdfac795e6fdeb2
3 files changed +113 -24
meshuser.js
+2 -3
@@ -3645,14 +3645,13 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
3645 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3646
3647 // Check input
3648 - if (typeof command.enabled != 'boolean') return;
3648 + if ((typeof command.enabled != 'boolean') || (command.enabled != false)) return;
3649
3650 // See if we really need to change the state
3651 - if ((command.enabled === true) && (user.otpduo != null)) return;
3651 if ((command.enabled === false) && (user.otpduo == null)) return;
3652
3653 // Change the duo 2FA of this user
3655 - if (command.enabled === true) { user.otpduo = {}; } else { delete user.otpduo; }
3654 + delete user.otpduo;
3655 parent.db.SetUser(user);
3656 ws.send(JSON.stringify({ action: 'otpduo', success: true, enabled: command.enabled })); // Report success
3657
views/default.handlebars
+9 -3
@@ -12887,9 +12887,15 @@
12887 function account_manageAuthDuo() {
12888 if (xxdialogMode || ((features2 & 0x20000000) == 0)) return;
12889 var duoU2Fenabled = ((userinfo.otpduo == 1));
12890 - setDialogMode(2, "Duo Authentication", 1, function () {
12891 - if (duoU2Fenabled != Q('duo2facheck').checked) { meshserver.send({ action: 'otpduo', enabled: Q('duo2facheck').checked }); }
12892 - }, "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>');
12890 + if (duoU2Fenabled == false) {
12891 + setDialogMode(2, "Duo Authentication", 3, function () {
12892 + window.location.href = '/add-duo?rurl=' + encodeURIComponentEx(window.location.href) + ((urlargs.key)?('&key=' + urlargs.key):'');
12893 + }, "Confirm enabling of Duo 2FA security. Once enabled you will be given the option to use Due security at login. Click ok to go thru the steps to enable Duo.");
12894 + } else {
12895 + setDialogMode(2, "Duo Authentication", 3, function () {
12896 + meshserver.send({ action: 'otpduo', enabled: false });
12897 + }, "Confirm disabling Duo security.");
12898 + }
12899 }
12900
12901 function account_manageAuthApp() {
webserver.js
+102 -18
@@ -1226,7 +1226,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1226 sec.duostate = client.generateState();
1227 req.session.e = parent.encryptSessionData(sec);
1228 parent.debug('web', 'Redirecting user ' + user._id + ' to Duo');
1229 - res.redirect(client.createAuthUrl(user._id, sec.duostate));
1229 + res.redirect(client.createAuthUrl(user._id.split('/')[2], sec.duostate));
1230 return;
1231 }
1232
@@ -6951,13 +6951,14 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6951 }
6952 }
6953
6954 - // Setup Duo callback if needed
6954 + // Setup Duo HTTP handlers if supported
6955 if ((typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) {
6956 + // Duo authentication handler
6957 obj.app.get(url + 'auth-duo', function (req, res){
6958 var domain = getDomain(req);
6959 const sec = parent.decryptSessionData(req.session.e);
6959 - if (req.query.state !== sec.duostate) {
6960 - // The state returned from Duo IS NOT the same as what was in the session, so must fail
6960 + if ((req.query.state !== sec.duostate) || (req.query.duo_code == null)) {
6961 + // The state returned from Duo is not the same as what was in the session, so must fail
6962 parent.debug('web', 'handleRootRequest: Duo 2FA state failed.');
6963 req.session.loginmode = 1;
6964 req.session.messageid = 117; // Invalid security check
@@ -6975,21 +6976,76 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6976 apiHost: domain.duo2factor.apihostname,
6977 redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('?key=' + domain.loginkey) : '')
6978 });
6978 - client.exchangeAuthorizationCodeFor2FAResult(req.query.duo_code, userid).then(function (data) {
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);
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));
6979 + client.exchangeAuthorizationCodeFor2FAResult(req.query.duo_code, userid.split('/')[2]).then(function (data) {
6980 + const sec = parent.decryptSessionData(req.session.e);
6981 + if ((sec != null) && (sec.duoconfig == 1)) {
6982 + // Duo 2FA exchange success
6983 + parent.debug('web', 'handleRootRequest: Duo 2FA configuration success.');
6984 +
6985 + // Enable Duo for this user
6986 + var user = obj.users[userid];
6987 + if (user.otpduo == null) {
6988 + user.otpduo = {};
6989 + db.SetUser(user);
6990 +
6991 + // Notify change
6992 + var targets = ['*', 'server-users', user._id];
6993 + if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
6994 + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 160, msg: "Enabled duo two-factor authentication.", domain: domain.id };
6995 + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
6996 + parent.DispatchEvent(targets, obj, event);
6997 + }
6998 +
6999 + // Clear the Duo state
7000 + delete sec.duostate;
7001 + delete sec.duoconfig;
7002 + req.session.e = parent.encryptSessionData(sec);
7003 +
7004 + var url = req.session.duorurl;
7005 + delete req.session.duorurl;
7006 + res.redirect(url ? url : domain.url); // Redirect back to the user's original page
7007 + } else {
7008 + // Duo 2FA exchange success
7009 + parent.debug('web', 'handleRootRequest: Duo 2FA authorization success.');
7010 + req.session.userid = userid;
7011 + delete req.session.currentNode;
7012 + req.session.ip = req.clientIp; // Bind this session to the IP address of the request
7013 + setSessionRandom(req);
7014 +
7015 + // Clear the Duo state
7016 + delete sec.duostate;
7017 + req.session.e = parent.encryptSessionData(sec);
7018 +
7019 + obj.parent.authLog('https', 'Accepted Duo authentication for ' + userid + ' from ' + req.clientIp + ':' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
7020 + res.redirect(domain.url + getQueryPortion(req));
7021 + }
7022 }).catch(function (err) {
6987 - // Duo 2FA exchange failed
6988 - console.log('err',err);
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));
7023 + console.log('err', err);
7024 + const sec = parent.decryptSessionData(req.session.e);
7025 + if ((sec != null) && (sec.duoconfig == 1)) {
7026 + // Duo 2FA exchange success
7027 + parent.debug('web', 'handleRootRequest: Duo 2FA configuration failed.');
7028 +
7029 + // Clear the Duo state
7030 + delete sec.duostate;
7031 + delete sec.duoconfig;
7032 + req.session.e = parent.encryptSessionData(sec);
7033 +
7034 + var url = req.session.duorurl;
7035 + delete req.session.duorurl;
7036 + res.redirect(url ? url : domain.url); // Redirect back to the user's original page
7037 + } else {
7038 + // Duo 2FA exchange failed
7039 + parent.debug('web', 'handleRootRequest: Duo 2FA authorization failed.');
7040 +
7041 + // Clear the Duo state
7042 + delete sec.duostate;
7043 + req.session.e = parent.encryptSessionData(sec);
7044 +
7045 + req.session.loginmode = 1;
7046 + req.session.messageid = 117; // Invalid security check
7047 + res.redirect(domain.url + getQueryPortion(req));
7048 + }
7049 });
7050 } else {
7051 // Login failed
@@ -6998,6 +7054,34 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7054 });
7055 }
7056 });
7057 +
7058 + // Configure Duo handler
7059 + obj.app.get(url + 'add-duo', function (req, res) {
7060 + var domain = getDomain(req);
7061 + const sec = parent.decryptSessionData(req.session.e);
7062 +
7063 + if (req.session.userid == null) {
7064 + res.sendStatus(404);
7065 + } else {
7066 + // Redirect to Duo here
7067 + const duo = require('@duosecurity/duo_universal');
7068 + const client = new duo.Client({
7069 + clientId: domain.duo2factor.integrationkey,
7070 + clientSecret: domain.duo2factor.secretkey,
7071 + apiHost: domain.duo2factor.apihostname,
7072 + redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('&key=' + domain.loginkey) : '')
7073 + });
7074 +
7075 + // Setup the Duo configuration
7076 + if (req.query.rurl) { req.session.duorurl = req.query.rurl; } // Set Duo return URL
7077 + const sec = parent.decryptSessionData(req.session.e);
7078 + sec.duostate = client.generateState();
7079 + sec.duoconfig = 1;
7080 + req.session.e = parent.encryptSessionData(sec);
7081 + parent.debug('web', 'Redirecting user ' + req.session.userid + ' to Duo');
7082 + res.redirect(client.createAuthUrl(req.session.userid.split('/')[2], sec.duostate));
7083 + }
7084 + });
7085 }
7086
7087 // Server redirects