bridge-node-server/node_server/utils/tokens.js
Martin Donnelly 57bd6c8e6a init
2018-06-24 21:15:03 +01:00

143 lines
4.5 KiB
JavaScript

/**
* @fileOverview Utils for handling integratin tokens
*/
'use strict';
const Q = require('q');
const debug = require('debug')('utils:tokens');
const jwt = require('jsonwebtoken');
const config = require(global.configFile);
const utils = require(global.pathPrefix + 'utils.js');
const mainDB = require(global.pathPrefix + 'mainDB.js');
const SECRET = require(global.configFile).integrationsTokenSecret;
const ALGORITHMS = ['HS256']; // HMAC + SHA256 only
const ISSUER = 'bridge-v1'; // Issuer string to validate
const ERRORS = {
TOKEN_INVALID: 'BRIDGE: Token is invalid',
CLIENT_NOT_FOUND: 'BRIDGE: Client not found for token'
};
module.exports = {
ERRORS: ERRORS,
validateToken: validateToken
};
/**
* Validates the token, and returns the client the token is for no success
*
* @param {string} token - the token to validate
* @returns {Promise} - Promise for the client the token belongs to
*/
function validateToken(token) {
//
// Check that we have a webtoken using our secret and available algorithms
// NOTE: We ignore expiration validation as (a) this is for long term use
// in a server, and (b) we only use the contents to lookup the merchant
// so a revoked token will be useless irrespective of expiry time
// WARNING: we MUST specifiy the algorithms ourselves to avoid a security issue
// @link https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
//
const JWT_OPTIONS = {
algorithms: ALGORITHMS,
ignoreExpiration: true,
issuer: ISSUER
};
let jwtP = Q.nfcall(
jwt.verify,
token,
SECRET,
JWT_OPTIONS
).catch((err) => {
debug('Failed to verify token:', err.message);
return Q.reject(ERRORS.TOKEN_INVALID);
});
let clientP = jwtP.then((decoded) => {
debug('Token validated', decoded);
//
// Get the client
//
return getClientFromToken(decoded)
.then((client) => {
if (client) {
debug('Valid client found');
return Q.resolve({
client: client,
decoded: decoded
});
} else {
debug('Client not found from token', decoded);
return Q.reject(ERRORS.CLIENT_NOT_FOUND);
}
});
});
return Q.all([jwtP, clientP]).spread((decoded, client) => client);
}
/**
* Gets a valid client based on the information in the token.
* We deliberately don't care about the specific reason we don't find the client,
* be it id, token, client status, etc. We don't want to give that information
* away, so it's all "Unauthorized".
*
* @param {Object} decoded - the decoded integration token
* @param {string} decoded.id - the id of the merchant the token is for
* @param {string} decoded.token - the integration token for the merchant
* @return {Promise} - Resolves if a matching client is found
*/
function getClientFromToken(decoded) {
/**
* Look for a client that matches the following criteria:
* 1. ClientID matches the id we have been given in the token
* 2. ClientStatus is active, not suspended, and doesn't have any KYC issues
* 3. Client has accepted the latest EULA
* 4. A token in the `IntegrationsToken` array matches the token we have been given
* 5. The client is an active merchant, and has a valid merchant name
* 6. The client has the feature flag 'token' set
*/
const query = {
ClientID: decoded.id,
ClientStatus: {
/* jshint -W016*/
$bitsAllSet: utils.ClientEmailVerifiedMask | utils.ClientDetailsMask,
$bitsAllClear: utils.ClientBarredMask | utils.ClientKycIncompleteMask
/* jshint +W016 */
},
EULAVersionAccepted: config.EULAVersion,
'IntegrationTokens.token': decoded.token,
Merchant: {
$elemMatch: {
MerchantStatus: 1,
CompanyAlias: {
$type: 'string',
$ne: ''
}
}
},
'FeatureFlags': 'tokens'
};
//
// Add a comment as findOne supports it, and this is a complicated query
// which might be worth tracking.
//
const options = {
comment: 'int_security:getClientFromToken'
};
return Q.nfcall(
mainDB.findOneObject,
mainDB.collectionClient,
query,
options,
true
);
}