More work on login tokens.
Ylian Saint-Hilaire committed
Apr 16, 2021 at 12:42 UTC
6c3e010ce95cd2b1136ed4abd8c766093b31cde6
4 files changed
+35
-24
db.js
+3
-3
@@ -1275,7 +1275,7 @@ module.exports.CreateDB = function (parent, func) {
1275
obj.GetEventsWithLimit = function (ids, domain, limit, func) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
1276
obj.GetUserEvents = function (ids, domain, username, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func); };
1277
obj.GetUserEventsWithLimit = function (ids, domain, username, limit, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
1278
- obj.GetUserLoginEvents = function (domain, userid, func) { obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }).project({ action: 1, time: 1, msgid: 1, msgArgs: 1 }).sort({ time: -1 }).toArray(func); };
1278
+ obj.GetUserLoginEvents = function (domain, userid, func) { obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }).project({ action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).toArray(func); };
1279
obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { obj.eventsfile.find({ domain: domain, nodeid: nodeid }).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
1280
obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { obj.eventsfile.find({ domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } }).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
1281
obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); };
@@ -1463,9 +1463,9 @@ module.exports.CreateDB = function (parent, func) {
1463
};
1464
obj.GetUserLoginEvents = function (domain, userid, func) {
1465
if (obj.databaseType == 1) {
1466
- obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1 }).sort({ time: -1 }).exec(func);
1466
+ obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).exec(func);
1467
} else {
1468
- obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1 }).sort({ time: -1 }, func);
1468
+ obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }, func);
1469
}
1470
};
1471
obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { obj.eventsfile.find({ domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func); } };
meshuser.js
+2
-2
@@ -5543,7 +5543,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
5543
db.GetUserLoginEvents(domain.id, user._id, function (err, docs) {
5544
if (err != null) return;
5545
var e = [];
5546
- for (var i in docs) { e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs }); }
5546
+ for (var i in docs) { e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs, tn: docs[i].tokenName }); }
5547
try { ws.send(JSON.stringify({ action: 'previousLogins', events: e })); } catch (ex) { }
5548
});
5549
} else {
@@ -5553,7 +5553,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
5553
var e = [];
5554
for (var i in docs) {
5555
if ((docs[i].msgArgs) && (docs[i].userid == user._id) && ((docs[i].action == 'authfail') || (docs[i].action == 'login'))) {
5556
- e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs });
5556
+ e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs, tn: docs[i].tokenName });
5557
}
5558
}
5559
try { ws.send(JSON.stringify({ action: 'previousLogins', events: e })); } catch (ex) { }
views/default.handlebars
+1
-1
@@ -3334,7 +3334,7 @@
3334
x += '<div style=max-height:260px;overflow-y:scroll;overflow-x:hidden><table>';
3335
for (var i in message.events) {
3336
var m = message.events[i].m;
3337
- if (m == 107) { m = "Valid login"; c = 'BBD1BB'; xx = ''; }
3337
+ if (m == 107) { m = "Valid login"; c = 'BBD1BB'; xx = ''; if (message.events[i].tn != null) { m = format("Token: {0}", message.events[i].tn); c = '88D188' } }
3338
else if (m == 108) { m = "Invalid 2FA"; c ='DD9DC3'; xx = 'x'; }
3339
else if (m == 109) { m = "Locked account"; c ='E1BBBB'; xx = 'x'; }
3340
else if (m == 110) { m = "Invalid password"; c = 'E1BBBB'; xx = 'x'; }
webserver.js
+29
-18
@@ -588,9 +588,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
588
if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
589
590
// Succesful login token authentication
591
- var loginOptions = { logintoken: 1 };
591
+ var loginOptions = { tokenName: loginToken.name, tokenUser: loginToken.tokenUser };
592
if (loginToken.expire != 0) { loginOptions.expire = loginToken.expire; }
593
- return fn(null, user._id, loginOptions);
593
+ return fn(null, user._id, null, loginOptions);
594
}
595
fn(new Error('invalid password'));
596
}, 0);
@@ -713,7 +713,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
713
}
714
715
// Return true if this user has 2-step auth active
716
- function checkUserOneTimePasswordRequired(domain, user, req) {
716
+ function checkUserOneTimePasswordRequired(domain, user, req, loginOptions) {
717
+ // If this login occured using a login token, no 2FA needed.
718
+ if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return false; }
719
+
720
// Check if we can skip 2nd factor auth because of the source IP address
721
if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
722
for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) return false; }
@@ -935,7 +938,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
938
if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { xusername = req.session.tokenusername; xpassword = req.session.tokenpassword; }
939
940
// Authenticate the user
938
- obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint) {
941
+ obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
942
if (userid) {
943
var user = obj.users[userid];
944
@@ -952,7 +955,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
955
var push2fa = ((parent.firebase != null) && (user.otpdev != null));
956
957
// Check if this user has 2-step login active
955
- if ((req.session.loginmode != '6') && checkUserOneTimePasswordRequired(domain, user, req)) {
958
+ if ((req.session.loginmode != '6') && checkUserOneTimePasswordRequired(domain, user, req, loginOptions)) {
959
if ((req.body.hwtoken == '**email**') && email2fa) {
960
user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
961
obj.db.SetUser(user);
@@ -1060,7 +1063,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1063
// Login successful
1064
if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
1065
parent.debug('web', 'handleLoginRequest: successful 2FA login');
1063
- completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct);
1066
+ completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
1067
}
1068
});
1069
return;
@@ -1081,7 +1084,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1084
// Login successful
1085
if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
1086
parent.debug('web', 'handleLoginRequest: successful login');
1084
- completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct);
1087
+ completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
1088
} else {
1089
// Login failed, log the error
1090
if (obj.parent.authlog) { obj.parent.authLog('https', 'Failed password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
@@ -1120,7 +1123,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1123
});
1124
}
1125
1123
- function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct) {
1126
+ function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions) {
1127
// Check if we need to change the password
1128
if ((typeof user.passchange == 'number') && ((user.passchange == -1) || ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.reset == 'number') && (user.passchange + (domain.passwordrequirements.reset * 86400) < Math.floor(Date.now() / 1000))))) {
1129
// Request a password change
@@ -1144,6 +1147,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1147
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1148
const ua = getUserAgentInfo(req);
1149
const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'] };
1150
+ if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) { loginEvent.tokenName = loginOptions.tokenName; loginEvent.tokenUser = loginOptions.tokenUser; } // If a login token was used, add it to the event.
1151
obj.parent.DispatchEvent(targets, obj, loginEvent);
1152
1153
// Regenerate session when signing in to prevent fixation
@@ -1164,6 +1168,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1168
req.session.domainid = domain.id;
1169
req.session.currentNode = '';
1170
req.session.ip = req.clientIp;
1171
+
1172
+ // If a login token was used, add this information and expire time to the session.
1173
+ if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) {
1174
+ req.session.loginToken = loginOptions.tokenUser;
1175
+ if (loginOptions.expire != null) { req.session.expire = loginOptions.expire; }
1176
+ }
1177
+
1178
if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; }
1179
if (req.body.host) {
1180
// TODO: This is a terrible search!!! FIX THIS.
@@ -1371,7 +1382,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1382
}
1383
1384
// Authenticate the user
1374
- obj.authenticate(req.session.resettokenusername, req.session.resettokenpassword, domain, function (err, userid, passhint) {
1385
+ obj.authenticate(req.session.resettokenusername, req.session.resettokenpassword, domain, function (err, userid, passhint, loginOptions) {
1386
if (userid) {
1387
// Login
1388
var user = obj.users[userid];
@@ -1428,7 +1439,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1439
req.session.userid = userid;
1440
req.session.domainid = domain.id;
1441
req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1431
- completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tokenusername, req.session.tokenpassword, direct);
1442
+ completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tokenusername, req.session.tokenpassword, direct, loginOptions);
1443
}, 0);
1444
}
1445
}, 0);
@@ -1989,7 +2000,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
2000
if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { parent.debug('web', 'handleDeleteAccountRequest: account settings locked.'); res.sendStatus(404); return; }
2001
2002
// Check if the password is correct
1992
- obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid) {
2003
+ obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid, passhint, loginOptions) {
2004
var deluser = obj.users[userid];
2005
if ((userid != null) && (deluser != null)) {
2006
// Remove all links to this user
@@ -2336,7 +2347,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
2347
});
2348
} else if (req.query.user && req.query.pass) {
2349
// User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option.
2339
- obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid) {
2350
+ obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
2351
if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
2352
parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.');
2353
req.session.userid = userid;
@@ -5975,7 +5986,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
5986
case 'userAuth': { // This command is used to perform user authentication.
5987
// Check username and password authentication
5988
if ((typeof command.username == 'string') && (typeof command.password == 'string')) {
5978
- obj.authenticate(Buffer.from(command.username, 'base64').toString(), Buffer.from(command.password, 'base64').toString(), domain, function (err, userid) {
5989
+ obj.authenticate(Buffer.from(command.username, 'base64').toString(), Buffer.from(command.password, 'base64').toString(), domain, function (err, userid, passhint, loginOptions) {
5990
var user = obj.users[userid];
5991
if ((err == null) && (user)) {
5992
// Check if a 2nd factor is needed
@@ -5985,7 +5996,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
5996
var twoFactorCookieDays = 30;
5997
if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
5998
5988
- if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
5999
+ if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
6000
// Figure out if email 2FA is allowed
6001
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
6002
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
@@ -6109,7 +6120,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
6120
// A web socket session can be authenticated in many ways (Default user, session, user/pass and cookie). Check authentication here.
6121
if ((req.query.user != null) && (req.query.pass != null)) {
6122
// A user/pass is provided in URL arguments
6112
- obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid) {
6123
+ obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
6124
6125
// See if we support two-factor trusted cookies
6126
var twoFactorCookieDays = 30;
@@ -6118,7 +6129,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
6129
var user = obj.users[userid];
6130
if ((err == null) && (user)) {
6131
// Check if a 2nd factor is needed
6121
- if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
6132
+ if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
6133
// Figure out if email 2FA is allowed
6134
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
6135
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
@@ -6224,11 +6235,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
6235
var s = req.headers['x-meshauth'].split(',');
6236
for (var i in s) { s[i] = Buffer.from(s[i], 'base64').toString(); }
6237
if ((s.length < 2) || (s.length > 3)) { try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); ws.close(); } catch (e) { } return; }
6227
- obj.authenticate(s[0], s[1], domain, function (err, userid) {
6238
+ obj.authenticate(s[0], s[1], domain, function (err, userid, passhint, loginOptions) {
6239
var user = obj.users[userid];
6240
if ((err == null) && (user)) {
6241
// Check if a 2nd factor is needed
6231
- if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
6242
+ if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
6243
6244
// See if we support two-factor trusted cookies
6245
var twoFactorCookieDays = 30;