310 lines
10 KiB
JavaScript
310 lines
10 KiB
JavaScript
/**
|
|
* Support utilities for dealing with validating credentials (passwords and
|
|
* pin numbers), incrementing failed attempt counts, etc..
|
|
*/
|
|
'use strict';
|
|
|
|
var Q = require('q');
|
|
var crypto = require('crypto');
|
|
var mongodb = require('mongodb');
|
|
var templates = require(global.pathPrefix + '../utils/templates.js');
|
|
var debug = require('debug')('utils:credentials');
|
|
var config = require(global.configFile);
|
|
var mainDB = require(global.pathPrefix + 'mainDB.js');
|
|
var mailer = require(global.pathPrefix + 'mailer.js');
|
|
var utils = require(global.pathPrefix + 'utils.js');
|
|
var hasherUtils = require(global.pathPrefix + '../utils/hashing.js');
|
|
|
|
const ERRORS = {
|
|
NOT_FOUND: 'Not found',
|
|
BARRED: 'Barred by Comcarde',
|
|
TOO_MANY_ATTEMPTS: 'Too many attempts',
|
|
CANT_UPDATE_ATTEMPTS_SUCCESS: 'Cant clear attempts count on success',
|
|
CANT_UPDATE_ATTEMPTS_FAIL: 'Cant update attempts count on fail',
|
|
CANT_SEND_WARNING_EMAIL: 'Cant send too many attempts warning email',
|
|
|
|
DEVICE_NOT_VERIFIED: 'Device not verified - SMS not confirmed.',
|
|
DEVICE_NOT_AUTHORISED: 'Device not authorised - PIN not set.',
|
|
DEVICE_SUSPENDED: 'Device suspended by the user.'
|
|
};
|
|
|
|
module.exports = {
|
|
validatePassword: validatePassword,
|
|
validateRawPassword: validateRawPassword,
|
|
|
|
ERRORS: ERRORS
|
|
};
|
|
|
|
/**
|
|
* Validates the email and password aganst the database values. It also:
|
|
* - Ensures they are not barred
|
|
* - Ensures they have not already exceeded the attempts limit
|
|
* - Increments the attempts limit on fail
|
|
* - Sends an email on reaching the attempts limit
|
|
* - Upgrades the password if neccessary
|
|
*
|
|
* @param {String} email - the email address
|
|
* @param {String} password - the password
|
|
*
|
|
* @returns {promise} - a promise that resolves on successful validation
|
|
*/
|
|
function validatePassword(email, password) {
|
|
//
|
|
// Setup the parameters
|
|
//
|
|
var query = {
|
|
ClientName: email
|
|
};
|
|
|
|
var collection = mainDB.collectionClient;
|
|
|
|
//
|
|
// Object validity function
|
|
//
|
|
var checkStatusFunc = function isValidClient(client) {
|
|
if (utils.bitsAllSet(client.ClientStatus, utils.ClientBarredMask)) {
|
|
return ERRORS.BARRED;
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
//
|
|
// Password information
|
|
//
|
|
var passwordField = 'Password';
|
|
var saltField = 'ClientSalt';
|
|
|
|
var maxAttempts = utils.passwordLockout;
|
|
|
|
//
|
|
// Warning email options
|
|
//
|
|
var warningEmailOptions = {
|
|
template: 'account-locked',
|
|
param: 'ClientName',
|
|
to: 'ClientName',
|
|
subject: 'Bridge Account Locked'
|
|
};
|
|
|
|
//
|
|
// Run the validation
|
|
//
|
|
debug('- validating');
|
|
return validate(
|
|
query,
|
|
collection,
|
|
checkStatusFunc,
|
|
password,
|
|
passwordField,
|
|
saltField,
|
|
maxAttempts,
|
|
warningEmailOptions
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This function validates raw passwords - i.e. passwords that have not already
|
|
* had a single pass of sha-256 run on then. This is most useful for the
|
|
* web API because the devices run the SHA-256 interally before sending to
|
|
* the server.
|
|
* This runs the single pass of sha-256 then calls the main validatePassword,
|
|
* so all comments on that function apply here as well.
|
|
*
|
|
* @param {string} email - the users email address
|
|
* @param {string} password - the raw password
|
|
*
|
|
* @returns {promise} - a promise that resolves on successful validation.
|
|
*/
|
|
function validateRawPassword(email, password) {
|
|
var deferred = Q.defer();
|
|
var promise = deferred.promise;
|
|
|
|
var hasher = crypto.createHash('sha256');
|
|
hasher.setEncoding('hex');
|
|
hasher.end(password, 'utf8');
|
|
|
|
hasher.on('readable', function() {
|
|
var passwordHash = hasher.read();
|
|
deferred.resolve(passwordHash);
|
|
});
|
|
|
|
return promise.then(function(passwordHash) {
|
|
return validatePassword(email, passwordHash);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validates the given secret and saly against the database values. It also:
|
|
* - Checks the object passes the given checkStatusFunc (e.g. barred, etc.)
|
|
* - Ensures they have not already exceeded the attempts limit
|
|
* - Increments the attempts count on fail
|
|
* - Upgrades the secret in the database if neccessary
|
|
*
|
|
* @param {Object} query - the query used to find the object
|
|
* @param {Object} collection - the collection containing the objects
|
|
* @param {Function} checkStatus - function to check the status of the object (barred etc.)
|
|
* @param {String} secret - the secret to validate
|
|
* @param {String} secretField - field containing the secret (password, pin, etc.)
|
|
* @param {String} saltField - the field containing the salt
|
|
* @param {Int} maxAttempts - the maximum attempts allowed
|
|
* @param {Object} emailOptions - options for the sending of the warning email
|
|
*/
|
|
function validate(query, collection, checkStatus, secret, secretField, saltField, maxAttempts, emailOptions) {
|
|
//
|
|
// Step 1. Find the database object
|
|
//
|
|
var object = null;
|
|
var getObjectP = Q.nfcall(mainDB.findOneObject, collection, query, undefined, false)
|
|
.then(function(result) {
|
|
// Check we found an object
|
|
if (!result) {
|
|
return Q.reject(ERRORS.NOT_FOUND);
|
|
} else {
|
|
object = result;
|
|
return Q.resolve(result);
|
|
}
|
|
});
|
|
|
|
//
|
|
// Step 2. Validate the object.
|
|
// Check the pre- requisites: passes the checkStatus,
|
|
// and not too many attempts.
|
|
// Then check the password matches
|
|
//
|
|
var validObjectP = getObjectP.then(function(object) {
|
|
var checkResult = checkStatus(object);
|
|
if (checkResult) {
|
|
return Q.reject(checkResult);
|
|
} else if (object.LoginAttempts >= maxAttempts) {
|
|
return Q.reject(ERRORS.TOO_MANY_ATTEMPTS);
|
|
} else {
|
|
return hasherUtils.verifyHash(
|
|
secret,
|
|
object[secretField],
|
|
object[saltField],
|
|
2 // TODO: make this a config item
|
|
);
|
|
}
|
|
});
|
|
|
|
//
|
|
// Step 3. Check the results of the verifyHash
|
|
//
|
|
var validResultsP = validObjectP
|
|
.then(function(validity) {
|
|
//
|
|
// Succeeded so reset the attempts flag
|
|
//
|
|
var update = {
|
|
$set: {LoginAttempts: 0}
|
|
};
|
|
|
|
//
|
|
// If we were given an updated password hash, then also update that
|
|
//
|
|
if (validity !== null) {
|
|
update.$set[secretField] = validity.hash;
|
|
update.$set[saltField] = validity.salt;
|
|
update.$set.LastUpdate = new Date();
|
|
update.$inc = {LastVersion: 1};
|
|
}
|
|
|
|
return Q.nfcall(
|
|
mainDB.updateObject,
|
|
collection,
|
|
query,
|
|
update,
|
|
undefined,
|
|
false)
|
|
.catch(function(result) {
|
|
return Q.reject(ERRORS.CANT_UPDATE_SUCCESS);
|
|
});
|
|
|
|
})
|
|
.catch(function(error) {
|
|
debug('Failed validResults', error);
|
|
//
|
|
// Failed. If this failed because the password was wrong then
|
|
// we need to update the attempts count. Other failures are
|
|
// just returned as is.
|
|
//
|
|
if (error !== hasherUtils.ERRORS.NO_MATCH) {
|
|
return Q.reject(error);
|
|
}
|
|
|
|
//
|
|
// It is a password failure, so update the LoginAttempts
|
|
// We also request the updated doc be returned so we can check if
|
|
// we need to send the "too many login fails" warning email
|
|
//
|
|
var update = {
|
|
$inc: {LoginAttempts: 1},
|
|
$set: {LastUpdate: new Date()}
|
|
};
|
|
var options = {
|
|
projection: {LoginAttempts: 1}, // Only need this field
|
|
upsert: false, // Don't add if it doesn't exist
|
|
returnOriginal: false // Want the updated doc
|
|
};
|
|
|
|
return Q.ninvoke(
|
|
collection,
|
|
'findOneAndUpdate',
|
|
query,
|
|
update,
|
|
options
|
|
).then(function(result) {
|
|
if (!result.value) {
|
|
// Didn't find anything to update
|
|
return Q.reject(ERRORS.CANT_UPDATE_ATTEMPTS_FAIL);
|
|
} else if (result.value.LoginAttempts === maxAttempts) {
|
|
// Need to send a warning email
|
|
// Set up the parameters then render the template
|
|
var emailParams = {};
|
|
emailParams[emailOptions.param] = object[emailOptions.param];
|
|
|
|
var htmlEmail = templates.render(
|
|
emailOptions.template,
|
|
emailParams
|
|
);
|
|
|
|
var mode = config.isDevEnv ? 'Test' : 'Live';
|
|
var to = object[emailOptions.to];
|
|
//
|
|
// Then try to send it
|
|
//
|
|
debug('- sending email: ', mode, to);
|
|
return Q.nfcall(
|
|
mailer.sendEmail,
|
|
mode,
|
|
to,
|
|
emailOptions.subject,
|
|
htmlEmail,
|
|
'credentials.validate'
|
|
).then(function success() {
|
|
debug('- warning email sent ok');
|
|
// Sent ok, so report too many attempts
|
|
return Q.reject(ERRORS.TOO_MANY_ATTEMPTS);
|
|
}, function fail(err) {
|
|
debug('- warning email send failed', err);
|
|
// Failed, so send the error
|
|
return Q.reject(ERRORS.CANT_SEND_WARNING_EMAIL);
|
|
});
|
|
} else {
|
|
// Otherwise updated with the max attempts
|
|
return Q.reject(hasherUtils.ERRORS.NO_MATCH);
|
|
}
|
|
});
|
|
});
|
|
|
|
return Q.all([getObjectP, validResultsP])
|
|
.then(function(results) {
|
|
// Successfully validated.
|
|
// Q.all returns an array of results, but we just want the
|
|
// client object (availablefrom the first request)
|
|
//
|
|
return Q.resolve(results[0]);
|
|
});
|
|
}
|