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

346 lines
10 KiB
JavaScript

/**
* @fileOverview File to manage integration authorisation token related operations
*/
'use strict';
const httpStatus = require('http-status-codes');
const Q = require('q');
const _ = require('lodash');
const jwt = require('jsonwebtoken');
const debug = require('debug')('webconsole-api:controllers:tokens');
const utils = require(global.pathPrefix + 'utils.js');
const mainDB = require(global.pathPrefix + 'mainDB.js');
const references = require(global.pathPrefix + '../utils/references.js');
const featureFlags = require(global.pathPrefix + '../utils/feature-flags/feature-flags.js');
const responsesUtils = require(global.pathPrefix + '../utils/responses.js');
const tokenUtils = require(global.pathPrefix + '../utils/tokens.js');
module.exports = {
listTokens: listTokens,
createToken: createToken,
deleteToken: deleteToken
};
const TOKENS_FEATURE_FLAG = 'tokens';
const JWT_SECRET = require(global.configFile).integrationsTokenSecret;
const JWT_ALGORITHM = 'HS256'; // HMAC + SHA256 only
const JWT_ISSUER = 'bridge-v1'; // Issuer string
const JWT_OPTIONS = {
algorithm: JWT_ALGORITHM,
issuer: JWT_ISSUER,
noTimestamp: true
};
const FAILED_CREATE_JWT = 'BRIDGE: failed to create the JWT';
const DATABASE_UPDATE_FAILED = 'BRIDGE: DB update failed';
/**
* Lists the tokens that belong to the current client.
*
* @param {Object} req - the request object
* @param {Object} res - the response object
*/
function listTokens(req, res) {
const clientID = req.session.data.clientID;
const clientP = references.getClient(clientID);
//
// Iterate through the tokens we have, and turn them into JWTs for returning
// to the caller
//
const listP = clientP.then((client) => {
let jwtPromises = [];
const tokens = client.IntegrationTokens || [];
for (let i = 0; i < tokens.length; ++i) {
//
// Define the payload
//
const jwtPayload = {
id: clientID,
token: tokens[i].token
};
const name = tokens[i].name;
//
// Call the JWT signing function
//
const jwtP = Q.nfcall(jwt.sign, jwtPayload, JWT_SECRET, JWT_OPTIONS)
.then((jwt) => {
debug('Token encoded', i);
return {
name: name,
token: jwt
};
})
.catch((error) => {
debug('Failed to encode', error, jwtPayload);
return Q.reject(FAILED_CREATE_JWT);
});
//
// Save the promises to the array that we will return
//
jwtPromises.push(jwtP);
}
return Q.all(jwtPromises);
});
//
// Send the response depending on results
//
Q.all([clientP, listP])
.spread((client, tokens) => {
res.status(httpStatus.OK).json(tokens);
})
.catch((error) => {
debug(' - error listing tokens', error);
const responses = [
[
'MongoError',
httpStatus.BAD_GATEWAY, 31101, 'Database Offline', true
],
[
references.ERRORS.INVALID_CLIENT,
httpStatus.BAD_REQUEST, 31102, 'Client not found', true
],
[
FAILED_CREATE_JWT,
httpStatus.INTERNAL_SERVER_ERROR, 31103, 'Failed to generate tokens list.'
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
});
}
/**
* Function to create a token for a merchant
*
* @param {Object} req - the request object
* @param {Object} res - the response object
*/
function createToken(req, res) {
//
// Check the client is a merchant
//
if (!req.session.data.isMerchant) {
res.status(httpStatus.FORBIDDEN).json({
code: 999,
info: 'Client is not a merchant.'
});
return;
}
//
// Get the current user's details from the session
//
const clientID = req.session.data.clientID;
const tokenName = req.swagger.params.body.value.name;
const clientP = references.getClient(clientID);
//
// Check that we have the feature flag enabled for this, and that we don't
// already have too many tokens.
//
const NOT_ENABLED = 'BRIDGE: Not enabled';
const TOO_MANY_TOKENS = 'BRIDGE: Too many tokens';
const enabledP = clientP.then((client) => {
if (!featureFlags.isEnabled(TOKENS_FEATURE_FLAG, client)) {
return Q.reject(NOT_ENABLED);
} else if (
_.isArray(client.IntegrationTokens) &&
client.IntegrationTokens.length >= utils.MaxIntegrationTokens
) {
return Q.reject(TOO_MANY_TOKENS);
} else {
return client; // So we can cascade
}
});
//
// Push a random token into the IntegrationsTokens array on the client object
//
const token = utils.timeBasedRandomCode();
const addedP = enabledP.then((client) => {
const query = {
_id: client._id
};
const update = {
$push: {
IntegrationTokens: {
token: token,
name: tokenName
}
},
$inc: {
LastVersion: 1
},
$currentDate: {
LastUpdate: true
}
};
return mainDB.collectionClient
.updateOne(query, update)
.then((res) => {
if (res.modifiedCount === 1) {
return Q.resolve();
} else {
return Q.reject(DATABASE_UPDATE_FAILED);
}
});
});
//
// Build a JWT based on the token (if it was added correctly)
//
const jwtPayload = {
id: clientID,
token: token
};
const jwtP = addedP.then(
() => Q.nfcall(jwt.sign, jwtPayload, JWT_SECRET, JWT_OPTIONS)
.catch(() => Q.reject(FAILED_CREATE_JWT))
);
Q.all([clientP, enabledP, addedP, jwtP])
.then(
(results) => {
const jwt = results[3];
res.status(httpStatus.OK).json({
token: jwt
});
})
.catch((error) => {
debug(' - error creating token', error);
const responses = [
[
'MongoError',
httpStatus.BAD_GATEWAY, 31111, 'Database Offline', true
],
[
references.ERRORS.INVALID_CLIENT,
httpStatus.BAD_REQUEST, 31112, 'Client not found', true
],
[
NOT_ENABLED,
httpStatus.BAD_REQUEST, 31113, 'Tokens not enabled.'
],
[
TOO_MANY_TOKENS,
httpStatus.CONFLICT, 31116, 'Too many tokens.'
],
[
DATABASE_UPDATE_FAILED,
httpStatus.BAD_GATEWAY, 31114, 'Failed to store token.'
],
[
FAILED_CREATE_JWT,
httpStatus.INTERNAL_SERVER_ERROR, 31115, 'Failed to produce final token.'
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
});
}
/**
* Deletes a token that belongs to the current client.
*
* @param {Object} req - the request object
* @param {Object} res - the response object
*/
function deleteToken(req, res) {
//
// Get the current user's details from the session
//
const clientID = req.session.data.clientID;
const token = req.swagger.params.token.value;
//
// Validate the token
//
const DIFFERENT_CLIENT = 'BRIDGE: Token belongs to a different client';
let validateP = tokenUtils.validateToken(token).then((result) => {
//
// The token is valid, but may belong to a different client
//
if (result.client.ClientID !== clientID) {
return Q.reject(DIFFERENT_CLIENT);
} else {
return result;
}
});
//
// Delete the token from the list
//
let deleteP = validateP.then((result) => {
const query = {
_id: result.client._id
};
const update = {
$pull: {
IntegrationTokens: {
token: result.decoded.token
}
},
$inc: {
LastVersion: 1
},
$currentDate: {
LastUpdate: true
}
};
return mainDB.collectionClient
.updateOne(query, update)
.then((res) => {
if (res.modifiedCount === 1) {
return Q.resolve();
} else {
return Q.reject(DATABASE_UPDATE_FAILED);
}
});
});
Q.all([validateP, deleteP])
.then(() => {
res.status(httpStatus.OK).json();
})
.catch((error) => {
debug(' - error creating token', error);
const responses = [
[
'MongoError',
httpStatus.BAD_GATEWAY, 31121, 'Database Offline', true
],
//
// Not that we give a similar error response to a number of cases
// to reduce the amount of information we return about tokens
//
[
tokenUtils.ERRORS.TOKEN_INVALID,
httpStatus.BAD_REQUEST, 31122, 'Invalid Token'
],
[
tokenUtils.ERRORS.CLIENT_NOT_FOUND,
httpStatus.BAD_REQUEST, 31123, 'Invalid Token'
],
[
DIFFERENT_CLIENT,
httpStatus.BAD_REQUEST, 31124, 'Invalid Token'
],
[
DATABASE_UPDATE_FAILED,
httpStatus.BAD_GATEWAY, 31125, 'Failed to delete token.'
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
});
}