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