From aa23c181f3f3eecf5adcb3dbae009a589d41eb22 Mon Sep 17 00:00:00 2001 From: Matteo Pietro Dazzi Date: Tue, 12 Nov 2024 19:42:52 +0100 Subject: [PATCH 1/5] feat: token hasher option --- src/verifier.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/verifier.js b/src/verifier.js index 3a6078e..c99707b 100644 --- a/src/verifier.js +++ b/src/verifier.js @@ -80,7 +80,8 @@ function cacheSet( maxAge, clockTimestamp, clockTolerance, - errorCacheTTL + errorCacheTTL, + tokenHasher }, value ) { @@ -93,7 +94,7 @@ function cacheSet( if (value instanceof TokenError) { const ttl = typeof errorCacheTTL === 'function' ? errorCacheTTL(value) : errorCacheTTL cacheValue[2] = (clockTimestamp || Date.now()) + clockTolerance + ttl - cache.set(hashToken(token), cacheValue) + cache.set(tokenHasher(token), cacheValue) return value } @@ -116,7 +117,7 @@ function cacheSet( const maxTTL = (clockTimestamp || Date.now()) + clockTolerance + cacheTTL cacheValue[2] = cacheValue[2] === 0 ? maxTTL : Math.min(cacheValue[2], maxTTL) - cache.set(hashToken(token), cacheValue) + cache.set(tokenHasher(token), cacheValue) return value } @@ -258,7 +259,8 @@ function verify( decode, cache, requiredClaims, - errorCacheTTL + errorCacheTTL, + tokenHasher }, token, cb @@ -275,12 +277,13 @@ function verify( ignoreNotBefore, maxAge, clockTimestamp, - clockTolerance + clockTolerance, + tokenHasher } // Check the cache if (cache) { - const [value, min, max] = cache.get(hashToken(token)) || [undefined, 0, 0] + const [value, min, max] = cache.get(tokenHasher(token)) || [undefined, 0, 0] const now = clockTimestamp || Date.now() // Validate time range @@ -392,8 +395,9 @@ module.exports = function createVerifier(options) { allowedIss, allowedSub, allowedNonce, - requiredClaims - } = { cacheTTL: 600000, clockTolerance: 0, errorCacheTTL: -1, ...options } + requiredClaims, + tokenHasher + } = { cacheTTL: 600000, clockTolerance: 0, errorCacheTTL: -1, tokenHasher: hashToken, ...options } // Validate options if (!Array.isArray(allowedAlgorithms)) { @@ -516,7 +520,8 @@ module.exports = function createVerifier(options) { validators, decode: createDecoder({ complete: true }), cache: createCache(cacheSize), - requiredClaims + requiredClaims, + tokenHasher } // Return the verifier From 3895f0d12139560741b892493d0c4d369f7fafda Mon Sep 17 00:00:00 2001 From: Matteo Pietro Dazzi Date: Tue, 12 Nov 2024 19:47:54 +0100 Subject: [PATCH 2/5] chore: verifier --- README.md | 2 ++ test/verifier.spec.js | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/README.md b/README.md index 973500e..283fa83 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`. +- `tokenHasher`: The function that will be used to hash the token. By default is used [the hashToken function](./src/utils.js). + 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. diff --git a/test/verifier.spec.js b/test/verifier.spec.js index b365d8a..8837e55 100644 --- a/test/verifier.spec.js +++ b/test/verifier.spec.js @@ -1021,6 +1021,27 @@ test('caching - sync', t => { t.assert.ok(verifier.cache.get(hashToken(invalidToken))[0] instanceof TokenError) }) +test('caching - sync - custom tokenHasher', t => { + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxfQ.57TF7smP9XDhIexBqPC-F1toZReYZLWb_YRU5tv0sxM' + const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxfQ.aaa' + + const verifier = createVerifier({ key: 'secret', cache: true, tokenHasher: (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' From 2aae9c6d402e0924527557a1edc71e0d4608c2e0 Mon Sep 17 00:00:00 2001 From: Matteo Pietro Dazzi Date: Wed, 13 Nov 2024 22:23:30 +0100 Subject: [PATCH 3/5] chore: renamed parameter --- README.md | 2 +- src/verifier.js | 18 +++++++++--------- test/verifier.spec.js | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 283fa83..1baa07f 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ 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`. -- `tokenHasher`: The function that will be used to hash the token. By default is used [the hashToken function](./src/utils.js). +- `cacheKeyBuilder`: The function that will be used to create the [cache's key](#caching). To avoid sensitive informations leaking, by default is used [the hashToken function](./src/utils.js). The verifier is a function which accepts a token (as Buffer or string) and returns the payload or the sections of the token. diff --git a/src/verifier.js b/src/verifier.js index c99707b..670245e 100644 --- a/src/verifier.js +++ b/src/verifier.js @@ -81,7 +81,7 @@ function cacheSet( clockTimestamp, clockTolerance, errorCacheTTL, - tokenHasher + cacheKeyBuilder }, value ) { @@ -94,7 +94,7 @@ function cacheSet( if (value instanceof TokenError) { const ttl = typeof errorCacheTTL === 'function' ? errorCacheTTL(value) : errorCacheTTL cacheValue[2] = (clockTimestamp || Date.now()) + clockTolerance + ttl - cache.set(tokenHasher(token), cacheValue) + cache.set(cacheKeyBuilder(token), cacheValue) return value } @@ -117,7 +117,7 @@ function cacheSet( const maxTTL = (clockTimestamp || Date.now()) + clockTolerance + cacheTTL cacheValue[2] = cacheValue[2] === 0 ? maxTTL : Math.min(cacheValue[2], maxTTL) - cache.set(tokenHasher(token), cacheValue) + cache.set(cacheKeyBuilder(token), cacheValue) return value } @@ -260,7 +260,7 @@ function verify( cache, requiredClaims, errorCacheTTL, - tokenHasher + cacheKeyBuilder }, token, cb @@ -278,12 +278,12 @@ function verify( maxAge, clockTimestamp, clockTolerance, - tokenHasher + cacheKeyBuilder } // Check the cache if (cache) { - const [value, min, max] = cache.get(tokenHasher(token)) || [undefined, 0, 0] + const [value, min, max] = cache.get(cacheKeyBuilder(token)) || [undefined, 0, 0] const now = clockTimestamp || Date.now() // Validate time range @@ -396,8 +396,8 @@ module.exports = function createVerifier(options) { allowedSub, allowedNonce, requiredClaims, - tokenHasher - } = { cacheTTL: 600000, clockTolerance: 0, errorCacheTTL: -1, tokenHasher: hashToken, ...options } + cacheKeyBuilder + } = { cacheTTL: 600000, clockTolerance: 0, errorCacheTTL: -1, cacheKeyBuilder: hashToken, ...options } // Validate options if (!Array.isArray(allowedAlgorithms)) { @@ -521,7 +521,7 @@ module.exports = function createVerifier(options) { decode: createDecoder({ complete: true }), cache: createCache(cacheSize), requiredClaims, - tokenHasher + cacheKeyBuilder } // Return the verifier diff --git a/test/verifier.spec.js b/test/verifier.spec.js index 8837e55..020a18a 100644 --- a/test/verifier.spec.js +++ b/test/verifier.spec.js @@ -1021,11 +1021,11 @@ test('caching - sync', t => { t.assert.ok(verifier.cache.get(hashToken(invalidToken))[0] instanceof TokenError) }) -test('caching - sync - custom tokenHasher', t => { +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, tokenHasher: (id) => id }) + const verifier = createVerifier({ key: 'secret', cache: true, cacheKeyBuilder: (id) => id }) t.assert.equal(verifier.cache.size, 0) t.assert.deepStrictEqual(verifier(token), { a: 1 }) From 4da7e7a7abfc4abe6772caadb1d2b668de13a9b1 Mon Sep 17 00:00:00 2001 From: Matteo Pietro Dazzi Date: Thu, 14 Nov 2024 12:11:55 +0100 Subject: [PATCH 4/5] Update README.md Co-authored-by: Simone Busoli --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1baa07f..e08f45c 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ 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). To avoid sensitive informations leaking, by default is used [the hashToken function](./src/utils.js). +- `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. From d3acb866ac9ceb0f5d34d37c59d4881472b31534 Mon Sep 17 00:00:00 2001 From: Matteo Pietro Dazzi Date: Wed, 20 Nov 2024 08:16:29 +0100 Subject: [PATCH 5/5] chore: performance of cacheKeyBuilder --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e08f45c..060def8 100644 --- a/README.md +++ b/README.md @@ -276,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