updated authentication

This commit is contained in:
Andris Reinman 2017-04-20 20:10:03 +03:00
parent eb47b871a8
commit a66c6dcd12
6 changed files with 471 additions and 11 deletions

View file

@ -66,7 +66,7 @@ module.exports = {
spamHeader: 'X-Rspamd-Spam',
// default quota storage in MB (can be overriden per user)
maxStorage: 1000,
maxStorage: 1024,
// default smtp recipients for 24h (can be overriden per user)
maxRecipients: 2000

View file

@ -71,7 +71,8 @@ function authenticate(connection, token, callback) {
connection._server.logger.info('[%s] Authentication failed for %s using %s', connection.id, username, 'PLAIN');
return callback(null, {
response: 'NO',
message: 'Authentication failure'
code: 'AUTHENTICATIONFAILED',
message: 'Invalid credentials'
});
}

View file

@ -52,7 +52,8 @@ module.exports = {
this._server.logger.info('[%s] Authentication failed for %s using %s', this.id, username, 'LOGIN');
return callback(null, {
response: 'NO',
message: 'Authentication failure'
code: 'AUTHENTICATIONFAILED',
message: 'Invalid credentials'
});
}

17
imap.js
View file

@ -7,13 +7,13 @@ const IMAPServerModule = require('./imap-core');
const IMAPServer = IMAPServerModule.IMAPServer;
const ImapNotifier = require('./lib/imap-notifier');
const imapHandler = IMAPServerModule.imapHandler;
const bcrypt = require('bcryptjs');
const ObjectID = require('mongodb').ObjectID;
const Indexer = require('./imap-core/lib/indexer/indexer');
const imapTools = require('./imap-core/lib/imap-tools');
const fs = require('fs');
const setupIndexes = require('./indexes.json');
const MessageHandler = require('./lib/message-handler');
const UserHandler = require('./lib/user-handler');
const db = require('./lib/db');
const packageData = require('./package.json');
@ -61,28 +61,28 @@ if (config.imap.cert) {
const server = new IMAPServer(serverOptions);
let messageHandler;
let userHandler;
server.onAuth = function (login, session, callback) {
let username = (login.username || '').toString().trim();
db.database.collection('users').findOne({
username
}, (err, user) => {
userHandler.authenticate(username, login.password, (err, result) => {
if (err) {
return callback(err);
}
if (!user) {
if (!result) {
return callback();
}
if (!bcrypt.compareSync(login.password, user.password)) {
if (result.scope === 'master' && result.enabled2fa) {
// master password not allowed if 2fa is enabled!
return callback();
}
callback(null, {
user: {
id: user._id,
username
id: result.user,
username: result.username
}
});
});
@ -1606,6 +1606,7 @@ module.exports = done => {
let start = () => {
messageHandler = new MessageHandler(db.database);
userHandler = new UserHandler(db.database);
server.indexer = new Indexer({
database: db.database

454
lib/user-handler.js Normal file
View file

@ -0,0 +1,454 @@
'use strict';
const config = require('config');
const log = require('npmlog');
const bcrypt = require('bcryptjs');
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
const tools = require('./tools');
// const generatePassword = require('generate-password');
const mailboxTranslations = {
en: {
'\\Sent': 'Sent Mail',
'\\Trash': 'Trash',
'\\Junk': 'Junk',
'\\Drafts': 'Drafts',
'\\Archive': 'Archive'
},
et: {
'\\Sent': 'Saadetud kirjad',
'\\Trash': 'Prügikast',
'\\Junk': 'Rämpspost',
'\\Drafts': 'Mustandid',
'\\Archive': 'Arhiiv'
}
};
class UserHandler {
constructor(database) {
this.database = database;
}
/**
* Authenticate user
*
* @param {String} username Either username or email address
*/
authenticate(username, password, callback) {
let checkAddress = next => {
if (username.indexOf('@') < 0) {
// assume regular username
return next(null, {
username
});
}
// try to find existing email address
let address = tools.normalizeAddress(username);
this.database.collection('addresses').findOne({
address
}, {
fields: {
user: true
}
}, (err, addressData) => {
if (err) {
return callback(err);
}
if (!addressData) {
return callback(null, false);
}
return next(null, {
_id: addressData.user
});
});
};
checkAddress(query => {
this.database.collection('users').findOne(query, {
fields: {
username: true,
password: true,
enabled2fa: true,
asp: true
}
}, (err, userData) => {
if (err) {
return callback(err);
}
if (!userData) {
return callback(null, false);
}
// try master password
if (bcrypt.compareSync(password, userData.password || '')) {
return callback(null, {
user: userData._id,
username: userData.username,
scope: 'master',
enabled2fa: userData.enabled2fa
});
}
/*
var password = generatePassword.generate({
length: 16,
uppercase: false,
numbers: false,
symbols: false
});
*/
// try application specific passwords
password = password.replace(/\s+/g, '').toLowerCase();
if (!userData.asp || !userData.asp.length || !/^[a-z]{16}$/.test(password)) {
// does not look like an application specific password
return callback(null, false);
}
for (let i = 0; i < userData.asp.length; i++) {
let asp = userData.asp[i];
if (bcrypt.compareSync(password, asp.password || '')) {
return callback(null, {
user: userData._id,
username: userData.username,
scope: 'application',
use2fa: false
});
}
}
return callback(null, false);
});
});
}
create(data, callback) {
this.database.collection('users').findOne({
username: data.username
}, {
fields: {
username: true
}
}, (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'));
}
if (userData) {
let err = new Error('This username already exists');
err.fields = {
username: err.message
};
return callback(err);
}
// Insert
let hash = bcrypt.hashSync(data.password, 11);
this.database.collection('users').insertOne({
username: data.username,
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,
quota: config.maxStorage * (1024 * 1024),
recipients: config.maxRecipients,
filters: [],
created: new Date(),
// until setup value is not true, this account is not usable
setup: false
}, (err, result) => {
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 user = result.insertedId;
let mailboxes = this.getMailboxes(data.language).map(mailbox => {
mailbox.user = user;
return mailbox;
});
this.database.collection('mailboxes').insertMany(mailboxes, {
w: 1,
ordered: false
}, err => {
if (err) {
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
return callback(new Error('Database Error, failed to create user'));
}
// insert alias address to email address registry
this.database.collection('addresses').insertOne({
user,
address: data.username + '@' + config.emailDomain,
created: new Date()
}, err => {
if (err) {
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
return callback(new Error('Database Error, failed to create user'));
}
// register this address as the default address for that user
return this.database.collection('users').findOneAndUpdate({
_id: user
}, {
$set: {
password: hash,
address: data.username + '@' + config.emailDomain,
setup: true
}
}, {}, err => {
if (err) {
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
return callback(new Error('Database Error, failed to create user'));
}
return callback(null, user);
});
});
});
});
});
}
setup2fa(username, callback) {
let secret = speakeasy.generateSecret({
length: 20
});
return this.database.collection('users').findOneAndUpdate({
username,
enabled2fa: false
}, {
$set: {
seed: secret.base32
}
}, {}, (err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL username=%s error=%s', username, err.message);
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'));
}
QRCode.toDataURL(secret.otpauth_url, (err, data_url) => {
if (err) {
log.error('DB', 'QRFAIL username=%s error=%s', username, err.message);
return callback(new Error('Failed to generate QR code'));
}
return callback(null, data_url);
});
});
}
enable2fa(username, userToken, callback) {
this.check2fa(username, userToken, (err, verified) => {
if (err) {
return callback(err);
}
if (!verified) {
return callback(null, false);
}
// token was valid, update user settings
return this.database.collection('users').findOneAndUpdate({
username,
enabled2fa: false
}, {
$set: {
enabled2fa: true
}
}, {}, (err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL username=%s error=%s', username, err.message);
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'));
}
return callback(null, true);
});
});
}
disable2fa(username, userToken, callback) {
this.check2fa(username, userToken, (err, verified) => {
if (err) {
return callback(err);
}
if (!verified) {
return callback(null, false);
}
// token was valid, update user settings
return this.database.collection('users').findOneAndUpdate({
username,
enabled2fa: true
}, {
$set: {
enabled2fa: false,
seed: ''
}
}, {}, (err, result) => {
if (err) {
log.error('DB', 'UPDATEFAIL username=%s error=%s', username, err.message);
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'));
}
return callback(null, true);
});
});
}
check2fa(username, userToken, callback) {
this.database.collection('users').findOne({
username
}, {
fields: {
username: true,
seed: true
}
}, (err, userData) => {
if (err) {
log.error('DB', 'LOADFAIL username=%s error=%s', username, err.message);
return callback(new Error('Database Error, failed to update user'));
}
if (!userData) {
let err = new Error('This username does not exist');
err.fields = {
username: err.message
};
return callback(err);
}
if (!userData.seed) {
// 2fa not set up
return callback(null, true);
}
let verified = speakeasy.totp.verify({
secret: userData.seed,
encoding: 'base32',
token: userToken
});
return callback(null, verified);
});
}
update(data, callback) {
this.database.collection('users').findOne({
username: data.username
}, {
fields: {
username: true,
password: true
}
}, (err, userData) => {
if (err) {
log.error('DB', 'UPDATEFAIL username=%s error=%s', data.username, err.message);
return callback(new Error('Database Error, failed to update user'));
}
if (!userData) {
let err = new Error('This username does not exist');
err.fields = {
username: err.message
};
return callback(err);
}
if (data.oldpassword && !bcrypt.compareSync(data.oldpassword, userData.password || '')) {
let err = new Error('Password does not match');
err.fields = {
oldpassword: err.message
};
return callback(err);
}
let update = {
name: data.name
};
if (data.password) {
update.password = bcrypt.hashSync(data.password, 11);
}
return this.database.collection('users').findOneAndUpdate({
_id: userData._id
}, {
$set: update
}, {}, err => {
if (err) {
log.error('DB', 'UPDATEFAIL username=%s error=%s', data.username, err.message);
return callback(new Error('Database Error, failed to update user'));
}
return callback(null, userData._id);
});
});
}
getMailboxes(language) {
let translation = mailboxTranslations.hasOwnProperty(language) ? mailboxTranslations[language] : mailboxTranslations.en;
let defaultMailboxes = [{
path: 'INBOX'
}, {
specialUse: '\\Sent'
}, {
specialUse: '\\Trash'
}, {
specialUse: '\\Drafts'
}, {
specialUse: '\\Junk'
}, {
specialUse: '\\Archive'
}];
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: []
}));
}
}
module.exports = UserHandler;

View file

@ -21,6 +21,7 @@
"dependencies": {
"bcryptjs": "^2.4.3",
"config": "^1.25.1",
"generate-password": "^1.3.0",
"grid-fs": "^1.0.1",
"html-to-text": "^3.2.0",
"iconv-lite": "^0.4.15",
@ -32,10 +33,12 @@
"mongodb": "^2.2.25",
"nodemailer": "^4.0.1",
"npmlog": "^4.0.2",
"qrcode": "^0.8.1",
"redfour": "^1.0.0",
"redis": "^2.7.1",
"restify": "^4.3.0",
"smtp-server": "^3.0.1",
"speakeasy": "^2.0.0",
"toml": "^2.3.2",
"utf7": "^1.0.2",
"uuid": "^3.0.1"