mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-12-27 02:10:52 +08:00
updated authentication
This commit is contained in:
parent
eb47b871a8
commit
a66c6dcd12
6 changed files with 471 additions and 11 deletions
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
17
imap.js
|
@ -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
454
lib/user-handler.js
Normal 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;
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue