@naturalcycles/nodejs-lib 15.116.3 → 15.118.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/jwt/jwt.service2.d.ts +67 -12
- package/dist/jwt/jwt.service2.js +102 -27
- package/package.json +1 -1
- package/src/jwt/jwt.service2.ts +117 -33
|
@@ -39,7 +39,7 @@ export interface JWTService2Cfg<T extends AnyObject = AnyObject> {
|
|
|
39
39
|
* (e.g a per-kid key set mixing EC and RSA keys). Keep the list as narrow as possible.
|
|
40
40
|
*
|
|
41
41
|
* The token's `alg` header chooses among these, but only within what the
|
|
42
|
-
* verification key can serve: a token/key algorithm mismatch fails with
|
|
42
|
+
* verification key can serve: a token/key algorithm mismatch fails with JWTInvalidError.
|
|
43
43
|
* (All JWTAlgorithms are asymmetric, so the classic algorithm-confusion attacks
|
|
44
44
|
* don't apply regardless.)
|
|
45
45
|
*/
|
|
@@ -149,7 +149,8 @@ export interface JWTDecoded<T extends AnyObject> {
|
|
|
149
149
|
/**
|
|
150
150
|
* Wraps the `jose` library, exposing an implementation-agnostic API:
|
|
151
151
|
* no jose types, options or errors leak out of this service.
|
|
152
|
-
* All errors are normalized into JWTError
|
|
152
|
+
* All errors are normalized into JWTError subclasses
|
|
153
|
+
* (JWTExpiredError / JWTNotYetValidError / JWTInvalidError), matchable with `instanceof`.
|
|
153
154
|
*
|
|
154
155
|
* Successor of JWTService (jsonwebtoken-based). Tokens are wire-compatible
|
|
155
156
|
* in both directions, so the two services can be swapped freely for the same key pair.
|
|
@@ -174,6 +175,22 @@ export declare class JWTService2<T extends AnyObject = AnyObject> {
|
|
|
174
175
|
constructor(cfg: JWTService2Cfg<T>);
|
|
175
176
|
sign<TT extends T = T>(payload: TT, opt: JWTSignOptions<TT>): Promise<JWTString>;
|
|
176
177
|
verify<TT extends T = T>(token: JWTString, opt?: JWTVerifyOptions<TT>): Promise<TT>;
|
|
178
|
+
/**
|
|
179
|
+
* Tries to Verify the token, falling back to unverified Decode on failure,
|
|
180
|
+
* so the caller can "peek" into the token's content even when it doesn't verify.
|
|
181
|
+
*
|
|
182
|
+
* Returns an [error, payload] tuple - ALWAYS check the error first:
|
|
183
|
+
* - [null, payload] - the token verified, payload can be trusted
|
|
184
|
+
* - [error, payload] - verification failed, but the token could still be decoded.
|
|
185
|
+
* When the error is JWTExpiredError/JWTNotYetValidError, the signature was already
|
|
186
|
+
* verified and only the time-claim check failed, so the payload is trustworthy
|
|
187
|
+
* (just expired/not yet valid). For any other error the payload is completely
|
|
188
|
+
* UNVERIFIED - never trust it, only peek (e.g to log/report the claimed identity).
|
|
189
|
+
* - [error, null] - the token could not even be decoded
|
|
190
|
+
*
|
|
191
|
+
* The decode fallback is raw: opt.schema/cfg.schema are only applied on the Verify path.
|
|
192
|
+
*/
|
|
193
|
+
tryToVerifyOrDecode<TT extends T = T>(token: JWTString, opt?: JWTVerifyOptions<TT>): Promise<[err: null, payload: TT] | [err: Error, payload: TT | null]>;
|
|
177
194
|
decode<TT extends T = T>(token: JWTString, opt?: JWTDecodeOptions<TT>): JWTDecoded<TT>;
|
|
178
195
|
/**
|
|
179
196
|
* Schema-validation errors on Verify/Decode are extended with cfg.errorData:
|
|
@@ -183,7 +200,7 @@ export declare class JWTService2<T extends AnyObject = AnyObject> {
|
|
|
183
200
|
*/
|
|
184
201
|
private validate;
|
|
185
202
|
/**
|
|
186
|
-
* jose errors are normalized into JWTError (extended with cfg.errorData).
|
|
203
|
+
* jose errors are normalized into JWTError subclasses (extended with cfg.errorData).
|
|
187
204
|
* Non-jose errors (e.g TypeError from passing a key that doesn't fit the algorithm)
|
|
188
205
|
* indicate a programming error and are passed through as-is.
|
|
189
206
|
*/
|
|
@@ -194,7 +211,7 @@ export declare class JWTService2<T extends AnyObject = AnyObject> {
|
|
|
194
211
|
* Standalone version of JWTService2.decode, for when there's no service instance at hand
|
|
195
212
|
* (no schema validation, no errorData extension).
|
|
196
213
|
*
|
|
197
|
-
* Throws
|
|
214
|
+
* Throws JWTInvalidError if the token cannot be decoded.
|
|
198
215
|
*/
|
|
199
216
|
export declare function jwtDecode<T extends AnyObject>(token: JWTString): JWTDecoded<T>;
|
|
200
217
|
export type JWTErrorCode = 'JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID';
|
|
@@ -202,17 +219,55 @@ export interface JWTErrorData extends ErrorData {
|
|
|
202
219
|
code: JWTErrorCode;
|
|
203
220
|
}
|
|
204
221
|
/**
|
|
205
|
-
* Thrown by JWTService2 on any Verify/Decode failure
|
|
222
|
+
* Thrown by JWTService2 on any Verify/Decode failure, always as one of its subclasses:
|
|
223
|
+
* - JWTExpiredError - `exp` claim check failed
|
|
224
|
+
* - JWTNotYetValidError - `nbf` claim check failed
|
|
225
|
+
* - JWTInvalidError - anything else (malformed token, wrong signature, other claim mismatches)
|
|
206
226
|
*
|
|
207
|
-
* `
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
227
|
+
* Match errors with `instanceof`, e.g `err instanceof JWTExpiredError`.
|
|
228
|
+
* `data.code` carries the same distinction ('JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID'),
|
|
229
|
+
* for when the error crosses a serialization boundary (e.g ErrorObject over HTTP)
|
|
230
|
+
* where `instanceof` no longer works.
|
|
211
231
|
*
|
|
212
|
-
* The
|
|
232
|
+
* The underlying jose error is deliberately NOT preserved in `cause`: everything useful
|
|
233
|
+
* from it is already carried (message is copied, the failed claim is the subclass,
|
|
234
|
+
* `payload` is exposed where trustworthy), and jose nests the raw JWT payload into its own
|
|
235
|
+
* `cause`, which would otherwise survive ErrorObject serialization and leak into logs.
|
|
213
236
|
*/
|
|
214
237
|
export declare class JWTError extends AppError<JWTErrorData> {
|
|
215
|
-
constructor(message: string, data: JWTErrorData
|
|
216
|
-
|
|
238
|
+
constructor(message: string, data: JWTErrorData);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The token is well-formed and its signature is valid, but the `exp` claim check failed.
|
|
242
|
+
*
|
|
243
|
+
* `payload` carries the decoded token payload: signature is verified before claims are
|
|
244
|
+
* checked, so the payload is trustworthy - it's just expired.
|
|
245
|
+
* (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
|
|
246
|
+
*/
|
|
247
|
+
export declare class JWTExpiredError extends JWTError {
|
|
248
|
+
readonly payload: AnyObject;
|
|
249
|
+
constructor(message: string, data?: ErrorData, opt?: {
|
|
250
|
+
payload?: AnyObject;
|
|
217
251
|
});
|
|
218
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* The token is well-formed and its signature is valid, but the `nbf` claim check failed.
|
|
255
|
+
*
|
|
256
|
+
* `payload` carries the decoded token payload: signature is verified before claims are
|
|
257
|
+
* checked, so the payload is trustworthy - it's just not valid yet.
|
|
258
|
+
* (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
|
|
259
|
+
*/
|
|
260
|
+
export declare class JWTNotYetValidError extends JWTError {
|
|
261
|
+
readonly payload: AnyObject;
|
|
262
|
+
constructor(message: string, data?: ErrorData, opt?: {
|
|
263
|
+
payload?: AnyObject;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The token could not be trusted: malformed token, wrong signature,
|
|
268
|
+
* or a claim mismatch other than exp/nbf (e.g issuer/audience).
|
|
269
|
+
* No payload is exposed - nothing from such a token should be used.
|
|
270
|
+
*/
|
|
271
|
+
export declare class JWTInvalidError extends JWTError {
|
|
272
|
+
constructor(message: string, data?: ErrorData);
|
|
273
|
+
}
|
package/dist/jwt/jwt.service2.js
CHANGED
|
@@ -5,7 +5,8 @@ import { decodeJwt, decodeProtectedHeader, errors, jwtVerify, SignJWT } from 'jo
|
|
|
5
5
|
/**
|
|
6
6
|
* Wraps the `jose` library, exposing an implementation-agnostic API:
|
|
7
7
|
* no jose types, options or errors leak out of this service.
|
|
8
|
-
* All errors are normalized into JWTError
|
|
8
|
+
* All errors are normalized into JWTError subclasses
|
|
9
|
+
* (JWTExpiredError / JWTNotYetValidError / JWTInvalidError), matchable with `instanceof`.
|
|
9
10
|
*
|
|
10
11
|
* Successor of JWTService (jsonwebtoken-based). Tokens are wire-compatible
|
|
11
12
|
* in both directions, so the two services can be swapped freely for the same key pair.
|
|
@@ -87,6 +88,40 @@ export class JWTService2 {
|
|
|
87
88
|
this.validate(data, schema);
|
|
88
89
|
return data;
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Tries to Verify the token, falling back to unverified Decode on failure,
|
|
93
|
+
* so the caller can "peek" into the token's content even when it doesn't verify.
|
|
94
|
+
*
|
|
95
|
+
* Returns an [error, payload] tuple - ALWAYS check the error first:
|
|
96
|
+
* - [null, payload] - the token verified, payload can be trusted
|
|
97
|
+
* - [error, payload] - verification failed, but the token could still be decoded.
|
|
98
|
+
* When the error is JWTExpiredError/JWTNotYetValidError, the signature was already
|
|
99
|
+
* verified and only the time-claim check failed, so the payload is trustworthy
|
|
100
|
+
* (just expired/not yet valid). For any other error the payload is completely
|
|
101
|
+
* UNVERIFIED - never trust it, only peek (e.g to log/report the claimed identity).
|
|
102
|
+
* - [error, null] - the token could not even be decoded
|
|
103
|
+
*
|
|
104
|
+
* The decode fallback is raw: opt.schema/cfg.schema are only applied on the Verify path.
|
|
105
|
+
*/
|
|
106
|
+
async tryToVerifyOrDecode(token, opt = {}) {
|
|
107
|
+
let verifyError;
|
|
108
|
+
try {
|
|
109
|
+
return [null, await this.verify(token, opt)];
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
verifyError = err;
|
|
113
|
+
}
|
|
114
|
+
// Expired/not-yet-valid errors already carry the (signature-verified) payload
|
|
115
|
+
if (verifyError instanceof JWTExpiredError || verifyError instanceof JWTNotYetValidError) {
|
|
116
|
+
return [verifyError, verifyError.payload];
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
return [verifyError, jwtDecode(token).payload];
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return [verifyError, null];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
90
125
|
decode(token, opt = {}) {
|
|
91
126
|
let decoded;
|
|
92
127
|
try {
|
|
@@ -120,33 +155,27 @@ export class JWTService2 {
|
|
|
120
155
|
}
|
|
121
156
|
}
|
|
122
157
|
/**
|
|
123
|
-
* jose errors are normalized into JWTError (extended with cfg.errorData).
|
|
158
|
+
* jose errors are normalized into JWTError subclasses (extended with cfg.errorData).
|
|
124
159
|
* Non-jose errors (e.g TypeError from passing a key that doesn't fit the algorithm)
|
|
125
160
|
* indicate a programming error and are passed through as-is.
|
|
126
161
|
*/
|
|
127
162
|
normalizeError(err) {
|
|
128
|
-
|
|
163
|
+
const { errorData } = this.cfg;
|
|
129
164
|
if (err instanceof errors.JWTExpired) {
|
|
130
|
-
|
|
165
|
+
return new JWTExpiredError(err.message, errorData, { payload: err.payload });
|
|
131
166
|
}
|
|
132
|
-
|
|
133
|
-
|
|
167
|
+
if (err instanceof errors.JWTClaimValidationFailed && err.claim === 'nbf') {
|
|
168
|
+
return new JWTNotYetValidError(err.message, errorData, { payload: err.payload });
|
|
134
169
|
}
|
|
135
|
-
|
|
136
|
-
|
|
170
|
+
if (err instanceof errors.JOSEError) {
|
|
171
|
+
return new JWTInvalidError(err.message, errorData);
|
|
137
172
|
}
|
|
138
|
-
|
|
173
|
+
if (this.cfg.verifyAlgorithms && err instanceof TypeError) {
|
|
139
174
|
// With multiple verifyAlgorithms, a token/key algorithm mismatch is reachable
|
|
140
175
|
// by untrusted input, and jose reports it as TypeError - treat it as an invalid token
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
return err;
|
|
176
|
+
return new JWTInvalidError(err.message, errorData);
|
|
145
177
|
}
|
|
146
|
-
return
|
|
147
|
-
...this.cfg.errorData,
|
|
148
|
-
code,
|
|
149
|
-
}, { cause: err });
|
|
178
|
+
return err;
|
|
150
179
|
}
|
|
151
180
|
}
|
|
152
181
|
/**
|
|
@@ -154,7 +183,7 @@ export class JWTService2 {
|
|
|
154
183
|
* Standalone version of JWTService2.decode, for when there's no service instance at hand
|
|
155
184
|
* (no schema validation, no errorData extension).
|
|
156
185
|
*
|
|
157
|
-
* Throws
|
|
186
|
+
* Throws JWTInvalidError if the token cannot be decoded.
|
|
158
187
|
*/
|
|
159
188
|
export function jwtDecode(token) {
|
|
160
189
|
let header;
|
|
@@ -164,7 +193,8 @@ export function jwtDecode(token) {
|
|
|
164
193
|
payload = decodeJwt(token);
|
|
165
194
|
}
|
|
166
195
|
catch (err) {
|
|
167
|
-
|
|
196
|
+
// The underlying message is folded in, as it carries the only specific detail
|
|
197
|
+
throw new JWTInvalidError(`invalid token, unable to decode: ${err.message}`);
|
|
168
198
|
}
|
|
169
199
|
return {
|
|
170
200
|
header,
|
|
@@ -173,17 +203,62 @@ export function jwtDecode(token) {
|
|
|
173
203
|
};
|
|
174
204
|
}
|
|
175
205
|
/**
|
|
176
|
-
* Thrown by JWTService2 on any Verify/Decode failure
|
|
206
|
+
* Thrown by JWTService2 on any Verify/Decode failure, always as one of its subclasses:
|
|
207
|
+
* - JWTExpiredError - `exp` claim check failed
|
|
208
|
+
* - JWTNotYetValidError - `nbf` claim check failed
|
|
209
|
+
* - JWTInvalidError - anything else (malformed token, wrong signature, other claim mismatches)
|
|
177
210
|
*
|
|
178
|
-
* `
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
211
|
+
* Match errors with `instanceof`, e.g `err instanceof JWTExpiredError`.
|
|
212
|
+
* `data.code` carries the same distinction ('JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID'),
|
|
213
|
+
* for when the error crosses a serialization boundary (e.g ErrorObject over HTTP)
|
|
214
|
+
* where `instanceof` no longer works.
|
|
182
215
|
*
|
|
183
|
-
* The
|
|
216
|
+
* The underlying jose error is deliberately NOT preserved in `cause`: everything useful
|
|
217
|
+
* from it is already carried (message is copied, the failed claim is the subclass,
|
|
218
|
+
* `payload` is exposed where trustworthy), and jose nests the raw JWT payload into its own
|
|
219
|
+
* `cause`, which would otherwise survive ErrorObject serialization and leak into logs.
|
|
184
220
|
*/
|
|
185
221
|
export class JWTError extends AppError {
|
|
186
|
-
constructor(message, data
|
|
187
|
-
|
|
222
|
+
constructor(message, data) {
|
|
223
|
+
// `new.target.name` makes subclasses report their own name
|
|
224
|
+
super(message, data, { name: new.target.name });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The token is well-formed and its signature is valid, but the `exp` claim check failed.
|
|
229
|
+
*
|
|
230
|
+
* `payload` carries the decoded token payload: signature is verified before claims are
|
|
231
|
+
* checked, so the payload is trustworthy - it's just expired.
|
|
232
|
+
* (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
|
|
233
|
+
*/
|
|
234
|
+
export class JWTExpiredError extends JWTError {
|
|
235
|
+
payload;
|
|
236
|
+
constructor(message, data = {}, opt = {}) {
|
|
237
|
+
super(message, { ...data, code: 'JWT_EXPIRED' });
|
|
238
|
+
this.payload = opt.payload || {};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The token is well-formed and its signature is valid, but the `nbf` claim check failed.
|
|
243
|
+
*
|
|
244
|
+
* `payload` carries the decoded token payload: signature is verified before claims are
|
|
245
|
+
* checked, so the payload is trustworthy - it's just not valid yet.
|
|
246
|
+
* (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
|
|
247
|
+
*/
|
|
248
|
+
export class JWTNotYetValidError extends JWTError {
|
|
249
|
+
payload;
|
|
250
|
+
constructor(message, data = {}, opt = {}) {
|
|
251
|
+
super(message, { ...data, code: 'JWT_NOT_YET_VALID' });
|
|
252
|
+
this.payload = opt.payload || {};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The token could not be trusted: malformed token, wrong signature,
|
|
257
|
+
* or a claim mismatch other than exp/nbf (e.g issuer/audience).
|
|
258
|
+
* No payload is exposed - nothing from such a token should be used.
|
|
259
|
+
*/
|
|
260
|
+
export class JWTInvalidError extends JWTError {
|
|
261
|
+
constructor(message, data = {}) {
|
|
262
|
+
super(message, { ...data, code: 'JWT_INVALID' });
|
|
188
263
|
}
|
|
189
264
|
}
|
package/package.json
CHANGED
package/src/jwt/jwt.service2.ts
CHANGED
|
@@ -62,7 +62,7 @@ export interface JWTService2Cfg<T extends AnyObject = AnyObject> {
|
|
|
62
62
|
* (e.g a per-kid key set mixing EC and RSA keys). Keep the list as narrow as possible.
|
|
63
63
|
*
|
|
64
64
|
* The token's `alg` header chooses among these, but only within what the
|
|
65
|
-
* verification key can serve: a token/key algorithm mismatch fails with
|
|
65
|
+
* verification key can serve: a token/key algorithm mismatch fails with JWTInvalidError.
|
|
66
66
|
* (All JWTAlgorithms are asymmetric, so the classic algorithm-confusion attacks
|
|
67
67
|
* don't apply regardless.)
|
|
68
68
|
*/
|
|
@@ -182,7 +182,8 @@ export interface JWTDecoded<T extends AnyObject> {
|
|
|
182
182
|
/**
|
|
183
183
|
* Wraps the `jose` library, exposing an implementation-agnostic API:
|
|
184
184
|
* no jose types, options or errors leak out of this service.
|
|
185
|
-
* All errors are normalized into JWTError
|
|
185
|
+
* All errors are normalized into JWTError subclasses
|
|
186
|
+
* (JWTExpiredError / JWTNotYetValidError / JWTInvalidError), matchable with `instanceof`.
|
|
186
187
|
*
|
|
187
188
|
* Successor of JWTService (jsonwebtoken-based). Tokens are wire-compatible
|
|
188
189
|
* in both directions, so the two services can be swapped freely for the same key pair.
|
|
@@ -273,6 +274,45 @@ export class JWTService2<T extends AnyObject = AnyObject> {
|
|
|
273
274
|
return data
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Tries to Verify the token, falling back to unverified Decode on failure,
|
|
279
|
+
* so the caller can "peek" into the token's content even when it doesn't verify.
|
|
280
|
+
*
|
|
281
|
+
* Returns an [error, payload] tuple - ALWAYS check the error first:
|
|
282
|
+
* - [null, payload] - the token verified, payload can be trusted
|
|
283
|
+
* - [error, payload] - verification failed, but the token could still be decoded.
|
|
284
|
+
* When the error is JWTExpiredError/JWTNotYetValidError, the signature was already
|
|
285
|
+
* verified and only the time-claim check failed, so the payload is trustworthy
|
|
286
|
+
* (just expired/not yet valid). For any other error the payload is completely
|
|
287
|
+
* UNVERIFIED - never trust it, only peek (e.g to log/report the claimed identity).
|
|
288
|
+
* - [error, null] - the token could not even be decoded
|
|
289
|
+
*
|
|
290
|
+
* The decode fallback is raw: opt.schema/cfg.schema are only applied on the Verify path.
|
|
291
|
+
*/
|
|
292
|
+
async tryToVerifyOrDecode<TT extends T = T>(
|
|
293
|
+
token: JWTString,
|
|
294
|
+
opt: JWTVerifyOptions<TT> = {},
|
|
295
|
+
): Promise<[err: null, payload: TT] | [err: Error, payload: TT | null]> {
|
|
296
|
+
let verifyError: Error
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
return [null, await this.verify<TT>(token, opt)]
|
|
300
|
+
} catch (err) {
|
|
301
|
+
verifyError = err as Error
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Expired/not-yet-valid errors already carry the (signature-verified) payload
|
|
305
|
+
if (verifyError instanceof JWTExpiredError || verifyError instanceof JWTNotYetValidError) {
|
|
306
|
+
return [verifyError, verifyError.payload as TT]
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
return [verifyError, jwtDecode<TT>(token).payload]
|
|
311
|
+
} catch {
|
|
312
|
+
return [verifyError, null]
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
276
316
|
decode<TT extends T = T>(token: JWTString, opt: JWTDecodeOptions<TT> = {}): JWTDecoded<TT> {
|
|
277
317
|
let decoded: JWTDecoded<TT>
|
|
278
318
|
|
|
@@ -308,35 +348,28 @@ export class JWTService2<T extends AnyObject = AnyObject> {
|
|
|
308
348
|
}
|
|
309
349
|
|
|
310
350
|
/**
|
|
311
|
-
* jose errors are normalized into JWTError (extended with cfg.errorData).
|
|
351
|
+
* jose errors are normalized into JWTError subclasses (extended with cfg.errorData).
|
|
312
352
|
* Non-jose errors (e.g TypeError from passing a key that doesn't fit the algorithm)
|
|
313
353
|
* indicate a programming error and are passed through as-is.
|
|
314
354
|
*/
|
|
315
355
|
private normalizeError(err: unknown): Error {
|
|
316
|
-
|
|
356
|
+
const { errorData } = this.cfg
|
|
317
357
|
|
|
318
358
|
if (err instanceof errors.JWTExpired) {
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
359
|
+
return new JWTExpiredError(err.message, errorData, { payload: err.payload })
|
|
360
|
+
}
|
|
361
|
+
if (err instanceof errors.JWTClaimValidationFailed && err.claim === 'nbf') {
|
|
362
|
+
return new JWTNotYetValidError(err.message, errorData, { payload: err.payload })
|
|
363
|
+
}
|
|
364
|
+
if (err instanceof errors.JOSEError) {
|
|
365
|
+
return new JWTInvalidError(err.message, errorData)
|
|
366
|
+
}
|
|
367
|
+
if (this.cfg.verifyAlgorithms && err instanceof TypeError) {
|
|
325
368
|
// With multiple verifyAlgorithms, a token/key algorithm mismatch is reachable
|
|
326
369
|
// by untrusted input, and jose reports it as TypeError - treat it as an invalid token
|
|
327
|
-
|
|
328
|
-
} else {
|
|
329
|
-
return err as Error
|
|
370
|
+
return new JWTInvalidError(err.message, errorData)
|
|
330
371
|
}
|
|
331
|
-
|
|
332
|
-
return new JWTError(
|
|
333
|
-
(err as Error).message,
|
|
334
|
-
{
|
|
335
|
-
...this.cfg.errorData,
|
|
336
|
-
code,
|
|
337
|
-
},
|
|
338
|
-
{ cause: err },
|
|
339
|
-
)
|
|
372
|
+
return err as Error
|
|
340
373
|
}
|
|
341
374
|
}
|
|
342
375
|
|
|
@@ -345,7 +378,7 @@ export class JWTService2<T extends AnyObject = AnyObject> {
|
|
|
345
378
|
* Standalone version of JWTService2.decode, for when there's no service instance at hand
|
|
346
379
|
* (no schema validation, no errorData extension).
|
|
347
380
|
*
|
|
348
|
-
* Throws
|
|
381
|
+
* Throws JWTInvalidError if the token cannot be decoded.
|
|
349
382
|
*/
|
|
350
383
|
export function jwtDecode<T extends AnyObject>(token: JWTString): JWTDecoded<T> {
|
|
351
384
|
let header: JWTHeader
|
|
@@ -353,9 +386,10 @@ export function jwtDecode<T extends AnyObject>(token: JWTString): JWTDecoded<T>
|
|
|
353
386
|
|
|
354
387
|
try {
|
|
355
388
|
header = decodeProtectedHeader(token) as JWTHeader
|
|
356
|
-
payload = decodeJwt(token)
|
|
389
|
+
payload = decodeJwt(token)
|
|
357
390
|
} catch (err) {
|
|
358
|
-
|
|
391
|
+
// The underlying message is folded in, as it carries the only specific detail
|
|
392
|
+
throw new JWTInvalidError(`invalid token, unable to decode: ${(err as Error).message}`)
|
|
359
393
|
}
|
|
360
394
|
|
|
361
395
|
return {
|
|
@@ -372,17 +406,67 @@ export interface JWTErrorData extends ErrorData {
|
|
|
372
406
|
}
|
|
373
407
|
|
|
374
408
|
/**
|
|
375
|
-
* Thrown by JWTService2 on any Verify/Decode failure
|
|
409
|
+
* Thrown by JWTService2 on any Verify/Decode failure, always as one of its subclasses:
|
|
410
|
+
* - JWTExpiredError - `exp` claim check failed
|
|
411
|
+
* - JWTNotYetValidError - `nbf` claim check failed
|
|
412
|
+
* - JWTInvalidError - anything else (malformed token, wrong signature, other claim mismatches)
|
|
376
413
|
*
|
|
377
|
-
* `
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
414
|
+
* Match errors with `instanceof`, e.g `err instanceof JWTExpiredError`.
|
|
415
|
+
* `data.code` carries the same distinction ('JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID'),
|
|
416
|
+
* for when the error crosses a serialization boundary (e.g ErrorObject over HTTP)
|
|
417
|
+
* where `instanceof` no longer works.
|
|
381
418
|
*
|
|
382
|
-
* The
|
|
419
|
+
* The underlying jose error is deliberately NOT preserved in `cause`: everything useful
|
|
420
|
+
* from it is already carried (message is copied, the failed claim is the subclass,
|
|
421
|
+
* `payload` is exposed where trustworthy), and jose nests the raw JWT payload into its own
|
|
422
|
+
* `cause`, which would otherwise survive ErrorObject serialization and leak into logs.
|
|
383
423
|
*/
|
|
384
424
|
export class JWTError extends AppError<JWTErrorData> {
|
|
385
|
-
constructor(message: string, data: JWTErrorData
|
|
386
|
-
|
|
425
|
+
constructor(message: string, data: JWTErrorData) {
|
|
426
|
+
// `new.target.name` makes subclasses report their own name
|
|
427
|
+
super(message, data, { name: new.target.name })
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The token is well-formed and its signature is valid, but the `exp` claim check failed.
|
|
433
|
+
*
|
|
434
|
+
* `payload` carries the decoded token payload: signature is verified before claims are
|
|
435
|
+
* checked, so the payload is trustworthy - it's just expired.
|
|
436
|
+
* (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
|
|
437
|
+
*/
|
|
438
|
+
export class JWTExpiredError extends JWTError {
|
|
439
|
+
readonly payload: AnyObject
|
|
440
|
+
|
|
441
|
+
constructor(message: string, data: ErrorData = {}, opt: { payload?: AnyObject } = {}) {
|
|
442
|
+
super(message, { ...data, code: 'JWT_EXPIRED' })
|
|
443
|
+
this.payload = opt.payload || {}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The token is well-formed and its signature is valid, but the `nbf` claim check failed.
|
|
449
|
+
*
|
|
450
|
+
* `payload` carries the decoded token payload: signature is verified before claims are
|
|
451
|
+
* checked, so the payload is trustworthy - it's just not valid yet.
|
|
452
|
+
* (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
|
|
453
|
+
*/
|
|
454
|
+
export class JWTNotYetValidError extends JWTError {
|
|
455
|
+
readonly payload: AnyObject
|
|
456
|
+
|
|
457
|
+
constructor(message: string, data: ErrorData = {}, opt: { payload?: AnyObject } = {}) {
|
|
458
|
+
super(message, { ...data, code: 'JWT_NOT_YET_VALID' })
|
|
459
|
+
this.payload = opt.payload || {}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* The token could not be trusted: malformed token, wrong signature,
|
|
465
|
+
* or a claim mismatch other than exp/nbf (e.g issuer/audience).
|
|
466
|
+
* No payload is exposed - nothing from such a token should be used.
|
|
467
|
+
*/
|
|
468
|
+
export class JWTInvalidError extends JWTError {
|
|
469
|
+
constructor(message: string, data: ErrorData = {}) {
|
|
470
|
+
super(message, { ...data, code: 'JWT_INVALID' })
|
|
387
471
|
}
|
|
388
472
|
}
|