bridge-node-server/node_server/utils/credentials.js

310 lines
10 KiB
JavaScript
Raw Normal View History

2018-06-24 20:15:03 +00:00
/**
* 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]);
});
}