Skip to content

Commit 3ebf056

Browse files
error caching ttl (nearform#277)
* feat: error caching ttl * feat: errorCacheTTL as function * format: changed arrowParens to avoid * feat: error codes exported * fix: improved documentation
1 parent 31209b3 commit 3ebf056

11 files changed

+323
-101
lines changed

README.md

+25-3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ The payload must be an object.
5656
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.
5757

5858
If the `key` is a passphrase protected private key, the `algorithm` option must be provided and must be either a `RS*` or `ES*` encoded key and the `key` option must be an object with the following structure:
59+
5960
```js
6061
{
6162
key: '<YOUR_RSA_ENCRYPTED_PRIVATE_KEY>',
@@ -147,6 +148,8 @@ Create a verifier function by calling `createVerifier` and providing one or more
147148
148149
- `cacheTTL`: The maximum time to live of a cache entry (in milliseconds). If the token has a earlier expiration or the verifier has a shorter `maxAge`, the earlier takes precedence. The default is `600000`, which is 10 minutes.
149150
151+
- `errorCacheTTL`: A number or function `function (tokenError) => number` that represents the maximum time to live of a cache error entry (in milliseconds). Example: the `key` function fails or does not return a secret or public key. By default **errors are not cached**, the `errorCacheTTL` default value is `0`.
152+
150153
- `allowedJti`: A string, a regular expression, an array of strings or an array of regular expressions containing allowed values for the id claim (`jti`). By default, all values are accepted.
151154

152155
- `allowedAud`: A string, a regular expression, an array of strings or an array of regular expressions containing allowed values for the audience claim (`aud`). By default, all values are accepted.
@@ -176,7 +179,7 @@ If the `key` option is a function, the signer will also accept a Node style call
176179
#### Examples
177180

178181
```javascript
179-
const { createVerifier } = require('fast-jwt')
182+
const { createVerifier, TOKEN_ERROR_CODES } = require('fast-jwt')
180183
const token =
181184
'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJiIjoyLCJjIjozLCJpYXQiOjE1Nzk1MjEyMTJ9.mIcxteEVjbh2MnKQ3EQlojZojGSyA_guqRBYHQURcfnCSSBTT2OShF8lo9_ogjAv-5oECgmCur_cDWB7x3X53g'
182185

@@ -186,7 +189,7 @@ const payload = verifySync(token)
186189
// => { a: 1, b: 2, c: 3, iat: 1579521212 }
187190

188191
// Callback style with complete return
189-
const verifyWithCallback = createVerifier({ key: (callback) => callback(null, 'secret'), complete: true })
192+
const verifyWithCallback = createVerifier({ key: callback => callback(null, 'secret'), complete: true })
190193

191194
verifyWithCallback(token, (err, sections) => {
192195
/*
@@ -206,6 +209,19 @@ async function test() {
206209
const payload = await verifyWithPromise(token)
207210
// => { a: 1, b: 2, c: 3, iat: 1579521212 }
208211
}
212+
213+
// custom errorCacheTTL verifier
214+
const verifier = createVerifier({
215+
key: 'secret',
216+
cache: true,
217+
errorCacheTTL: tokenError => {
218+
// customize the ttl based on the error code
219+
if (tokenError.code === TOKEN_ERROR_CODES.invalidKey) {
220+
return 1000
221+
}
222+
return 2000
223+
}
224+
})
209225
```
210226
211227
## Algorithms supported
@@ -235,12 +251,18 @@ fast-jwt supports caching of verified tokens.
235251
236252
The cache layer, powered by [mnemonist](https://www.npmjs.com/package/mnemonist), is a LRU cache which dimension is controlled by the user, as described in the options list.
237253
238-
When caching is enabled, verified tokens are always stored in cache. If the verification fails once, the error is cached as well and the operation is not retried.
254+
When caching is enabled, verified tokens are always stored in cache. If the verification fails once, the error is cached as well for the time set by `errorCacheTTL` and the operation is not retried.
239255
240256
For verified tokens, caching considers the time sensitive claims of the token (`iat`, `nbf` and `exp`) and make sure the verification is retried after a token becomes valid or after a token becomes expired.
241257
242258
Performances improvements varies by uses cases and by the type of the operation performed and the algorithm used.
243259
260+
> **_Note:_** Errors are not cached by default, to change this behaviour use the `errorCacheTTL` option.
261+
262+
## Token Error Codes
263+
264+
[Error codes](https://github.com/nearform/fast-jwt/blob/master/src/error.js) exported by `TOKEN_ERROR_CODES`.
265+
244266
## JWKS
245267
246268
JWKS is supported via [get-jwks](https://github.com/nearform/get-jwks). Check out the documentation for integration examples.

prettier.config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ module.exports = {
33
semi: false,
44
singleQuote: true,
55
bracketSpacing: true,
6-
trailingComma: 'none'
6+
trailingComma: 'none',
7+
arrowParens: 'avoid'
78
}

src/crypto.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const {
1818
let { sign: directSign, verify: directVerify } = require('crypto')
1919
const { joseToDer, derToJose } = require('ecdsa-sig-formatter')
2020
const Cache = require('mnemonist/lru-cache')
21-
const TokenError = require('./error')
21+
const { TokenError } = require('./error')
2222

2323
const useNewCrypto = typeof directSign === 'function'
2424

src/decoder.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict'
22

3-
const TokenError = require('./error')
3+
const { TokenError } = require('./error')
44

55
function decode({ complete, checkTyp }, token) {
66
// Make sure the token is a string or a Buffer - Other cases make no sense to even try to validate

src/error.js

+26-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
'use strict'
22

3+
const TOKEN_ERROR_CODES = {
4+
invalidType: 'FAST_JWT_INVALID_TYPE', // Invalid token type
5+
invalidOption: 'FAST_JWT_INVALID_OPTION', // The option object is not valid
6+
invalidAlgorithm: 'FAST_JWT_INVALID_ALGORITHM', // The token algorithm is invalid
7+
invalidClaimType: 'FAST_JWT_INVALID_CLAIM_TYPE', // The claim type is not supported
8+
invalidClaimValue: 'FAST_JWT_INVALID_CLAIM_VALUE', // The claim type is not a positive integer or an number array
9+
invalidKey: 'FAST_JWT_INVALID_KEY', // The key is not a string or a buffer or is unsupported
10+
invalidSignature: 'FAST_JWT_INVALID_SIGNATURE', // The token signature is invalid
11+
invalidPayload: 'FAST_JWT_INVALID_PAYLOAD', // The payload to be decoded must be an object
12+
malformed: 'FAST_JWT_MALFORMED', // The token is malformed
13+
inactive: 'FAST_JWT_INACTIVE', // The token is not valid yet
14+
expired: 'FAST_JWT_EXPIRED', // The token is expired
15+
missingKey: 'FAST_JWT_MISSING_KEY', // The key option is missing
16+
keyFetchingError: 'FAST_JWT_KEY_FETCHING_ERROR', // Could not retrieve the key
17+
signError: 'FAST_JWT_SIGN_ERROR', // Cannot create the signature
18+
verifyError: 'FAST_JWT_VERIFY_ERROR', // Cannot verify the signature
19+
missingRequiredClaim: 'FAST_JWT_MISSING_REQUIRED_CLAIM', // A required claim is missing
20+
missingSignature: 'FAST_JWT_MISSING_SIGNATURE' // The token signature is missing
21+
}
22+
323
class TokenError extends Error {
424
constructor(code, message, additional) {
525
super(message)
@@ -15,31 +35,17 @@ class TokenError extends Error {
1535
}
1636
}
1737

18-
TokenError.codes = {
19-
invalidType: 'FAST_JWT_INVALID_TYPE',
20-
invalidOption: 'FAST_JWT_INVALID_OPTION',
21-
invalidAlgorithm: 'FAST_JWT_INVALID_ALGORITHM',
22-
invalidClaimType: 'FAST_JWT_INVALID_CLAIM_TYPE',
23-
invalidClaimValue: 'FAST_JWT_INVALID_CLAIM_VALUE',
24-
invalidKey: 'FAST_JWT_INVALID_KEY',
25-
invalidSignature: 'FAST_JWT_INVALID_SIGNATURE',
26-
invalidPayload: 'FAST_JWT_INVALID_PAYLOAD',
27-
malformed: 'FAST_JWT_MALFORMED',
28-
inactive: 'FAST_JWT_INACTIVE',
29-
expired: 'FAST_JWT_EXPIRED',
30-
missingKey: 'FAST_JWT_MISSING_KEY',
31-
keyFetchingError: 'FAST_JWT_KEY_FETCHING_ERROR',
32-
signError: 'FAST_JWT_SIGN_ERROR',
33-
verifyError: 'FAST_JWT_VERIFY_ERROR',
34-
missingRequiredClaim: 'FAST_JWT_MISSING_REQUIRED_CLAIM'
35-
}
38+
TokenError.codes = TOKEN_ERROR_CODES
3639

37-
TokenError.wrap = function(originalError, code, message) {
40+
TokenError.wrap = function (originalError, code, message) {
3841
if (originalError instanceof TokenError) {
3942
return originalError
4043
}
4144

4245
return new TokenError(code, message, { originalError })
4346
}
4447

45-
module.exports = TokenError
48+
module.exports = {
49+
TokenError,
50+
TOKEN_ERROR_CODES
51+
}

src/index.d.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,26 @@ export type Algorithm =
1616
| 'PS512'
1717
| 'EdDSA'
1818

19+
export type TokenValidationErrorCode =
20+
| 'FAST_JWT_INVALID_TYPE'
21+
| 'FAST_JWT_INVALID_OPTION'
22+
| 'FAST_JWT_INVALID_ALGORITHM'
23+
| 'FAST_JWT_INVALID_CLAIM_TYPE'
24+
| 'FAST_JWT_INVALID_CLAIM_VALUE'
25+
| 'FAST_JWT_INVALID_KEY'
26+
| 'FAST_JWT_INVALID_SIGNATURE'
27+
| 'FAST_JWT_INVALID_PAYLOAD'
28+
| 'FAST_JWT_MALFORMED'
29+
| 'FAST_JWT_INACTIVE'
30+
| 'FAST_JWT_EXPIRED'
31+
| 'FAST_JWT_MISSING_KEY'
32+
| 'FAST_JWT_KEY_FETCHING_ERROR'
33+
| 'FAST_JWT_SIGN_ERROR'
34+
| 'FAST_JWT_VERIFY_ERROR'
35+
| 'FAST_JWT_MISSING_SIGNATURE'
36+
1937
declare class TokenError extends Error {
20-
static wrap(originalError: Error, code: string, message: string): TokenError
38+
static wrap(originalError: Error, code: TokenValidationErrorCode, message: string): TokenError
2139
static codes: {
2240
invalidType: 'FAST_JWT_INVALID_TYPE'
2341
invalidOption: 'FAST_JWT_INVALID_OPTION'
@@ -34,9 +52,10 @@ declare class TokenError extends Error {
3452
keyFetchingError: 'FAST_JWT_KEY_FETCHING_ERROR'
3553
signError: 'FAST_JWT_SIGN_ERROR'
3654
verifyError: 'FAST_JWT_VERIFY_ERROR'
55+
missingSignature: 'FAST_JWT_MISSING_SIGNATURE'
3756
}
3857

39-
code: string;
58+
code: TokenValidationErrorCode;
4059
[key: string]: any
4160
}
4261

@@ -94,6 +113,7 @@ export interface VerifierOptions {
94113
complete: boolean
95114
cache: boolean | number
96115
cacheTTL: number
116+
errorCacheTTL: number | ((tokenError: TokenError) => number)
97117
allowedJti: string | RegExp | Array<string | RegExp>
98118
allowedAud: string | RegExp | Array<string | RegExp>
99119
allowedIss: string | RegExp | Array<string | RegExp>

src/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use strict'
22

3-
const TokenError = require('./error')
3+
const { TokenError, TOKEN_ERROR_CODES } = require('./error')
44
const createDecoder = require('./decoder')
55
const createVerifier = require('./verifier')
66
const createSigner = require('./signer')
77

88
module.exports = {
99
TokenError,
10+
TOKEN_ERROR_CODES,
1011
createDecoder,
1112
createVerifier,
1213
createSigner

src/signer.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const {
1111
detectPrivateKeyAlgorithm,
1212
createSignature
1313
} = require('./crypto')
14-
const TokenError = require('./error')
14+
const { TokenError } = require('./error')
1515
const { getAsyncKey, ensurePromiseCallback } = require('./utils')
1616
const { createPrivateKey, createSecretKey } = require('crypto')
1717

src/verifier.js

+61-10
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const Cache = require('mnemonist/lru-cache')
55

66
const { useNewCrypto, hsAlgorithms, verifySignature, detectPublicKeyAlgorithms } = require('./crypto')
77
const createDecoder = require('./decoder')
8-
const TokenError = require('./error')
8+
const { TokenError } = require('./error')
99
const { getAsyncKey, ensurePromiseCallback, hashToken } = require('./utils')
1010

1111
const defaultCacheSize = 1000
@@ -70,19 +70,38 @@ function createCache(rawSize) {
7070
}
7171

7272
function cacheSet(
73-
{ cache, token, cacheTTL, payload, ignoreExpiration, ignoreNotBefore, maxAge, clockTimestamp, clockTolerance },
73+
{
74+
cache,
75+
token,
76+
cacheTTL,
77+
payload,
78+
ignoreExpiration,
79+
ignoreNotBefore,
80+
maxAge,
81+
clockTimestamp,
82+
clockTolerance,
83+
errorCacheTTL
84+
},
7485
value
7586
) {
7687
if (!cache) {
7788
return value
7889
}
7990

8091
const cacheValue = [value, 0, 0]
92+
93+
if (value instanceof TokenError) {
94+
const ttl = typeof errorCacheTTL === 'function' ? errorCacheTTL(value) : errorCacheTTL
95+
cacheValue[2] = (clockTimestamp || Date.now()) + clockTolerance + ttl
96+
cache.set(hashToken(token), cacheValue)
97+
return value
98+
}
99+
81100
const hasIat = payload && typeof payload.iat === 'number'
82101

83102
// Add time range of the token
84103
if (hasIat) {
85-
cacheValue[1] = !ignoreNotBefore && typeof payload.nbf === 'number' ? (payload.nbf * 1000 - clockTolerance) : 0
104+
cacheValue[1] = !ignoreNotBefore && typeof payload.nbf === 'number' ? payload.nbf * 1000 - clockTolerance : 0
86105

87106
if (!ignoreExpiration) {
88107
if (typeof payload.exp === 'number') {
@@ -233,7 +252,8 @@ function verify(
233252
validators,
234253
decode,
235254
cache,
236-
requiredClaims
255+
requiredClaims,
256+
errorCacheTTL
237257
},
238258
token,
239259
cb
@@ -244,6 +264,7 @@ function verify(
244264
cache,
245265
token,
246266
cacheTTL,
267+
errorCacheTTL,
247268
payload: undefined,
248269
ignoreExpiration,
249270
ignoreNotBefore,
@@ -258,9 +279,14 @@ function verify(
258279
const now = clockTimestamp || Date.now()
259280

260281
// Validate time range
261-
if (typeof value !== 'undefined' &&
262-
(min === 0 || (now < min && value.code === 'FAST_JWT_INACTIVE') || (now >= min && value.code !== 'FAST_JWT_INACTIVE')) &&
263-
(max === 0 || now <= max)) {
282+
if (
283+
/* istanbul ignore next */
284+
typeof value !== 'undefined' &&
285+
(min === 0 ||
286+
(now < min && value.code === 'FAST_JWT_INACTIVE') ||
287+
(now >= min && value.code !== 'FAST_JWT_INACTIVE')) &&
288+
(max === 0 || now <= max)
289+
) {
264290
// Cache hit
265291
return handleCachedResult(value, callback, promise)
266292
}
@@ -349,6 +375,7 @@ module.exports = function createVerifier(options) {
349375
complete,
350376
cache: cacheSize,
351377
cacheTTL,
378+
errorCacheTTL,
352379
checkTyp,
353380
clockTimestamp,
354381
clockTolerance,
@@ -361,7 +388,7 @@ module.exports = function createVerifier(options) {
361388
allowedSub,
362389
allowedNonce,
363390
requiredClaims
364-
} = { cacheTTL: 600000, clockTolerance: 0, ...options }
391+
} = { cacheTTL: 600000, clockTolerance: 0, errorCacheTTL: 0, ...options }
365392

366393
// Validate options
367394
if (!Array.isArray(allowedAlgorithms)) {
@@ -401,6 +428,16 @@ module.exports = function createVerifier(options) {
401428
throw new TokenError(TokenError.codes.invalidOption, 'The cacheTTL option must be a positive number.')
402429
}
403430

431+
if (
432+
(errorCacheTTL && typeof errorCacheTTL !== 'function' && typeof errorCacheTTL !== 'number') ||
433+
errorCacheTTL < 0
434+
) {
435+
throw new TokenError(
436+
TokenError.codes.invalidOption,
437+
'The errorCacheTTL option must be a positive number or a function.'
438+
)
439+
}
440+
404441
if (requiredClaims && !Array.isArray(requiredClaims)) {
405442
throw new TokenError(TokenError.codes.invalidOption, 'The requiredClaims option must be an array.')
406443
}
@@ -409,11 +446,24 @@ module.exports = function createVerifier(options) {
409446
const validators = []
410447

411448
if (!ignoreNotBefore) {
412-
validators.push({ type: 'date', claim: 'nbf', errorCode: 'inactive', errorVerb: 'will be active', greater: true, modifier: -clockTolerance })
449+
validators.push({
450+
type: 'date',
451+
claim: 'nbf',
452+
errorCode: 'inactive',
453+
errorVerb: 'will be active',
454+
greater: true,
455+
modifier: -clockTolerance
456+
})
413457
}
414458

415459
if (!ignoreExpiration) {
416-
validators.push({ type: 'date', claim: 'exp', errorCode: 'expired', errorVerb: 'has expired', modifier: +clockTolerance })
460+
validators.push({
461+
type: 'date',
462+
claim: 'exp',
463+
errorCode: 'expired',
464+
errorVerb: 'has expired',
465+
modifier: +clockTolerance
466+
})
417467
}
418468

419469
if (typeof maxAge === 'number') {
@@ -450,6 +500,7 @@ module.exports = function createVerifier(options) {
450500
allowedAlgorithms,
451501
complete,
452502
cacheTTL,
503+
errorCacheTTL,
453504
checkTyp: normalizedTyp,
454505
clockTimestamp,
455506
clockTolerance,

0 commit comments

Comments
 (0)