wildduck/lib/user-handler.js

1117 lines
42 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');
const bcrypt = require('bcryptjs');
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');
2017-07-30 03:08:43 +08:00
const base32 = require('base32.js');
const MailComposer = require('nodemailer/lib/mail-composer');
const humanname = require('humanname');
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;
this.messageHandler = options.messageHandler;
this.counters = this.messageHandler ? this.messageHandler.counters : counters(this.redis);
2017-04-21 01:10:03 +08:00
}
/**
* Authenticate user
*
* @param {String} username Either username or email address
*/
2017-07-24 21:44:08 +08:00
authenticate(username, password, requiredScope, meta, callback) {
2017-04-22 03:16:01 +08:00
if (!callback && typeof meta === 'function') {
callback = meta;
meta = {};
}
2017-07-24 21:44:08 +08:00
meta = meta || {};
meta.requiredScope = requiredScope;
if (!password) {
// do not allow signing in without a password
return callback(null, false);
}
2017-04-21 01:10:03 +08:00
let checkAddress = next => {
if (username.indexOf('@') < 0) {
// assume regular username
return next(null, {
2017-07-31 15:59:18 +08:00
unameview: username.replace(/\./g, '')
2017-04-21 01:10:03 +08:00
});
}
// try to find existing email address
let address = tools.normalizeAddress(username);
this.users.collection('addresses').findOne({
2017-07-31 15:59:18 +08:00
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@'))
2017-04-21 01:10:03 +08:00
}, {
fields: {
user: true
}
}, (err, addressData) => {
if (err) {
return callback(err);
}
2017-04-21 17:07:38 +08:00
2017-04-21 01:10:03 +08:00
if (!addressData) {
2017-07-24 21:44:08 +08:00
meta.address = address;
meta.result = 'unknown';
return this.logAuthEvent(null, meta, () => callback(null, false));
2017-04-21 01:10:03 +08:00
}
2017-04-21 17:07:38 +08:00
next(null, {
2017-04-21 01:10:03 +08:00
_id: addressData.user
});
});
};
2017-04-21 17:07:38 +08:00
checkAddress((err, query) => {
if (err) {
return callback(err);
}
this.users.collection('users').findOne(query, {
2017-04-21 01:10:03 +08:00
fields: {
2017-07-24 21:44:08 +08:00
_id: true,
2017-04-21 01:10:03 +08:00
username: true,
password: true,
2017-07-25 18:56:50 +08:00
enabled2fa: true,
2017-08-05 20:39:31 +08:00
requirePasswordChange: true,
2017-07-25 18:56:50 +08:00
disabled: true
2017-04-21 01:10:03 +08:00
}
}, (err, userData) => {
if (err) {
return callback(err);
}
if (!userData) {
2017-07-31 15:59:18 +08:00
if (query.unameview) {
meta.username = query.unameview;
2017-07-24 21:44:08 +08:00
} else {
meta.user = query._id;
}
meta.result = 'unknown';
return this.logAuthEvent(null, meta, () => callback(null, false));
2017-04-21 01:10:03 +08:00
}
2017-07-25 18:56:50 +08:00
if (userData.disabled) {
// disabled users can not log in
meta.result = 'disabled';
2017-08-08 18:20:03 +08:00
meta.groupKey = ['authenticate', meta.protocol, meta.result, meta.ip].join(':');
2017-07-25 18:56:50 +08:00
return this.logAuthEvent(userData._id, meta, () => callback(null, false));
}
let rlkey = 'auth:' + userData._id.toString();
this.counters.ttlcounter(rlkey, 0, consts.AUTH_FAILURES, consts.AUTH_WINDOW, (err, res) => {
2017-07-24 21:44:08 +08:00
if (err) {
return callback(err);
}
if (!res.success) {
let err = new Error('Authentication was rate limited. Check again in ' + res.ttl + ' seconds');
err.response = 'NO';
return callback(err);
2017-07-24 21:44:08 +08:00
}
2017-04-21 01:10:03 +08:00
let authSuccess = (...args) => {
// clear rate limit counter on success
this.redis.del(rlkey, () => false);
callback(...args);
};
2017-04-21 01:10:03 +08:00
let authFail = (...args) => {
// increment rate limit counter on failure
this.counters.ttlcounter(rlkey, 1, consts.AUTH_FAILURES, consts.AUTH_WINDOW, () => {
callback(...args);
});
};
2017-07-24 21:44:08 +08:00
// try master password
bcrypt.compare(password, userData.password || '', (err, success) => {
if (err) {
return callback(err);
}
if (success) {
meta.result = 'success';
meta.source = 'master';
if (userData.enabled2fa) {
meta.require2fa = true;
}
2017-08-08 18:20:03 +08:00
meta.groupKey = ['authenticate', meta.protocol, meta.result, meta.source, meta.ip].join(':');
return this.logAuthEvent(userData._id, meta, () =>
authSuccess(null, {
user: userData._id,
username: userData.username,
scope: 'master',
// if 2FA is enabled then require token validation
require2fa: !!userData.enabled2fa
})
);
}
2017-04-21 01:10:03 +08:00
if (requiredScope === 'master') {
// only master password can be used for management tasks
meta.result = 'fail';
meta.source = 'master';
2017-08-08 18:20:03 +08:00
meta.groupKey = ['authenticate', meta.protocol, meta.result, meta.source, meta.ip].join(':');
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
}
2017-07-28 21:34:22 +08:00
// try application specific passwords
password = password.replace(/\s+/g, '').toLowerCase();
2017-07-24 21:44:08 +08:00
if (!/^[a-z]{16}$/.test(password)) {
// does not look like an application specific password
meta.result = 'fail';
meta.source = 'master';
2017-08-08 18:20:03 +08:00
meta.groupKey = ['authenticate', meta.protocol, meta.result, meta.source, meta.ip].join(':');
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
}
2017-07-24 21:44:08 +08:00
let prefix = crypto.createHash('md5').update(password.substr(0, 4)).digest('hex');
this.users
.collection('asps')
.find({
user: userData._id
})
.toArray((err, asps) => {
if (err) {
return callback(err);
2017-07-24 21:44:08 +08:00
}
if (!asps || !asps.length) {
// user does not have app specific passwords set
meta.result = 'fail';
2017-08-08 18:20:03 +08:00
meta.source = 'master';
meta.groupKey = ['authenticate', meta.protocol, meta.result, meta.source, meta.ip].join(':');
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
2017-07-28 21:34:22 +08:00
}
2017-07-24 21:44:08 +08:00
let pos = 0;
let checkNext = () => {
if (pos >= asps.length) {
meta.result = 'fail';
2017-08-08 18:20:03 +08:00
meta.source = 'master';
meta.groupKey = ['authenticate', meta.protocol, meta.result, meta.source, meta.ip].join(':');
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
2017-07-24 21:44:08 +08:00
}
let asp = asps[pos++];
if (asp.prefix && asp.prefix !== prefix) {
// no need to check, definitely a wrong one
2017-07-24 21:44:08 +08:00
return setImmediate(checkNext);
}
bcrypt.compare(password, asp.password || '', (err, success) => {
if (err) {
return callback(err);
}
if (!success) {
return setImmediate(checkNext);
}
if (!asp.scopes.includes('*') && !asp.scopes.includes(requiredScope)) {
meta.result = 'fail';
meta.source = 'asp';
meta.asp = asp._id.toString();
2017-08-08 18:20:03 +08:00
meta.groupKey = ['authenticate', meta.protocol, meta.result, meta.asp, 'scope', meta.ip].join(':');
return this.logAuthEvent(userData._id, meta, () => authFail(new Error('Authentication failed. Invalid scope')));
}
meta.result = 'success';
2017-07-24 21:44:08 +08:00
meta.source = 'asp';
meta.asp = asp._id.toString();
2017-08-08 18:20:03 +08:00
meta.groupKey = ['authenticate', meta.protocol, meta.result, meta.asp, meta.ip].join(':');
return this.logAuthEvent(userData._id, meta, () => {
this.redis.del(rlkey, () => false);
authSuccess(null, {
user: userData._id,
username: userData.username,
scope: requiredScope,
asp: asp._id.toString(),
require2fa: false, // application scope never requires 2FA
requirePasswordChange: !!userData.requirePasswordChange // true, if password was reset
});
});
});
};
checkNext();
});
});
2017-07-24 21:44:08 +08:00
});
2017-04-21 01:10:03 +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 prefix = crypto.createHash('md5').update(password.substr(0, 4)).digest('hex');
2017-07-24 21:44:08 +08:00
let allowedScopes = ['imap', 'pop3', 'smtp'];
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;
} else {
scopeSet.add(scope);
}
});
if (hasAllScopes || scopeSet.size === allowedScopes.length) {
scopes = ['*'];
} else {
scopes = Array.from(scopeSet).sort();
}
let passwordData = {
id: new ObjectID(),
2017-07-24 21:44:08 +08:00
user,
2017-07-25 01:32:22 +08:00
description: data.description,
2017-07-24 21:44:08 +08:00
scopes,
2017-08-05 20:39:31 +08:00
password: bcrypt.hashSync(password, consts.BCRYPT_ROUNDS),
2017-07-28 21:34:22 +08:00
prefix,
2017-07-24 21:44:08 +08:00
created: new Date()
};
// register this address as the default address for that user
2017-07-24 21:44:08 +08:00
return this.users.collection('users').findOne({
_id: user
}, {
2017-07-24 21:44:08 +08:00
fields: {
_id: true
}
2017-07-24 21:44:08 +08:00
}, (err, userData) => {
if (err) {
2017-07-24 21:44:08 +08:00
log.error('DB', 'DBFAIL generateASP id=%s error=%s', user, err.message);
return callback(new Error('Database Error, failed to find user'));
}
2017-07-24 21:44:08 +08:00
if (!userData) {
return callback(new Error('User not found'));
}
2017-07-24 21:44:08 +08:00
this.users.collection('asps').insertOne(passwordData, err => {
if (err) {
return callback(err);
}
2017-07-25 01:32:22 +08:00
return this.logAuthEvent(
user,
{
action: 'create asp',
asp: passwordData._id,
result: 'success',
ip: data.ip
},
() =>
callback(null, {
id: passwordData._id,
password
})
);
2017-07-24 21:44:08 +08:00
});
});
}
2017-07-25 01:32:22 +08:00
deleteASP(user, asp, data, callback) {
this.users.collection('asps').deleteOne({
_id: asp,
user
}, (err, r) => {
if (err) {
return callback(err);
}
if (!r.deletedCount) {
return callback(new Error('Application Specific Password was not found'));
}
return this.logAuthEvent(
user,
{
action: 'delete asp',
asp,
result: 'success',
ip: data.ip
},
() => callback(null, true)
);
});
}
2017-04-21 01:10:03 +08:00
create(data, callback) {
this.users.collection('users').findOne({
2017-07-31 15:59:18 +08:00
username: data.username.replace(/\./g, '')
2017-04-21 01:10:03 +08:00
}, {
fields: {
2017-07-31 15:59:18 +08:00
unameview: true
2017-04-21 01:10:03 +08:00
}
}, (err, userData) => {
if (err) {
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
return callback(new Error('Database Error, failed to create user'));
}
2017-07-17 21:32:31 +08:00
2017-04-21 01:10:03 +08:00
if (userData) {
let err = new Error('This username already exists');
return callback(err);
}
2017-07-17 21:32:31 +08:00
let junkRetention = consts.JUNK_RETENTION;
2017-08-05 20:39:31 +08:00
// Insert user data
// Users with an empty password can not log in
let hash = data.password ? bcrypt.hashSync(data.password, consts.BCRYPT_ROUNDS) : '';
2017-07-17 21:32:31 +08:00
let id = new ObjectID();
this.users.collection('users').insertOne({
2017-07-17 21:32:31 +08:00
_id: id,
2017-04-21 01:10:03 +08:00
username: data.username,
2017-07-31 15:59:18 +08:00
// dotless version
unameview: data.username.replace(/\./g, ''),
2017-04-21 01:10:03 +08:00
name: data.name,
// security
password: '', // set this later. having no password prevents login
asp: [], // list of application specific passwords
enabled2fa: false,
seed: '', // 2fa seed value
// default email address
address: '', // set this later
// quota
storageUsed: 0,
2017-07-17 21:32:31 +08:00
quota: data.quota || 0,
recipients: data.recipients || 0,
forwards: data.forwards || 0,
2017-04-21 01:10:03 +08:00
2017-07-30 23:07:35 +08:00
// autoreply status
// off by default, can be changed later by user through the API
2017-07-30 23:07:35 +08:00
autoreply: false,
pubKey: data.pubKey || '',
encryptMessages: !!data.encryptMessages,
// default retention for user mailboxes
2017-07-17 21:32:31 +08:00
retention: data.retention || 0,
2017-04-21 01:10:03 +08:00
created: new Date(),
requirePasswordChange: false,
2017-04-21 01:10:03 +08:00
// until setup value is not true, this account is not usable
2017-07-20 21:10:36 +08:00
activated: false,
2017-08-08 18:20:03 +08:00
disabled: true
2017-07-17 21:32:31 +08:00
}, err => {
2017-04-21 01:10:03 +08:00
if (err) {
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
return callback(new Error('Database Error, failed to create user'));
}
let mailboxes = this.getMailboxes(data.language).map(mailbox => {
2017-07-17 21:32:31 +08:00
mailbox.user = id;
if (['\\Trash', '\\Junk'].includes(mailbox.specialUse)) {
2017-07-17 21:32:31 +08:00
mailbox.retention = data.retention ? Math.min(data.retention, junkRetention) : junkRetention;
} else {
2017-07-17 21:32:31 +08:00
mailbox.retention = data.retention;
}
2017-04-21 01:10:03 +08:00
return mailbox;
});
this.database.collection('mailboxes').insertMany(mailboxes, {
w: 1,
ordered: false
}, err => {
if (err) {
2017-07-17 21:32:31 +08:00
// try to rollback
this.users.collection('users').deleteOne({ _id: id }, () => false);
2017-04-21 01:10:03 +08:00
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
return callback(new Error('Database Error, failed to create user'));
}
2017-07-12 02:38:23 +08:00
let address = data.address ? data.address : data.username + '@' + (config.emailDomain || os.hostname()).toLowerCase();
2017-05-17 20:58:42 +08:00
2017-04-21 01:10:03 +08:00
// insert alias address to email address registry
this.users.collection('addresses').insertOne({
2017-07-17 21:32:31 +08:00
user: id,
2017-05-17 20:58:42 +08:00
address,
2017-07-31 15:59:18 +08:00
// dotless version
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@')),
2017-04-21 01:10:03 +08:00
created: new Date()
}, err => {
if (err) {
2017-07-17 21:32:31 +08:00
// try to rollback
this.users.collection('users').deleteOne({ _id: id }, () => false);
this.database.collection('mailboxes').deleteMany({ user: id }, () => false);
2017-04-21 01:10:03 +08:00
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
2017-07-20 21:10:36 +08:00
let response;
switch (err.code) {
case 11000:
response = 'Selected email address already exists';
break;
default:
response = 'Database Error, failed to create user';
}
return callback(new Error(response));
2017-04-21 01:10:03 +08:00
}
// register this address as the default address for that user
return this.users.collection('users').findOneAndUpdate({
2017-07-17 21:32:31 +08:00
_id: id,
2017-07-20 21:10:36 +08:00
activated: false
2017-04-21 01:10:03 +08:00
}, {
$set: {
password: hash,
2017-05-17 20:58:42 +08:00
address,
2017-07-20 21:10:36 +08:00
activated: true,
disabled: false
2017-04-21 01:10:03 +08:00
}
2017-08-03 20:26:44 +08:00
}, { returnOriginal: false }, (err, result) => {
2017-04-21 01:10:03 +08:00
if (err) {
2017-07-17 21:32:31 +08:00
// try to rollback
this.users.collection('users').deleteOne({ _id: id }, () => false);
this.database.collection('mailboxes').deleteMany({ user: id }, () => false);
2017-04-21 01:10:03 +08:00
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
return callback(new Error('Database Error, failed to create user'));
}
2017-08-03 20:26:44 +08:00
let userData = result.value;
if (!userData) {
// should never happen
return callback(null, id);
}
2017-08-08 18:20:03 +08:00
let createSuccess = () =>
this.logAuthEvent(
id,
{
action: 'account created',
result: 'success',
ip: data.ip
},
() => callback(null, id)
);
if (!this.messageHandler) {
2017-08-08 18:20:03 +08:00
return createSuccess();
}
let parsedName = humanname.parse(userData.name);
this.pushDefaultMessages(
2017-08-03 20:26:44 +08:00
userData,
{
2017-08-03 20:26:44 +08:00
NAME: userData.name || address,
FNAME: parsedName.firstName,
LNAME: parsedName.lastName,
DOMAIN: address.substr(address.indexOf('@') + 1),
EMAIL: address
},
2017-08-08 18:20:03 +08:00
() => createSuccess()
);
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',
time: Date.now()
},
flags,
raw: message
},
2017-08-03 20:26:44 +08:00
insertMessages
);
});
});
};
insertMessages();
});
}
reset(user, callback) {
2017-06-12 17:51:44 +08:00
let password = generatePassword.generate({
length: 12,
uppercase: true,
numbers: true,
symbols: false
});
return this.users.collection('users').findOneAndUpdate({
_id: user
2017-06-12 17:51:44 +08:00
}, {
$set: {
enabled2fa: false,
seed: '',
requirePasswordChange: true,
2017-08-05 20:39:31 +08:00
password: bcrypt.hashSync(password, consts.BCRYPT_ROUNDS)
2017-06-12 17:51:44 +08:00
}
}, {}, (err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
2017-06-12 17:51:44 +08:00
return callback(new Error('Database Error, failed to reset user credentials'));
}
if (!result || !result.value) {
return callback(new Error('Could not update user ' + user));
2017-06-12 17:51:44 +08:00
}
return callback(null, password);
});
}
2017-07-24 21:44:08 +08:00
setup2fa(user, data, callback) {
return this.users.collection('users').findOne({
2017-07-24 21:44:08 +08:00
_id: user
2017-04-21 01:10:03 +08:00
}, {
2017-04-21 21:38:03 +08:00
fields: {
2017-07-24 21:44:08 +08:00
username: true,
2017-07-30 03:08:43 +08:00
enabled2fa: true,
seed: true
2017-04-21 01:10:03 +08:00
}
2017-07-24 21:44:08 +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-04-21 21:38:03 +08:00
return callback(new Error('Database Error, failed to check user'));
2017-04-21 01:10:03 +08:00
}
2017-07-24 21:44:08 +08:00
if (!userData) {
2017-04-21 21:38:03 +08:00
return callback(new Error('Could not find user data'));
2017-04-21 01:10:03 +08:00
}
2017-07-24 21:44:08 +08:00
if (userData.enabled2fa) {
2017-04-21 21:38:03 +08:00
return callback(new Error('2FA is already enabled for this user'));
2017-04-21 01:10:03 +08:00
}
2017-04-21 21:38:03 +08:00
2017-07-30 03:08:43 +08:00
if (!data.fresh && userData.seed) {
if (userData.seed) {
let secret = userData.seed;
if (userData.seed.charAt(0) === '$' && config.totp && config.totp.secret) {
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');
}
2017-07-30 03:08:43 +08:00
let otpauth_url = speakeasy.otpauthURL({
secret: base32.decode(secret),
2017-07-30 03:08:43 +08:00
label: userData.username,
issuer: data.issuer || 'Wild Duck'
});
2017-07-30 03:08:43 +08:00
return QRCode.toDataURL(otpauth_url, (err, data_url) => {
if (err) {
log.error('DB', 'QRFAIL username=%s error=%s', userData.username, err.message);
return callback(new Error('Failed to generate QR code'));
}
return callback(null, data_url);
});
}
}
2017-04-21 21:38:03 +08:00
let secret = speakeasy.generateSecret({
length: 20,
2017-07-24 21:44:08 +08:00
name: userData.username
2017-04-21 21:38:03 +08:00
});
let seed = secret.base32;
if (config.totp && config.totp.secret) {
let cipher = crypto.createCipher(config.totp.cipher || 'aes192', config.totp.secret);
seed = '$' + cipher.update(seed, 'utf8', 'hex');
seed += cipher.final('hex');
}
return this.users.collection('users').findOneAndUpdate({
2017-07-24 21:44:08 +08:00
_id: user,
2017-04-21 01:10:03 +08:00
enabled2fa: false
}, {
$set: {
seed
2017-04-21 01:10:03 +08:00
}
}, {}, (err, result) => {
if (err) {
2017-07-24 21:44:08 +08:00
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
2017-04-21 01:10:03 +08:00
return callback(new Error('Database Error, failed to update user'));
}
if (!result || !result.value) {
return callback(new Error('Could not update user, check if 2FA is not already enabled'));
}
2017-06-12 16:30:28 +08:00
let otpauth_url = speakeasy.otpauthURL({
secret: secret.ascii,
2017-07-24 21:44:08 +08:00
label: userData.username,
issuer: data.issuer || 'Wild Duck'
2017-06-12 16:30:28 +08:00
});
QRCode.toDataURL(otpauth_url, (err, data_url) => {
2017-04-21 21:38:03 +08:00
if (err) {
2017-07-24 21:44:08 +08:00
log.error('DB', 'QRFAIL id=%s error=%s', user, err.message);
2017-04-21 21:38:03 +08:00
return callback(new Error('Failed to generate QR code'));
}
2017-07-24 21:44:08 +08:00
return this.logAuthEvent(
user,
{
action: 'new 2fa seed',
ip: data.ip
},
() => callback(null, data_url)
);
2017-04-21 21:38:03 +08:00
});
2017-04-21 01:10:03 +08:00
});
});
}
2017-07-24 21:44:08 +08:00
enable2fa(user, data, callback) {
this.users.collection('users').findOne({
_id: user
}, {
fields: {
enabled2fa: true,
username: true,
seed: true
}
}, (err, userData) => {
2017-04-21 01:10:03 +08:00
if (err) {
2017-07-24 21:44:08 +08:00
log.error('DB', 'LOADFAIL id=%s error=%s', user, err.message);
return callback(new Error('Database Error, failed to update user'));
}
if (!userData) {
let err = new Error('This username does not exist');
2017-04-21 01:10:03 +08:00
return callback(err);
}
2017-07-24 21:44:08 +08:00
if (!userData.seed) {
// 2fa not set up
let err = new Error('2FA is not initialized for this user');
return callback(err);
}
if (userData.enabled2fa) {
// 2fa not set up
let err = new Error('2FA is already enabled for this user');
return callback(err);
}
let secret = userData.seed;
if (userData.seed.charAt(0) === '$' && config.totp && config.totp.secret) {
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');
}
2017-07-24 21:44:08 +08:00
let verified = speakeasy.totp.verify({
secret,
2017-07-24 21:44:08 +08:00
encoding: 'base32',
token: data.token,
window: 6
});
2017-04-21 01:10:03 +08:00
if (!verified) {
2017-07-24 21:44:08 +08:00
return this.logAuthEvent(
user,
{
action: 'enable 2fa',
result: 'fail',
ip: data.ip
},
() => callback(null, false)
);
2017-04-21 01:10:03 +08:00
}
// token was valid, update user settings
return this.users.collection('users').findOneAndUpdate({
2017-07-24 21:44:08 +08:00
_id: user,
seed: userData.seed
2017-04-21 01:10:03 +08:00
}, {
$set: {
2017-04-21 21:38:03 +08:00
enabled2fa: true
2017-04-21 01:10:03 +08:00
}
}, {}, (err, result) => {
if (err) {
2017-07-24 21:44:08 +08:00
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
2017-04-21 01:10:03 +08:00
return callback(new Error('Database Error, failed to update user'));
}
if (!result || !result.value) {
2017-07-24 21:44:08 +08:00
return callback(new Error('Failed to set up 2FA. Check if it is not already enabled'));
2017-04-21 01:10:03 +08:00
}
2017-07-24 21:44:08 +08:00
return this.logAuthEvent(
user,
{
action: 'enable 2fa',
result: 'success',
ip: data.ip
},
() => callback(null, true)
);
2017-04-21 01:10:03 +08:00
});
});
}
2017-07-24 21:44:08 +08:00
disable2fa(user, data, callback) {
return this.users.collection('users').findOneAndUpdate({
2017-07-25 01:32:22 +08:00
_id: user,
enabled2fa: true
2017-04-21 21:38:03 +08:00
}, {
$set: {
enabled2fa: false,
seed: ''
}
}, {}, (err, result) => {
if (err) {
2017-07-24 21:44:08 +08:00
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
2017-04-21 21:38:03 +08:00
return callback(new Error('Database Error, failed to update user'));
}
if (!result || !result.value) {
return callback(new Error('Could not update user, check if 2FA is not already disabled'));
}
2017-07-24 21:44:08 +08:00
return this.logAuthEvent(
user,
{
action: 'disable 2fa',
ip: data.ip
},
() => callback(null, true)
);
2017-04-21 21:38:03 +08:00
});
}
2017-07-24 21:44:08 +08:00
check2fa(user, data, callback) {
let rlkey = 'totp:' + user.toString();
this.counters.ttlcounter(rlkey, 0, consts.AUTH_FAILURES, consts.AUTH_WINDOW * 3, (err, res) => {
2017-04-21 01:10:03 +08:00
if (err) {
return callback(err);
}
if (!res.success) {
let err = new Error('Authentication was rate limited. Check again in ' + res.ttl + ' seconds');
err.response = 'NO';
2017-07-25 01:32:22 +08:00
return callback(err);
2017-04-21 01:10:03 +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
this.counters.ttlcounter(rlkey, 1, consts.TOTP_FAILURES, consts.TOTP_WINDOW, () => {
callback(...args);
});
};
2017-04-21 01:10:03 +08:00
this.users.collection('users').findOne({
_id: user
}, {
fields: {
username: true,
enabled2fa: true,
seed: true
}
}, (err, userData) => {
if (err) {
log.error('DB', 'LOADFAIL id=%s error=%s', user, err.message);
return callback(new Error('Database Error, failed to find user'));
}
if (!userData) {
let err = new Error('This user does not exist');
return callback(err);
}
if (!userData.seed || !userData.enabled2fa) {
// 2fa not set up
let err = new Error('2FA is not enabled for this user');
return callback(err);
}
let secret = userData.seed;
if (userData.seed.charAt(0) === '$' && config.totp && config.totp.secret) {
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');
}
let verified = speakeasy.totp.verify({
secret,
encoding: 'base32',
token: data.token,
window: 6
});
return this.logAuthEvent(
user,
{
action: '2fa',
ip: data.ip,
result: verified ? 'success' : 'fail'
},
() => {
if (verified) {
authSuccess(null, verified);
} else {
authFail(null, verified);
}
}
);
});
2017-04-21 01:10:03 +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
2017-07-24 21:44:08 +08:00
Object.keys(data).forEach(key => {
if (['user', 'existingPassword', 'ip'].includes(key)) {
return;
2017-05-07 20:09:14 +08:00
}
2017-07-24 21:44:08 +08:00
if (key === 'password') {
2017-08-05 20:39:31 +08:00
$set.password = bcrypt.hashSync(data[key], consts.BCRYPT_ROUNDS);
$set.requirePasswordChange = false;
2017-07-24 21:44:08 +08:00
$set.passwordChange = new Date();
passwordChanged = true;
return;
2017-05-07 22:21:44 +08:00
}
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) {
$set.unameview = $set.username.replace(/\./g, '');
}
2017-07-24 21:44:08 +08:00
if (!updates) {
return callback(new Error('Nothing was updated'));
}
2017-05-07 22:21:44 +08:00
2017-07-24 21:44:08 +08:00
let verifyExistingPassword = next => {
if (!data.existingPassword) {
return next();
2017-05-07 20:09:14 +08:00
}
2017-07-24 21:44:08 +08:00
this.users.collection('users').findOne({ _id: user }, { fields: { password: true } }, (err, userData) => {
if (err) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message);
return callback(new Error('Database Error, failed to find user'));
}
2017-05-07 20:09:14 +08:00
2017-07-24 21:44:08 +08:00
if (!userData) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, 'User was not found');
return callback(new Error('User was not found'));
}
2017-04-21 01:10:03 +08:00
2017-07-24 21:44:08 +08:00
if (bcrypt.compareSync(data.existingPassword, userData.password || '')) {
return next();
} else {
return this.logAuthEvent(
user,
{
action: 'password change',
result: 'fail',
ip: data.ip
},
() => callback(new Error('Password verification failed'))
);
}
});
};
2017-06-12 17:10:29 +08:00
2017-07-24 21:44:08 +08:00
verifyExistingPassword(() => {
this.users.collection('users').findOneAndUpdate({
_id: user
2017-04-21 01:10:03 +08:00
}, {
2017-07-24 21:44:08 +08:00
$set
}, {
returnOriginal: false
}, (err, result) => {
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-04-21 01:10:03 +08:00
return callback(new Error('Database Error, failed to update user'));
}
2017-07-24 21:44:08 +08:00
if (!result || !result.value) {
log.error('DB', 'UPDATEFAIL id=%s error=%s', user, 'User was not found');
return callback(new Error('user was not found'));
}
if (passwordChanged) {
return this.logAuthEvent(
user,
{
action: 'password change',
result: 'success',
ip: data.ip
},
() => callback(null, true)
);
} else {
return callback(null, true);
}
2017-04-21 01:10:03 +08:00
});
});
}
getMailboxes(language) {
let translation = mailboxTranslations.hasOwnProperty(language) ? mailboxTranslations[language] : mailboxTranslations.en;
2017-06-03 14:51:58 +08:00
let defaultMailboxes = [
{
path: 'INBOX'
},
{
specialUse: '\\Sent'
},
{
specialUse: '\\Trash'
},
{
specialUse: '\\Drafts'
},
{
specialUse: '\\Junk'
},
{
specialUse: '\\Archive'
}
];
2017-04-21 01:10:03 +08:00
let uidValidity = Math.floor(Date.now() / 1000);
return defaultMailboxes.map(mailbox => ({
path: translation[mailbox.specialUse || mailbox.path] || mailbox.path,
specialUse: mailbox.specialUse,
uidValidity,
uidNext: 1,
modifyIndex: 0,
subscribed: true,
flags: []
}));
}
2017-07-24 21:44:08 +08:00
2017-08-08 18:20:03 +08:00
logAuthEvent(user, entry, callback) {
2017-07-24 21:44:08 +08:00
if (user) {
2017-08-08 18:20:03 +08:00
entry.user = user;
2017-07-24 21:44:08 +08:00
}
2017-08-08 18:20:03 +08:00
entry.action = entry.action || 'authentication';
entry.created = new Date();
entry.expires = new Date(Date.now() + config.log.authlogExpireDays * 24 * 3600 * 1000);
if (!entry.groupKey) {
return this.users.collection('authlog').insertOne(entry, callback);
}
entry.count = 1;
entry.groupKey = crypto.createHash('sha1').update(entry.groupKey + ':' + Math.floor(Date.now() / (3600 * 1000))).digest('base64');
entry.updated = entry.created;
this.users.collection('authlog').findOneAndUpdate({
user,
group: entry.groupKey
}, {
$inc: { count: 1 },
$set: {
updated: entry.updated
}
}, (err, r) => {
if (err) {
return callback(err);
}
if (r && r.value) {
// an existing entry was updated
return callback(null, true);
}
// add new entry
this.users.collection('authlog').insertOne(entry, callback);
});
2017-07-24 21:44:08 +08:00
}
2017-04-21 01:10:03 +08:00
}
module.exports = UserHandler;