express-jwt-blacklist
Version:
express-jwt plugin for token blacklisting
181 lines (157 loc) • 5.51 kB
JavaScript
;
/**
* JWT token should contain a unique identifier `sub` and issued
* at timestamp `iat`, we can use that to store information for example:
*
* ae234b24-f2bd-11e4-a6ec-7cdb000003fb: {revoke: [1431020932, 1431020932], purge: 1431020932}
*
* isRevoked middleware works like this:
*
* - For type `revoked` we check if the index claim, (default: `iat`) matches any of the `revoke` values
* - For type `purge` we check if `iat` is older then the timestamp the `purge` timestamp
*
* Considerations
*
* 1. Issued at timestamp(in seconds) is not ideal way to ensure uniqueness and can cause collision
* in extreme cases but since it's in the context of a single user it should have no effect.
* 2. There's no need to store the blacklist data indefinitely since the JWT tokens have expiration built in.
* Store expiration is set to match the token expiration. Since we are storing multiple timestamps under
* a single key this might result in storing some of the information past expiration date.
*/
var debug = require('./debug').log;
var utils = require('./utils');
// Defaults
var store = require('./store')({type: 'memory'});
var tokenId = 'sub';
var indexBy = 'iat';
var keyPrefix = 'jwt-blacklist:';
var strict = false;
/**
* Session revocation types:
*
* - revoke: revoke all matched iat timestamps
* - purge: revoke all timestamps older than iat
*/
var TYPE = exports.TYPE = {
revoke: 'revoke',
purge: 'purge'
};
/**
* Configuration
*
* @param {String} opts.store.type Store type
* @param {String} opts.store.host Store host
* @param {String} opts.store.port Store port
* @param {String} opts.store.keyPrefix Store key prefix
* @param {String} opts.store.options Store client options
* @param {String} opts.tokenId Unique JWT token identifier
* @param {String} opts.strict Strict mode
*/
exports.configure = function(opts) {
opts = opts || {};
if (opts.store) {
if (opts.store.type) {
store = require('./store')(opts.store);
if (opts.store.keyPrefix) {
utils.checkString(opts.store.keyPrefix, 'keyPrefix');
keyPrefix = opts.store.keyPrefix;
}
}
else if (typeof opts.store.get === 'function' && typeof opts.store.set === 'function') {
store = opts.store;
}
}
if (opts.tokenId) {
utils.checkString(opts.tokenId, 'tokenId');
tokenId = opts.tokenId;
}
if (opts.indexBy) {
utils.checkString(opts.indexBy, 'indexBy');
indexBy = opts.indexBy;
}
if (opts.strict) {
utils.checkBoolean(opts.strict, 'strict');
strict = opts.strict;
}
};
/**
* Check if JWT token is revoked
*
* @param {Object} req Express request object
* @param {Object} user Express JWT user object
* @param {Function} fn Callback function
*/
exports.isRevoked = middleware;
/**
* Revoke a single JWT token
*
* @param {Object} user JWT user payload
* @param {Number} Optional lifetime (in seconds) for this entry
* @param {Function} [fn] Optional callback function
*/
exports.revoke = operation.bind(null, TYPE.revoke);
/**
* Pure all existing JWT tokens
*
* @param {Object} user JWT user payload
* @param {Number} Optional lifetime (in seconds) for this entry
* @param {Function} [fn] Optional callback function
*/
exports.purge = operation.bind(null, TYPE.purge);
/**
* Enable/disable debugging
*/
exports.__defineSetter__('debug', function(val) {
require('./debug').debug = !!val;
});
function middleware(req, user, fn) {
var revoked = strict;
var id = user[tokenId];
if (!id) return fn(new Error('JWT missing tokenId claim' + tokenId));
var index = user[indexBy];
if (!index) return fn(new Error('JWT missing indexBy claim' + tokenId));
var key = keyPrefix + id;
store.get(key, function(err, res) {
if (err) debug('middleware [' + key + '] error:', err);
if (!res) return fn(null, revoked);
debug('middleware [' + key + ']', res);
if (res[TYPE.revoke] && res[TYPE.revoke].indexOf(index) !== -1) revoked = true;
else if (res[TYPE.purge] >= user.iat) revoked = true;
else revoked = false;
fn(null, revoked);
});
};
function operation(type, user, lifetime, fn) {
if (typeof fn !== 'function') fn = utils.optionalCallback;
if (typeof lifetime === 'function') {
fn = lifetime;
lifetime = undefined;
} else if (lifetime && typeof lifetime !== 'number') {
return fn(new Error('Invalid lifetime value'));
}
if (!user) return fn(new Error('User payload missing'));
if (!lifetime && typeof user.iat !== 'number') return fn(new Error('Invalid user.iat value'));
var id = user[tokenId];
if (!id) return fn(new Error('JWT missing tokenId claim' + tokenId));
var key = keyPrefix + id;
store.get(key, function(err, res) {
if (err) return fn(err);
var data = res || {};
debug('revoke [' + key + '] ' + index, data);
if (type === TYPE.revoke) {
var index = user[indexBy];
if (!index) return fn(new Error('JWT missing indexBy claim' + tokenId));
if (data[TYPE.revoke]) {
if (data[TYPE.revoke].indexOf(index) === -1) {
data[TYPE.revoke].push(index);
}
}
else data[TYPE.revoke] = [index];
}
if (type === TYPE.purge) {
data[TYPE.purge] = utils.nowInSeconds() - 1;
}
lifetime = lifetime ? lifetime : (user.exp ? user.exp - user.iat : 0);
store.set(key, data, lifetime, fn);
});
};