143 lines
4.5 KiB
JavaScript
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
|
|
);
|
|
}
|