Properly detect new user agents and location
This commit is contained in:
+5
-2
@@ -142,9 +142,12 @@ function sendInvite(user, invitor, inviteLink) {
|
||||
});
|
||||
}
|
||||
|
||||
function sendNewLoginLocation(user, newLocation) {
|
||||
function sendNewLoginLocation(user, ip, userAgent, country, city) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof newLocation, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof userAgent, 'string');
|
||||
assert.strictEqual(typeof country, 'string');
|
||||
assert.strictEqual(typeof city, 'string');
|
||||
|
||||
debug('Sending new login location mail');
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ let assert = require('assert'),
|
||||
externalLdap = require('../externalldap.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
mailer = require('../mailer.js'),
|
||||
sysinfo = require('../sysinfo.js'),
|
||||
system = require('../system.js'),
|
||||
tokendb = require('../tokendb.js'),
|
||||
@@ -51,6 +50,7 @@ function login(req, res, next) {
|
||||
|
||||
const type = req.body.type || tokens.ID_WEBADMIN;
|
||||
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
const userAgent = req.headers['user-agent'] || '';
|
||||
const auditSource = { authType: 'basic', ip: ip };
|
||||
|
||||
const error = tokens.validateTokenType(type);
|
||||
@@ -59,15 +59,11 @@ function login(req, res, next) {
|
||||
tokens.add(type, req.user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, token) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], ip, 1, 100, function (error, result) {
|
||||
if (error) console.error(error);
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) });
|
||||
|
||||
if (!error && result.length === 0) mailer.sendNewLoginLocation(req.user, ip);
|
||||
users.checkLoginLocation(req.user, ip, userAgent);
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) });
|
||||
|
||||
next(new HttpSuccess(200, token));
|
||||
});
|
||||
next(new HttpSuccess(200, token));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+12
-2
@@ -27,11 +27,12 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
database = require('./database.js'),
|
||||
mysql = require('mysql');
|
||||
mysql = require('mysql'),
|
||||
safe = require('safetydance');
|
||||
|
||||
// the avatar field is special and not added here to reduce response sizes
|
||||
const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'createdAt', 'resetToken', 'displayName',
|
||||
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime' ].join(',');
|
||||
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'locationJson' ].join(',');
|
||||
|
||||
const APP_PASSWORD_FIELDS = [ 'id', 'name', 'userId', 'identifier', 'hashedPassword', 'creationTime' ].join(',');
|
||||
|
||||
@@ -41,6 +42,11 @@ function postProcess(result) {
|
||||
result.twoFactorAuthenticationEnabled = !!result.twoFactorAuthenticationEnabled;
|
||||
result.active = !!result.active;
|
||||
|
||||
// we remove the JSON first locations property, it is only there to have a valid JSON, no toplevel array
|
||||
const tmp = safe.JSON.parse(result.locationJson) || { locations: [] };
|
||||
result.locations = tmp.locations || [];
|
||||
delete result.locationJson;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -238,6 +244,7 @@ function update(userId, user, callback) {
|
||||
assert(!('twoFactorAuthenticationEnabled' in user) || (typeof user.twoFactorAuthenticationEnabled === 'boolean'));
|
||||
assert(!('role' in user) || (typeof user.role === 'string'));
|
||||
assert(!('active' in user) || (typeof user.active === 'boolean'));
|
||||
assert(!('locations' in user) || (Array.isArray(user.locations)));
|
||||
|
||||
var args = [ ];
|
||||
var fields = [ ];
|
||||
@@ -245,6 +252,9 @@ function update(userId, user, callback) {
|
||||
if (k === 'twoFactorAuthenticationEnabled' || k === 'active') {
|
||||
fields.push(k + ' = ?');
|
||||
args.push(user[k] ? 1 : 0);
|
||||
} else if (k === 'locations') {
|
||||
fields.push('locationJson = ?');
|
||||
args.push(JSON.stringify({ locations: user[k] || []}));
|
||||
} else {
|
||||
fields.push(k + ' = ?');
|
||||
args.push(user[k]);
|
||||
|
||||
@@ -30,6 +30,8 @@ exports = module.exports = {
|
||||
|
||||
sendPasswordResetByIdentifier,
|
||||
|
||||
checkLoginLocation,
|
||||
|
||||
setupAccount,
|
||||
getAvatarUrl,
|
||||
setAvatar,
|
||||
@@ -72,6 +74,7 @@ let assert = require('assert'),
|
||||
tokens = require('./tokens.js'),
|
||||
userdb = require('./userdb.js'),
|
||||
uuid = require('uuid'),
|
||||
superagent = require('superagent'),
|
||||
validator = require('validator'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -527,6 +530,38 @@ function sendPasswordResetByIdentifier(identifier, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkLoginLocation(user, ip, userAgent) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof userAgent, 'string');
|
||||
|
||||
debug(`checkLoginLocation: ${user.id} ${ip} ${userAgent}`);
|
||||
|
||||
superagent.get('https://geolocation.cloudron.io/json').query({ ip: ip }).end(function (error, result) {
|
||||
if (error) return console.error('Failed to get geoip info:', error);
|
||||
|
||||
const country = result.body.country.names.en;
|
||||
const city = result.body.city.names.en;
|
||||
|
||||
const knownLogin = user.locations.find(function (l) {
|
||||
return l.userAgent === userAgent && l.country === country && l.city === city;
|
||||
});
|
||||
|
||||
if (knownLogin) return;
|
||||
|
||||
// purge potentially old locations where ts > now() - 6 months
|
||||
const sixMonthsBack = Date.now() - 6 * 30 * 24 * 60 * 60 * 1000;
|
||||
var locations = user.locations.filter(function (l) { return l.ts > sixMonthsBack; });
|
||||
|
||||
locations.push({ ts: Date.now(), ip, userAgent, country, city });
|
||||
userdb.update(user.id, { locations }, function (error) {
|
||||
if (error) console.error('checkLoginLocation: Failed to update user location.', error);
|
||||
|
||||
mailer.sendNewLoginLocation(user, ip, userAgent, country, city);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setPassword(user, newPassword, callback) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof newPassword, 'string');
|
||||
|
||||
Reference in New Issue
Block a user