From 6c3e010ce95cd2b1136ed4abd8c766093b31cde6 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Fri, 16 Apr 2021 12:42:54 -0700 Subject: [PATCH] More work on login tokens. --- db.js | 6 ++--- meshuser.js | 4 ++-- views/default.handlebars | 2 +- webserver.js | 47 +++++++++++++++++++++++++--------------- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/db.js b/db.js index 3e3ac250..07e2eae1 100644 --- a/db.js +++ b/db.js @@ -1275,7 +1275,7 @@ module.exports.CreateDB = function (parent, func) { 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); }; 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); }; 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); }; - 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); }; + 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); }; 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); }; 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); }; obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); }; @@ -1463,9 +1463,9 @@ module.exports.CreateDB = function (parent, func) { }; obj.GetUserLoginEvents = function (domain, userid, func) { if (obj.databaseType == 1) { - 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); + 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); } else { - 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); + 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); } }; 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); } }; diff --git a/meshuser.js b/meshuser.js index d55a9412..582c3b4e 100644 --- a/meshuser.js +++ b/meshuser.js @@ -5543,7 +5543,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use db.GetUserLoginEvents(domain.id, user._id, function (err, docs) { if (err != null) return; var e = []; - for (var i in docs) { e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs }); } + for (var i in docs) { e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs, tn: docs[i].tokenName }); } try { ws.send(JSON.stringify({ action: 'previousLogins', events: e })); } catch (ex) { } }); } else { @@ -5553,7 +5553,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var e = []; for (var i in docs) { if ((docs[i].msgArgs) && (docs[i].userid == user._id) && ((docs[i].action == 'authfail') || (docs[i].action == 'login'))) { - e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs }); + e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs, tn: docs[i].tokenName }); } } try { ws.send(JSON.stringify({ action: 'previousLogins', events: e })); } catch (ex) { } diff --git a/views/default.handlebars b/views/default.handlebars index 908683ec..e0df6f25 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -3334,7 +3334,7 @@ x += '
'; for (var i in message.events) { var m = message.events[i].m; - if (m == 107) { m = "Valid login"; c = 'BBD1BB'; xx = ''; } + 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' } } else if (m == 108) { m = "Invalid 2FA"; c ='DD9DC3'; xx = 'x'; } else if (m == 109) { m = "Locked account"; c ='E1BBBB'; xx = 'x'; } else if (m == 110) { m = "Invalid password"; c = 'E1BBBB'; xx = 'x'; } diff --git a/webserver.js b/webserver.js index 22e9b3be..34d68751 100644 --- a/webserver.js +++ b/webserver.js @@ -588,9 +588,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; } // Succesful login token authentication - var loginOptions = { logintoken: 1 }; + var loginOptions = { tokenName: loginToken.name, tokenUser: loginToken.tokenUser }; if (loginToken.expire != 0) { loginOptions.expire = loginToken.expire; } - return fn(null, user._id, loginOptions); + return fn(null, user._id, null, loginOptions); } fn(new Error('invalid password')); }, 0); @@ -713,7 +713,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // Return true if this user has 2-step auth active - function checkUserOneTimePasswordRequired(domain, user, req) { + function checkUserOneTimePasswordRequired(domain, user, req, loginOptions) { + // If this login occured using a login token, no 2FA needed. + if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return false; } + // Check if we can skip 2nd factor auth because of the source IP address if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) { 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) { if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { xusername = req.session.tokenusername; xpassword = req.session.tokenpassword; } // Authenticate the user - obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint) { + obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) { if (userid) { var user = obj.users[userid]; @@ -952,7 +955,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { var push2fa = ((parent.firebase != null) && (user.otpdev != null)); // Check if this user has 2-step login active - if ((req.session.loginmode != '6') && checkUserOneTimePasswordRequired(domain, user, req)) { + if ((req.session.loginmode != '6') && checkUserOneTimePasswordRequired(domain, user, req, loginOptions)) { if ((req.body.hwtoken == '**email**') && email2fa) { user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() }; obj.db.SetUser(user); @@ -1060,7 +1063,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Login successful if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); } parent.debug('web', 'handleLoginRequest: successful 2FA login'); - completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct); + completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions); } }); return; @@ -1081,7 +1084,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Login successful if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); } parent.debug('web', 'handleLoginRequest: successful login'); - completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct); + completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions); } else { // Login failed, log the error 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) { }); } - function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct) { + function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions) { // Check if we need to change the password 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))))) { // Request a password change @@ -1144,6 +1147,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } } const ua = getUserAgentInfo(req); 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'] }; + 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. obj.parent.DispatchEvent(targets, obj, loginEvent); // Regenerate session when signing in to prevent fixation @@ -1164,6 +1168,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { req.session.domainid = domain.id; req.session.currentNode = ''; req.session.ip = req.clientIp; + + // If a login token was used, add this information and expire time to the session. + if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) { + req.session.loginToken = loginOptions.tokenUser; + if (loginOptions.expire != null) { req.session.expire = loginOptions.expire; } + } + if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; } if (req.body.host) { // TODO: This is a terrible search!!! FIX THIS. @@ -1371,7 +1382,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // Authenticate the user - obj.authenticate(req.session.resettokenusername, req.session.resettokenpassword, domain, function (err, userid, passhint) { + obj.authenticate(req.session.resettokenusername, req.session.resettokenpassword, domain, function (err, userid, passhint, loginOptions) { if (userid) { // Login var user = obj.users[userid]; @@ -1428,7 +1439,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { req.session.userid = userid; req.session.domainid = domain.id; req.session.ip = req.clientIp; // Bind this session to the IP address of the request - completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tokenusername, req.session.tokenpassword, direct); + completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tokenusername, req.session.tokenpassword, direct, loginOptions); }, 0); } }, 0); @@ -1989,7 +2000,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { parent.debug('web', 'handleDeleteAccountRequest: account settings locked.'); res.sendStatus(404); return; } // Check if the password is correct - obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid) { + obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid, passhint, loginOptions) { var deluser = obj.users[userid]; if ((userid != null) && (deluser != null)) { // Remove all links to this user @@ -2336,7 +2347,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }); } else if (req.query.user && req.query.pass) { // User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option. - obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid) { + obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) { if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); } parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.'); req.session.userid = userid; @@ -5975,7 +5986,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { case 'userAuth': { // This command is used to perform user authentication. // Check username and password authentication if ((typeof command.username == 'string') && (typeof command.password == 'string')) { - obj.authenticate(Buffer.from(command.username, 'base64').toString(), Buffer.from(command.password, 'base64').toString(), domain, function (err, userid) { + obj.authenticate(Buffer.from(command.username, 'base64').toString(), Buffer.from(command.password, 'base64').toString(), domain, function (err, userid, passhint, loginOptions) { var user = obj.users[userid]; if ((err == null) && (user)) { // Check if a 2nd factor is needed @@ -5985,7 +5996,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { var twoFactorCookieDays = 30; if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; } - if (checkUserOneTimePasswordRequired(domain, user, req) == true) { + if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) { // Figure out if email 2FA is allowed var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null)); 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) { // A web socket session can be authenticated in many ways (Default user, session, user/pass and cookie). Check authentication here. if ((req.query.user != null) && (req.query.pass != null)) { // A user/pass is provided in URL arguments - obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid) { + obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) { // See if we support two-factor trusted cookies var twoFactorCookieDays = 30; @@ -6118,7 +6129,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { var user = obj.users[userid]; if ((err == null) && (user)) { // Check if a 2nd factor is needed - if (checkUserOneTimePasswordRequired(domain, user, req) == true) { + if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) { // Figure out if email 2FA is allowed var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null)); 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) { var s = req.headers['x-meshauth'].split(','); for (var i in s) { s[i] = Buffer.from(s[i], 'base64').toString(); } if ((s.length < 2) || (s.length > 3)) { try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); ws.close(); } catch (e) { } return; } - obj.authenticate(s[0], s[1], domain, function (err, userid) { + obj.authenticate(s[0], s[1], domain, function (err, userid, passhint, loginOptions) { var user = obj.users[userid]; if ((err == null) && (user)) { // Check if a 2nd factor is needed - if (checkUserOneTimePasswordRequired(domain, user, req) == true) { + if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) { // See if we support two-factor trusted cookies var twoFactorCookieDays = 30;