Allow enabling custom 2FA that effectively disables account password for IMAP/SMTP/POP3

This commit is contained in:
Andris Reinman 2018-04-13 14:32:58 +03:00
parent e5376d8a4d
commit 74100be5f0
9 changed files with 631 additions and 299 deletions

2
api.js
View file

@ -18,6 +18,7 @@ const messagesRoutes = require('./lib/api/messages');
const filtersRoutes = require('./lib/api/filters');
const aspsRoutes = require('./lib/api/asps');
const totpRoutes = require('./lib/api/2fa/totp');
const custom2faRoutes = require('./lib/api/2fa/custom');
const u2fRoutes = require('./lib/api/2fa/u2f');
const updatesRoutes = require('./lib/api/updates');
const authRoutes = require('./lib/api/auth');
@ -153,6 +154,7 @@ module.exports = done => {
filtersRoutes(db, server);
aspsRoutes(db, server, userHandler);
totpRoutes(db, server, userHandler);
custom2faRoutes(db, server, userHandler);
u2fRoutes(db, server, userHandler);
updatesRoutes(db, server, notifier);
authRoutes(db, server, userHandler);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-02-19T14:11:09.580Z", "url": "http://apidocjs.com", "version": "0.17.6" } });
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-04-13T11:32:14.959Z", "url": "http://apidocjs.com", "version": "0.17.6" } });

View file

@ -1 +1 @@
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-02-19T14:11:09.580Z", "url": "http://apidocjs.com", "version": "0.17.6" } }
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-04-13T11:32:14.959Z", "url": "http://apidocjs.com", "version": "0.17.6" } }

185
lib/api/2fa/custom.js Normal file
View file

@ -0,0 +1,185 @@
'use strict';
const Joi = require('joi');
const ObjectID = require('mongodb').ObjectID;
// Custom 2FA needs to be enabled if your website handles its own 2FA and you want to disable
// master password usage for IMAP/POP/SMTP clients
module.exports = (db, server, userHandler) => {
/**
* @api {put} /users/:user/2fa/custom Enable custom 2FA for an user
* @apiName EnableCustom2FA
* @apiGroup TwoFactorAuth
* @apiDescription This method disables account password for IMAP/POP3/SMTP
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
* @apiHeaderExample {json} Header-Example:
* {
* "X-Access-Token": "59fc66a03e54454869460e45"
* }
*
* @apiParam {String} user ID of the User
* @apiParam {String} [sess] Session identifier for the logs
* @apiParam {String} [ip] IP address for the logs
*
* @apiSuccess {Boolean} success Indicates successful response
*
* @apiError error Description of the error
*
* @apiExample {curl} Example usage:
* curl -i -XPUT http://localhost:8080/users/59fc66a03e54454869460e45/2fa/custom \
* -H 'Content-type: application/json' \
* -d '{
* "ip": "127.0.0.1"
* }'
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success": true
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "This username does not exist"
* "code": "UserNotFound"
* }
*/
server.put('/users/:user/2fa/custom', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
sess: Joi.string().max(255),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.json({
error: result.error.message,
code: 'InputValidationError'
});
return next();
}
let user = new ObjectID(result.value.user);
userHandler.enableCustom2fa(user, result.value, (err, success) => {
if (err) {
res.json({
error: err.message,
code: err.code
});
return next();
}
res.json({
success
});
return next();
});
});
/**
* @api {delete} /users/:user/2fa/custom Disable custom 2FA for an user
* @apiName DisableCustom2FA
* @apiGroup TwoFactorAuth
* @apiDescription This method disables custom 2FA. If it was the only 2FA set up, then account password for IMAP/POP3/SMTP gets enabled again
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
* @apiHeaderExample {json} Header-Example:
* {
* "X-Access-Token": "59fc66a03e54454869460e45"
* }
*
* @apiParam {String} user ID of the User
* @apiParam {String} [sess] Session identifier for the logs
* @apiParam {String} [ip] IP address for the logs
*
* @apiSuccess {Boolean} success Indicates successful response
*
* @apiError error Description of the error
*
* @apiExample {curl} Example usage:
* curl -i -XDELETE http://localhost:8080/users/59fc66a03e54454869460e45/2fa/custom \
* -H 'Content-type: application/json' \
* -d '{
* "ip": "127.0.0.1"
* }'
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success": true
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "This username does not exist"
* "code": "UserNotFound"
* }
*/
server.del('/users/:user/2fa/custom', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
sess: Joi.string().max(255),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
req.query.user = req.params.user;
const result = Joi.validate(req.query, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.json({
error: result.error.message,
code: 'InputValidationError'
});
return next();
}
let user = new ObjectID(result.value.user);
userHandler.disableCustom2fa(user, result.value, (err, success) => {
if (err) {
res.json({
error: err.message,
code: err.code
});
return next();
}
res.json({
success
});
return next();
});
});
};

View file

@ -159,7 +159,7 @@ module.exports = (db, server, userHandler) => {
let user = new ObjectID(result.value.user);
userHandler.disableTotp(user, result.value, (err, result) => {
userHandler.disableTotp(user, result.value, (err, success) => {
if (err) {
res.json({
error: err.message,
@ -168,16 +168,8 @@ module.exports = (db, server, userHandler) => {
return next();
}
if (!result) {
res.json({
error: 'Invalid authentication token',
code: 'InvalidToken'
});
return next();
}
res.json({
success: true
success
});
return next();
@ -278,7 +270,7 @@ module.exports = (db, server, userHandler) => {
let user = new ObjectID(result.value.user);
userHandler.disable2fa(user, result.value, (err, result) => {
userHandler.disable2fa(user, result.value, (err, success) => {
if (err) {
res.json({
error: err.message,
@ -287,16 +279,8 @@ module.exports = (db, server, userHandler) => {
return next();
}
if (!result) {
res.json({
error: 'Failed to disable U2F',
code: 'TotpDisableFailed'
});
return next();
}
res.json({
success: true
success
});
return next();

View file

@ -250,11 +250,11 @@ class UserHandler {
* @param {Integer} count
* @param {Function} callback
*/
rateLimitIP (meta, count, callback) {
rateLimitIP(meta, count, callback) {
if (meta.ip) {
this.counters.ttlcounter('auth_ip:' + meta.ip, count, consts.IP_AUTH_FAILURES, consts.IP_AUTH_WINDOW, callback);
return this.counters.ttlcounter('auth_ip:' + meta.ip, count, consts.IP_AUTH_FAILURES, consts.IP_AUTH_WINDOW, callback);
}
return callback(null, {success: true});
return callback(null, { success: true });
}
/**
@ -263,7 +263,7 @@ class UserHandler {
* @param {Integer} count
* @param {Function} callback
*/
rateLimitUser (tokenID, count, callback) {
rateLimitUser(tokenID, count, callback) {
this.counters.ttlcounter('auth_user:' + tokenID, count, consts.USER_AUTH_FAILURES, consts.USER_AUTH_WINDOW, callback);
}
@ -273,11 +273,11 @@ class UserHandler {
* @param {Integer} count
* @param {Function} callback
*/
rateLimitReleaseUser (tokenID, callback) {
rateLimitReleaseUser(tokenID, callback) {
this.redis.del('auth_user:' + tokenID, callback);
}
/**
/**
* rateLimit
* @param {String} tokenID user identifier
* @param {Object} meta
@ -285,7 +285,7 @@ class UserHandler {
* @param {Integer} count
* @param {Function} callback
*/
rateLimit (tokenID, meta, count, callback) {
rateLimit(tokenID, meta, count, callback) {
this.rateLimitIP(meta, count, (err, ipRes) => {
if (err) {
return callback(err);
@ -303,7 +303,6 @@ class UserHandler {
});
}
/**
* Authenticate user
*
@ -333,292 +332,295 @@ class UserHandler {
return rateLimitResponse(res, callback);
}
this.checkAddress(username, (err, query) => {
if (err) {
return callback(err);
}
this.checkAddress(username, (err, query) => {
if (err) {
return callback(err);
}
if (!query) {
meta.username = username;
meta.result = 'unknown';
return this.logAuthEvent(null, meta, () => callback(null, false));
}
if (!query) {
meta.username = username;
meta.result = 'unknown';
return this.logAuthEvent(null, meta, () => callback(null, false));
}
this.users.collection('users').findOne(
query,
{
fields: {
_id: true,
username: true,
tempPassword: true,
password: true,
enabled2fa: true,
u2f: true,
disabled: true
}
},
(err, userData) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!userData) {
if (query.unameview) {
meta.username = query.unameview;
} else {
meta.user = query._id;
this.users.collection('users').findOne(
query,
{
fields: {
_id: true,
username: true,
tempPassword: true,
password: true,
enabled2fa: true,
u2f: true,
disabled: true
}
meta.result = 'unknown';
return this.logAuthEvent(null, meta, () => {
// rate limit failed authentication attempts against non-existent users as well
let ustring = (query.unameview || query._id || '').toString();
this.rateLimit(ustring, meta, 1, (err, res) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!res.success) {
return rateLimitResponse(res, callback);
}
callback(null, false);
});
});
}
this.rateLimitUser(userData._id, 0, (err, res) => {
},
(err, userData) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!res.success) {
return rateLimitResponse(res, callback);
}
if (userData.disabled) {
// disabled users can not log in
meta.result = 'disabled';
return this.logAuthEvent(userData._id, meta, () => callback(null, false));
}
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
let getU2fAuthRequest = done => {
if (!enabled2fa.includes('u2f') || !userData.u2f || !userData.u2f.keyHandle) {
return done(null, false);
if (!userData) {
if (query.unameview) {
meta.username = query.unameview;
} else {
meta.user = query._id;
}
this.generateU2fAuthRequest(userData._id, userData.u2f.keyHandle, done);
};
meta.result = 'unknown';
return this.logAuthEvent(null, meta, () => {
// rate limit failed authentication attempts against non-existent users as well
let ustring = (query.unameview || query._id || '').toString();
let authSuccess = (...args) => {
// clear rate limit counter on success
this.rateLimitReleaseUser(userData._id, () => false);
callback(...args);
};
let authFail = (...args) => {
// increment rate limit counter on failure
this.rateLimit(userData._id, meta, 1, () => {
callback(...args);
});
};
let requirePasswordChange = false;
let usingTemporaryPassword = false;
let checkMasterPassword = next => {
let checkAccountPassword = () => {
bcrypt.compare(password, userData.password || '', next);
};
if (userData.tempPassword && userData.tempPassword.created > new Date(Date.now() - 24 * 3600 * 1000)) {
// try temporary password
return bcrypt.compare(password, userData.tempPassword.password || '', (err, success) => {
if (err) {
return next(err);
}
if (success) {
if (userData.validAfter > new Date()) {
let err = new Error('Temporary password is not yet activated');
err.code = 'TempPasswordNotYetValid';
return next(err);
}
requirePasswordChange = true;
usingTemporaryPassword = true;
return next(null, true);
}
checkAccountPassword();
});
}
checkAccountPassword();
};
// try master password
checkMasterPassword((err, success) => {
if (err) {
err.code = err.code || 'BcryptError';
return callback(err);
}
if (success) {
meta.result = 'success';
meta.source = !usingTemporaryPassword ? 'master' : 'temporary';
if (enabled2fa.length) {
meta.require2fa = enabled2fa.length ? enabled2fa.join(',') : false;
}
if (requiredScope !== 'master' && (enabled2fa.length || usingTemporaryPassword)) {
// master password can not be used for other stuff if 2FA is enabled
// temporary password is only valid for master
meta.result = 'fail';
return this.logAuthEvent(userData._id, meta, () => authFail(new Error('Authentication failed. Invalid scope')));
}
return this.logAuthEvent(userData._id, meta, () => {
let authResponse = {
user: userData._id,
username: userData.username,
scope: meta.scope,
// if 2FA is enabled then require token validation
require2fa: enabled2fa.length && !usingTemporaryPassword ? enabled2fa : false,
requirePasswordChange // true, if password was reset and using temporary password
};
if (enabled2fa.length && !usingTemporaryPassword) {
authResponse.enabled2fa = enabled2fa;
return getU2fAuthRequest((err, u2fAuthRequest) => {
if (err) {
log.error('DB', 'U2FREFAIL u2fAuthRequest id=%s error=%s', userData._id, err.message);
}
if (u2fAuthRequest) {
authResponse.u2fAuthRequest = u2fAuthRequest;
}
authSuccess(null, authResponse);
});
}
authSuccess(null, authResponse);
});
}
if (requiredScope === 'master') {
// only master password can be used for management tasks
meta.result = 'fail';
meta.source = 'master';
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
}
// try application specific passwords
password = password.replace(/\s+/g, '').toLowerCase();
if (!/^[a-z]{16}$/.test(password)) {
// does not look like an application specific password
meta.result = 'fail';
meta.source = 'master';
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
}
let selector = getStringSelector(password);
this.users
.collection('asps')
.find({
user: userData._id
})
.toArray((err, asps) => {
this.rateLimit(ustring, meta, 1, (err, res) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!res.success) {
return rateLimitResponse(res, callback);
}
callback(null, false);
});
});
}
if (!asps || !asps.length) {
// user does not have app specific passwords set
meta.result = 'fail';
meta.source = 'master';
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
this.rateLimitUser(userData._id, 0, (err, res) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!res.success) {
return rateLimitResponse(res, callback);
}
if (userData.disabled) {
// disabled users can not log in
meta.result = 'disabled';
return this.logAuthEvent(userData._id, meta, () => callback(null, false));
}
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
let getU2fAuthRequest = done => {
if (!enabled2fa.includes('u2f') || !userData.u2f || !userData.u2f.keyHandle) {
return done(null, false);
}
this.generateU2fAuthRequest(userData._id, userData.u2f.keyHandle, done);
};
let authSuccess = (...args) => {
// clear rate limit counter on success
this.rateLimitReleaseUser(userData._id, () => false);
callback(...args);
};
let authFail = (...args) => {
// increment rate limit counter on failure
this.rateLimit(userData._id, meta, 1, () => {
callback(...args);
});
};
let requirePasswordChange = false;
let usingTemporaryPassword = false;
let checkMasterPassword = next => {
let checkAccountPassword = () => {
bcrypt.compare(password, userData.password || '', next);
};
if (userData.tempPassword && userData.tempPassword.created > new Date(Date.now() - 24 * 3600 * 1000)) {
// try temporary password
return bcrypt.compare(password, userData.tempPassword.password || '', (err, success) => {
if (err) {
return next(err);
}
if (success) {
if (userData.validAfter > new Date()) {
let err = new Error('Temporary password is not yet activated');
err.code = 'TempPasswordNotYetValid';
return next(err);
}
requirePasswordChange = true;
usingTemporaryPassword = true;
return next(null, true);
}
checkAccountPassword();
});
}
checkAccountPassword();
};
// try master password
checkMasterPassword((err, success) => {
if (err) {
err.code = err.code || 'BcryptError';
return callback(err);
}
if (success) {
meta.result = 'success';
meta.source = !usingTemporaryPassword ? 'master' : 'temporary';
if (enabled2fa.length) {
meta.require2fa = enabled2fa.length ? enabled2fa.join(',') : false;
}
let pos = 0;
let checkNext = () => {
if (pos >= asps.length) {
if (requiredScope !== 'master' && (enabled2fa.length || usingTemporaryPassword)) {
// master password can not be used for other stuff if 2FA is enabled
// temporary password is only valid for master
meta.result = 'fail';
let err = new Error('Authentication failed. Invalid scope');
err.response = 'NO'; // imap response code
return this.logAuthEvent(userData._id, meta, () => authFail(err));
}
return this.logAuthEvent(userData._id, meta, () => {
let authResponse = {
user: userData._id,
username: userData.username,
scope: meta.scope,
// if 2FA is enabled then require token validation
require2fa: enabled2fa.length && !usingTemporaryPassword ? enabled2fa : false,
requirePasswordChange // true, if password was reset and using temporary password
};
if (enabled2fa.length && !usingTemporaryPassword) {
authResponse.enabled2fa = enabled2fa;
return getU2fAuthRequest((err, u2fAuthRequest) => {
if (err) {
log.error('DB', 'U2FREFAIL u2fAuthRequest id=%s error=%s', userData._id, err.message);
}
if (u2fAuthRequest) {
authResponse.u2fAuthRequest = u2fAuthRequest;
}
authSuccess(null, authResponse);
});
}
authSuccess(null, authResponse);
});
}
if (requiredScope === 'master') {
// only master password can be used for management tasks
meta.result = 'fail';
meta.source = 'master';
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
}
// try application specific passwords
password = password.replace(/\s+/g, '').toLowerCase();
if (!/^[a-z]{16}$/.test(password)) {
// does not look like an application specific password
meta.result = 'fail';
meta.source = 'master';
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
}
let selector = getStringSelector(password);
this.users
.collection('asps')
.find({
user: userData._id
})
.toArray((err, asps) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
}
if (!asps || !asps.length) {
// user does not have app specific passwords set
meta.result = 'fail';
meta.source = 'master';
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
}
let asp = asps[pos++];
if (asp.selector && asp.selector !== selector) {
// no need to check, definitely a wrong one
return setImmediate(checkNext);
}
bcrypt.compare(password, asp.password || '', (err, success) => {
if (err) {
err.code = 'BcryptError';
return callback(err);
let pos = 0;
let checkNext = () => {
if (pos >= asps.length) {
meta.result = 'fail';
meta.source = 'master';
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
}
if (!success) {
let asp = asps[pos++];
if (asp.selector && asp.selector !== selector) {
// no need to check, definitely a wrong one
return setImmediate(checkNext);
}
if (!asp.scopes.includes('*') && !asp.scopes.includes(requiredScope)) {
meta.result = 'fail';
bcrypt.compare(password, asp.password || '', (err, success) => {
if (err) {
err.code = 'BcryptError';
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;
meta.aname = asp.description;
return this.logAuthEvent(userData._id, meta, () =>
authFail(new Error('Authentication failed. Invalid scope'))
);
}
meta.result = 'success';
meta.source = 'asp';
meta.asp = asp._id;
meta.aname = asp.description;
return this.logAuthEvent(userData._id, meta, () => authFail(new Error('Authentication failed. Invalid scope')));
}
meta.result = 'success';
meta.source = 'asp';
meta.asp = asp._id;
meta.aname = asp.description;
this.logAuthEvent(userData._id, meta, (err, r) => {
if (err) {
// don't really care
}
this.rateLimitReleaseUser(userData._id, () => false);
this.users.collection('asps').findOneAndUpdate(
{
_id: asp._id
},
{
$set: {
used: new Date(),
authEvent: r.insertedId
}
},
() => {
authSuccess(null, {
user: userData._id,
username: userData.username,
scope: requiredScope,
asp: asp._id.toString(),
require2fa: false // application scope never requires 2FA
});
this.logAuthEvent(userData._id, meta, (err, r) => {
if (err) {
// don't really care
}
);
this.rateLimitReleaseUser(userData._id, () => false);
this.users.collection('asps').findOneAndUpdate(
{
_id: asp._id
},
{
$set: {
used: new Date(),
authEvent: r.insertedId
}
},
() => {
authSuccess(null, {
user: userData._id,
username: userData.username,
scope: requiredScope,
asp: asp._id.toString(),
require2fa: false // application scope never requires 2FA
});
}
);
});
});
});
};
};
checkNext();
});
checkNext();
});
});
});
});
}
);
}
);
});
});
});
}
/**
@ -1594,7 +1596,9 @@ class UserHandler {
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
if (!enabled2fa.includes('totp')) {
return callback(new Error('Could not update user, check if 2FA TOTP is not already disabled'));
let err = new Error('Could not update user, check if 2FA TOTP is not already disabled');
err.code = 'TotpDisabled';
return callback(err);
}
let update =
@ -1629,7 +1633,9 @@ class UserHandler {
}
if (!result || !result.value) {
return callback(new Error('Could not update user, check if 2FA is not already disabled'));
let err = new Error('This username does not exist');
err.code = 'UserNotFound';
return callback(err);
}
return this.logAuthEvent(
@ -1649,7 +1655,7 @@ class UserHandler {
checkTotp(user, data, callback) {
let userRlKey = 'totp:' + user;
this.rateLimit(userRlKey, data, 0, (err, res) => { // NOT Sure why this used "consts.USER_AUTH_WINDOW * 3"
this.rateLimit(userRlKey, data, 0, (err, res) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
@ -1746,6 +1752,161 @@ class UserHandler {
});
}
enableCustom2fa(user, data, callback) {
this.users.collection('users').findOne(
{
_id: user
},
{
fields: {
enabled2fa: true,
username: true
}
},
(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,
{},
(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
},
{
fields: {
enabled2fa: true,
username: true
}
},
(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,
{},
(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)
);
}
);
}
);
}
setupU2f(user, data, callback) {
let registrationRequest;
try {

View file

@ -23,25 +23,25 @@
"grunt-shell-spawn": "^0.3.10",
"grunt-wait": "^0.1.0",
"icedfrisby": "^1.5.0",
"mailparser": "^2.2.0",
"mailparser": "2.2.0",
"markdown-toc": "^1.2.0",
"mocha": "^5.0.1",
"request": "^2.83.0"
"mocha": "^5.1.0",
"request": "^2.85.0"
},
"dependencies": {
"addressparser": "1.0.1",
"bcryptjs": "2.4.3",
"bugsnag": "2.1.3",
"bugsnag": "2.3.1",
"generate-password": "1.4.0",
"he": "1.1.1",
"html-to-text": "3.3.0",
"html-to-text": "4.0.0",
"humanname": "0.2.2",
"humanparser": "1.5.0",
"iconv-lite": "0.4.19",
"iconv-lite": "0.4.21",
"ioredfour": "1.0.2-ioredis",
"ioredis": "3.2.2",
"joi": "13.1.2",
"js-yaml": "3.10.0",
"js-yaml": "3.11.0",
"key-fingerprint": "1.1.0",
"libbase64": "1.0.2",
"libmime": "3.1.0",
@ -50,19 +50,19 @@
"mailsplit": "4.1.2",
"mobileconfig": "2.1.0",
"mongo-cursor-pagination-node6": "5.0.0",
"mongodb": "3.0.2",
"mongodb": "3.0.6",
"mongodb-extended-json": "1.10.0",
"node-forge": "0.7.1",
"nodemailer": "4.4.2",
"node-forge": "0.7.5",
"nodemailer": "4.6.4",
"npmlog": "4.1.2",
"openpgp": "2.6.2",
"openpgp": "3.0.4",
"qrcode": "1.2.0",
"restify": "6.3.4",
"restify": "7.1.1",
"restify-logger": "2.0.1",
"seq-index": "1.1.0",
"smtp-server": "3.4.1",
"smtp-server": "3.4.2",
"speakeasy": "2.0.0",
"tlds": "1.199.0",
"tlds": "1.203.1",
"u2f": "0.1.3",
"utf7": "1.0.2",
"uuid": "3.2.1",