diff --git a/README.md b/README.md index 973500e..060def8 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,8 @@ Create a verifier function by calling `createVerifier` and providing one or more - `clockTolerance`: Timespan in milliseconds is the tolerance to apply to the current timestamp when performing time comparisons. Default is `0`. +- `cacheKeyBuilder`: The function that will be used to create the [cache's key](#caching) for each token. To mitigate the risk of leaking sensitive information and generate collisions, [a hashing function](./src/utils.js) is used by default. + The verifier is a function which accepts a token (as Buffer or string) and returns the payload or the sections of the token. If the `key` option is a function, the signer will also accept a Node style callback and will return a promise, supporting therefore both callback and async/await styles. @@ -274,6 +276,10 @@ For verified tokens, caching considers the time sensitive claims of the token (` Performances improvements varies by uses cases and by the type of the operation performed and the algorithm used. +The default `cacheKeyBuilder` is a function that hashes the token. This provides a good level of protection against sensitive information leaks, but it also has a significant performance impact (almost 10x slower, as it's a CPU bound operation). If you are using caching and you are not concerned about potential information leaks you can use the identity function as `cacheKeyBuilder` to improve them. + +For a detailed discussion about it, take a look at [this issue](https://github.com/nearform/fast-jwt/issues/503). + > **_Note:_** Errors are not cached by default, to change this behaviour use the `errorCacheTTL` option. ## Token Error Codes diff --git a/src/verifier.js b/src/verifier.js index a6b0823..934b0e2 100644 --- a/src/verifier.js +++ b/src/verifier.js @@ -68,7 +68,8 @@ function cacheSet( maxAge, clockTimestamp = Date.now(), clockTolerance, - errorCacheTTL + errorCacheTTL, + cacheKeyBuilder }, value ) { @@ -81,7 +82,7 @@ function cacheSet( if (value instanceof TokenError) { const ttl = typeof errorCacheTTL === 'function' ? errorCacheTTL(value) : errorCacheTTL cacheValue[2] = clockTimestamp + clockTolerance + ttl - cache.set(hashToken(token), cacheValue) + cache.set(cacheKeyBuilder(token), cacheValue) return value } @@ -104,7 +105,7 @@ function cacheSet( const maxTTL = clockTimestamp + clockTolerance + cacheTTL cacheValue[2] = cacheValue[2] === 0 ? maxTTL : Math.min(cacheValue[2], maxTTL) - cache.set(hashToken(token), cacheValue) + cache.set(cacheKeyBuilder(token), cacheValue) return value } @@ -241,7 +242,8 @@ function verify( decode, cache, requiredClaims, - errorCacheTTL + errorCacheTTL, + cacheKeyBuilder }, token, cb @@ -250,7 +252,7 @@ function verify( // Check the cache if (cache) { - const [value, min, max] = cache.get(hashToken(token)) || [undefined, 0, 0] + const [value, min, max] = cache.get(cacheKeyBuilder(token)) || [undefined, 0, 0] const now = clockTimestamp || Date.now() // Validate time range @@ -294,7 +296,8 @@ function verify( maxAge, clockTimestamp, clockTolerance, - payload + payload, + cacheKeyBuilder } const validationContext = { validators, allowedAlgorithms, checkTyp, clockTimestamp, clockTolerance, requiredClaims } @@ -373,8 +376,9 @@ module.exports = function createVerifier(options) { allowedIss, allowedSub, allowedNonce, - requiredClaims - } = { cacheTTL: 600_000, clockTolerance: 0, errorCacheTTL: -1, ...options } + requiredClaims, + cacheKeyBuilder + } = { cacheTTL: 600_000, clockTolerance: 0, errorCacheTTL: -1, cacheKeyBuilder: hashToken, ...options } // Validate options if (!Array.isArray(allowedAlgorithms)) { @@ -494,7 +498,8 @@ module.exports = function createVerifier(options) { validators, decode: createDecoder({ complete: true }), cache: createCache(cacheSize), - requiredClaims + requiredClaims, + cacheKeyBuilder } // Return the verifier diff --git a/test/verifier.spec.js b/test/verifier.spec.js index ec9aa15..80b2f06 100644 --- a/test/verifier.spec.js +++ b/test/verifier.spec.js @@ -1004,6 +1004,27 @@ test('caching - sync', t => { t.assert.ok(verifier.cache.get(hashToken(invalidToken))[0] instanceof TokenError) }) +test('caching - sync - custom cacheKeyBuilder', t => { + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxfQ.57TF7smP9XDhIexBqPC-F1toZReYZLWb_YRU5tv0sxM' + const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxfQ.aaa' + + const verifier = createVerifier({ key: 'secret', cache: true, cacheKeyBuilder: (id) => id }) + + t.assert.equal(verifier.cache.size, 0) + t.assert.deepStrictEqual(verifier(token), { a: 1 }) + t.assert.equal(verifier.cache.size, 1) + t.assert.deepStrictEqual(verifier(token), { a: 1 }) + t.assert.equal(verifier.cache.size, 1) + + t.assert.throws(() => verifier(invalidToken), { message: 'The token signature is invalid.' }) + t.assert.equal(verifier.cache.size, 2) + t.assert.throws(() => verifier(invalidToken), { message: 'The token signature is invalid.' }) + t.assert.equal(verifier.cache.size, 2) + + t.assert.deepStrictEqual(verifier.cache.get(token)[0], { a: 1 }) + t.assert.ok(verifier.cache.get(invalidToken)[0] instanceof TokenError) +}) + test('caching - async', async t => { const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxfQ.57TF7smP9XDhIexBqPC-F1toZReYZLWb_YRU5tv0sxM' const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxfQ.aaa'