Martin Donnelly 57bd6c8e6a init
2018-06-24 21:15:03 +01:00

1088 lines
34 KiB
JavaScript

/**
* Controller to manage the account recovery functions
*/
'use strict';
const httpStatus = require('http-status-codes');
const debug = require('debug')('webconsole-api:controllers:recovery');
const _ = require('lodash');
const Q = require('q');
const moment = require('moment');
const utils = require(global.pathPrefix + 'utils.js');
const mailer = require(global.pathPrefix + 'mailer.js');
const mainDB = require(global.pathPrefix + 'mainDB.js');
const sms = require(global.pathPrefix + 'sms.js');
const references = require(global.pathPrefix + '../utils/references.js');
const responseUtils = require(global.pathPrefix + '../utils/responses.js');
const templates = require(global.pathPrefix + '../utils/templates.js');
const clientUtils = require(global.pathPrefix + '../utils/client/client.js');
const apiUtils = require(global.pathPrefix + '../swagger_api/api_utils.js');
/**
* Predefined errors from shared functions
*/
const NO_MATCH = 'BRIDGE: Invalid token';
const NO_RETRIES = 'BRIDGE: Too many retries';
const WRONG_STATE = 'BRIDGE: Wrong state';
const HASH_FAILED = 'BRIDGE: Failed to hash password';
const FAILED_UPDATE_DB = 'BRIDGE: Failed to store new password';
/**
* States in the recovery process state machine.
* We use these to ensure the correct request is being received at the correct
* time, so e.g. a client can't try to complete password reset with just an email
* token when they should also be confirming a device.
*/
const STATES = {
START: 0,
WAITING_FOR_EMAIL_TOKEN_PASSWORD: 1,
WAITING_FOR_EMAIL_TOKEN: 2,
WAITING_FOR_ANSWERS: 3,
WAITING_FOR_SMS_TOKEN_PASSWORD: 4
};
/**
* Types of questions
*/
const QTYPE = {
POSTCODE: 'postcode',
CARD: 'card',
TRANSACTION: 'transactions',
DEVICE: 'device',
DOB: 'dob'
};
/**
* Exports from this module
*/
module.exports = {
startRecovery,
completeRecoveryEmailPw,
confirmRecoveryEmail,
confirmAnswers,
completeRecoveryDevicePw
};
/**
* Sets the next expected state and any associated data for that state.
* This is stored in the session and used to verify that we are in the appropriate state.
* We also initialise the number of retries we are allowed for this next state.
*
* @param {Object} req - The request object (which holds the session info).
* @param {number} state - The next state. MUST be a member of STATES.
* @param {any} data - The data for the next state.
*/
function setNextState(req, state, data) {
debug(
'STATE TRANSITION: ',
_.get(req, 'session.data.nextState', 'n/a'), '=>', state
);
_.set(req, 'session.data.nextState', state);
_.set(req, 'session.data.stateData', data);
_.set(req, 'session.data.stateRetries', utils.recoveryRetries);
}
/**
* Gets the data for the expected state after verifying that this is the state
* we should be in, and that we still have retries left.
*
* @param {Object} req - The request object (which holds the session data).
* @param {number} expectedState - The state we expect to be in. MUST be from STATES.
* @returns {Promise<any>} - The data for this state.
*/
function getStateData(req, expectedState) {
debug('STATE START: ', expectedState, '[expecting: ', req.session.data.nextState, ']');
/**
* Check we are in the right state
*/
if (expectedState !== req.session.data.nextState) {
return Q.reject(WRONG_STATE);
}
/**
* Check we still have retries left (and reduce the count if we do)
*/
if (req.session.data.stateRetries <= 0) {
// Destroy session so it can no longer be used
req.session.destroy();
return Q.reject(NO_RETRIES);
} else {
req.session.data.stateRetries -= 1;
}
/**
* All good so return the data
*/
return Q.resolve(req.session.data.stateData);
}
/**
* Shared function to update the password in the database.
*
* @param {string} clientID - The ID of the client to update.
* @param {string} newPassword - The new password for that client.
*
* @returns {Promise} - Result of updating the password.
*/
function updatePassword(clientID, newPassword) {
/**
* Step 1. Hash the password
*/
const hashP = apiUtils.encodePassword(newPassword)
.catch((error) => {
debug('Failed to hash password', error);
return Q.reject(HASH_FAILED);
});
/**
* Step 2. Update the account
*/
const updateP = hashP.then((hashed) => {
const hashedPassword = hashed.hash;
const salt = hashed.salt;
const query = {
ClientID: clientID
};
const update = {
$set: {
Password: hashedPassword,
ClientSalt: salt,
'PasswordManagement.0.RecoveryLimits.Attempts': 0 // Complete recovery, so reset
},
$inc: {
LastVersion: 1
},
$currentDate: {
LastUpdate: true,
'PasswordManagement.0.RecoveryLimits.AllowAfter': true // Complete recovery, so reset
}
};
const options = {
upsert: false
};
return Q.nfcall(
mainDB.updateObject,
mainDB.collectionClient,
query,
update,
options,
false // Don't suppress errors - we expect this to succed
).then((res) => {
if (res.result.nModified === 1) {
return Q.resolve();
} else {
debug('DB ran, but didnt update 1 entry:', res.result.nModified);
return Q.reject(FAILED_UPDATE_DB);
}
}).catch(() => Q.reject(FAILED_UPDATE_DB));
});
return Q.all([hashP, updateP]);
}
/**
* Gets a list of Knowledge-Based Authentication questions and answers for a client.
*
* @param {string} clientID - ID of the client in question.
* @returns {Promise<Object>} - Promise for the questions and answers.
*/
function getKba(clientID) {
//
// Knoweldge Based Authentication questions are generated by a number of
// different functions. They all have the same format, so we can iterate
// over them to get to the full list of questions to select from.
//
const kbaFuncs = [
getKbaAddresses,
getKbaCards,
getKbaClientDetails,
getKbaDevices,
getKbaTransactions
];
const kbaPs = [];
for (let i = 0; i < kbaFuncs.length; ++i) {
kbaPs.push(kbaFuncs[i](clientID));
}
//
// Wait for all the questions to be generated, then prepare the results
//
return Q.all(kbaPs).then((results) => {
// Merge all the sub results
const all = [].concat(...results);
//
// Pick a random sample of them, then split out the questions and
// answers into seperate arrays (questions to send, answers to keep).
//
const kbas = _.sampleSize(all, utils.recoveryQuestionsCount);
const result = {
questions: _.map(
kbas,
(kba) => _.pick(kba, ['questionID', 'questionType', 'questionText'])
),
answers: _.map(
kbas,
(kba) => _.pick(kba, ['questionID', 'questionType', 'answer'])
)
};
return result;
});
}
/**
* Gets a set of KBA questions related to addresses. Specifically, the postcode
* of an address with the given description.
*
* @param {string} clientID - The client id.
* @returns {Promise<Object[]>} - Promise for the questions and answers.
*/
function getKbaAddresses(clientID) {
return mainDB.collectionAddresses
.find({
ClientID: clientID
})
.project({
AddressDescription: 1,
PostCode: 1
})
.toArray()
.then((addresses) => {
const kbas = [];
for (let i = 0; i < addresses.length; ++i) {
const id = utils.timeBasedRandomCode();
kbas.push({
questionID: id,
questionType: QTYPE.POSTCODE,
questionText: addresses[i].AddressDescription,
answer: addresses[i].PostCode
});
}
return kbas;
});
}
/**
* Gets KBA questions based on credit/debit card details. We give them the
* name; they give us back the last 3 digits.
*
* @param {string} clientID - The client ID.
* @returns {Promise<Object[]>} - Array of question and answer objects.
*/
function getKbaCards(clientID) {
return mainDB.collectionAccount
.find({
ClientID: clientID,
AccountType: 'Credit/Debit Payment Card',
$or: [
{AccountStatus: 0},
{AccountStatus: 1}
]
})
.project({
ClientAccountName: 1,
CardPAN: 1
})
.toArray()
.then((accounts) => {
const kbas = [];
for (let i = 0; i < accounts.length; ++i) {
const id = utils.timeBasedRandomCode();
//
// The card PAN can be anonymised in a number of ways depending
// on how many characters it has. In particular, the last 3
// digits might have a space in the middle depending on how the
// groups of 4 end up. e.g.
// - ...***1 23
// - ... **** 123
// - ... *123
//
// So we grab the last 4 digits, and remove the space/* wherever
// we find it.
//
const anonPan = accounts[i].CardPAN.slice(-4); // Last 4 characters
const last3 = anonPan.replace(/[ *]/, ''); // Remove any spaces or *s
kbas.push({
questionID: id,
questionType: QTYPE.CARD,
questionText: accounts[i].ClientAccountName,
answer: last3
});
}
return kbas;
});
}
/**
* Get KBA questions based on recent transactions.
* At present we only ask how many transactions you have PAID in the past week
* with Bridge.
*
* @param {string} clientID - The client ID.
* @returns {Promise<Object[]>} - Array of question and answer objects.
*/
function getKbaTransactions(clientID) {
const since = moment().subtract(7, 'days').startOf('day').utc();
debug('Finding transactions since', since);
return mainDB.collectionTransaction
.find({
CustomerClientID: clientID,
TransactionStatus: utils.TransactionStatus.COMPLETE,
SaleTime: {
$gte: since.toDate()
}
})
.project({
_id: 1
})
.limit(5) // We only care for up to 5 transactions
.toArray()
.then((transactions) => {
const id = utils.timeBasedRandomCode();
const kbas = [{
questionID: id,
questionType: QTYPE.TRANSACTION,
questionText: since.toISOString(),
answer: transactions.length
}];
return kbas;
});
}
/**
* Gets KBA questions based on device details. We give them the
* name; they give us back the phone number.
*
* @param {string} clientID - The client ID.
* @returns {Promise<Object[]>} - Array of question and answer objects.
*/
function getKbaDevices(clientID) {
return mainDB.collectionDevice
.find({
ClientID: clientID
})
.project({
DeviceName: 1,
DeviceNumber: 1
})
.toArray()
.then((devices) => {
const kbas = [];
for (let i = 0; i < devices.length; ++i) {
const id = utils.timeBasedRandomCode();
kbas.push({
questionID: id,
questionType: QTYPE.DEVICE,
questionText: devices[i].DeviceName,
answer: devices[i].DeviceNumber
});
}
return kbas;
});
}
/**
* Gets KBA questions based on personal details. i.e. what is their date of birth.
*
* @param {string} clientID - The client ID.
* @returns {Promise<Object[]>} - Array of question and answer objects.
*/
function getKbaClientDetails(clientID) {
return references.getClient(clientID)
.then((client) => {
const kbas = [];
const dob = _.get(client, 'KYC[0].DateOfBirth', '');
const id = utils.timeBasedRandomCode();
if (dob !== '') {
kbas.push({
questionID: id,
questionType: QTYPE.DOB,
questionText: '',
answer: dob
});
}
return kbas;
});
}
/**
* Verifies that the answers provided to the KBA questions are correct.
*
* @param {Object[]} responses - Array of the answers provided by the caller.
* @param {Object[]} expected - Array of expected answers.
* @returns {Promise} - Promise for the verification.
*/
function verifyAnswers(responses, expected) {
//
// Check we have the correct number of responses
//
if (responses.length !== expected.length) {
return Q.reject(NO_MATCH);
}
//
// Sort the responses and expected answers by the ID to ensure they are
// in the same order
//
const sortedResponses = _.sortBy(responses, 'questionID');
const sortedExpected = _.sortBy(expected, 'questionID');
//
// Now compare them in order where the IDs and answers should match.
// Each question type has a verifier that compares them in different ways.
//
const verifierLookup = {};
verifierLookup[QTYPE.POSTCODE] = verifyKbaPostcode;
verifierLookup[QTYPE.CARD] = verifyKbaCard;
verifierLookup[QTYPE.TRANSACTION] = verifyKbaTransactions;
verifierLookup[QTYPE.DEVICE] = verifyKbaDevice;
verifierLookup[QTYPE.DOB] = verifyKbaDob;
for (let i = 0; i < sortedResponses.length; ++i) {
if (sortedResponses[i].questionID !== sortedExpected[i].questionID) {
return Q.reject(NO_MATCH);
}
const verifier = verifierLookup[sortedExpected[i].questionType];
if (!verifier || !verifier(sortedResponses[i].answer, sortedExpected[i].answer)) {
debug(sortedResponses[i], '!==', sortedExpected[i]);
return Q.reject(NO_MATCH);
}
}
return Q.resolve(); // All matched
}
/**
* Verifies that a postcode matches the expected answer. We compare after
* removing any spaces and uppercasing to avoid any formatting errors.
*
* @param {string} answer - The answer provided by the user.
* @param {string} expected - The expected answer stored in the system.
*
* @returns {boolean} - True if the answers match.
*/
function verifyKbaPostcode(answer, expected) {
const fixedAnswer = answer.replace(/ /g, '').toUpperCase();
const fixedExpected = expected.replace(/ /g, '').toUpperCase();
return fixedAnswer === fixedExpected;
}
/**
* Verifies that the correct last 3 digits of the card are given. We ensure
* both are strings, and are exactly equal. E.g. `'012'` is NOT equal to `12`.
*
* @param {string} answer - The answer provided by the user.
* @param {string} expected - The expected answer stored in the system.
*
* @returns {boolean} - True if the answers match.
*/
function verifyKbaCard(answer, expected) {
return _.isString(answer) && _.isString(expected) && answer === expected;
}
/**
* Verifies that the correct transaction count is given. We have stored the
* exact number of transactions, but only expect answers in a smaller set of
* buckets (0, 1-2, 3-4, 5+), so check that fits.
*
* @param {string} answer - The answer provided by the user.
* @param {string} expected - The expected answer stored in the system.
*
* @returns {boolean} - True if the answers match.
*/
function verifyKbaTransactions(answer, expected) {
const expectedN = Number(expected);
switch (Number(answer)) {
case 0:
return expectedN === 0;
case 1:
return expectedN === 1 || expectedN === 2;
case 3:
return expectedN === 3 || expectedN === 4;
case 5:
return expectedN >= 5;
default:
return false;
}
}
/**
* Verifies that the correct phone number for a device is given. We compare
* directly without any changes.
*
* @param {string} answer - The answer provided by the user.
* @param {string} expected - The expected answer stored in the system.
*
* @returns {boolean} - True if the answers match.
*/
function verifyKbaDevice(answer, expected) {
return answer === expected;
}
/**
* Verifies that the date of birth is given. We compare using moment.js to
* convert to a date and checking that they are the same. This allows us to be
* a little flexible in the exact format (e.g. with or without a time) so long
* as it matches ISO 8601 (which we enforce).
*
* @param {string} answer - The answer provided by the user.
* @param {string} expected - The expected answer stored in the system.
*
* @returns {boolean} - True if the answers match.
*/
function verifyKbaDob(answer, expected) {
const momAnswer = moment(answer, moment.ISO_8601, true); // ISO 8601 strict mode.
const momExpected = moment(expected, moment.ISO_8601, true); // ISO 8601 strict mode.
return momAnswer.isSame(momExpected, 'day'); // 'day' also matches month and year
}
/**
* Starts a recovery process.
*
* @param {Object} req - Express request object.
* @param {Object} res - Express response object.
*/
function startRecovery(req, res) {
const email = req.swagger.params.body.value.email;
debug('Start recovery for: ', email);
/**
* Step 1. Find the client, and check that we haven't hit the recovery rate limit
*/
const TOO_SOON = 'BRIDGE: Recovery attempted too soon. Must wait longer.';
const clientP = references.getClientByEmail(email)
.then((client) => {
debug('Found client for ', email);
//
// Check if recovery is alloed already (or no limit has been set)
//
const now = new Date();
const allowAfter = _.get(client, 'PasswordManagement.0.RecoveryLimits.AllowAfter', now);
if (now < allowAfter) {
return Q.reject({
name: TOO_SOON,
validAfter: allowAfter
});
}
//
// Otherwise we are all good, so just keep going
//
return client;
});
/**
* Step 2. Create an email token. As it needs to be copied from an email
* we make it shorter than the usual ones
*/
const token = utils.randomCode(utils.paycodeString, utils.SMStokenLength);
/**
* Step 3. Check if the client has any devices so we can tell if they
* need to do SMS confirmation or not.
*/
const deviceInfoP = clientP.then((client) => clientUtils.getDevicesInfo(client.ClientID));
/**
* Step 4. Send an email with the recovery token in it
*/
const EMAIL_FAIL = 'BRIDGE: Failed to send email';
const emailP = Q.all([clientP, deviceInfoP]).spread((client) => {
//
// Build and send the email
//
const htmlEmail = templates.render(
'account-recovery',
{
emailValidationCode: token
}
);
debug('Sending email...', token);
return Q.nfcall(
mailer.sendEmail,
'Live',
client.ClientName,
'Bridge Account Recovery',
htmlEmail,
'startRecovery'
).catch(() => Q.reject(EMAIL_FAIL));
});
/**
* Step 5. Increase the delay before the next allowed recovery
*/
const timeoutP = Q.all([clientP, emailP, deviceInfoP]).spread((client) => {
debug('Email sent. Updating client');
const query = {
ClientID: client.ClientID
};
//
// Calculate the exponential backoff between attempts
//
const attempts = _.get(client, 'client.PasswordManagement.0.RecoveryLimits.Attempts', 0);
const delay = Math.pow(2, attempts) * utils.recoveryInitialDelay * 60; // Delay in seconds
const after = moment().add(delay, 'seconds').toDate();
//
// Update the RecoveryLimits of the client for next time.
// These will be reset on successful password reset.
//
const update = {
$set: {
'PasswordManagement.0.RecoveryLimits.AllowAfter': after
},
$inc: {
LastVersion: 1,
'PasswordManagement.0.RecoveryLimits.Attempts': 1
},
$currentDate: {
LastUpdate: true
}
};
const options = {
upsert: false
};
return Q.nfcall(
mainDB.updateObject,
mainDB.collectionClient,
query,
update,
options,
false // Don't suppress errors - we expect this to succed
).catch(() => Q.reject(FAILED_UPDATE_DB));
});
/**
* Step 6. Setup the recovery session
*/
const sessionP = Q.all([clientP, deviceInfoP, timeoutP])
.spread((client) => apiUtils.initRecoverySession(req, client));
/**
* Step 7. Return the result
* If the client has devices we return 202 ACCEPTED to say that we
* will need SMS authentication as a later step
* If the parents don't have devices, return 200 OK to say we only
* need email confirmation
*/
return Q.all([clientP, emailP, timeoutP, sessionP, deviceInfoP])
.then((results) => {
debug('All done!');
const sessionResponse = results[3];
const deviceInfo = results[4];
//
// Setup the next expected state. This depends on whether we have
// devices to validate with an SMS token or not.
//
let nextState = STATES.WAITING_FOR_EMAIL_TOKEN;
let result = httpStatus.ACCEPTED;
if (!deviceInfo.hasDevices) {
nextState = STATES.WAITING_FOR_EMAIL_TOKEN_PASSWORD;
result = httpStatus.OK; // No devices, so accept just an email token
}
setNextState(
req,
nextState,
{
emailToken: token
}
);
return res.status(result).json(sessionResponse);
})
.catch((error) => {
debug('Error starting recovery', error);
const responses = [
[
'MongoError',
httpStatus.BAD_GATEWAY, 31131, 'Database Offline', true
],
[
references.ERRORS.INVALID_CLIENT,
httpStatus.BAD_REQUEST, 31132, 'Client not found', true
],
[
TOO_SOON,
httpStatus.TOO_MANY_REQUESTS, 31133, 'Too many recovery attempts.', true
],
[
EMAIL_FAIL,
httpStatus.BAD_GATEWAY, 31134, 'Failed to send validation email.'
],
[
FAILED_UPDATE_DB,
httpStatus.BAD_GATEWAY, 31135, 'Failed to update database.'
],
[
apiUtils.ERRORS.SESSION_REGEN_FAILED,
httpStatus.INTERNAL_SERVER_ERROR, 31136, 'Failed to create session.'
]
];
const responseHandler = new responseUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
});
}
/**
* Completes the recovery process in the cases where we only need to confirm
* the email address. In these case, we send the new password along with the
* email token so that it is confirmed and the new password set in 1 step.
*
* @param {Object} req - Express request object.
* @param {Object} res - Express response object.
*/
function completeRecoveryEmailPw(req, res) {
const emailToken = req.swagger.params.body.value.validationToken;
const newPassword = req.swagger.params.body.value.newPassword;
const clientID = req.session.data.clientID;
const dataP = getStateData(req, STATES.WAITING_FOR_EMAIL_TOKEN_PASSWORD);
debug('Completing recover for: ', clientID);
/**
* Step 1. Check the tokens match
*/
const matchP = dataP.then(
(data) => {
return emailToken === data.emailToken ? Q.resolve() : Q.reject(NO_MATCH);
}
);
/**
* Update the password
*/
const updateP = matchP.then(() => updatePassword(clientID, newPassword));
/**
* Check the results
*/
return Q.all([dataP, matchP, updateP])
.then(() => {
debug('Password updated');
res.status(httpStatus.OK).json();
// All good, so we also destroy the session so they have to log
// in normally to get a normal session
return req.session.destroy();
})
.catch((error) => {
debug('Error resetting password', error);
const responses = [
[
'MongoError',
httpStatus.BAD_GATEWAY, 31141, 'Database Offline', true
],
[
WRONG_STATE,
httpStatus.FORBIDDEN, 31142, 'Operation not allowed'
],
[
NO_RETRIES,
httpStatus.FORBIDDEN, 31143, 'Too many failures'
],
[
NO_MATCH,
httpStatus.BAD_REQUEST, 31144, 'Invalid email token'
],
[
HASH_FAILED,
httpStatus.INTERNAL_SERVER_ERROR, 31145, 'Failed to initialise password'
],
[
FAILED_UPDATE_DB,
httpStatus.BAD_GATEWAY, 31146, 'Failed to update database.'
]
];
const responseHandler = new responseUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
});
}
/**
* Confirms the email token, then produce a set of KBA questions for the client
* to answer.
*
* @param {Object} req - Express request object.
* @param {Object} res - Express response object.
*/
function confirmRecoveryEmail(req, res) {
const clientID = req.session.data.clientID;
const token = req.swagger.params.body.value.validationToken;
const dataP = getStateData(req, STATES.WAITING_FOR_EMAIL_TOKEN);
debug('Confirming email for: ', clientID);
/**
* Step 1. Check the tokens match
*/
const matchP = dataP.then(
(data) => {
return token === data.emailToken ? Q.resolve() : Q.reject(NO_MATCH);
}
);
/**
* Step 2. Build the KBA questions
*/
const kbaP = getKba(clientID);
/**
* Check if everything passed
*/
return Q.all([dataP, matchP, kbaP])
.spread((data, match, kba) => {
debug('Email token validations');
setNextState(
req,
STATES.WAITING_FOR_ANSWERS,
{
answers: kba.answers
});
res.status(httpStatus.OK).json({
questions: kba.questions
});
})
.catch((error) => {
debug('Error resetting password', error);
const responses = [
[
'MongoError',
httpStatus.BAD_GATEWAY, 31151, 'Database Offline', true
],
[
WRONG_STATE,
httpStatus.FORBIDDEN, 31152, 'Operation not allowed'
],
[
NO_RETRIES,
httpStatus.FORBIDDEN, 31153, 'Too many failures'
],
[
NO_MATCH,
httpStatus.BAD_REQUEST, 31154, 'Invalid email token'
]
];
const responseHandler = new responseUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
});
}
/**
* Confirm the answers to the KBA answers provided. Checks the provided phone
* number is correct for this client, and sends a reset token to the phone.
*
* @param {Object} req - Express request object.
* @param {Object} res - Express response object.
*/
function confirmAnswers(req, res) {
const clientID = req.session.data.clientID;
const phoneNumber = req.swagger.params.body.value.DeviceNumber;
const answers = req.swagger.params.body.value.Answers;
const dataP = getStateData(req, STATES.WAITING_FOR_ANSWERS);
debug('Confirming answers for: ', clientID);
/**
* Validate the answers to the questions we asked
*/
const checkKbaP = dataP.then(
(data) => {
return verifyAnswers(answers, data.answers);
}
);
/**
* Check the device is a valid one for this client
*/
const deviceP = checkKbaP.then(() => references.getDevice(phoneNumber, clientID));
/**
* Send an SMS to that device
*/
const token = utils.randomCode(utils.paycodeString, utils.SMStokenLength);
const SMS_SEND_FAIL = 'BRIDGE: Failed to send SMS';
const smsP = deviceP.then(() => {
debug('Sending reset SMS:', token);
return Q.nfcall(
sms.sendSMS,
null, // or 'TEST'
phoneNumber,
'Your Bridge verification code is ' + token
).catch(() => Q.reject(SMS_SEND_FAIL));
});
/**
* Check everything worked and reply ok
*/
return Q.all([dataP, checkKbaP, deviceP, smsP])
.then(() => {
debug('KBA complete. Sent SMS token');
setNextState(
req,
STATES.WAITING_FOR_SMS_TOKEN_PASSWORD,
{
smsToken: token
});
return res.status(httpStatus.OK).json();
})
.catch((error) => {
debug('Error verifying KBA', error);
const responses = [
[
'MongoError',
httpStatus.BAD_GATEWAY, 31161, 'Database Offline', true
],
[
WRONG_STATE,
httpStatus.FORBIDDEN, 31162, 'Operation not allowed'
],
[
NO_RETRIES,
httpStatus.FORBIDDEN, 31163, 'Too many failures'
],
[
NO_MATCH,
httpStatus.BAD_REQUEST, 31164, 'Incorrect answers'
],
[
SMS_SEND_FAIL,
httpStatus.BAD_GATEWAY, 31165, 'Failed to send recovery token'
],
[
references.ERRORS.INVALID_DEVICE,
httpStatus.BAD_REQUEST, 31166,
'Device number is not registered for this client, or has been disabled', true
]
];
const responseHandler = new responseUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
});
}
/**
* Completes the recovery process when we have an SMS validation token.
*
* @param {Object} req - Express request object.
* @param {Object} res - Express response object.
*/
function completeRecoveryDevicePw(req, res) {
const newPassword = req.swagger.params.body.value.newPassword;
const token = req.swagger.params.body.value.validationToken;
const clientID = req.session.data.clientID;
const dataP = getStateData(req, STATES.WAITING_FOR_SMS_TOKEN_PASSWORD);
debug('Completing recover via SMS for: ', clientID);
/**
* Step 1. Check the tokens match,
* If not, check if we have any retries left
*/
const matchP = dataP.then(
(data) => {
return token === data.smsToken ? Q.resolve() : Q.reject(NO_MATCH);
}
);
/**
* Step 2. Update password
*/
const updateP = matchP.then(() => updatePassword(clientID, newPassword));
/**
* Check the results
*/
return Q.all([dataP, matchP, updateP])
.then(() => {
debug('Password updated');
res.status(httpStatus.OK).json();
// All good, so we also destroy the session so they have to log
// in normally to get a normal session
return req.session.destroy();
})
.catch((error) => {
debug('Error resetting password', error);
const responses = [
[
'MongoError',
httpStatus.BAD_GATEWAY, 31171, 'Database Offline', true
],
[
WRONG_STATE,
httpStatus.FORBIDDEN, 31172, 'Operation not allowed'
],
[
NO_RETRIES,
httpStatus.FORBIDDEN, 31173, 'Too many failures'
],
[
NO_MATCH,
httpStatus.BAD_REQUEST, 31174, 'Invalid sms token'
],
[
HASH_FAILED,
httpStatus.INTERNAL_SERVER_ERROR, 31175, 'Failed to initialise password'
],
[
FAILED_UPDATE_DB,
httpStatus.BAD_GATEWAY, 31176, 'Failed to update database.'
]
];
const responseHandler = new responseUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
});
}