This commit is contained in:
Andris Reinman 2018-08-23 11:32:21 +03:00
parent 225aa9ec8b
commit ac205c2622
3 changed files with 67 additions and 43 deletions

View file

@ -31,6 +31,8 @@ module.exports = (db, server, userHandler) => {
* @apiSuccess {Boolean} requirePasswordChange Indicates if account hassword has been reset and should be replaced
*
* @apiError error Description of the error
* @apiError [code] Error code
* @apiError [id] User ID if the user exists
*
* @apiExample {curl} Example usage:
* curl -i -XPOST http://localhost:8080/authenticate \
@ -57,7 +59,9 @@ module.exports = (db, server, userHandler) => {
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "Authentication failed. Invalid scope"
* "error": "Authentication failed. Invalid scope",
* "code": "InvalidAuthScope",
* "id": "5b22283d45e8d47572eb0381"
* }
*/
server.post('/authenticate', (req, res, next) => {
@ -115,18 +119,30 @@ module.exports = (db, server, userHandler) => {
meta.appId = result.value.appId;
}
userHandler.authenticate(result.value.username, result.value.password, result.value.scope, meta, (err, authData) => {
userHandler.authenticate(result.value.username, result.value.password, result.value.scope, meta, (err, authData, user) => {
if (err) {
res.json({
let response = {
error: err.message
});
};
if (err.code) {
response.code = err.code;
}
if (user) {
response.id = user.toString();
}
res.json(response);
return next();
}
if (!authData) {
res.json({
error: 'Authentication failed'
});
let response = {
error: 'Authentication failed',
code: 'AuthFailed'
};
if (user) {
response.id = user.toString();
}
res.json(response);
return next();
}

View file

@ -320,29 +320,29 @@ class UserHandler {
if (!password) {
// do not allow signing in without a password
return callback(null, false);
return callback(null, false, false);
}
// first check if client IP is not used too much
this.rateLimitIP(meta, 0, (err, res) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
return callback(err, false, false);
}
if (!res.success) {
// too many failed attempts from this IP
return rateLimitResponse(res, callback);
return rateLimitResponse(res, err => callback(err, false, false));
}
this.checkAddress(username, (err, query) => {
if (err) {
return callback(err);
return callback(err, false, false);
}
if (!query) {
// nothing to do here
return callback(null, false);
return callback(null, false, false);
}
this.users.collection('users').findOne(
@ -361,7 +361,7 @@ class UserHandler {
(err, userData) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
return callback(err, false, false);
}
if (!userData) {
@ -370,13 +370,13 @@ class UserHandler {
return this.rateLimit(ustring, meta, 1, (err, res) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
return callback(err, false, false);
}
if (!res.success) {
// does not really matter but respond with a rate limit error, not auth fail error
return rateLimitResponse(res, callback);
return rateLimitResponse(res, err => callback(err, false, false));
}
callback(null, false);
callback(null, false, false);
});
}
@ -384,18 +384,18 @@ class UserHandler {
this.rateLimitUser(userData._id, 0, (err, res) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
return callback(err, false, userData._id);
}
if (!res.success) {
// too many failed attempts for this user
return rateLimitResponse(res, callback);
return rateLimitResponse(res, err => callback(err, false, userData._id));
}
if (userData.disabled) {
// disabled users can not log in
meta.result = 'disabled';
// TODO: should we send some specific error message?
return this.logAuthEvent(userData._id, meta, () => callback(null, false));
return this.logAuthEvent(userData._id, meta, () => callback(null, false, userData._id));
}
let enabled2fa = Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []);
@ -432,6 +432,7 @@ class UserHandler {
// try temporary password
return bcrypt.compare(password, userData.tempPassword.password || '', (err, success) => {
if (err) {
err.code = 'BcryptError';
return next(err);
}
if (success) {
@ -455,8 +456,7 @@ class UserHandler {
// try master password
checkMasterPassword((err, success) => {
if (err) {
err.code = err.code || 'BcryptError';
return callback(err);
return callback(err, false, userData._id);
}
if (success) {
@ -472,8 +472,9 @@ class UserHandler {
// temporary password is only valid for master
meta.result = 'fail';
let err = new Error('Authentication failed. Invalid scope');
err.code = 'InvalidAuthScope';
err.response = 'NO'; // imap response code
return this.logAuthEvent(userData._id, meta, () => authFail(err));
return this.logAuthEvent(userData._id, meta, () => authFail(err, false, userData._id));
}
return this.logAuthEvent(userData._id, meta, () => {
@ -496,11 +497,11 @@ class UserHandler {
if (u2fAuthRequest) {
authResponse.u2fAuthRequest = u2fAuthRequest;
}
authSuccess(null, authResponse);
authSuccess(null, authResponse, userData._id);
});
}
authSuccess(null, authResponse);
authSuccess(null, authResponse, userData._id);
});
}
@ -508,7 +509,7 @@ class UserHandler {
// only master password can be used for management tasks
meta.result = 'fail';
meta.source = 'master';
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
return this.logAuthEvent(userData._id, meta, () => authFail(null, false, userData._id));
}
// try application specific passwords
@ -518,7 +519,7 @@ class UserHandler {
// does not look like an application specific password
meta.result = 'fail';
meta.source = 'master';
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
return this.logAuthEvent(userData._id, meta, () => authFail(null, false, userData._id));
}
let selector = getStringSelector(password);
@ -531,14 +532,14 @@ class UserHandler {
.toArray((err, asps) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
return callback(err, false, userData._id);
}
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));
return this.logAuthEvent(userData._id, meta, () => authFail(null, false, userData._id));
}
let pos = 0;
@ -546,7 +547,7 @@ class UserHandler {
if (pos >= asps.length) {
meta.result = 'fail';
meta.source = 'master';
return this.logAuthEvent(userData._id, meta, () => authFail(null, false));
return this.logAuthEvent(userData._id, meta, () => authFail(null, false, userData._id));
}
let asp = asps[pos++];
@ -558,7 +559,7 @@ class UserHandler {
bcrypt.compare(password, asp.password || '', (err, success) => {
if (err) {
err.code = 'BcryptError';
return callback(err);
return callback(err, false, userData._id);
}
if (!success) {
@ -572,9 +573,12 @@ class UserHandler {
if (!asp.scopes.includes('*') && !asp.scopes.includes(requiredScope)) {
meta.result = 'fail';
return this.logAuthEvent(userData._id, meta, () =>
authFail(new Error('Authentication failed. Invalid scope'))
);
return this.logAuthEvent(userData._id, meta, () => {
let err = new Error('Authentication failed. Invalid scope');
err.code = 'InvalidAuthScope';
err.response = 'NO'; // imap response code
authFail(err, false, userData._id);
});
}
meta.result = 'success';
@ -596,13 +600,17 @@ class UserHandler {
}
},
() => {
authSuccess(null, {
user: userData._id,
username: userData.username,
scope: requiredScope,
asp: asp._id.toString(),
require2fa: false // application scope never requires 2FA
});
authSuccess(
null,
{
user: userData._id,
username: userData.username,
scope: requiredScope,
asp: asp._id.toString(),
require2fa: false // application scope never requires 2FA
},
userData._id
);
}
);
});

View file

@ -1,6 +1,6 @@
{
"name": "wildduck",
"version": "1.4.1",
"version": "1.4.2",
"description": "IMAP/POP3 server built with Node.js and MongoDB",
"main": "server.js",
"scripts": {
@ -27,7 +27,7 @@
"grunt-shell-spawn": "0.3.10",
"grunt-wait": "0.1.0",
"icedfrisby": "1.5.0",
"mailparser": "2.3.2",
"mailparser": "2.3.3",
"markdown-toc": "1.2.0",
"mocha": "5.2.0",
"request": "2.88.0"
@ -40,7 +40,7 @@
"he": "1.1.1",
"html-to-text": "4.0.0",
"humanname": "0.2.2",
"iconv-lite": "0.4.23",
"iconv-lite": "0.4.24",
"ioredfour": "1.0.2-ioredis-02",
"ioredis": "4.0.0",
"isemail": "3.1.3",