express-jwt-blacklist
Advanced tools
Comparing version 1.0.2 to 1.1.0
@@ -10,3 +10,3 @@ 'use strict'; | ||
* | ||
* - For type `revoked` we check if the token `iat` matches any of the `revoke` timestamps | ||
* - 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 | ||
@@ -29,2 +29,3 @@ * | ||
var tokenId = 'sub'; | ||
var indexBy = 'iat'; | ||
var keyPrefix = 'jwt-blacklist:'; | ||
@@ -76,2 +77,6 @@ var strict = false; | ||
} | ||
if (opts.indexBy) { | ||
utils.checkString(opts.indexBy, 'indexBy'); | ||
indexBy = opts.indexBy; | ||
} | ||
if (opts.strict) { | ||
@@ -96,2 +101,3 @@ utils.checkBoolean(opts.strict, 'strict'); | ||
* @param {Object} user JWT user payload | ||
* @param {Number} Optional lifetime (in seconds) for this entry | ||
* @param {Function} [fn] Optional callback function | ||
@@ -105,2 +111,3 @@ */ | ||
* @param {Object} user JWT user payload | ||
* @param {Number} Optional lifetime (in seconds) for this entry | ||
* @param {Function} [fn] Optional callback function | ||
@@ -121,3 +128,5 @@ */ | ||
var id = user[tokenId]; | ||
if (!id) return fn(new Error('JWT missing tokenId ' + 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)); | ||
@@ -130,3 +139,3 @@ var key = keyPrefix + id; | ||
if (res[TYPE.revoke] && res[TYPE.revoke].indexOf(user.iat) !== -1) revoked = true; | ||
if (res[TYPE.revoke] && res[TYPE.revoke].indexOf(index) !== -1) revoked = true; | ||
else if (res[TYPE.purge] >= user.iat) revoked = true; | ||
@@ -139,9 +148,15 @@ else revoked = false; | ||
function operation(type, user, fn) { | ||
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 (typeof user.iat !== 'number') return fn(new Error('Invalid user.iat value')); | ||
if (typeof fn !== 'function') fn = utils.optionalCallback; | ||
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 ' + tokenId)); | ||
if (!id) return fn(new Error('JWT missing tokenId claim' + tokenId)); | ||
@@ -153,11 +168,13 @@ var key = keyPrefix + id; | ||
var data = res || {}; | ||
debug('revoke [' + key + '] ' + user.iat, data); | ||
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(user.iat) === -1) { | ||
data[TYPE.revoke].push(user.iat); | ||
if (data[TYPE.revoke].indexOf(index) === -1) { | ||
data[TYPE.revoke].push(index); | ||
} | ||
} | ||
else data[TYPE.revoke] = [user.iat]; | ||
else data[TYPE.revoke] = [index]; | ||
} | ||
@@ -169,5 +186,5 @@ | ||
var lifetime = user.exp ? user.exp - user.iat : 0; | ||
lifetime = lifetime ? lifetime : (user.exp ? user.exp - user.iat : 0); | ||
store.set(key, data, lifetime, fn); | ||
}); | ||
}; |
@@ -15,3 +15,3 @@ 'use strict'; | ||
var memcached = new Memcached(host + ':' + port, store.options || {}); | ||
var memcached = store.client || new Memcached(host + ':' + port, store.options || {}); | ||
memcached.on('issue', issue); | ||
@@ -18,0 +18,0 @@ memcached.on('failure', failure); |
@@ -16,3 +16,3 @@ 'use strict'; | ||
var client = redis.createClient(port, host, store.options || {}); | ||
var client = store.client || redis.createClient(port, host, store.options || {}); | ||
client.on('error', error); | ||
@@ -22,2 +22,6 @@ | ||
set: function(key, value, lifetime, fn) { | ||
// Serialize array | ||
if (value[blacklist.TYPE.revoke]) { | ||
value[blacklist.TYPE.revoke] = value[blacklist.TYPE.revoke].toString(); | ||
} | ||
client.hmset(key, value, fn); | ||
@@ -28,6 +32,6 @@ if (lifetime) client.expire(key, lifetime); | ||
client.hgetall(key, function(err, res) { | ||
// De-serialize comma separated value to iat numbers | ||
// De-serialize comma separated value, convert to numbers if necessary | ||
if (res && res[blacklist.TYPE.revoke]) { | ||
res[blacklist.TYPE.revoke] = res[blacklist.TYPE.revoke].split(',').map(function(i) { | ||
return parseInt(i, 10); | ||
return (isNaN(i)) ? i : parseInt(i, 10); | ||
}); | ||
@@ -34,0 +38,0 @@ } |
@@ -18,3 +18,3 @@ 'use strict'; | ||
exports.nowInSeconds = function() { | ||
return Math.round(new Date().getTime() / 1000); | ||
return Math.floor(new Date().getTime() / 1000); | ||
} |
{ | ||
"name": "express-jwt-blacklist", | ||
"version": "1.0.2", | ||
"version": "1.1.0", | ||
"description": "express-jwt plugin for token blacklisting", | ||
@@ -23,3 +23,3 @@ "main": "lib/index.js", | ||
"memcached": "^2.2.1", | ||
"redis": "^2.4.2" | ||
"redis": "^2.5.3" | ||
}, | ||
@@ -26,0 +26,0 @@ "devDependencies": { |
@@ -44,2 +44,3 @@ # express-jwt-blacklist | ||
- `store.type` - Store type `memory`, `memcached` or `redis` (default: `memory`) | ||
- `store.client` - Client object, obviates store.host, store.port, store.options | ||
- `store.host` - Store host (default: `127.0.0.1`) | ||
@@ -49,3 +50,4 @@ - `store.port` - Store port (default: `11211` memcached, `6379` redis) | ||
- `store.options` - Additional store client options (default: `{}`) | ||
- `tokenId` - Unique JWT token identifier (default: `sub`) | ||
- `tokenId` - JWT claim unique to user (default: `sub`) | ||
- `indexBy` - JWT claim used for revocation (default: `iat`), note that purge still uses `iat` | ||
- `strict` - Strict revocation policy will return revoked `true` on store failure (default: `false`) | ||
@@ -73,10 +75,20 @@ | ||
### blacklist.revoke(user) | ||
### blacklist.revoke(user, [optionalLifetime], [optionalCallbackFn]) | ||
This function will revoke a token, by passing in the `req.user` set by express-jwt library. | ||
This function will revoke a token, by passing in a token payload skeleton in the `req.user` format set by the express-jwt library. The lifetime of the revocation entry in the store, can optionally be set explicitly (in seconds), and is otherwise calculated from the `exp` claim. If no argument is provided and the token is missing the `exp` claim, the revocation entry will not expire. An optional callback function can be supplied that will be called on error with the error as its only argument. | ||
### blacklist.purge(user) | ||
Typically, the server backend will call this function when a particular route is hit and the token to be revoked is the same one supplied for authentication, i.e. in a logout route initiated by the user in question. Alternatively, the backend can construct a token payload skeleton, which may be useful in a case where an admin user would like to forcibly logout a user from a single session. In the latter case, it may be useful to set the lifetime argument explicitly, as the proper value for the `exp` claim will likely be unavailable. | ||
This function will purge **all** tokens older than current timestamp, by passing in the `req.user` set by express-jwt library. | ||
By default, revocation is based on the claim specified by `tokenId` as well as the `iat` claim, resulting in revocation of only the provided `req.user` token. The optional `index` configuration argument allows revocation of all tokens issued for a specific user that share the same value for the specified claim with `req.user`. | ||
The `index` argument may be useful if tokens are being refreshed, and you would therefore like to invalidate some, but not all, of the previously issued tokens, e.g. only those from a specific session. | ||
In particular, your token scheme may use the `sub` claim to represent the user, and the `jti` claim to represent a session, where the original and all subsequent refreshed tokens contain identical `sub` and `jti` claims, but other sessions for the user contain an identical `sub` claim, but different `jti` claims. In this scenario, `tokenId` would be set to `sub` (the default), and the `index` should be set to `jti`. Note that if one user in this scenario is issued a token with a `jti` claim identical to a token that has been revoked for a different user, it will still not be marked as revoked, as revocation is always based on the `tokenId` as well as the `index` argument. | ||
### blacklist.purge(user, [optionalLifetime], [optionalCallbackFn]) | ||
This function will purge **all** tokens older than current timestamp, by passing in a a token payload skeleton in the `req.user` format set by the express-jwt library. The lifetime of the revocation entry in the store, can optionally be set explicitly (in seconds), and is otherwise calculated from the `exp` claim. If no argument is provided and the token is missing the `exp` claim, the revocation entry will not expire. An optional callback function can be supplied that will be called on error with the error as its only argument. | ||
Typically, the server backend will call this function when a particular route is hit and the tokens to be purged are similar to the one supplied for authentication, i.e. in a password change route initiated by the user in question. Alternatively, the backend can construct a token payload skeleton, which may be useful in a case where an admin user would like to forcibly logout all sessions for a different user. In the latter case, it may be useful to set the lifetime argument explicitly, as the proper value for the `exp` claim will likely be unavailable. | ||
### Custom store | ||
@@ -89,9 +101,9 @@ | ||
### Considerations | ||
### Token Payload Considerations | ||
User object `req.user` that's being set by the express-jwt library **should** contain and match `tokenId` from configuration. | ||
User object `req.user` that's being set by the express-jwt library **should** contain claims matching `tokenId` and 'indexBy' from configuration. | ||
- You need to set either `sub` or `jti` or some other key in the payload when siging a JWT token. | ||
- Issued at `iat` timestamp should be present. | ||
- Expiration timestamp `exp` is optional but desired. | ||
- At a minimum, you need to set either `sub` or `jti` or some other claim in the payload when signing a JWT token to identify a user. | ||
- Expiration timestamp `exp` claim is optional but desired, as it will allow for expiration of revocation entries from the store, increasing the speed of the `isRevoked` check. Alternatively, a specified lifetime value can be passed to each revoke/purge call by the backend. | ||
- Issued at `iat` timestamp claim must be present, even if `indexBy` is set to another claim, so as to allow purge operations to work. `iat` is also used to calculate token lifetime, if no specified lifetime is set, and `exp` is present. | ||
@@ -98,0 +110,0 @@ ## Why blacklist? |
@@ -1,2 +0,2 @@ | ||
/*globals describe it*/ | ||
/*globals describe, it, before*/ | ||
'use strict'; | ||
@@ -7,8 +7,2 @@ | ||
var blacklist = require('../../lib'); | ||
blacklist.configure({ | ||
store: { | ||
type: 'memcached', | ||
keyPrefix: 'express-jwt-blacklist-test:' | ||
} | ||
}); | ||
@@ -22,2 +16,12 @@ var JWT_USER = { | ||
describe('Blacklist memcached operations', function() { | ||
before(function(done) { | ||
blacklist.configure({ | ||
store: { | ||
type: 'memcached', | ||
keyPrefix: 'express-jwt-blacklist-test:' | ||
} | ||
}); | ||
done(); | ||
}); | ||
it('isRevoked should return false', function(done) { | ||
@@ -24,0 +28,0 @@ blacklist.isRevoked({}, JWT_USER, function(err, revoked) { |
@@ -1,2 +0,2 @@ | ||
/*globals describe it*/ | ||
/*globals describe, it, before*/ | ||
'use strict'; | ||
@@ -7,8 +7,2 @@ | ||
var blacklist = require('../../lib'); | ||
blacklist.configure({ | ||
store: { | ||
type: 'redis', | ||
keyPrefix: 'express-jwt-blacklist-test:' | ||
} | ||
}); | ||
@@ -22,2 +16,12 @@ var JWT_USER = { | ||
describe('Blacklist redis operations', function() { | ||
before(function(done) { | ||
blacklist.configure({ | ||
store: { | ||
type: 'redis', | ||
keyPrefix: 'express-jwt-blacklist-test:' | ||
} | ||
}); | ||
done(); | ||
}); | ||
it('isRevoked should return false', function(done) { | ||
@@ -24,0 +28,0 @@ blacklist.isRevoked({}, JWT_USER, function(err, revoked) { |
@@ -1,2 +0,2 @@ | ||
/*globals describe it*/ | ||
/*globals describe, it*/ | ||
'use strict'; | ||
@@ -52,2 +52,12 @@ | ||
}); | ||
it('should throw error on invalid indexBy configuration', function() { | ||
try { | ||
blacklist.configure({ | ||
indexBy: 123 | ||
}); | ||
} catch(e) { | ||
should.exist(e); | ||
} | ||
}); | ||
@@ -116,2 +126,22 @@ it('should throw error on invalid keyPrefix configuration', function() { | ||
}); | ||
it('revoke should revoke another JWT token without a callback', function(done) { | ||
JWT_USER.iat += 10; | ||
blacklist.revoke(JWT_USER) | ||
blacklist.isRevoked({}, JWT_USER, function(err, revoked) { | ||
should.not.exist(err); | ||
revoked.should.be.true(); | ||
done(); | ||
}); | ||
}); | ||
it('revoke should revoke another JWT token without the full original token', function(done) { | ||
JWT_USER.iat += 10; | ||
blacklist.revoke({ iat: JWT_USER.iat, sub: JWT_USER.sub }) | ||
blacklist.isRevoked({}, JWT_USER, function(err, revoked) { | ||
should.not.exist(err); | ||
revoked.should.be.true(); | ||
done(); | ||
}); | ||
}); | ||
}); |
@@ -1,2 +0,2 @@ | ||
/*globals describe it*/ | ||
/*globals describe, it, before*/ | ||
'use strict'; | ||
@@ -17,3 +17,3 @@ | ||
describe('Blacklist custom store', function() { | ||
beforeEach(function() { | ||
before(function() { | ||
blacklist.configure({ | ||
@@ -20,0 +20,0 @@ store: { |
@@ -1,2 +0,2 @@ | ||
/*globals describe it*/ | ||
/*globals describe, it*/ | ||
'use strict'; | ||
@@ -3,0 +3,0 @@ |
Sorry, the diff of this file is not supported yet
39507
585
140
Updatedredis@^2.5.3