wildduck/lib/user-handler.js

3351 lines
126 KiB
JavaScript
Raw Normal View History

2017-04-21 01:10:03 +08:00
'use strict';
2017-07-16 19:37:33 +08:00
const config = require('wild-config');
2017-04-21 01:10:03 +08:00
const log = require('npmlog');
2018-09-07 15:56:11 +08:00
const hashes = require('./hashes');
2017-04-21 01:10:03 +08:00
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
const tools = require('./tools');
2017-07-17 21:32:31 +08:00
const consts = require('./consts');
const counters = require('./counters');
const ObjectID = require('mongodb').ObjectID;
const generatePassword = require('generate-password');
2017-07-12 02:38:23 +08:00
const os = require('os');
2017-07-28 21:34:22 +08:00
const crypto = require('crypto');
const mailboxTranslations = require('./translations');
const MailComposer = require('nodemailer/lib/mail-composer');
const humanname = require('humanname');
2017-10-10 16:19:10 +08:00
const u2f = require('u2f');
2017-12-08 20:29:00 +08:00
const UserCache = require('./user-cache');
2018-05-11 19:39:23 +08:00
const isemail = require('isemail');
2017-04-21 01:10:03 +08:00
2018-11-28 21:50:57 +08:00
const TOTP_SETUP_TTL = 6 * 3600 * 1000;
2017-04-21 01:10:03 +08:00
class UserHandler {
constructor(options) {
this.database = options.database;
this.users = options.users || options.database;
this.redis = options.redis;
2018-10-18 15:37:32 +08:00
this.loggelf = options.loggelf || (() => false);
this.messageHandler = options.messageHandler;
this.counters = this.messageHandler ? this.messageHandler.counters : counters(this.redis);
2017-09-25 17:19:24 +08:00
2017-12-08 20:29:00 +08:00
this.userCache = new UserCache({
users: this.users,
redis: this.redis
});
2017-09-25 17:19:24 +08:00
if (!('authlogExpireDays' in options)) {
this.authlogExpireDays = 30;
} else {
this.authlogExpireDays = options.authlogExpireDays;
}
2017-04-21 01:10:03 +08:00
}
2017-12-28 16:14:30 +08:00
resolveAddress(address, options, callback) {
2019-04-05 20:08:46 +08:00
this.asyncResolveAddress(address, options)
.catch(err => callback(err))
.then(result => callback(null, result));
}
async asyncResolveAddress(address, options) {
2017-12-28 16:14:30 +08:00
options = options || {};
let wildcard = !!options.wildcard;
2018-08-28 14:42:42 +08:00
address = tools.normalizeAddress(address, false, {
removeLabel: true,
removeDots: true
});
2017-12-28 16:14:30 +08:00
2019-04-05 20:08:46 +08:00
let atPos = address.indexOf('@');
let username = address.substr(0, atPos);
let domain = address.substr(atPos + 1);
2017-12-28 16:14:30 +08:00
2018-08-15 04:45:45 +08:00
let projection = {
2018-01-09 19:50:29 +08:00
user: true,
targets: true
};
2018-08-15 04:45:45 +08:00
Object.keys(options.projection || {}).forEach(key => {
projection[key] = true;
2018-01-09 19:50:29 +08:00
});
2019-05-10 18:32:49 +08:00
if (options.projection === false) {
// do not use projection
projection = false;
}
2019-04-05 20:08:46 +08:00
try {
let addressData;
// try exact match
addressData = await this.users.collection('addresses').findOne(
{
addrview: username + '@' + domain
},
{
projection,
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-12-28 16:14:30 +08:00
}
2019-04-05 20:08:46 +08:00
);
2017-12-28 16:14:30 +08:00
2019-04-05 20:08:46 +08:00
if (addressData) {
return addressData;
}
2017-12-28 16:14:30 +08:00
2019-04-05 20:08:46 +08:00
// try an alias
let aliasDomain;
let aliasData = await this.users.collection('domainaliases').findOne(
{ alias: domain },
{
maxTimeMS: consts.DB_MAX_TIME_USERS
}
);
2017-12-28 16:14:30 +08:00
2019-04-05 20:08:46 +08:00
if (aliasData) {
aliasDomain = aliasData.domain;
2017-12-28 16:14:30 +08:00
2019-04-05 20:08:46 +08:00
addressData = await this.users.collection('addresses').findOne(
{
addrview: username + '@' + aliasDomain
},
{
projection,
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-12-28 16:14:30 +08:00
}
2019-04-05 20:08:46 +08:00
);
2017-12-28 16:14:30 +08:00
2019-04-05 20:08:46 +08:00
if (addressData) {
return addressData;
}
}
2017-12-28 16:14:30 +08:00
2019-04-05 20:08:46 +08:00
if (!wildcard) {
// wildcard not allowed, so there is nothing else to check for
return false;
}
2017-12-28 16:14:30 +08:00
2019-05-10 18:32:49 +08:00
let partialWildcards = tools.getWildcardAddresses(username, domain);
2019-04-05 20:08:46 +08:00
let query = {
2019-05-10 18:32:49 +08:00
addrview: { $in: partialWildcards }
2019-04-05 20:08:46 +08:00
};
2017-12-28 16:14:30 +08:00
2019-05-10 18:32:49 +08:00
let sortedDomainPartials = partialWildcards.map(addr => addr.replace(/^\*/, '')).sort((a, b) => b.lenght - a.length);
let sortedAliasPartials = [];
2019-04-05 20:08:46 +08:00
if (aliasDomain) {
// search for alias domain as well
2019-05-10 18:32:49 +08:00
let aliasWildcards = tools.getWildcardAddresses(username, aliasDomain);
query.addrview.$in = query.addrview.$in.concat(aliasWildcards);
sortedAliasPartials = aliasWildcards.map(addr => addr.replace(/^\*/, '')).sort((a, b) => a.lenght - b.length);
2019-04-05 20:08:46 +08:00
}
2017-12-28 16:14:30 +08:00
2019-05-10 18:32:49 +08:00
let sortedPartials = sortedDomainPartials.concat(sortedAliasPartials);
// try to find a catch-all address while preferring the longest match
let addressMatches = await this.users
.collection('addresses')
.find(query, {
projection,
maxTimeMS: consts.DB_MAX_TIME_USERS
})
.toArray();
if (addressMatches && addressMatches.length) {
let matchingPartials = new WeakMap();
addressMatches.forEach(addressData => {
let partialMatch = sortedPartials.find(partial => addressData.addrview.indexOf(partial) >= 0);
if (partialMatch) {
matchingPartials.set(addressData, sortedPartials.indexOf(partialMatch));
}
});
addressData = addressMatches.sort((a, b) => {
let aPos = matchingPartials.has(a) ? matchingPartials.get(a) : Infinity;
let bPos = matchingPartials.has(b) ? matchingPartials.get(b) : Infinity;
return aPos - bPos;
})[0];
}
2017-12-28 16:14:30 +08:00
2019-04-05 20:08:46 +08:00
if (addressData) {
return addressData;
}
2017-12-28 16:14:30 +08:00
2019-04-05 20:08:46 +08:00
// try to find a catch-all user (eg. "postmaster@*")
addressData = await this.users.collection('addresses').findOne(
{
addrview: username + '@*'
},
{
projection,
maxTimeMS: consts.DB_MAX_TIME_USERS
}
);
2017-12-28 16:14:30 +08:00
2019-04-05 20:08:46 +08:00
if (addressData) {
return addressData;
2017-12-28 16:14:30 +08:00
}
2019-04-05 20:08:46 +08:00
} catch (err) {
err.code = 'InternalDatabaseError';
throw err;
}
// no match was found
return false;
2017-12-28 16:14:30 +08:00
}
2019-07-11 15:52:43 +08:00
get(username, extraFields, callback) {
this.asyncGet(username, extraFields)
.catch(err => callback(err))
.then(result => callback(null, result));
}
2017-08-31 20:52:13 +08:00
/**
* Reolve user by username/address
*
* @param {String} username Either username or email address
* @param {Object} [extraFields] Optional projection fields object
*/
2019-07-11 15:52:43 +08:00
async asyncGet(username, extraFields) {
2017-08-31 20:52:13 +08:00
let fields = {
_id: true,
quota: true,
storageUsed: true,
disabled: true
};
Object.keys(extraFields || {}).forEach(field => {
fields[field] = true;
});
2019-07-11 15:52:43 +08:00
let addressData;
let query;
if (tools.isId(username)) {
query = { _id: new ObjectID(username) };
} else if (username.indexOf('@') < 0) {
// assume regular username
query = { unameview: tools.uview(username) };
} else {
addressData = await this.asyncResolveAddress(username, { projection: { name: true } });
2017-12-28 16:14:30 +08:00
2019-07-11 15:52:43 +08:00
if (addressData.user) {
query = { _id: addressData.user };
2017-08-31 20:52:13 +08:00
}
2019-07-11 15:52:43 +08:00
}
2017-08-31 20:52:13 +08:00
2019-07-11 15:52:43 +08:00
if (!query) {
return false;
}
try {
let userData = await this.users.collection('users').findOne(query, {
projection: fields,
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-12-28 16:14:30 +08:00
});
2017-08-31 20:52:13 +08:00
2019-07-11 15:52:43 +08:00
if (userData && fields.name && addressData && addressData.name) {
// override name
userData.name = addressData.name;
2017-08-31 20:52:13 +08:00
}
2019-07-11 15:52:43 +08:00
return userData;
} catch (err) {
err.code = 'InternalDatabaseError';
throw err;
}
2017-08-31 20:52:13 +08:00
}
2018-04-13 10:22:49 +08:00
/**
* rateLimitIP
* if ip is not available will always return success object
* @param {Object} meta
* @param {String} meta.ip request remote ip address
* @param {Integer} count
* @param {Function} callback
*/
2019-07-11 15:52:43 +08:00
async rateLimitIP(meta, count) {
2018-11-05 15:25:43 +08:00
if (!meta || !meta.ip || !consts.IP_AUTH_FAILURES) {
2019-07-11 15:52:43 +08:00
return { success: true };
2018-04-13 10:22:49 +08:00
}
2019-07-11 15:52:43 +08:00
let wlKey = 'rl-wl';
// $ redis-cli
// > SADD "rl-wl" "1.2.3.4"
2019-07-11 15:52:43 +08:00
try {
let isMember = await this.redis.sismember(wlKey, meta.ip);
if (isMember) {
// whitelisted IP
2019-07-11 15:52:43 +08:00
return { success: true };
}
2019-07-11 15:52:43 +08:00
} catch (err) {
log.error('Redis', 'SMFAIL key=%s value=%s error=%s', wlKey, meta.ip, err.message);
// ignore errors
return { success: true };
}
2019-07-11 15:52:43 +08:00
return await this.counters.asyncTTLCounter('auth_ip:' + meta.ip, count, consts.IP_AUTH_FAILURES, consts.IP_AUTH_WINDOW);
2018-04-13 10:22:49 +08:00
}
/**
* rateLimitUser
* @param {String} tokenID user identifier
2019-07-11 15:52:43 +08:00
* @param {Object} meta
2018-04-13 10:22:49 +08:00
* @param {Integer} count
*/
2019-07-11 15:52:43 +08:00
async rateLimitUser(tokenID, meta, count) {
if (meta && meta.ip) {
// check if whitelisted IP
let wlKey = 'rl-wl';
// $ redis-cli
// > SADD "rl-wl" "1.2.3.4"
try {
let isMember = await this.redis.sismember(wlKey, meta.ip);
if (isMember) {
// whitelisted IP, allow authentication attempt without rate limits
return { success: true };
}
} catch (err) {
2018-09-26 15:25:52 +08:00
log.error('Redis', 'SMFAIL key=%s value=%s error=%s', wlKey, meta.ip, err.message);
// ignore errors
}
2019-07-11 15:52:43 +08:00
}
return await this.counters.asyncTTLCounter('auth_user:' + tokenID, count, consts.USER_AUTH_FAILURES, consts.USER_AUTH_WINDOW);
2018-04-13 10:22:49 +08:00
}
/**
* rateLimitReleaseUser
* @param {String} tokenID user identifier
* @param {Integer} count
*/
2019-07-11 15:52:43 +08:00
async rateLimitReleaseUser(tokenID) {
await this.redis.del('auth_user:' + tokenID);
2018-04-13 10:22:49 +08:00
}
/**
2018-04-13 10:22:49 +08:00
* rateLimit
* @param {String} tokenID user identifier
* @param {Object} meta
* @param {String} meta.ip request remote ip address
* @param {Integer} count
*/
2019-07-11 15:52:43 +08:00
async rateLimit(tokenID, meta, count) {
let ipRes = await this.rateLimitIP(meta, count);
2018-04-13 10:22:49 +08:00
2019-07-11 15:52:43 +08:00
let userRes = await this.rateLimitUser(tokenID, meta, count);
if (!ipRes.success) {
return ipRes;
}
return userRes;
2018-04-13 10:22:49 +08:00
}
2017-04-21 01:10:03 +08:00
/**
* Authenticate user
*
* @param {String} username Either username or email address
2019-07-11 15:52:43 +08:00
* @param {String} password Password for authentication
* @param {String} [requiredScope="master"] Which scope to use
* @param {Object} [meta] Additional meta info
* @param {String} [meta.ip] IP address of the client
* @param {String} [meta.session] Session ID
2017-04-21 01:10:03 +08:00
*/
2019-07-11 15:52:43 +08:00
async authenticate(username, password, requiredScope, meta) {
meta = meta || {};
requiredScope = requiredScope || 'master';
2017-04-22 03:16:01 +08:00
2018-11-14 21:28:12 +08:00
username = (username || '').toString();
let userDomain = username.indexOf('@') >= 0 ? username.split('@').pop() : '';
2019-07-11 15:52:43 +08:00
let now = new Date();
let passwordType = 'master'; // try 'master' first and 'asp' later
2018-11-15 16:37:02 +08:00
let passwordId;
2017-07-24 21:44:08 +08:00
meta = meta || {};
meta.requiredScope = requiredScope;
if (!password) {
// do not allow signing in without a password
2018-10-18 16:53:14 +08:00
this.loggelf({
short_message: '[AUTHFAIL] ' + username,
_error: 'Empty password',
2018-11-15 16:24:42 +08:00
_auth_result: 'fail',
2018-10-18 16:53:14 +08:00
_username: username,
2018-11-14 21:28:12 +08:00
_domain: userDomain,
2018-10-18 16:53:14 +08:00
_scope: requiredScope,
_ip: meta.ip
});
2019-07-11 15:52:43 +08:00
return [false, false];
}
2018-08-15 03:45:18 +08:00
// first check if client IP is not used too much
2019-07-11 15:52:43 +08:00
try {
let rateLimitRes = await this.rateLimitIP(meta, 0);
if (!rateLimitRes.success) {
// too many failed attempts from this IP
this.loggelf({
short_message: '[AUTHFAIL] ' + username,
_error: 'Rate limited',
_auth_result: 'ratelimited',
_username: username,
_domain: userDomain,
_scope: requiredScope,
_ip: meta.ip
});
throw rateLimitResponse(rateLimitRes);
}
} catch (err) {
err.code = 'InternalDatabaseError';
this.loggelf({
short_message: '[AUTHFAIL] ' + username,
full_message: err.stack,
_error: err.message,
_code: err.code,
_auth_result: 'error',
_username: username,
_domain: userDomain,
_scope: requiredScope,
_ip: meta.ip
});
// return as failed auth
return [false, false];
}
let userQuery;
try {
userQuery = await this.checkAddress(username);
} catch (err) {
this.loggelf({
short_message: '[AUTHFAIL] ' + username,
_error: 'Unknown user',
_auth_result: 'unknown',
_username: username,
_domain: userDomain,
_scope: requiredScope,
_ip: meta.ip
});
return [false, false];
}
if (!userQuery) {
// nothing to do here
return [false, false];
}
let userData;
try {
userData = await this.users.collection('users').findOne(userQuery, {
projection: {
_id: true,
username: true,
address: true,
tempPassword: true,
password: true,
enabled2fa: true,
u2f: true,
disabled: true,
disabledScopes: true
},
maxTimeMS: consts.DB_MAX_TIME_USERS
});
} catch (err) {
err.code = 'InternalDatabaseError';
this.loggelf({
short_message: '[AUTHFAIL] ' + username,
full_message: err.stack,
_error: err.message,
_code: err.code,
_auth_result: 'error',
_username: username,
_domain: userDomain,
_scope: requiredScope,
_ip: meta.ip
});
// return as failed auth
return [false, false];
}
if (!userData) {
// rate limit failed authentication attempts against non-existent users as well
try {
let ustring = (userQuery.unameview || userQuery._id || '').toString();
let rateLimitRes = await this.rateLimit(ustring, meta, 1);
if (!rateLimitRes.success) {
// does not really matter but respond with a rate limit error, not auth fail error
this.loggelf({
short_message: '[AUTHFAIL] ' + username,
_error: 'Rate limited',
_auth_result: 'ratelimited',
_username: username,
_domain: userDomain,
_scope: requiredScope,
_ip: meta.ip
});
throw rateLimitResponse(rateLimitRes);
}
this.loggelf({
short_message: '[AUTHFAIL] ' + username,
_error: 'Unknown user',
_auth_result: 'unknown',
_username: username,
_domain: userDomain,
_scope: requiredScope,
_ip: meta.ip
});
return [false, false];
} catch (err) {
2018-04-13 10:22:49 +08:00
err.code = 'InternalDatabaseError';
2018-10-18 16:53:14 +08:00
this.loggelf({
short_message: '[AUTHFAIL] ' + username,
full_message: err.stack,
_error: err.message,
_code: err.code,
2018-11-15 16:24:42 +08:00
_auth_result: 'error',
2018-10-18 16:53:14 +08:00
_username: username,
2018-11-14 21:28:12 +08:00
_domain: userDomain,
2018-10-18 16:53:14 +08:00
_scope: requiredScope,
_ip: meta.ip
});
2019-07-11 15:52:43 +08:00
// return as failed auth
return [false, false];
2018-04-13 10:22:49 +08:00
}
2019-07-11 15:52:43 +08:00
}
2018-04-13 10:22:49 +08:00
2019-07-11 15:52:43 +08:00
// make sure we use the primary domain if available
userDomain = (userData.address || '').split('@').pop() || userDomain;
try {
// check if there are not too many auth attempts for that user
let rateLimitRes = await this.rateLimitUser(userData._id, meta, 0);
if (!rateLimitRes.success) {
// too many failed attempts for this user
2018-10-18 16:53:14 +08:00
this.loggelf({
short_message: '[AUTHFAIL] ' + username,
_error: 'Rate limited',
2018-11-15 16:24:42 +08:00
_auth_result: 'ratelimited',
2018-10-18 16:53:14 +08:00
_username: username,
2018-11-14 21:28:12 +08:00
_domain: userDomain,
2019-07-11 15:52:43 +08:00
_user: userData._id,
2018-10-18 16:53:14 +08:00
_scope: requiredScope,
_ip: meta.ip
});
2019-07-11 15:52:43 +08:00
let err = rateLimitResponse(rateLimitRes);
err.user = userData._id;
throw err;
2018-04-13 10:22:49 +08:00
}
2019-07-11 15:52:43 +08:00
} catch (err) {
err.code = 'InternalDatabaseError';
this.loggelf({
short_message: '[AUTHFAIL] ' + username,
full_message: err.stack,
_error: err.message,
_code: err.code,
_auth_result: 'error',
_username: username,
_domain: userDomain,
_user: userData._id,
_scope: requiredScope,
_ip: meta.ip
});
err.user = userData._id;
throw err;
}
2018-04-13 10:22:49 +08:00
2019-07-11 15:52:43 +08:00
if (userData.disabled) {
// disabled users can not log in
meta.result = 'disabled';
// TODO: should we send some specific error message?
this.loggelf({
short_message: '[AUTHFAIL] ' + username,
_error: 'User is disabled',
_auth_result: 'disabled',
_username: username,
_domain: userDomain,
_user: userData._id,
_scope: requiredScope,
_ip: meta.ip
});
await this.logAuthEvent(userData._id, meta);
return [false, userData._id];
}
2018-01-26 17:39:08 +08:00
2019-07-11 15:52:43 +08:00
try {
let authSuccess = async authResponse => {
// clear rate limit counter on success
try {
await this.rateLimitReleaseUser(userData._id);
} catch (err) {
//ignore
}
2017-04-21 01:10:03 +08:00
2019-07-11 15:52:43 +08:00
this.loggelf({
short_message: '[AUTHOK] ' + username,
_auth_result: 'success',
_username: username,
_domain: userDomain,
_user: userData._id,
_password_type: passwordType,
_password_id: passwordId,
_scope: requiredScope,
_ip: meta.ip
});
2017-11-23 17:51:37 +08:00
2019-07-11 15:52:43 +08:00
return [authResponse, userData._id];
};
2018-10-18 17:08:59 +08:00
2019-07-11 15:52:43 +08:00
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
let requirePasswordChange = false;
let usingTemporaryPassword = false;
2019-07-11 15:52:43 +08:00
let success;
if (userData.tempPassword && userData.tempPassword.created > new Date(now.getTime() - consts.TEMP_PASS_WINDOW)) {
// try temporary password first
2018-11-14 21:28:12 +08:00
2019-07-11 15:52:43 +08:00
try {
success = await hashes.asyncCompare(password, userData.tempPassword.password);
} catch (err) {
err.code = 'HashError';
throw err;
}
if (success) {
if (userData.validAfter > now) {
let err = new Error('Temporary password is not yet activated');
err.code = 'TempPasswordNotYetValid';
throw err;
}
2017-11-23 17:51:37 +08:00
2019-07-11 15:52:43 +08:00
requirePasswordChange = true;
usingTemporaryPassword = true;
}
}
2017-11-23 17:51:37 +08:00
2019-07-11 15:52:43 +08:00
if (!success) {
// temporary password did not match, try actual password
success = await hashes.asyncCompare(password, userData.password);
}
2017-11-23 17:51:37 +08:00
2019-07-11 15:52:43 +08:00
if (success) {
// master password matched
2018-09-07 15:56:11 +08:00
2019-07-11 15:52:43 +08:00
meta.result = 'success';
meta.source = !usingTemporaryPassword ? 'master' : 'temporary';
2018-09-07 15:56:11 +08:00
2019-07-11 15:52:43 +08:00
if (enabled2fa.length) {
meta.require2fa = enabled2fa.length ? enabled2fa.join(',') : false;
}
2018-09-07 15:56:11 +08:00
2019-07-11 15:52:43 +08:00
if (hashes.shouldRehash(userData.password)) {
// master password needs rehashing
let hash;
try {
hash = await hashes.asyncHash(password);
if (hash) {
// should this even happen???
throw new Error('Failed to rehash password');
}
2018-09-07 15:56:11 +08:00
2019-07-11 15:52:43 +08:00
try {
let r = await this.users.collection('users').updateOne(
{
_id: userData._id
},
{
$set: {
password: hash
}
},
{ w: 'majority' }
);
2018-09-07 15:56:11 +08:00
2019-07-11 15:52:43 +08:00
if (r.modifiedCount) {
log.info('DB', 'REHASHED user=%s algo=%s', userData._id, consts.DEFAULT_HASH_ALGO);
}
} catch (err) {
log.error('DB', 'DBFAIL rehash user=%s error=%s', userData._id, err.message);
}
} catch (err) {
log.error('DB', 'HASHFAIL rehash user=%s error=%s', userData._id, err.message);
// ignore DB error, rehash some other time
}
}
2018-01-26 17:39:08 +08:00
2019-07-11 15:52:43 +08:00
let disabledScopes = userData.disabledScopes || [];
2018-01-26 17:39:08 +08:00
2019-07-11 15:52:43 +08:00
if (requiredScope !== 'master' && (enabled2fa.length || usingTemporaryPassword || disabledScopes.includes(requiredScope))) {
// master password can not be used for other scopes than 'master' if 2FA is enabled
// temporary password is also only valid for master
meta.result = 'fail';
let err = new Error('Authentication failed. Invalid scope');
err.code = 'InvalidAuthScope';
err.response = 'NO'; // imap response code
await this.logAuthEvent(userData._id, meta);
2018-01-26 17:39:08 +08:00
2019-07-11 15:52:43 +08:00
throw err;
}
2017-11-28 21:45:43 +08:00
2019-07-11 15:52:43 +08:00
try {
let authEvent = await this.logAuthEvent(userData._id, meta);
await this.users.collection('users').updateOne(
{
_id: userData._id
},
{
$set: {
lastLogin: {
time: now,
authEvent,
ip: meta.ip
2017-11-28 21:45:43 +08:00
}
2019-07-11 15:52:43 +08:00
}
},
{
maxTimeMS: consts.DB_MAX_TIME_USERS
}
);
} catch (err) {
// ignore
}
2017-11-28 21:45:43 +08:00
2019-07-11 15:52:43 +08:00
let authResponse = {
user: userData._id,
username: userData.username,
scope: meta.requiredScope,
// if 2FA is enabled then require token validation
require2fa: enabled2fa.length && !usingTemporaryPassword ? enabled2fa : false,
requirePasswordChange // true, if password was reset and using temporary password
};
2018-01-26 17:39:08 +08:00
2019-07-11 15:52:43 +08:00
if (enabled2fa.length && !usingTemporaryPassword) {
authResponse.enabled2fa = enabled2fa;
2018-01-26 17:39:08 +08:00
2019-07-11 15:52:43 +08:00
if (enabled2fa.includes('u2f') && userData.u2f && userData.u2f.keyHandle) {
try {
let u2fAuthRequest = await this.generateU2fAuthRequest(userData._id, userData.u2f.keyHandle, meta.appId);
if (u2fAuthRequest) {
authResponse.u2fAuthRequest = u2fAuthRequest;
}
} catch (err) {
log.error('DB', 'U2FREFAIL u2fAuthRequest id=%s error=%s', userData._id, err.message);
}
}
}
2018-10-03 15:16:18 +08:00
2019-07-11 15:52:43 +08:00
return await authSuccess(authResponse);
}
2018-01-26 17:39:08 +08:00
2019-07-11 15:52:43 +08:00
if (requiredScope === 'master') {
// only master password can be used for management tasks
meta.result = 'fail';
meta.source = 'master';
await this.logAuthEvent(userData._id, meta);
2018-10-03 15:16:18 +08:00
2019-07-11 15:52:43 +08:00
let err = new Error('Invalid Auth');
err.code = 'AuthFail'; // will be returned as failed auth, not an error
throw err;
}
2018-10-03 15:16:18 +08:00
2019-07-11 15:52:43 +08:00
// try application specific passwords
password = password.replace(/\s+/g, '').toLowerCase();
2017-04-21 01:10:03 +08:00
2019-07-11 15:52:43 +08:00
if (!/^[a-z]{16}$/.test(password)) {
// does not look like an application specific password
meta.result = 'fail';
meta.source = 'master';
await this.logAuthEvent(userData._id, meta);
2017-07-28 21:34:22 +08:00
2019-07-11 15:52:43 +08:00
let err = new Error('Invalid Auth');
err.code = 'AuthFail'; // will be returned as failed auth, not an error
throw err;
}
2017-07-24 21:44:08 +08:00
2019-07-11 15:52:43 +08:00
let selector = getStringSelector(password);
2017-07-24 21:44:08 +08:00
2019-07-11 15:52:43 +08:00
let asps;
try {
asps = await this.users
.collection('asps')
.find({
user: userData._id
})
.maxTimeMS(consts.DB_MAX_TIME_USERS)
.toArray();
} catch (err) {
err.code = 'InternalDatabaseError';
throw err;
}
2019-07-11 15:52:43 +08:00
if (!asps || !asps.length) {
// user does not have app specific passwords set
meta.result = 'fail';
meta.source = 'master';
await this.logAuthEvent(userData._id, meta);
2017-07-24 21:44:08 +08:00
2019-07-11 15:52:43 +08:00
let err = new Error('Invalid Auth');
err.code = 'AuthFail'; // will be returned as failed auth, not an error
throw err;
}
2019-07-11 15:52:43 +08:00
for (let asp of asps) {
if (asp.selector && asp.selector !== selector) {
// no need to check, definitely a wrong one
continue;
}
2019-07-11 15:52:43 +08:00
let success;
try {
success = await hashes.compare(password, asp.password);
} catch (err) {
err.code = 'HashError';
throw err;
}
2017-07-24 21:44:08 +08:00
2019-07-11 15:52:43 +08:00
if (!success) {
continue;
}
2019-07-11 15:52:43 +08:00
meta.source = 'asp';
meta.asp = asp._id;
// store ASP name in case the ASP gets deleted and for faster listing
meta.aname = asp.description;
2017-11-23 17:51:37 +08:00
2019-07-11 15:52:43 +08:00
passwordType = 'asp';
passwordId = asp._id.toString();
2017-11-23 17:51:37 +08:00
2019-07-11 15:52:43 +08:00
if (!asp.scopes.includes('*') && !asp.scopes.includes(requiredScope)) {
meta.result = 'fail';
2018-01-11 17:20:12 +08:00
2019-07-11 15:52:43 +08:00
await this.logAuthEvent(userData._id, meta);
2017-11-23 17:51:37 +08:00
2019-07-11 15:52:43 +08:00
let err = new Error('Authentication failed. Invalid scope');
err.code = 'InvalidAuthScope';
err.response = 'NO'; // imap response code
throw err;
}
2019-07-11 15:52:43 +08:00
meta.result = 'success';
2019-07-11 15:52:43 +08:00
let authEvent;
try {
authEvent = await this.logAuthEvent(userData._id, meta);
} catch (err) {
// don't really care
}
2018-11-14 19:32:40 +08:00
2019-07-11 15:52:43 +08:00
let aspUpdates = {
used: now,
authEvent,
authIp: meta.ip
};
2018-11-14 19:32:40 +08:00
2019-07-11 15:52:43 +08:00
if (asp.ttl) {
// extend temporary password ttl every time it is used
aspUpdates.expires = new Date(now.getTime() + asp.ttl * 1000);
}
2019-07-11 15:52:43 +08:00
try {
await this.users.collection('asps').findOneAndUpdate(
{
_id: asp._id
},
{
$set: aspUpdates
},
{
maxTimeMS: consts.DB_MAX_TIME_USERS
}
);
} catch (err) {
// ignore
}
return await authSuccess({
user: userData._id,
username: userData.username,
scope: requiredScope,
asp: asp._id.toString(),
require2fa: false // application scope never requires 2FA
});
}
// no suitable password found
meta.result = 'fail';
meta.source = 'master';
await this.logAuthEvent(userData._id, meta);
let err = new Error('Invalid Auth');
err.code = 'AuthFail'; // will be returned as failed auth, not an error
throw err;
} catch (err) {
this.loggelf({
short_message: '[AUTHFAIL] ' + username,
full_message: err.stack,
_error: err.message || 'Authentication failed',
_code: err.code,
_auth_result: 'fail',
_username: username,
_domain: userDomain,
_user: userData._id,
_password_type: passwordType,
_password_id: passwordId,
_scope: requiredScope,
_ip: meta.ip
});
2019-07-11 15:52:43 +08:00
// increment rate limit counter on failure
await this.rateLimit(userData._id, meta, 1);
if (err.code !== 'AuthFail') {
err.user = userData._id;
throw err;
}
return [false, userData._id];
}
2017-04-21 01:10:03 +08:00
}
2018-01-26 17:39:08 +08:00
/**
* Authenticate user using an older password. Needed for account recovery.
* TODO: check if this is even used anywhere?
2018-01-26 17:39:08 +08:00
*
* @param {String} username Either username or email address
*/
authenticateUsingOldPassword(username, password, callback) {
if (!password) {
// do not allow signing in without a password
return callback(null, false);
}
this.checkAddress(username, (err, query) => {
if (err) {
return callback(err);
}
if (!query) {
return callback(null, false);
}
this.users.collection('users').findOne(
query,
{
2018-08-15 04:45:45 +08:00
projection: {
2018-01-26 17:39:08 +08:00
_id: true,
username: true,
oldPasswords: true,
disabled: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2018-01-26 17:39:08 +08:00
},
(err, userData) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!userData) {
return callback(null, false);
}
if (userData.disabled) {
return callback(null, false);
}
// FIXME: use IP in rlkey
2018-01-26 17:39:08 +08:00
let rlkey = 'outh:' + userData._id.toString();
2018-04-13 10:22:49 +08:00
this.counters.ttlcounter(rlkey, 0, consts.USER_AUTH_FAILURES, consts.USER_AUTH_WINDOW, (err, res) => {
2018-01-26 17:39:08 +08:00
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!res.success) {
return rateLimitResponse(res, callback);
2018-01-26 17:39:08 +08:00
}
let authSuccess = (...args) => {
// clear rate limit counter on success
this.redis.del(rlkey, () => false);
callback(...args);
};
let authFail = (...args) => {
// increment rate limit counter on failure
2018-04-13 10:22:49 +08:00
this.counters.ttlcounter(rlkey, 1, consts.USER_AUTH_FAILURES, consts.USER_AUTH_WINDOW, () => {
2018-01-26 17:39:08 +08:00
callback(...args);
});
};
if (!userData.oldPasswords || !userData.oldPasswords.length) {
return authFail(null, false);
}
// do not check too many passwords
if (userData.oldPasswords.length > 30) {
userData.oldPasswords = userData.oldPasswords.slice(-30);
}
let curPos = 0;
let checkNext = () => {
if (curPos >= userData.oldPasswords.length) {
return authFail(null, false);
}
let oldPassword = userData.oldPasswords[curPos++];
2019-01-25 21:19:51 +08:00
hashes.compare(password, oldPassword.hash, (err, success) => {
2018-01-26 17:39:08 +08:00
if (err) {
err.code = 'HashError';
2018-01-26 17:39:08 +08:00
return callback(err);
}
if (!success) {
return setImmediate(checkNext);
}
return authSuccess(null, userData._id);
});
};
return setImmediate(checkNext);
});
}
);
});
}
2019-07-11 15:52:43 +08:00
async generateU2fAuthRequest(user, keyHandle, appId) {
2017-10-10 16:19:10 +08:00
let authRequest;
try {
2018-06-28 14:12:31 +08:00
authRequest = u2f.request(appId || config.u2f.appId, keyHandle);
2019-07-11 15:52:43 +08:00
} catch (err) {
log.error('U2F', 'U2FFAIL request id=%s error=%s', user, err.message);
2017-10-10 16:19:10 +08:00
}
if (!authRequest) {
2019-07-11 15:52:43 +08:00
return false;
2017-10-10 16:19:10 +08:00
}
2019-07-11 15:52:43 +08:00
try {
let results = await this.redis
.multi()
.set('u2f:auth:' + user, JSON.stringify(authRequest))
.expire('u2f:auth:' + user, 1 * 3600)
.exec();
if (!results || !results[0]) {
throw new Error('Invalid DB response');
} else if (results && results[0] && results[0][0]) {
throw typeof results[0][0] !== 'object' ? new Error(results[0][0]) : results[0][0];
}
return authRequest;
} catch (err) {
err.code = 'InternalDatabaseError';
throw err;
}
2017-10-10 16:19:10 +08:00
}
2017-07-25 01:32:22 +08:00
generateASP(user, data, callback) {
let password = generatePassword.generate({
length: 16,
uppercase: false,
numbers: false,
symbols: false
});
2017-07-28 21:34:22 +08:00
// We need a quick hash key that can be used to identify the password.
// Otherwise, when authenticating, we'd need to check the password against all stored bcrypt
// hashes which would make forever if the user has a longer list of application specific passwords
let selector = getStringSelector(password);
2017-11-06 23:32:45 +08:00
let allowedScopes = [...consts.SCOPES];
2017-07-24 21:44:08 +08:00
let hasAllScopes = false;
let scopeSet = new Set();
2017-07-25 01:32:22 +08:00
let scopes = [].concat(data.scopes || []);
2017-07-24 21:44:08 +08:00
2017-07-25 01:32:22 +08:00
scopes.forEach(scope => {
2017-07-24 21:44:08 +08:00
scope = scope.toLowerCase().trim();
if (scope === '*') {
hasAllScopes = true;
2018-10-03 14:35:44 +08:00
} else if (allowedScopes.includes(scope)) {
2017-07-24 21:44:08 +08:00
scopeSet.add(scope);
}
});
2018-10-03 14:35:44 +08:00
2017-07-24 21:44:08 +08:00
if (hasAllScopes || scopeSet.size === allowedScopes.length) {
scopes = ['*'];
} else {
2018-10-03 16:39:31 +08:00
scopes = Array.from(scopeSet).sort((a, b) => a.localeCompare(b));
2017-07-24 21:44:08 +08:00
}
hashes.hash(password, (err, hash) => {
2018-05-14 15:14:44 +08:00
if (err) {
log.error('DB', 'HASHFAIL generateASP id=%s error=%s', user, err.message);
err.code = 'HashError';
2018-05-14 15:14:44 +08:00
return callback(err);
}
2018-05-14 15:14:44 +08:00
let passwordData = {
2018-10-03 14:35:44 +08:00
_id: new ObjectID(),
2018-05-14 15:14:44 +08:00
user,
description: data.description,
scopes,
password: hash,
selector,
2018-10-03 15:16:18 +08:00
used: false,
authEvent: false,
authIp: false,
2018-05-14 15:14:44 +08:00
created: new Date()
};
2017-11-23 17:51:37 +08:00
2018-11-14 19:32:40 +08:00
if (data.ttl) {
passwordData.ttl = data.ttl;
passwordData.expires = new Date(Date.now() + data.ttl * 1000);
}
2018-05-14 15:14:44 +08:00
// register this address as the default address for that user
return this.users.collection('users').findOne(
{
_id: user
},
{
2018-08-15 04:45:45 +08:00
projection: {
2018-05-14 15:14:44 +08:00
_id: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2018-05-14 15:14:44 +08:00
},
(err, userData) => {
2017-11-23 17:51:37 +08:00
if (err) {
2018-05-14 15:14:44 +08:00
log.error('DB', 'DBFAIL generateASP id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to find user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
2018-05-14 15:14:44 +08:00
if (!userData) {
return callback(new Error('User not found'));
}
this.users.collection('asps').insertOne(passwordData, err => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
}
return this.logAuthEvent(
user,
{
action: 'create asp',
asp: passwordData._id,
aname: passwordData.description,
2018-11-14 19:32:40 +08:00
temporary: passwordData.ttl ? true : false,
2018-05-14 15:14:44 +08:00
result: 'success',
sess: data.sess,
ip: data.ip
},
() =>
callback(null, {
id: passwordData._id,
password
})
);
});
}
);
});
2017-11-23 17:51:37 +08:00
}
2017-11-23 17:51:37 +08:00
deleteASP(user, asp, data, callback) {
2018-01-11 17:20:12 +08:00
return this.users.collection('asps').findOne(
2017-11-23 17:51:37 +08:00
{
_id: asp,
user
},
2019-01-24 05:38:29 +08:00
{
maxTimeMS: consts.DB_MAX_TIME_USERS
2019-01-24 05:38:29 +08:00
},
2018-01-11 17:20:12 +08:00
(err, asp) => {
2017-07-24 21:44:08 +08:00
if (err) {
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-07-24 21:44:08 +08:00
return callback(err);
}
2017-11-23 17:51:37 +08:00
2018-01-11 17:20:12 +08:00
if (!asp) {
let err = new Error('Application Specific Password was not found');
err.code = 'AspNotFound';
return callback(err);
2017-11-23 17:51:37 +08:00
}
2018-01-11 17:20:12 +08:00
this.users.collection('asps').deleteOne({ _id: asp._id }, (err, r) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
}
if (r.deletedCount) {
return this.logAuthEvent(
user,
{
action: 'delete asp',
asp: asp._id,
aname: asp.description,
result: 'success',
2018-01-12 16:16:16 +08:00
sess: data.sess,
2018-01-11 17:20:12 +08:00
ip: data.ip
},
() => callback(null, true)
);
} else {
return callback(null, true);
}
});
2017-07-25 01:32:22 +08:00
}
2017-11-23 17:51:37 +08:00
);
2017-07-25 01:32:22 +08:00
}
2017-04-21 01:10:03 +08:00
create(data, callback) {
2017-11-23 17:51:37 +08:00
this.users.collection('users').findOne(
{
username: data.username.replace(/\./g, '')
},
{
2018-08-15 04:45:45 +08:00
projection: {
2017-11-23 17:51:37 +08:00
unameview: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-11-23 17:51:37 +08:00
},
(err, userData) => {
if (err) {
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
err.message = 'Database Error, failed to create user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
2017-08-05 20:39:31 +08:00
2017-11-23 17:51:37 +08:00
if (userData) {
let err = new Error('This username already exists');
2017-12-21 16:31:34 +08:00
err.code = 'UserExistsError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
2017-11-03 20:11:59 +08:00
2018-05-11 19:39:23 +08:00
let address = data.address ? data.address : false;
if (!address) {
try {
if (isemail.validate(data.username)) {
address = data.username;
}
} catch (E) {
// ignore
}
}
if (!address) {
address = data.username.split('@').shift() + '@' + (config.emailDomain || os.hostname()).toLowerCase();
}
address = tools.normalizeAddress(address, false, { removeLabel: true });
2018-05-11 19:39:23 +08:00
let addrview = tools.uview(address);
2017-07-17 21:32:31 +08:00
2018-10-03 15:16:18 +08:00
let allowedScopes = [...consts.SCOPES];
let scopeSet = new Set();
let disabledScopes = [].concat(data.disabledScopes || []);
disabledScopes.forEach(scope => {
scope = scope.toLowerCase().trim();
if (allowedScopes.includes(scope)) {
scopeSet.add(scope);
}
});
2018-10-03 16:39:31 +08:00
disabledScopes = Array.from(scopeSet).sort((a, b) => a.localeCompare(b));
2018-10-03 15:16:18 +08:00
2018-01-24 19:37:57 +08:00
let checkAddress = done => {
if (data.emptyAddress) {
return done();
}
2017-07-31 15:59:18 +08:00
2018-01-24 19:37:57 +08:00
this.users.collection('addresses').findOne(
{
addrview
},
{
2018-08-15 04:45:45 +08:00
projection: {
2018-01-24 19:37:57 +08:00
_id: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2018-01-24 19:37:57 +08:00
},
(err, addressData) => {
if (err) {
log.error('DB', 'CREATEFAIL username=%s address=%s error=%s', data.username, address, err.message);
err.message = 'Database Error, failed to create user';
err.code = 'InternalDatabaseError';
return callback(err);
}
2017-04-21 01:10:03 +08:00
2018-01-24 19:37:57 +08:00
if (addressData) {
let err = new Error('This address already exists');
err.code = 'AddressExistsError';
return callback(err);
}
2017-04-21 01:10:03 +08:00
2018-01-24 19:37:57 +08:00
done();
}
);
};
2017-04-21 01:10:03 +08:00
2018-01-24 19:37:57 +08:00
checkAddress(() => {
let junkRetention = consts.JUNK_RETENTION;
2017-04-21 01:10:03 +08:00
2018-01-24 19:37:57 +08:00
// Insert user data
2017-04-21 01:10:03 +08:00
2018-05-14 15:14:44 +08:00
let hashPassword = done => {
if (!data.password) {
// Users with an empty password can not log in
return done(null, '');
2018-05-14 15:14:44 +08:00
}
if (data.hashedPassword) {
// try if the bcrypt library can handle it?
2018-09-07 15:56:11 +08:00
return hashes.compare('whatever', data.password, err => {
if (err) {
return done(err);
}
// did not throw, so probably OK
return done(null, data.password);
});
}
hashes.hash(data.password, done);
2018-05-14 15:14:44 +08:00
};
2017-10-24 19:30:33 +08:00
2018-05-14 15:14:44 +08:00
hashPassword((err, hash) => {
if (err) {
log.error('DB', 'HASHFAIL user.create id=%s error=%s', data.username, err.message);
err.code = 'HashError';
2018-05-14 15:14:44 +08:00
return callback(err);
}
2018-01-30 22:14:15 +08:00
2018-05-14 15:14:44 +08:00
let id = new ObjectID();
2017-07-30 23:07:35 +08:00
2018-05-14 15:14:44 +08:00
// spamLevel is from 0 (everything is spam) to 100 (accept everything)
let spamLevel = 'spamLevel' in data && !isNaN(data.spamLevel) ? Number(data.spamLevel) : 50;
if (spamLevel < 0) {
spamLevel = 0;
}
if (spamLevel > 100) {
spamLevel = 100;
}
2018-05-14 15:14:44 +08:00
userData = {
_id: id,
2018-05-14 15:14:44 +08:00
username: data.username,
// dotless version
unameview: tools.uview(data.username),
2017-04-21 01:10:03 +08:00
2018-05-14 15:14:44 +08:00
name: data.name,
2018-05-14 15:14:44 +08:00
// security
password: '', // set this later. having no password prevents login
2017-11-03 20:11:59 +08:00
2018-05-14 15:14:44 +08:00
enabled2fa: [],
seed: '', // 2fa seed value
2018-11-28 21:50:57 +08:00
pendingSeed: '',
pendingSeedChanged: false,
2017-11-03 20:11:59 +08:00
2018-05-14 15:14:44 +08:00
// default email address
address: '', // set this later
2017-09-12 16:02:22 +08:00
2018-10-22 14:27:43 +08:00
language: data.language,
2018-05-14 15:14:44 +08:00
// quota
storageUsed: 0,
quota: data.quota || 0,
2018-05-14 15:14:44 +08:00
recipients: data.recipients || 0,
forwards: data.forwards || 0,
2017-09-12 16:02:22 +08:00
2018-06-12 16:35:37 +08:00
imapMaxUpload: data.imapMaxUpload || 0,
imapMaxDownload: data.imapMaxDownload || 0,
pop3MaxDownload: data.pop3MaxDownload || 0,
imapMaxConnections: data.imapMaxConnections || 0,
2018-06-12 18:45:02 +08:00
receivedMax: data.receivedMax || 0,
2018-06-12 16:35:37 +08:00
2018-05-14 15:14:44 +08:00
targets: [].concat(data.targets || []),
2017-04-21 01:10:03 +08:00
2018-05-14 15:14:44 +08:00
// autoreply status
// off by default, can be changed later by user through the API
autoreply: false,
2018-01-30 22:14:15 +08:00
2018-09-13 14:12:41 +08:00
uploadSentMessages: !!data.uploadSentMessages,
2018-05-14 15:14:44 +08:00
pubKey: data.pubKey || '',
encryptMessages: !!data.encryptMessages,
encryptForwarded: !!data.encryptForwarded,
2017-11-23 17:51:37 +08:00
2018-05-14 15:14:44 +08:00
spamLevel,
2018-01-24 19:37:57 +08:00
2018-05-14 15:14:44 +08:00
// default retention for user mailboxes
retention: data.retention || 0,
2018-01-24 19:37:57 +08:00
2018-10-03 15:16:18 +08:00
disabledScopes,
lastLogin: {
time: false,
authEvent: false,
ip: false
},
2017-04-21 01:10:03 +08:00
2018-11-28 18:25:54 +08:00
metaData: data.metaData || '',
2018-05-14 15:14:44 +08:00
// until setup value is not true, this account is not usable
activated: false,
2018-10-03 15:16:18 +08:00
disabled: true,
created: new Date()
2018-05-14 15:14:44 +08:00
};
2017-11-23 17:51:37 +08:00
2018-05-14 15:14:44 +08:00
if (data.tags && data.tags.length) {
userData.tags = data.tags;
userData.tagsview = data.tagsview;
}
2017-11-23 17:51:37 +08:00
2018-10-25 00:21:10 +08:00
this.users.collection('users').insertOne(userData, { w: 'majority' }, err => {
2018-05-14 15:14:44 +08:00
if (err) {
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
let response;
switch (err.code) {
case 11000:
response = 'Selected user already exists';
err.code = 'UserExistsError';
break;
default:
response = 'Database Error, failed to create user';
err.code = 'InternalDatabaseError';
}
2018-05-14 15:14:44 +08:00
err.message = response;
return callback(err);
2017-11-23 17:51:37 +08:00
}
2018-10-01 18:28:27 +08:00
let mailboxes = this.getMailboxes(data.language, data.mailboxes).map(mailbox => {
2018-05-14 15:14:44 +08:00
mailbox.user = id;
2018-01-16 19:37:06 +08:00
2018-05-14 15:14:44 +08:00
if (['\\Trash', '\\Junk'].includes(mailbox.specialUse)) {
mailbox.retention = data.retention ? Math.min(data.retention, junkRetention) : junkRetention;
} else {
mailbox.retention = data.retention;
2018-01-24 19:37:57 +08:00
}
2018-05-14 15:14:44 +08:00
return mailbox;
});
2018-01-24 19:37:57 +08:00
2018-05-14 15:14:44 +08:00
this.database.collection('mailboxes').insertMany(
mailboxes,
{
2018-10-25 00:21:10 +08:00
w: 'majority',
2018-05-14 15:14:44 +08:00
ordered: false
},
err => {
2018-01-24 19:37:57 +08:00
if (err) {
2018-05-14 15:14:44 +08:00
// try to rollback
this.users.collection('users').deleteOne({ _id: id }, () => false);
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
err.message = 'Database Error, failed to create user';
err.code = 'InternalDatabaseError';
2018-01-24 19:37:57 +08:00
return callback(err);
}
2018-05-14 15:14:44 +08:00
let ensureAddress = done => {
if (data.emptyAddress) {
return done(null, '');
}
2018-02-06 19:17:49 +08:00
2018-05-14 15:14:44 +08:00
let addressData = {
user: id,
address,
// dotless version
addrview,
2018-02-06 19:17:49 +08:00
created: new Date()
};
2018-05-14 15:14:44 +08:00
if (data.tags && data.tags.length && data.addTagsToAddress) {
addressData.tags = data.tags;
addressData.tagsview = data.tagsview;
}
// insert alias address to email address registry
2018-10-25 00:21:10 +08:00
this.users.collection('addresses').insertOne(addressData, { w: 'majority' }, err => {
2018-01-24 19:37:57 +08:00
if (err) {
// try to rollback
this.users.collection('users').deleteOne({ _id: id }, () => false);
this.database.collection('mailboxes').deleteMany({ user: id }, () => false);
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
2018-05-14 15:14:44 +08:00
let response;
switch (err.code) {
case 11000:
response = 'Selected email address already exists';
err.code = 'AddressExistsError';
break;
default:
response = 'Database Error, failed to create user';
err.code = 'InternalDatabaseError';
}
2018-01-24 19:37:57 +08:00
2018-05-14 15:14:44 +08:00
err.message = response;
return done(err);
2018-01-24 19:37:57 +08:00
}
2018-05-14 15:14:44 +08:00
done(null, address);
});
};
ensureAddress((err, address) => {
if (err) {
return callback(err);
}
let updates = {
address,
activated: true,
disabled: false
};
if (data.requirePasswordChange) {
updates.tempPassword = {
validAfter: new Date(),
password: hash,
created: new Date()
};
} else {
updates.password = hash;
}
// register this address as the default address for that user
return this.users.collection('users').findOneAndUpdate(
{
_id: id,
activated: false
},
{
$set: updates
},
{
returnOriginal: false,
maxTimeMS: consts.DB_MAX_TIME_USERS
},
2018-05-14 15:14:44 +08:00
(err, result) => {
if (err) {
// try to rollback
this.users.collection('users').deleteOne({ _id: id }, () => false);
this.database.collection('mailboxes').deleteMany({ user: id }, () => false);
this.users.collection('addresses').deleteOne({ user: id }, () => false);
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
err.message = 'Database Error, failed to create user';
err.code = 'InternalDatabaseError';
return callback(err);
}
let userData = result.value;
if (!userData) {
// should never happen
return callback(null, id);
}
let createSuccess = () =>
this.logAuthEvent(
id,
{
action: 'account created',
result: 'success',
sess: data.sess,
ip: data.ip
},
() => callback(null, id)
);
if (!this.messageHandler || data.emptyAddress) {
return createSuccess();
}
let parsedName = humanname.parse(userData.name || '');
this.pushDefaultMessages(
userData,
2018-01-24 19:37:57 +08:00
{
2018-05-14 15:14:44 +08:00
NAME: userData.name || userData.username || address,
FNAME: parsedName.firstName,
LNAME: parsedName.lastName,
DOMAIN: address.substr(address.indexOf('@') + 1),
EMAIL: address
2018-01-24 19:37:57 +08:00
},
2018-05-14 15:14:44 +08:00
() => createSuccess()
2018-01-24 19:37:57 +08:00
);
}
2018-05-14 15:14:44 +08:00
);
});
}
);
});
2018-01-24 19:37:57 +08:00
});
2017-04-21 01:10:03 +08:00
});
2017-11-23 17:51:37 +08:00
}
);
2017-04-21 01:10:03 +08:00
}
2017-08-03 20:26:44 +08:00
pushDefaultMessages(userData, tags, callback) {
tools.getEmailTemplates(tags, (err, messages) => {
if (err || !messages || !messages.length) {
return callback();
}
let pos = 0;
let insertMessages = () => {
if (pos >= messages.length) {
return callback();
}
let data = messages[pos++];
let compiler = new MailComposer(data);
compiler.compile().build((err, message) => {
if (err) {
return insertMessages();
}
let mailboxQueryKey = 'path';
let mailboxQueryValue = 'INBOX';
if (['sent', 'trash', 'junk', 'drafts', 'archive'].includes((data.mailbox || '').toString().toLowerCase())) {
mailboxQueryKey = 'specialUse';
mailboxQueryValue = '\\' + data.mailbox.toLowerCase().replace(/^./g, c => c.toUpperCase());
}
let flags = [];
if (data.seen) {
flags.push('\\Seen');
}
if (data.flag) {
flags.push('\\Flagged');
}
2017-08-03 20:26:44 +08:00
this.messageHandler.encryptMessage(userData.encryptMessages ? userData.pubKey : false, message, (err, encrypted) => {
if (!err && encrypted) {
message = encrypted;
}
this.messageHandler.add(
{
user: userData._id,
[mailboxQueryKey]: mailboxQueryValue,
meta: {
source: 'AUTO',
2017-11-12 21:13:32 +08:00
time: new Date()
2017-08-03 20:26:44 +08:00
},
flags,
raw: message
},
2017-08-03 20:26:44 +08:00
insertMessages
);
});
});
};
insertMessages();
});
}
2017-11-03 20:11:59 +08:00
reset(user, data, callback) {
2017-06-12 17:51:44 +08:00
let password = generatePassword.generate({
length: 12,
uppercase: true,
numbers: true,
symbols: false
});
hashes.hash(password, (err, hash) => {
2018-05-14 15:14:44 +08:00
if (err) {
log.error('DB', 'HASHFAIL user.reset id=%s error=%s', user, err.message);
err.code = 'HashError';
2018-05-14 15:14:44 +08:00
return callback(err);
}
return this.users.collection('users').findOneAndUpdate(
{
_id: user
},
{
$set: {
tempPassword: {
validAfter: data.validAfter || new Date(),
password: hash,
created: new Date()
}
}
},
{
maxTimeMS: consts.DB_MAX_TIME_USERS
},
2018-05-14 15:14:44 +08:00
(err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to reset user credentials';
err.code = 'InternalDatabaseError';
return callback(err);
2018-01-26 17:39:08 +08:00
}
2017-04-21 01:10:03 +08:00
2018-05-14 15:14:44 +08:00
if (!result || !result.value) {
return callback(new Error('Could not update user ' + user));
}
2017-04-21 01:10:03 +08:00
2018-05-14 15:14:44 +08:00
return this.logAuthEvent(
user,
{
action: 'reset',
sess: data.sess,
ip: data.ip
},
() => callback(null, password)
);
}
);
});
2017-11-23 17:51:37 +08:00
}
2017-04-21 01:10:03 +08:00
2017-11-23 17:51:37 +08:00
setupTotp(user, data, callback) {
return this.users.collection('users').findOne(
{
_id: user
},
{
2018-08-15 04:45:45 +08:00
projection: {
2017-11-23 17:51:37 +08:00
username: true,
enabled2fa: true,
seed: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-11-23 17:51:37 +08:00
},
(err, userData) => {
2017-04-21 01:10:03 +08:00
if (err) {
2017-07-24 21:44:08 +08:00
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
2017-11-23 17:51:37 +08:00
err.message = 'Database Error, failed to check user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-09-12 16:02:22 +08:00
return callback(err);
2017-04-21 01:10:03 +08:00
}
2017-11-23 17:51:37 +08:00
if (!userData) {
2018-11-28 21:50:57 +08:00
let err = new Error('Could not find user data');
err.code = 'UserNotFound';
return callback(err);
2017-04-21 01:10:03 +08:00
}
2017-11-23 17:51:37 +08:00
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
2017-04-21 01:10:03 +08:00
2017-11-23 17:51:37 +08:00
if (enabled2fa.includes('totp')) {
2018-11-28 21:50:57 +08:00
let err = new Error('TOTP 2FA is already enabled for this user');
err.code = 'TotpEnabled';
return callback(err);
2017-10-10 16:19:10 +08:00
}
2017-11-23 17:51:37 +08:00
let secret = speakeasy.generateSecret({
length: 20,
name: userData.username
});
let seed = secret.base32;
if (config.totp && config.totp.secret) {
2018-01-02 19:46:32 +08:00
try {
let cipher = crypto.createCipher(config.totp.cipher || 'aes192', config.totp.secret);
seed = '$' + cipher.update(seed, 'utf8', 'hex');
seed += cipher.final('hex');
} catch (E) {
log.error('DB', 'TOTPFAIL cipher failed id=%s error=%s', user, E.message);
let err = new Error('Database Error, failed to update user');
err.code = 'InternalDatabaseError';
return callback(err);
}
2017-10-10 16:19:10 +08:00
}
2017-11-23 17:51:37 +08:00
return this.users.collection('users').findOneAndUpdate(
2017-10-10 16:19:10 +08:00
{
2017-11-23 17:51:37 +08:00
_id: user,
enabled2fa: { $not: { $eq: 'totp' } }
2017-10-10 16:19:10 +08:00
},
2017-11-23 17:51:37 +08:00
{
$set: {
2018-11-28 21:50:57 +08:00
pendingSeed: seed,
pendingSeedChanged: new Date()
2017-11-23 17:51:37 +08:00
}
},
{ maxTimeMS: consts.DB_MAX_TIME_USERS },
2017-11-23 17:51:37 +08:00
(err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to update user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
2017-04-21 21:38:03 +08:00
2017-11-23 17:51:37 +08:00
if (!result || !result.value) {
return callback(new Error('Could not update user, check if 2FA is not already enabled'));
}
2017-04-21 01:10:03 +08:00
2017-11-23 17:51:37 +08:00
let otpauth_url = speakeasy.otpauthURL({
secret: secret.ascii,
2018-02-12 18:57:34 +08:00
label: data.label || userData.username,
2018-01-02 21:04:01 +08:00
issuer: data.issuer || 'WildDuck'
2017-11-23 17:51:37 +08:00
});
2018-11-28 21:50:57 +08:00
QRCode.toDataURL(otpauth_url, (err, dataUrl) => {
2017-11-23 17:51:37 +08:00
if (err) {
log.error('DB', 'QRFAIL id=%s error=%s', user, err.message);
err.message = 'Failed to generate QR code';
2017-12-21 16:31:34 +08:00
err.code = 'QRError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
2018-11-28 21:50:57 +08:00
callback(null, {
secret: secret.base32,
dataUrl
});
2017-11-23 17:51:37 +08:00
});
}
);
}
);
}
2017-04-21 01:10:03 +08:00
2017-11-23 17:51:37 +08:00
enableTotp(user, data, callback) {
this.users.collection('users').findOne(
{
_id: user
2017-11-23 17:51:37 +08:00
},
{
2018-08-15 04:45:45 +08:00
projection: {
enabled2fa: true,
2017-11-23 17:51:37 +08:00
username: true,
2018-11-28 21:50:57 +08:00
pendingSeed: true,
pendingSeedChanged: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-11-23 17:51:37 +08:00
},
(err, userData) => {
if (err) {
log.error('DB', 'LOADFAIL id=%s error=%s', user, err.message);
2017-11-23 17:51:37 +08:00
err.message = 'Database Error, failed to fetch user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-09-12 16:02:22 +08:00
return callback(err);
}
if (!userData) {
2017-11-23 17:51:37 +08:00
let err = new Error('This username does not exist');
2017-12-21 16:31:34 +08:00
err.code = 'UserNotFound';
return callback(err);
}
2017-10-10 16:19:10 +08:00
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
2018-11-28 21:50:57 +08:00
if (enabled2fa.includes('totp')) {
// 2fa not set up
2018-11-28 21:50:57 +08:00
let err = new Error('TOTP 2FA is already enabled for this user');
err.code = 'TotpEnabled';
2017-11-23 17:51:37 +08:00
return callback(err);
}
2018-11-28 21:50:57 +08:00
if (!userData.pendingSeed || (userData.pendingSeedChanged && userData.pendingSeedChanged < new Date(Date.now() - TOTP_SETUP_TTL))) {
2017-11-23 17:51:37 +08:00
// 2fa not set up
2018-11-28 21:50:57 +08:00
let err = new Error('TOTP 2FA is not initialized for this user');
err.code = 'TotpDisabled';
return callback(err);
}
2018-11-28 21:50:57 +08:00
let secret = userData.pendingSeed;
if (secret.charAt(0) === '$' && config.totp && config.totp.secret) {
2018-01-02 19:46:32 +08:00
try {
let decipher = crypto.createDecipher(config.totp.cipher || 'aes192', config.totp.secret);
2018-11-28 21:50:57 +08:00
secret = decipher.update(secret.substr(1), 'hex', 'utf-8');
2018-01-02 19:46:32 +08:00
secret += decipher.final('utf8');
} catch (E) {
log.error('DB', 'TOTPFAIL decipher failed id=%s error=%s', user, E.message);
let err = new Error('Can not use decrypted secret');
err.code = 'InternalConfigError';
return callback(err);
}
}
let verified = speakeasy.totp.verify({
secret,
encoding: 'base32',
token: data.token,
window: 6
});
2017-11-23 17:51:37 +08:00
if (!verified) {
return this.logAuthEvent(
user,
{
action: 'enable 2fa totp',
result: 'fail',
2018-01-12 16:16:16 +08:00
sess: data.sess,
2017-11-23 17:51:37 +08:00
ip: data.ip
},
() => callback(null, false)
);
}
let update =
!userData.enabled2fa || typeof userData.enabled2fa === 'boolean'
? {
2018-01-09 19:50:29 +08:00
$set: {
2018-11-28 21:50:57 +08:00
seed: userData.pendingSeed,
pendingSeed: '',
pendingSeedChanged: false,
2018-01-09 19:50:29 +08:00
enabled2fa: ['totp']
}
}
2017-11-23 17:51:37 +08:00
: {
2018-11-28 21:50:57 +08:00
$set: {
seed: userData.pendingSeed,
pendingSeed: '',
pendingSeedChanged: false
},
2018-01-09 19:50:29 +08:00
$addToSet: {
enabled2fa: 'totp'
}
};
2017-11-23 17:51:37 +08:00
// token was valid, update user settings
return this.users.collection('users').findOneAndUpdate(
{
2017-11-23 17:51:37 +08:00
_id: user,
2018-11-28 21:50:57 +08:00
pendingSeed: userData.pendingSeed
},
2017-11-23 17:51:37 +08:00
update,
{ maxTimeMS: consts.DB_MAX_TIME_USERS },
2017-11-23 17:51:37 +08:00
(err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to update user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
2017-11-23 17:51:37 +08:00
if (!result || !result.value) {
2017-12-21 16:31:34 +08:00
err = new Error('Failed to set up 2FA. Check if it is not already enabled');
err.code = 'TotpEnabled';
return callback(err);
2017-11-23 17:51:37 +08:00
}
return this.logAuthEvent(
user,
{
action: 'enable 2fa totp',
result: 'success',
2018-01-12 16:16:16 +08:00
sess: data.sess,
2017-11-23 17:51:37 +08:00
ip: data.ip
},
() => callback(null, true)
);
}
);
2017-11-23 17:51:37 +08:00
}
);
}
disableTotp(user, data, callback) {
this.users.collection('users').findOne(
{
_id: user
},
{
2018-08-15 04:45:45 +08:00
projection: {
2017-11-23 17:51:37 +08:00
enabled2fa: true,
username: true,
seed: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-11-23 17:51:37 +08:00
},
(err, userData) => {
if (err) {
log.error('DB', 'LOADFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to update user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
if (!userData) {
let err = new Error('This username does not exist');
2017-12-21 16:31:34 +08:00
err.code = 'UserNotFound';
2017-11-23 17:51:37 +08:00
return callback(err);
}
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
if (!enabled2fa.includes('totp')) {
let err = new Error('Could not update user, check if 2FA TOTP is not already disabled');
err.code = 'TotpDisabled';
return callback(err);
2017-11-23 17:51:37 +08:00
}
let update =
!userData.enabled2fa || typeof userData.enabled2fa === 'boolean'
? {
2018-01-09 19:50:29 +08:00
$set: {
enabled2fa: [],
2018-11-28 21:50:57 +08:00
seed: '',
pendingSeed: '',
pendingSeedChanged: false
2018-01-09 19:50:29 +08:00
}
}
2017-11-23 17:51:37 +08:00
: {
2018-01-09 19:50:29 +08:00
$pull: {
enabled2fa: 'totp'
},
$set: {
2018-11-28 21:50:57 +08:00
seed: '',
pendingSeed: '',
pendingSeedChanged: false
2018-01-09 19:50:29 +08:00
}
};
2017-11-23 17:51:37 +08:00
return this.users.collection('users').findOneAndUpdate(
{
_id: user
},
update,
{ maxTimeMS: consts.DB_MAX_TIME_USERS },
2017-11-23 17:51:37 +08:00
(err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to update user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
if (!result || !result.value) {
let err = new Error('This username does not exist');
err.code = 'UserNotFound';
return callback(err);
2017-11-23 17:51:37 +08:00
}
return this.logAuthEvent(
user,
{
action: 'disable 2fa totp',
2018-01-12 16:16:16 +08:00
sess: data.sess,
2017-11-23 17:51:37 +08:00
ip: data.ip
},
() => callback(null, true)
);
}
);
}
);
}
checkTotp(user, data, callback) {
2018-04-13 10:22:49 +08:00
let userRlKey = 'totp:' + user;
this.rateLimit(userRlKey, data, 0, (err, res) => {
2017-11-23 17:51:37 +08:00
if (err) {
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
if (!res.success) {
return rateLimitResponse(res, callback);
2017-11-23 17:51:37 +08:00
}
let authSuccess = (...args) => {
// clear rate limit counter on success
2018-04-13 10:22:49 +08:00
this.rateLimitReleaseUser(userRlKey, () => false);
2017-11-23 17:51:37 +08:00
callback(...args);
};
let authFail = (...args) => {
// increment rate limit counter on failure
2018-04-13 10:22:49 +08:00
this.rateLimit(userRlKey, data, 1, () => {
2017-11-23 17:51:37 +08:00
callback(...args);
});
};
this.users.collection('users').findOne(
{
_id: user
},
{
2018-08-15 04:45:45 +08:00
projection: {
2017-11-23 17:51:37 +08:00
username: true,
enabled2fa: true,
seed: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-11-23 17:51:37 +08:00
},
(err, userData) => {
if (err) {
log.error('DB', 'LOADFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to find user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
if (!userData) {
let err = new Error('This user does not exist');
2017-12-21 16:31:34 +08:00
err.code = 'UserNotFound';
2017-11-23 17:51:37 +08:00
return callback(err);
}
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
if (!userData.seed || !enabled2fa.includes('totp')) {
// 2fa not set up
let err = new Error('2FA TOTP is not enabled for this user');
2017-12-21 16:31:34 +08:00
err.code = 'TotpDisabled';
2017-11-23 17:51:37 +08:00
return callback(err);
}
let secret = userData.seed;
if (userData.seed.charAt(0) === '$' && config.totp && config.totp.secret) {
2018-01-02 19:46:32 +08:00
try {
let decipher = crypto.createDecipher(config.totp.cipher || 'aes192', config.totp.secret);
secret = decipher.update(userData.seed.substr(1), 'hex', 'utf-8');
secret += decipher.final('utf8');
} catch (E) {
log.error('DB', 'TOTPFAIL decipher failed id=%s error=%s', user, E.message);
let err = new Error('Can not use decrypted secret');
err.code = 'InternalConfigError';
return callback(err);
}
2017-11-23 17:51:37 +08:00
}
let verified = speakeasy.totp.verify({
secret,
encoding: 'base32',
token: data.token,
window: 6
});
return this.logAuthEvent(
user,
{
action: 'check 2fa totp',
result: verified ? 'success' : 'fail',
2018-01-12 16:16:16 +08:00
sess: data.sess,
2017-11-23 17:51:37 +08:00
ip: data.ip
},
() => {
if (verified) {
authSuccess(null, verified);
} else {
authFail(null, verified);
}
}
);
}
);
2017-04-21 01:10:03 +08:00
});
}
enableCustom2fa(user, data, callback) {
this.users.collection('users').findOne(
{
_id: user
},
{
2018-08-15 04:45:45 +08:00
projection: {
enabled2fa: true,
username: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
},
(err, userData) => {
if (err) {
log.error('DB', 'LOADFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to fetch user';
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!userData) {
let err = new Error('This username does not exist');
err.code = 'UserNotFound';
return callback(err);
}
// previous versions used {enabled2fa: true} for TOTP based 2FA
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
if (enabled2fa.includes('custom')) {
// 2fa not set up
let err = new Error('Custom 2FA is already enabled for this user');
err.code = 'CustomEnabled';
return callback(err);
}
let update =
!userData.enabled2fa || typeof userData.enabled2fa === 'boolean'
? {
$set: {
enabled2fa: ['custom'].concat(userData.enabled2fa ? 'totp' : [])
}
}
: {
$addToSet: {
enabled2fa: 'custom'
}
};
// update user settings
return this.users.collection('users').findOneAndUpdate(
{
_id: user
},
update,
{ maxTimeMS: consts.DB_MAX_TIME_USERS },
(err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to update user';
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!result || !result.value) {
let err = new Error('This username does not exist');
err.code = 'UserNotFound';
return callback(err);
}
return this.logAuthEvent(
user,
{
action: 'enable 2fa custom',
result: 'success',
sess: data.sess,
ip: data.ip
},
() => callback(null, true)
);
}
);
}
);
}
disableCustom2fa(user, data, callback) {
this.users.collection('users').findOne(
{
_id: user
},
{
2018-08-15 04:45:45 +08:00
projection: {
enabled2fa: true,
username: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
},
(err, userData) => {
if (err) {
log.error('DB', 'LOADFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to update user';
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!userData) {
let err = new Error('This username does not exist');
err.code = 'UserNotFound';
return callback(err);
}
if (!Array.isArray(userData.enabled2fa) || !userData.enabled2fa.includes('custom')) {
let err = new Error('Could not update user, check if custom 2FA is not already disabled');
err.code = 'CustomDisabled';
return callback(err);
}
let update = {
$pull: {
enabled2fa: 'custom'
}
};
return this.users.collection('users').findOneAndUpdate(
{
_id: user
},
update,
{
maxTimeMS: consts.DB_MAX_TIME_USERS
},
(err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to update user';
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!result || !result.value) {
let err = new Error('This username does not exist');
err.code = 'UserNotFound';
return callback(err);
}
return this.logAuthEvent(
user,
{
action: 'disable 2fa custom',
sess: data.sess,
ip: data.ip
},
() => callback(null, true)
);
}
);
}
);
}
2018-06-28 14:52:29 +08:00
setupU2f(user, data, callback) {
2017-10-10 16:19:10 +08:00
let registrationRequest;
try {
2018-06-28 14:12:31 +08:00
registrationRequest = u2f.request(data.appId || config.u2f.appId);
2017-10-10 16:19:10 +08:00
} catch (E) {
log.error('U2F', 'U2FFAIL request id=%s error=%s', user, E.message);
}
if (!registrationRequest) {
return callback(null, false);
}
2017-11-23 17:51:37 +08:00
this.users.collection('users').findOne(
{
_id: user
},
{
2018-08-15 04:45:45 +08:00
projection: {
2017-11-23 17:51:37 +08:00
username: true,
enabled2fa: true,
seed: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-11-23 17:51:37 +08:00
},
(err, userData) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to check user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
2017-10-10 16:19:10 +08:00
2017-11-23 17:51:37 +08:00
if (!userData) {
2017-12-21 16:31:34 +08:00
err = new Error('Could not find user data');
err.code = 'UserNotFound';
return callback(err);
2017-11-23 17:51:37 +08:00
}
2017-10-10 16:19:10 +08:00
2017-11-23 17:51:37 +08:00
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
2017-10-10 16:19:10 +08:00
2017-11-23 17:51:37 +08:00
if (enabled2fa.includes('u2f')) {
return callback(new Error('U2F 2FA is already enabled for this user'));
}
2017-10-10 16:19:10 +08:00
2017-11-23 17:51:37 +08:00
// store registration request to Redis
this.redis
.multi()
.set('u2f:req:' + user, JSON.stringify(registrationRequest))
.expire('u2f:req:' + user, 1 * 3600)
.exec((err, results) => {
if ((!err && !results) || !results[0]) {
err = new Error('Invalid DB response');
} else if (!err && results && results[0] && results[0][0]) {
err = results[0][0];
}
if (err) {
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
2017-10-10 16:19:10 +08:00
2017-11-23 17:51:37 +08:00
callback(null, registrationRequest);
});
}
);
2017-10-10 16:19:10 +08:00
}
enableU2f(user, data, callback) {
this.redis
.multi()
.get('u2f:req:' + user)
.del('u2f:req:' + user)
.exec((err, results) => {
if ((!err && !results) || !results[0]) {
err = new Error('Invalid DB response');
} else if (!err && results && results[0] && results[0][0]) {
err = results[0][0];
}
if (err) {
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-10-10 16:19:10 +08:00
return callback(err);
}
let registrationRequest = results[0][1];
if (!registrationRequest) {
let err = new Error('U2F 2FA is not initialized for this user');
2017-12-21 16:31:34 +08:00
err.code = 'U2fDisabled';
2017-10-10 16:19:10 +08:00
return callback(err);
}
try {
registrationRequest = JSON.parse(registrationRequest);
} catch (E) {
return callback(new Error('Invalid 2FA data stored'));
}
let registrationResponse = {};
Object.keys(data || {}).forEach(key => {
if (['clientData', 'registrationData', 'version', 'challenge'].includes(key)) {
registrationResponse[key] = data[key];
}
});
let result;
try {
result = u2f.checkRegistration(registrationRequest, registrationResponse);
} catch (E) {
log.error('U2F', 'U2FFAIL checkRegistration id=%s error=%s', user, E.message);
}
if (!result || !result.successful) {
return callback(new Error((result && result.errorMessage) || 'Failed to validate U2F response'));
}
2017-11-23 17:51:37 +08:00
this.users.collection('users').findOne(
{
_id: user
},
{
2018-08-15 04:45:45 +08:00
projection: {
2017-11-23 17:51:37 +08:00
enabled2fa: true,
username: true,
2018-01-24 17:29:12 +08:00
u2f: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-11-23 17:51:37 +08:00
},
(err, userData) => {
if (err) {
log.error('DB', 'LOADFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to fetch user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
if (!userData) {
let err = new Error('This username does not exist');
2017-12-21 16:31:34 +08:00
err.code = 'UserNotFound';
2017-11-23 17:51:37 +08:00
return callback(err);
}
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
if (enabled2fa.includes('u2f')) {
// 2fa not set up
let err = new Error('U2F 2FA is already enabled for this user');
2017-12-21 16:31:34 +08:00
err.code = 'U2fEnabled';
2017-11-23 17:51:37 +08:00
return callback(err);
}
let curDate = new Date();
let update =
!userData.enabled2fa || typeof userData.enabled2fa === 'boolean'
? {
2018-01-09 19:50:29 +08:00
$set: {
enabled2fa: ['u2f'],
2018-01-24 17:29:12 +08:00
u2f: {
keyHandle: result.keyHandle,
pubKey: result.publicKey,
cert: result.certificate,
date: curDate
}
2018-01-09 19:50:29 +08:00
}
}
2017-11-23 17:51:37 +08:00
: {
2018-01-09 19:50:29 +08:00
$addToSet: {
enabled2fa: 'u2f'
},
$set: {
2018-01-24 17:29:12 +08:00
u2f: {
keyHandle: result.keyHandle,
pubKey: result.publicKey,
cert: result.certificate,
date: curDate
}
2018-01-09 19:50:29 +08:00
}
};
2017-11-23 17:51:37 +08:00
return this.users.collection('users').findOneAndUpdate(
{
_id: user
},
update,
{ maxTimeMS: consts.DB_MAX_TIME_USERS },
2017-11-23 17:51:37 +08:00
(err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to update user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
if (!result || !result.value) {
2017-12-21 16:31:34 +08:00
err = new Error('Failed to set up 2FA. User not found');
err.code = 'UserNotFound';
return callback(err);
2017-11-23 17:51:37 +08:00
}
return this.logAuthEvent(
user,
{
action: 'enable 2fa u2f',
result: 'success',
2018-01-12 16:16:16 +08:00
sess: data.sess,
2017-11-23 17:51:37 +08:00
ip: data.ip
},
() => callback(null, true)
);
}
);
2017-10-10 16:19:10 +08:00
}
2017-11-23 17:51:37 +08:00
);
});
}
disableU2f(user, data, callback) {
this.users.collection('users').findOne(
{
_id: user
},
{
2018-08-15 04:45:45 +08:00
projection: {
2017-11-23 17:51:37 +08:00
enabled2fa: true,
username: true,
2018-01-24 17:29:12 +08:00
u2f: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-11-23 17:51:37 +08:00
},
(err, userData) => {
if (err) {
log.error('DB', 'LOADFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to update user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
if (!userData) {
let err = new Error('This username does not exist');
2017-12-21 16:31:34 +08:00
err.code = 'UserNotFound';
2017-11-23 17:51:37 +08:00
return callback(err);
}
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
if (!enabled2fa.includes('u2f')) {
return callback(new Error('Could not update user, check if U2F 2FA is not already disabled'));
}
2018-01-24 17:29:12 +08:00
let curDate = new Date();
2017-11-23 17:51:37 +08:00
let update =
!userData.enabled2fa || typeof userData.enabled2fa === 'boolean'
? {
2018-01-09 19:50:29 +08:00
$set: {
enabled2fa: [],
2018-01-24 17:29:12 +08:00
u2f: {
keyHandle: '',
pubKey: '',
cert: '',
date: curDate
}
2018-01-09 19:50:29 +08:00
}
}
2017-11-23 17:51:37 +08:00
: {
2018-01-09 19:50:29 +08:00
$pull: {
enabled2fa: 'u2f'
},
$set: {
2018-01-24 17:29:12 +08:00
u2f: {
keyHandle: '',
pubKey: '',
cert: '',
date: curDate
}
2018-01-09 19:50:29 +08:00
}
};
2017-11-23 17:51:37 +08:00
return this.users.collection('users').findOneAndUpdate(
{
_id: user
},
update,
{
maxTimeMS: consts.DB_MAX_TIME_USERS
},
2017-11-23 17:51:37 +08:00
(err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to update user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
if (!result || !result.value) {
2017-12-21 16:31:34 +08:00
err = new Error('Could not update user, check if 2FA is not already disabled');
err.code = 'UserNotFound';
return callback(err);
2017-11-23 17:51:37 +08:00
}
return this.logAuthEvent(
user,
{
action: 'disable 2fa u2f',
2018-01-12 16:16:16 +08:00
sess: data.sess,
2017-11-23 17:51:37 +08:00
ip: data.ip
},
() => callback(null, true)
);
}
);
}
);
}
startU2f(user, data, callback) {
this.users.collection('users').findOne(
{
_id: user
},
{
2018-08-15 04:45:45 +08:00
projection: {
2017-11-23 17:51:37 +08:00
enabled2fa: true,
username: true,
2018-01-24 17:29:12 +08:00
u2f: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-11-23 17:51:37 +08:00
},
(err, userData) => {
if (err) {
log.error('DB', 'LOADFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to find user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
if (!userData) {
let err = new Error('This user does not exist');
2017-12-21 16:31:34 +08:00
err.code = 'UserNotFound';
2017-11-23 17:51:37 +08:00
return callback(err);
}
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
2018-01-24 17:29:12 +08:00
if (!enabled2fa.includes('u2f') || !userData.u2f || !userData.u2f.keyHandle) {
2017-11-23 17:51:37 +08:00
// 2fa not set up
let err = new Error('2FA U2F is not enabled for this user');
2017-12-21 16:31:34 +08:00
err.code = 'U2fDisabled';
2017-11-23 17:51:37 +08:00
return callback(err);
}
2018-06-28 14:12:31 +08:00
this.generateU2fAuthRequest(user, userData.u2f.keyHandle, data.appId, (err, authRequest) => {
2017-10-10 16:19:10 +08:00
if (err) {
return callback(err);
}
2017-11-23 17:51:37 +08:00
if (!authRequest) {
return callback(null, false);
2017-10-10 16:19:10 +08:00
}
2017-11-23 17:51:37 +08:00
callback(null, authRequest);
});
}
);
}
2017-10-10 16:19:10 +08:00
2017-11-23 17:51:37 +08:00
checkU2f(user, data, callback) {
this.users.collection('users').findOne(
{
_id: user
},
{
2018-08-15 04:45:45 +08:00
projection: {
2017-11-23 17:51:37 +08:00
enabled2fa: true,
username: true,
2018-01-24 17:29:12 +08:00
u2f: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-11-23 17:51:37 +08:00
},
(err, userData) => {
if (err) {
log.error('DB', 'LOADFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to find user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
if (!userData) {
let err = new Error('This user does not exist');
2017-12-21 16:31:34 +08:00
err.code = 'UserNotFound';
2017-11-23 17:51:37 +08:00
return callback(err);
}
2017-10-10 16:19:10 +08:00
2017-11-23 17:51:37 +08:00
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
2017-10-10 16:19:10 +08:00
2018-01-24 17:29:12 +08:00
if (!enabled2fa.includes('u2f') || !userData.u2f || !userData.u2f.keyHandle) {
2017-11-23 17:51:37 +08:00
// 2fa not set up
let err = new Error('2FA U2F is not enabled for this user');
2017-12-21 16:31:34 +08:00
err.code = 'U2fDisabled';
2017-11-23 17:51:37 +08:00
return callback(err);
}
2017-10-10 16:19:10 +08:00
2017-11-23 17:51:37 +08:00
this.redis
.multi()
.get('u2f:auth:' + user)
.del('u2f:auth:' + user)
.exec((err, results) => {
if ((!err && !results) || !results[0]) {
err = new Error('Invalid DB response');
} else if (!err && results && results[0] && results[0][0]) {
err = results[0][0];
}
2017-10-10 16:19:10 +08:00
if (err) {
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-10-10 16:19:10 +08:00
return callback(err);
}
2017-11-23 17:51:37 +08:00
let authRequest = results[0][1];
if (!authRequest) {
return callback(null, false);
}
try {
authRequest = JSON.parse(authRequest);
} catch (E) {
return callback(null, false);
}
let authResponse = {};
Object.keys(data || {}).forEach(key => {
if (['clientData', 'signatureData'].includes(key)) {
authResponse[key] = data[key];
}
});
let result;
try {
2018-01-24 17:29:12 +08:00
result = u2f.checkSignature(authRequest, authResponse, userData.u2f.pubKey);
2017-11-23 17:51:37 +08:00
} catch (E) {
// ignore
log.error('U2F', 'U2FFAIL checkSignature id=%s error=%s', user, E.message);
2017-10-10 16:19:10 +08:00
}
2017-11-23 17:51:37 +08:00
let verified = result && result.successful;
2017-10-10 16:19:10 +08:00
return this.logAuthEvent(
user,
{
2017-11-23 17:51:37 +08:00
action: 'check 2fa u2f',
result: verified ? 'success' : 'fail',
2018-01-12 16:16:16 +08:00
sess: data.sess,
2017-10-10 16:19:10 +08:00
ip: data.ip
},
2017-11-23 17:51:37 +08:00
() => {
callback(null, verified);
}
2017-10-10 16:19:10 +08:00
);
});
}
2017-11-23 17:51:37 +08:00
);
}
2017-10-10 16:19:10 +08:00
2017-11-23 17:51:37 +08:00
disable2fa(user, data, callback) {
this.users.collection('users').findOneAndUpdate(
{
2017-10-10 16:19:10 +08:00
_id: user
2017-11-23 17:51:37 +08:00
},
{
$set: {
enabled2fa: [],
seed: '',
2018-11-28 21:50:57 +08:00
pendingSeed: '',
pendingSeedChanged: false,
2018-01-24 17:29:12 +08:00
u2f: {
keyHandle: '',
pubKey: '',
cert: '',
date: new Date()
}
2017-11-23 17:51:37 +08:00
}
},
{
maxTimeMS: consts.DB_MAX_TIME_USERS
},
2017-11-23 17:51:37 +08:00
(err, result) => {
2017-10-10 16:19:10 +08:00
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to update user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-10-10 16:19:10 +08:00
return callback(err);
}
if (!result || !result.value) {
2017-12-21 16:31:34 +08:00
let err = new Error('Could not update user, check if 2FA is not already disabled');
err.code = 'U2fDisabled';
return callback(err);
2017-10-10 16:19:10 +08:00
}
return this.logAuthEvent(
user,
{
2017-11-23 17:51:37 +08:00
action: 'disable 2fa',
2018-01-12 16:16:16 +08:00
sess: data.sess,
2017-10-10 16:19:10 +08:00
ip: data.ip
},
() => callback(null, true)
);
}
2017-11-23 17:51:37 +08:00
);
2017-10-10 16:19:10 +08:00
}
2017-07-24 21:44:08 +08:00
update(user, data, callback) {
let $set = {};
let updates = false;
let passwordChanged = false;
2017-04-21 01:10:03 +08:00
// if some of the counter keys are modified, then reset the according value in Redis
let resetKeys = new Map([
['recipients', 'wdr'],
['forwards', 'wdf'],
['imapMaxUpload', 'iup'],
['imapMaxDownload', 'idw'],
['pop3MaxDownload', 'pdw'],
['receivedMax', 'rl:rcpt']
]);
let flushKeys = [];
let flushHKeys = [];
2017-07-24 21:44:08 +08:00
Object.keys(data).forEach(key => {
if (['user', 'existingPassword', 'hashedPassword', 'allowUnsafe', 'ip', 'sess'].includes(key)) {
2017-07-24 21:44:08 +08:00
return;
2017-05-07 20:09:14 +08:00
}
if (resetKeys.has(key)) {
flushKeys.push(resetKeys.get(key) + ':' + user);
}
if (key === 'imapMaxConnections') {
flushHKeys.push({ key: 'lim:imap', value: user.toString() });
}
2017-07-24 21:44:08 +08:00
if (key === 'password') {
if (!data[key]) {
2018-02-06 19:17:49 +08:00
// removes current password (if set)
$set.password = '';
} else {
2018-05-14 15:14:44 +08:00
$set.password = data[key]; // hashed below
2018-02-06 19:17:49 +08:00
}
2018-01-26 17:39:08 +08:00
$set.tempPassword = false;
2017-07-24 21:44:08 +08:00
$set.passwordChange = new Date();
passwordChanged = true;
return;
2017-05-07 22:21:44 +08:00
}
2018-01-26 17:39:08 +08:00
if (key === 'disable2fa') {
if (data.disable2fa) {
$set.enabled2fa = [];
$set.seed = '';
2018-11-28 21:50:57 +08:00
$set.pendingSeed = '';
$set.pendingSeedChanged = false;
2018-01-26 17:39:08 +08:00
$set.u2f = {
keyHandle: '',
pubKey: '',
cert: '',
date: new Date()
};
}
2018-01-31 17:46:44 +08:00
updates = true;
2018-01-26 17:39:08 +08:00
return;
}
2018-01-30 22:14:15 +08:00
if (key === 'spamLevel') {
// spamLevel is from 0 (everything is spam) to 100 (accept everything)
let spamLevel = !isNaN(data.spamLevel) ? Number(data.spamLevel) : 50;
if (spamLevel < 0) {
spamLevel = 0;
}
if (spamLevel > 100) {
spamLevel = 100;
}
$set.spamLevel = data.spamLevel;
2018-01-31 17:46:44 +08:00
updates = true;
2018-01-30 22:14:15 +08:00
return;
}
2018-10-03 15:16:18 +08:00
if (key === 'disabledScopes') {
let allowedScopes = [...consts.SCOPES];
let scopeSet = new Set();
let disabledScopes = [].concat(data.disabledScopes || []);
disabledScopes.forEach(scope => {
scope = scope.toLowerCase().trim();
if (allowedScopes.includes(scope)) {
scopeSet.add(scope);
}
});
2018-10-03 16:39:31 +08:00
$set.disabledScopes = Array.from(scopeSet).sort((a, b) => a.localeCompare(b));
2018-10-03 15:16:18 +08:00
updates = true;
return;
}
2017-07-24 21:44:08 +08:00
$set[key] = data[key];
updates = true;
});
2017-05-07 22:21:44 +08:00
2017-07-31 15:59:18 +08:00
if ($set.username) {
2018-05-11 19:39:23 +08:00
$set.unameview = tools.uview($set.username);
2017-07-31 15:59:18 +08:00
}
2017-12-20 21:17:34 +08:00
if (!updates && !passwordChanged) {
2017-07-24 21:44:08 +08:00
return callback(new Error('Nothing was updated'));
}
2017-05-07 22:21:44 +08:00
2018-05-14 15:14:44 +08:00
let hashPassword = done => {
if (!$set.password) {
return done();
}
if (data.hashedPassword) {
// try if the bcrypt library can handle it?
2018-09-07 15:56:11 +08:00
return hashes.compare('whatever', $set.password, err => {
if (err) {
return done(err);
}
// did not throw, so probably OK, no need to update `$set.password`
return done();
});
}
hashes.hash($set.password, (err, hash) => {
2017-07-24 21:44:08 +08:00
if (err) {
2018-05-14 15:14:44 +08:00
return done(err);
2017-07-24 21:44:08 +08:00
}
2018-05-14 15:14:44 +08:00
$set.password = hash;
done();
2017-07-24 21:44:08 +08:00
});
};
2017-06-12 17:10:29 +08:00
2018-05-14 15:14:44 +08:00
hashPassword(err => {
if (err) {
log.error('DB', 'HASHFAIL user.update id=%s error=%s', data.username, err.message);
err.code = 'HashError';
2018-05-14 15:14:44 +08:00
return callback(err);
}
let verifyExistingPassword = next => {
2019-01-24 05:38:29 +08:00
this.users.collection('users').findOne(
{ _id: user },
{
projection: {
password: true,
oldPasswords: true
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2019-01-24 05:38:29 +08:00
},
(err, userData) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to find user';
err.code = 'InternalDatabaseError';
return callback(err);
}
2018-05-14 15:14:44 +08:00
2019-01-24 05:38:29 +08:00
if (!userData) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, 'User was not found');
err = new Error('User was not found');
err.code = 'UserNotFound';
return callback(err);
}
2018-05-14 15:14:44 +08:00
2019-01-24 05:38:29 +08:00
// push current password to old passwords list on password change (and not temporary password)
if ($set.password && userData && userData.password) {
let oldPasswords = [].concat(userData.oldPasswords || []);
oldPasswords.push({
date: new Date(),
hash: userData.password
});
$set.oldPasswords = oldPasswords;
}
2018-05-14 15:14:44 +08:00
2019-01-24 05:38:29 +08:00
if (!data.existingPassword) {
return next();
2018-05-14 15:14:44 +08:00
}
2019-01-24 05:38:29 +08:00
if (!userData.password) {
2018-05-14 15:14:44 +08:00
return next();
}
2017-12-08 20:29:00 +08:00
2019-01-24 05:38:29 +08:00
hashes.compare(data.existingPassword, userData.password, (err, success) => {
if (err) {
log.error('DB', 'HASHFAIL user.update id=%s error=%s', data.username, err.message);
err.code = err.code || 'HashError';
return callback(err);
}
if (success) {
return next();
}
return this.logAuthEvent(
user,
{
action: 'password change',
result: 'fail',
sess: data.sess,
ip: data.ip
},
() => callback(new Error('Password verification failed'))
);
});
}
);
2018-05-14 15:14:44 +08:00
};
verifyExistingPassword(() => {
this.users.collection('users').findOneAndUpdate(
{
_id: user
},
{
$set
},
{
2019-01-24 05:38:29 +08:00
returnOriginal: false,
maxTimeMS: consts.DB_MAX_TIME_USERS
2018-05-14 15:14:44 +08:00
},
(err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to update user';
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!result || !result.value) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, 'User was not found');
err = new Error('user was not found');
err.code = 'UserNotFound';
return callback(err);
}
// check if we need to reset any ttl counters
if (flushKeys.length || flushHKeys.length) {
let flushreq = this.redis.multi();
flushKeys.forEach(key => {
flushreq = flushreq.del(key);
});
flushHKeys.forEach(entry => {
flushreq = flushreq.hdel(entry.key, entry.value);
});
// just call the operations and hope for the best, no problems if fails
flushreq.exec(() => false);
}
2018-05-14 15:14:44 +08:00
this.userCache.flush(user, () => false);
if (passwordChanged) {
return this.logAuthEvent(
user,
{
action: 'password change',
result: 'success',
sess: data.sess,
ip: data.ip
},
() => callback(null, true)
);
} else {
return callback(null, true);
}
2017-11-23 17:51:37 +08:00
}
2018-05-14 15:14:44 +08:00
);
});
2017-04-21 01:10:03 +08:00
});
}
2018-10-01 18:28:27 +08:00
getMailboxes(language, defaults) {
defaults = defaults || {};
2018-10-22 14:27:43 +08:00
let lcode = (language || '')
.toLowerCase()
.split('_')
.shift();
let translation = lcode && mailboxTranslations.hasOwnProperty(lcode) ? mailboxTranslations[lcode] : mailboxTranslations.en;
2017-06-03 14:51:58 +08:00
let defaultMailboxes = [
{
path: 'INBOX'
},
{
specialUse: '\\Sent'
},
{
specialUse: '\\Trash'
},
{
specialUse: '\\Drafts'
},
{
specialUse: '\\Junk'
}
];
2017-04-21 01:10:03 +08:00
let uidValidity = Math.floor(Date.now() / 1000);
return defaultMailboxes.map(mailbox => ({
2018-10-01 18:28:27 +08:00
path: mailbox.path === 'INBOX' ? 'INBOX' : defaults[mailbox.specialUse] || translation[mailbox.specialUse || mailbox.path] || mailbox.path,
2017-04-21 01:10:03 +08:00
specialUse: mailbox.specialUse,
uidValidity,
uidNext: 1,
modifyIndex: 0,
subscribed: true,
flags: []
}));
}
2017-07-24 21:44:08 +08:00
2019-07-11 15:52:43 +08:00
async logAuthEvent(user, entry) {
2018-08-15 03:45:18 +08:00
// only log auth events if we have a valid user id and logging is not disabled
if (!user || !tools.isId(user) || this.authlogExpireDays === false) {
2019-07-11 15:52:43 +08:00
return false;
2017-09-25 17:19:24 +08:00
}
2018-08-15 03:45:18 +08:00
let now = new Date();
2017-08-08 18:20:03 +08:00
2018-08-15 03:45:18 +08:00
entry.user = typeof user === 'string' ? new ObjectID(user) : user;
2017-08-08 18:20:03 +08:00
entry.action = entry.action || 'authentication';
2018-08-15 03:45:18 +08:00
entry.created = now;
2017-09-25 17:19:24 +08:00
if (typeof this.authlogExpireDays === 'number' && this.authlogExpireDays !== 0) {
2018-08-15 03:45:18 +08:00
// this entry expires in set days
2017-09-25 17:19:24 +08:00
entry.expires = new Date(Date.now() + Math.abs(this.authlogExpireDays) * 24 * 3600 * 1000);
}
2017-08-08 18:20:03 +08:00
2018-08-15 03:45:18 +08:00
// key is for merging similar events
entry.key = crypto
.createHash('md5')
.update([entry.protocol, entry.ip, entry.action, entry.result].map(v => (v || '').toString()).join('^'))
.digest();
2019-07-11 15:52:43 +08:00
let r = await this.users.collection('authlog').findOneAndUpdate(
2018-08-15 03:45:18 +08:00
{
user: entry.user,
created: {
2018-08-15 15:17:02 +08:00
// merge similar events into buckets of time
$gte: new Date(Date.now() - consts.AUTHLOG_BUCKET)
2018-08-15 03:45:18 +08:00
},
2018-08-15 15:17:02 +08:00
// events are merged based on this key
2018-08-15 03:45:18 +08:00
key: entry.key
},
{
$setOnInsert: entry,
$inc: {
events: 1
},
$set: {
last: now
}
},
{
upsert: true,
projection: { _id: true },
2019-01-24 05:38:29 +08:00
returnOriginal: false,
maxTimeMS: consts.DB_MAX_TIME_USERS
2018-08-15 03:45:18 +08:00
}
);
2019-07-11 15:52:43 +08:00
return r && r.value && r.value._id;
2017-07-24 21:44:08 +08:00
}
logout(user, reason, callback) {
// register this address as the default address for that user
2017-11-23 17:51:37 +08:00
return this.users.collection('users').findOne(
{
_id: new ObjectID(user)
},
{
2018-08-15 04:45:45 +08:00
projection: {
2017-11-23 17:51:37 +08:00
_id: true
2019-01-24 05:38:29 +08:00
},
maxTimeMS: consts.DB_MAX_TIME_USERS
2017-11-23 17:51:37 +08:00
},
(err, userData) => {
if (err) {
log.error('DB', 'DBFAIL logout id=%s error=%s', user, err.message);
err.message = 'Database Error, failed to find user';
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2017-11-23 17:51:37 +08:00
return callback(err);
}
if (!userData) {
2017-12-21 16:31:34 +08:00
err = new Error('User not found');
err.code = 'UserNotFound';
return callback(err);
2017-11-23 17:51:37 +08:00
}
2017-11-23 17:51:37 +08:00
if (!this.messageHandler || !this.messageHandler.notifier) {
return callback(null, false);
}
this.messageHandler.notifier.fire(userData._id, {
command: 'LOGOUT',
2017-11-23 17:51:37 +08:00
reason
});
return callback(null, true);
}
);
}
2017-11-17 19:37:53 +08:00
// This method deletes non expireing records from database
2017-11-23 17:51:37 +08:00
delete(user, meta, callback) {
meta = meta || {};
// clear limits in Redis
this.redis.del('limits:' + user, () => false);
2018-10-11 16:48:12 +08:00
let tryCount = 0;
let tryDelete = err => {
if (tryCount++ > 10) {
return callback(err);
}
this.users.collection('addresses').deleteMany({ user }, err => {
2017-11-23 17:51:37 +08:00
if (err) {
2018-10-11 16:48:12 +08:00
log.error('USERDEL', 'Failed to delete addresses for id=%s error=%s', user, err.message);
2017-12-21 16:31:34 +08:00
err.code = 'InternalDatabaseError';
2018-10-11 16:48:12 +08:00
if (tryCount > 4) {
return setTimeout(() => tryDelete(err), 100);
}
2017-11-17 19:37:53 +08:00
}
2018-10-11 16:48:12 +08:00
this.users.collection('users').deleteOne({ _id: user }, err => {
if (err) {
log.error('USERDEL', 'Failed to delete user id=%s error=%s', user, err.message);
err.code = 'InternalDatabaseError';
return setTimeout(() => tryDelete(err), 100);
2017-11-17 19:37:53 +08:00
}
2018-10-11 16:48:12 +08:00
// set up a task to delete user messages
let now = new Date();
this.database.collection('tasks').insertOne(
{
task: 'user-delete',
locked: false,
lockedUntil: now,
created: now,
status: 'queued',
user
},
() =>
this.logAuthEvent(
user,
{
action: 'delete user',
result: 'success',
sess: meta.session,
ip: meta.ip
},
() => callback(null, true)
)
2017-11-23 17:51:37 +08:00
2018-10-11 16:48:12 +08:00
);
});
});
};
setImmediate(tryDelete);
2017-11-17 19:37:53 +08:00
}
2018-01-26 17:39:08 +08:00
2019-07-09 00:14:55 +08:00
// returns a query to find a user based on address or username
2019-07-11 15:52:43 +08:00
async checkAddress(username) {
2018-01-26 17:39:08 +08:00
if (username.indexOf('@') < 0) {
2019-01-13 19:13:22 +08:00
// not formatted as an address, assume regular username
2019-07-11 15:52:43 +08:00
return {
2018-05-11 19:39:23 +08:00
unameview: tools.uview(username)
2019-07-11 15:52:43 +08:00
};
2018-01-26 17:39:08 +08:00
}
2019-07-11 15:52:43 +08:00
let addressData = await this.asyncResolveAddress(username, {
wildcard: false,
projection: {
user: true
}
});
2019-01-13 19:13:22 +08:00
2019-07-11 15:52:43 +08:00
if (addressData && !addressData.user) {
// found a non-user address
return false;
}
2019-01-13 19:13:22 +08:00
2019-07-11 15:52:43 +08:00
if (!addressData) {
// fall back to username formatted as an address
return {
unameview: tools.normalizeAddress(username, false, {
removeLabel: true,
removeDots: true
})
};
}
2018-01-26 17:39:08 +08:00
2019-07-11 15:52:43 +08:00
return {
_id: addressData.user
};
2018-01-26 17:39:08 +08:00
}
async generateAuthToken(user) {
let accessToken = crypto.randomBytes(20).toString('hex');
let tokenHash = crypto
.createHash('sha256')
.update(accessToken)
.digest('hex');
let key = 'tn:token:' + tokenHash;
2019-04-05 20:08:46 +08:00
let ttl = config.api.accessControl.tokenTTL || consts.ACCESS_TOKEN_DEFAULT_TTL;
let tokenData = {
user: user.toString(),
role: 'user',
created: Date.now(),
ttl,
// signature
s: crypto
.createHmac('sha256', config.api.accessControl.secret)
.update(
JSON.stringify({
token: accessToken,
user: user.toString(),
role: 'user'
})
)
.digest('hex')
};
await this.redis
.multi()
.hmset(key, tokenData)
.expire(key, ttl)
.exec();
return accessToken;
}
2017-04-21 01:10:03 +08:00
}
2019-07-11 15:52:43 +08:00
function rateLimitResponse(res) {
let err = new Error('Authentication was rate limited. Check again in ' + res.ttl + ' seconds');
err.response = 'NO';
err.code = 'RateLimitedError';
2019-07-11 15:52:43 +08:00
return err;
}
// high collision hash function
function getStringSelector(str) {
let hash = crypto
.createHash('sha1')
.update(str)
.digest();
let sum = 0;
for (let i = 0, len = hash.length; i < len; i++) {
sum += hash[i];
}
2018-01-11 17:20:12 +08:00
return (sum % 32).toString(16);
}
2017-04-21 01:10:03 +08:00
module.exports = UserHandler;