/** * @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); }); }