predix-fast-token
Version:
Node module to verify UAA tokens used when protecting REST endpoints
179 lines (172 loc) • 6.96 kB
JavaScript
;
const jwt = require('jsonwebtoken');
const rp = require('request-promise');
const url = require('url');
const debug = require('debug')('predix-fast-token');
const NodeCache = require('node-cache');
const tokenCache = new NodeCache();
let token_utils = {};
let oauthKeyCache = {};
token_utils._tokenCache = tokenCache; // Exposed for testing
// This will fetch and cache the public key of the UAA used for this tenant.
// This key can them be used to verify the JWT token presented so that the
// details contained within the token can be trusted. Such as the user, expiry and scopes.
const getKey = (keyURL, tenantUuid) => {
// URL for the token is <UAA_Server>/token_key
return new Promise((resolve, reject) => {
if (tenantUuid && oauthKeyCache[keyURL + '-' + tenantUuid]) {
// Already have it w/ tenantUuid.
resolve(oauthKeyCache[keyURL + '-' + tenantUuid]);
} else if (!tenantUuid && oauthKeyCache[keyURL]) {
// Already have it.
resolve(oauthKeyCache[keyURL]);
} else {
// Fetch it and cache it for later
debug('Fetching key from:', keyURL);
const options = tenantUuid ? {
uri: keyURL,
headers: {
tenant: tenantUuid,
},
} : {uri: keyURL};
rp.get(options).then(body => {
debug('Fetched key');
const data = JSON.parse(body);
// Cache it
if (tenantUuid) {
oauthKeyCache[keyURL + '-' + tenantUuid] = data.value;
} else { oauthKeyCache[keyURL] = data.value; }
resolve(data.value);
}).catch(err => {
debug('Error reading token key from', keyURL, err);
reject(err);
});
}
});
};
token_utils.clearCache = () => {
debug('Clearing key cache');
oauthKeyCache = {};
tokenCache.flushAll();
};
/**
* Verifies that a token was signed by a trusted UAA server and that it's still valid.
*
* @param {string} token - The access token.
* @param {string} trusted_issuers - A list of trusted issuer URIs
* @param {string} tenantUuid - (optional) Used for tenant based UAA
* @returns {promise} - A promise to verify the token.
* Resolves with the decoded token if valid.
* Rejected with an error if invalid or an error occurs.
*/
token_utils.verify = (token, trusted_issuers, tenantUuid) => {
return new Promise((resolve, reject) => {
// Decode the token to get the issuer
let prelim = null;
try {
prelim = jwt.decode(token);
} catch (err) {
debug('Error decoding token', err);
}
// Is this token claiming to be from a trusted issuer.
if (prelim && trusted_issuers.indexOf(prelim.iss) > -1) {
const issuer = url.parse(prelim.iss);
// Just want the protocol and host, and any path before '/oauth/token'.
const uaa_path = issuer.pathname.replace('/oauth/token', '');
const uaa_server = url.format({ protocol: issuer.protocol, host: issuer.host, pathname: uaa_path + '/token_key' });
// Grab the key for this UAA server and check.
getKey(uaa_server, tenantUuid).then((key) => {
jwt.verify(token, key, (err, decoded) => {
if (err) {
debug('Invalid', err);
reject(err);
} else {
resolve(decoded);
}
});
}).catch((error) => {
debug('get UAA key error', error);
reject(error);
});
} else if (prelim) {
// The decoded issuer is not in the trusted list.
// No need to check that it's signed correctly
reject(new Error('Not a trusted issuer'));
} else {
// The token is not a valid JWT token
reject(new Error('Not a valid token'));
}
});
};
/**
* Verifies that a token was signed by a trusted UAA server and that it's still valid and has not been revoked
* by checking it against the UAA check_token endpoint.
*
* @param {string} token - The access token.
* @param {string} issuer - The UAA issuer URI
* @param {string} clientId - Your client id for the UAA issuer
* @param {string} clientSecret - Your client secret for the UAA issuer
* @param {Object} [opts={}] - A dictionary of options
* @param {int} [opts.ttl=0] - The maximum time to live in cache for a validated token, in seconds.
* @param {bool} [opts.useCache=true] - Whether cached values should be used for verification.
* @returns {promise} - A promise to verify the token.
* Resolves with the decoded/assocaited token if valid.
* Rejected with an error if invalid or an error occurs.
*/
token_utils.remoteVerify = (token, issuer, clientId, clientSecret, opts) => {
opts = opts || {};
debug('remoteVerify called with options', opts);
opts.ttl = opts.ttl || 0; // Default to don't cache
opts.useCache = (typeof opts.useCache === 'undefined') ? true : opts.useCache;
// See if token is in cache
debug('remoteVerify default options applied', opts);
if (opts.useCache) {
const cachedJwt = tokenCache.get(token);
if (cachedJwt && cachedJwt.exp * 1000 > Date.now()) {
// Valid cached token
debug('Valid token found in cache', cachedJwt);
return Promise.resolve(cachedJwt);
} else {
debug('No valid token found in cache', cachedJwt);
}
}
// If not cached, send against the check_token endpoint
const issuer_url = url.parse(issuer);
const check_token_url = url.format({ protocol: issuer_url.protocol, host: issuer_url.host, pathname: '/check_token' });
debug(`Using ${check_token_url} to test token`);
const request_opts = {
uri: check_token_url,
auth: {
username: clientId,
password: clientSecret
},
method: 'POST',
form: {
token: token
},
json: true
};
return rp.post(request_opts)
.then((jwt) => {
// Successful
debug('Check_token returned success', jwt);
if (opts.ttl > 0) {
const cacheExpire = Date.now() + opts.ttl * 1000;
tokenCache.set(token, jwt, opts.ttl);
debug(`Token cached until ${cacheExpire}`);
} else {
debug('Token caching disabled');
}
return jwt;
})
.catch((error) => {
// Invalid token or failed request
debug('UAA check_token returned error', error);
if (error.error) {
throw error.error;
} else {
throw error;
}
});
};
module.exports = token_utils;