@naturalcycles/nodejs-lib 15.119.0 → 15.121.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/index.d.ts +0 -1
- package/dist/jwt/index.js +0 -1
- package/dist/jwt/jwt.service.d.ts +244 -26
- package/dist/jwt/jwt.service.js +244 -38
- package/dist/validation/ajv/jSchema.d.ts +9 -0
- package/dist/validation/ajv/jSchema.js +12 -1
- package/dist/validation/regexes.d.ts +1 -0
- package/dist/validation/regexes.js +2 -0
- package/package.json +1 -3
- package/src/jwt/index.ts +0 -1
- package/src/jwt/jwt.service.ts +414 -74
- package/src/validation/ajv/jSchema.ts +13 -0
- package/src/validation/regexes.ts +2 -0
- package/dist/jwt/jwt.service2.d.ts +0 -282
- package/dist/jwt/jwt.service2.js +0 -276
- package/src/jwt/jwt.service2.ts +0 -486
package/dist/jwt/index.d.ts
CHANGED
package/dist/jwt/index.js
CHANGED
|
@@ -1,49 +1,166 @@
|
|
|
1
1
|
import type { ErrorData } from '@naturalcycles/js-lib/error';
|
|
2
|
-
import
|
|
3
|
-
import type {
|
|
4
|
-
import jsonwebtoken from 'jsonwebtoken';
|
|
2
|
+
import { AppError } from '@naturalcycles/js-lib/error/error.util.js';
|
|
3
|
+
import type { AnyObject, JWTString, NumberOfSeconds, UnixTimestamp } from '@naturalcycles/js-lib/types';
|
|
5
4
|
import type { AjvSchema, JSchema } from '../validation/ajv/jSchema.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Asymmetric JWS algorithms supported by JWTService.
|
|
7
|
+
*/
|
|
8
|
+
export type JWTAlgorithm = 'ES256' | 'ES384' | 'ES512' | 'RS256' | 'RS384' | 'RS512' | 'PS256' | 'PS384' | 'PS512' | 'EdDSA';
|
|
9
|
+
export interface JWTServiceCfg<T extends AnyObject = AnyObject> {
|
|
9
10
|
/**
|
|
10
11
|
* Public key is required to Verify incoming tokens.
|
|
11
12
|
* Optional if you only want to Decode or Sign.
|
|
13
|
+
*
|
|
14
|
+
* PEM string/Buffer. Both SPKI ("PUBLIC KEY") and a private key PEM
|
|
15
|
+
* (public key is derived from it) are accepted.
|
|
12
16
|
*/
|
|
13
17
|
publicKey?: string | Buffer;
|
|
14
18
|
/**
|
|
15
19
|
* Private key is required to Sign (create) outgoing tokens.
|
|
16
20
|
* Optional if you only want to Decode or Verify.
|
|
21
|
+
*
|
|
22
|
+
* PEM string/Buffer. Both PKCS8 ("PRIVATE KEY") and SEC1 ("EC PRIVATE KEY",
|
|
23
|
+
* as generated by `openssl ecparam`) are accepted.
|
|
17
24
|
*/
|
|
18
25
|
privateKey?: string | Buffer;
|
|
19
26
|
/**
|
|
20
27
|
* Recommended: ES256
|
|
21
28
|
* Keys (private/public) should be generated using proper settings
|
|
22
29
|
* that fit the used Algorithm.
|
|
30
|
+
*
|
|
31
|
+
* Used for Sign, and (unless `verifyAlgorithms` is set) it's also
|
|
32
|
+
* the only algorithm accepted on Verify.
|
|
23
33
|
*/
|
|
24
|
-
algorithm:
|
|
34
|
+
algorithm: JWTAlgorithm;
|
|
25
35
|
/**
|
|
26
|
-
*
|
|
36
|
+
* JWS algorithms accepted on Verify. Defaults to `[algorithm]`.
|
|
37
|
+
*
|
|
38
|
+
* Only needed when one verifier must accept keys of different types
|
|
39
|
+
* (e.g a per-kid key set mixing EC and RSA keys). Keep the list as narrow as possible.
|
|
40
|
+
*
|
|
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 JWTInvalidError.
|
|
43
|
+
* (All JWTAlgorithms are asymmetric, so the classic algorithm-confusion attacks
|
|
44
|
+
* don't apply regardless.)
|
|
45
|
+
*/
|
|
46
|
+
verifyAlgorithms?: JWTAlgorithm[];
|
|
47
|
+
/**
|
|
48
|
+
* If provided - payloads are validated against it on every Sign/Verify/Decode.
|
|
49
|
+
* Can be overridden per-call via `opt.schema`.
|
|
50
|
+
*
|
|
51
|
+
* Note: on Verify/Decode the schema is applied to the raw JWT payload, which includes
|
|
52
|
+
* the standard claims (exp/nbf/iss/aud/sub/iat/jti) if they were set on Sign.
|
|
53
|
+
* Declare (or allow) them in the schema, otherwise a strict schema will
|
|
54
|
+
* strip them from the returned payload (default AjvSchema behavior: removeAdditional).
|
|
27
55
|
*/
|
|
28
|
-
|
|
56
|
+
schema?: JSchema<T, any> | AjvSchema<T>;
|
|
29
57
|
/**
|
|
30
58
|
* If provided - will be applied to every Sign operation.
|
|
59
|
+
* Absolute-timestamp options (expiresAt/notBefore/issuedAt) are excluded,
|
|
60
|
+
* as they would be fixed to the same moment for all tokens signed by this service.
|
|
61
|
+
*/
|
|
62
|
+
signOptions?: Omit<JWTSignOptions, 'expiresAt' | 'notBefore' | 'issuedAt' | 'schema'>;
|
|
63
|
+
/**
|
|
64
|
+
* If provided - will be applied to every Verify operation.
|
|
31
65
|
*/
|
|
32
|
-
verifyOptions?:
|
|
66
|
+
verifyOptions?: Omit<JWTVerifyOptions, 'schema' | 'publicKey'>;
|
|
33
67
|
/**
|
|
34
|
-
* If set -
|
|
68
|
+
* If set - JWTErrors thrown from this service will be extended
|
|
35
69
|
* with this errorData (in err.data)
|
|
36
70
|
*/
|
|
37
71
|
errorData?: ErrorData;
|
|
38
72
|
}
|
|
73
|
+
export interface JWTSignOptions<T extends AnyObject = AnyObject> {
|
|
74
|
+
/**
|
|
75
|
+
* Sets the `exp` claim, as an absolute UnixTimestamp.
|
|
76
|
+
* Convenient to build with LocalTime, e.g:
|
|
77
|
+
* `localTime.now().plus(30, 'minute').unix`
|
|
78
|
+
*
|
|
79
|
+
* Required, to protect from accidentally issuing never-expiring tokens.
|
|
80
|
+
* Pass `null` to explicitly sign a token without expiration.
|
|
81
|
+
*/
|
|
82
|
+
expiresAt: UnixTimestamp | null;
|
|
83
|
+
/**
|
|
84
|
+
* Sets the `nbf` (not before) claim, as an absolute UnixTimestamp.
|
|
85
|
+
*/
|
|
86
|
+
notBefore?: UnixTimestamp;
|
|
87
|
+
issuer?: string;
|
|
88
|
+
audience?: string | string[];
|
|
89
|
+
subject?: string;
|
|
90
|
+
jwtid?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Sets the `iat` claim, as an absolute UnixTimestamp (e.g `localTime.nowUnix()`).
|
|
93
|
+
* By default `iat` is NOT set (same as the legacy jsonwebtoken-based JWTService
|
|
94
|
+
* with its `noTimestamp: true` default).
|
|
95
|
+
*/
|
|
96
|
+
issuedAt?: UnixTimestamp;
|
|
97
|
+
/**
|
|
98
|
+
* Sets the `kid` (key id) header, required by some APIs (e.g Apple App Store Connect).
|
|
99
|
+
*/
|
|
100
|
+
kid?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Overrides cfg.schema for this call.
|
|
103
|
+
*/
|
|
104
|
+
schema?: JSchema<T, any> | AjvSchema<T>;
|
|
105
|
+
}
|
|
106
|
+
export interface JWTVerifyOptions<T extends AnyObject = AnyObject> {
|
|
107
|
+
audience?: string | string[];
|
|
108
|
+
issuer?: string | string[];
|
|
109
|
+
subject?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Clock skew tolerance, in seconds.
|
|
112
|
+
*/
|
|
113
|
+
clockTolerance?: NumberOfSeconds;
|
|
114
|
+
/**
|
|
115
|
+
* Maximum allowed age of the token (based on its `iat` claim), in seconds.
|
|
116
|
+
*/
|
|
117
|
+
maxTokenAge?: NumberOfSeconds;
|
|
118
|
+
/**
|
|
119
|
+
* "Now" override, useful in tests.
|
|
120
|
+
*/
|
|
121
|
+
now?: UnixTimestamp;
|
|
122
|
+
requiredClaims?: string[];
|
|
123
|
+
/**
|
|
124
|
+
* Overrides cfg.schema for this call.
|
|
125
|
+
*/
|
|
126
|
+
schema?: JSchema<T, any> | AjvSchema<T>;
|
|
127
|
+
/**
|
|
128
|
+
* Overrides cfg.publicKey for this call,
|
|
129
|
+
* e.g when verifying tokens signed with different keys (kid-based).
|
|
130
|
+
*/
|
|
131
|
+
publicKey?: string | Buffer;
|
|
132
|
+
}
|
|
133
|
+
export interface JWTDecodeOptions<T extends AnyObject = AnyObject> {
|
|
134
|
+
/**
|
|
135
|
+
* Overrides cfg.schema for this call.
|
|
136
|
+
*/
|
|
137
|
+
schema?: JSchema<T, any> | AjvSchema<T>;
|
|
138
|
+
}
|
|
139
|
+
export interface JWTHeader {
|
|
140
|
+
alg: string;
|
|
141
|
+
typ?: string;
|
|
142
|
+
kid?: string;
|
|
143
|
+
[k: string]: unknown;
|
|
144
|
+
}
|
|
145
|
+
export interface JWTDecoded<T extends AnyObject> {
|
|
146
|
+
header: JWTHeader;
|
|
147
|
+
payload: T;
|
|
148
|
+
signature: string;
|
|
149
|
+
}
|
|
39
150
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
151
|
+
* Wraps the `jose` library, exposing an implementation-agnostic API:
|
|
152
|
+
* no jose types, options or errors leak out of this service.
|
|
153
|
+
* All errors are normalized into JWTError subclasses
|
|
154
|
+
* (JWTExpiredError / JWTNotYetValidError / JWTInvalidError), matchable with `instanceof`.
|
|
155
|
+
*
|
|
156
|
+
* Successor of the legacy jsonwebtoken-based JWTService. Tokens are wire-compatible
|
|
157
|
+
* in both directions, so the two services can be swapped freely for the same key pair.
|
|
158
|
+
* Difference is that Sign and Verify are async (jose is WebCrypto-based, which is Promise-only),
|
|
159
|
+
* while Decode remains sync (pure base64url/JSON parsing, no crypto).
|
|
44
160
|
*
|
|
45
|
-
* Wraps popular `jsonwebtoken` library.
|
|
46
161
|
* You should create one instance of JWTService for each pair of private/public key.
|
|
162
|
+
* Providing cfg.schema types the service to its payload and validates it
|
|
163
|
+
* on every Sign/Verify/Decode; it can be overridden per-call via opt.schema.
|
|
47
164
|
*
|
|
48
165
|
* Generate key pair like this.
|
|
49
166
|
* Please note that parameters should be different for different algorithms.
|
|
@@ -52,14 +169,115 @@ export interface JWTServiceCfg {
|
|
|
52
169
|
* openssl ecparam -name prime256v1 -genkey -noout -out key.pem
|
|
53
170
|
* openssl ec -in key.pem -pubout > key.pub.pem
|
|
54
171
|
*/
|
|
55
|
-
export declare class JWTService {
|
|
56
|
-
cfg: JWTServiceCfg
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
172
|
+
export declare class JWTService<T extends AnyObject = AnyObject> {
|
|
173
|
+
cfg: JWTServiceCfg<T>;
|
|
174
|
+
private privateKey?;
|
|
175
|
+
private publicKey?;
|
|
176
|
+
constructor(cfg: JWTServiceCfg<T>);
|
|
177
|
+
sign<TT extends T = T>(payload: TT, opt: JWTSignOptions<TT>): Promise<JWTString>;
|
|
178
|
+
verify<TT extends T = T>(token: JWTString, opt?: JWTVerifyOptions<TT>): Promise<TT>;
|
|
179
|
+
/**
|
|
180
|
+
* Tries to Verify the token, returning an [error, payload] tuple instead of throwing:
|
|
181
|
+
* - [null, payload] - the token verified, payload can be trusted
|
|
182
|
+
* - [error, null] - verification failed
|
|
183
|
+
*
|
|
184
|
+
* Same contract as tryToVerifyOrDecode, but without the unverified-decode fallback:
|
|
185
|
+
* the payload is only returned when the token verified.
|
|
186
|
+
*/
|
|
187
|
+
tryToVerify<TT extends T = T>(token: JWTString, opt?: JWTVerifyOptions<TT>): Promise<[err: null, payload: TT] | [err: Error, payload: null]>;
|
|
188
|
+
/**
|
|
189
|
+
* Tries to Verify the token, falling back to unverified Decode on failure,
|
|
190
|
+
* so the caller can "peek" into the token's content even when it doesn't verify.
|
|
191
|
+
*
|
|
192
|
+
* Returns an [error, payload] tuple - ALWAYS check the error first:
|
|
193
|
+
* - [null, payload] - the token verified, payload can be trusted
|
|
194
|
+
* - [error, payload] - verification failed, but the token could still be decoded.
|
|
195
|
+
* When the error is JWTExpiredError/JWTNotYetValidError, the signature was already
|
|
196
|
+
* verified and only the time-claim check failed, so the payload is trustworthy
|
|
197
|
+
* (just expired/not yet valid). For any other error the payload is completely
|
|
198
|
+
* UNVERIFIED - never trust it, only peek (e.g to log/report the claimed identity).
|
|
199
|
+
* - [error, null] - the token could not even be decoded
|
|
200
|
+
*
|
|
201
|
+
* The decode fallback is raw: opt.schema/cfg.schema are only applied on the Verify path.
|
|
202
|
+
*/
|
|
203
|
+
tryToVerifyOrDecode<TT extends T = T>(token: JWTString, opt?: JWTVerifyOptions<TT>): Promise<[err: null, payload: TT] | [err: Error, payload: TT | null]>;
|
|
204
|
+
decode<TT extends T = T>(token: JWTString, opt?: JWTDecodeOptions<TT>): JWTDecoded<TT>;
|
|
205
|
+
/**
|
|
206
|
+
* Schema-validation errors on Verify/Decode are extended with cfg.errorData:
|
|
207
|
+
* a token with a non-conforming payload is as unauthorized as an invalid one.
|
|
208
|
+
* (On Sign the payload comes from own code, not from user input, so a validation
|
|
209
|
+
* error there indicates a programming error and is thrown as-is.)
|
|
210
|
+
*/
|
|
211
|
+
private validate;
|
|
212
|
+
/**
|
|
213
|
+
* jose errors are normalized into JWTError subclasses (extended with cfg.errorData).
|
|
214
|
+
* Non-jose errors (e.g TypeError from passing a key that doesn't fit the algorithm)
|
|
215
|
+
* indicate a programming error and are passed through as-is.
|
|
216
|
+
*/
|
|
217
|
+
private normalizeError;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Decodes a JWT without verifying its signature (no key needed) - never trust the result.
|
|
221
|
+
* Standalone version of JWTService.decode, for when there's no service instance at hand
|
|
222
|
+
* (no schema validation, no errorData extension).
|
|
223
|
+
*
|
|
224
|
+
* Throws JWTInvalidError if the token cannot be decoded.
|
|
225
|
+
*/
|
|
226
|
+
export declare function jwtDecode<T extends AnyObject>(token: JWTString): JWTDecoded<T>;
|
|
227
|
+
export type JWTErrorCode = 'JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID';
|
|
228
|
+
export interface JWTErrorData extends ErrorData {
|
|
229
|
+
code: JWTErrorCode;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Thrown by JWTService on any Verify/Decode failure, always as one of its subclasses:
|
|
233
|
+
* - JWTExpiredError - `exp` claim check failed
|
|
234
|
+
* - JWTNotYetValidError - `nbf` claim check failed
|
|
235
|
+
* - JWTInvalidError - anything else (malformed token, wrong signature, other claim mismatches)
|
|
236
|
+
*
|
|
237
|
+
* Match errors with `instanceof`, e.g `err instanceof JWTExpiredError`.
|
|
238
|
+
* `data.code` carries the same distinction ('JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID'),
|
|
239
|
+
* for when the error crosses a serialization boundary (e.g ErrorObject over HTTP)
|
|
240
|
+
* where `instanceof` no longer works.
|
|
241
|
+
*
|
|
242
|
+
* The underlying jose error is deliberately NOT preserved in `cause`: everything useful
|
|
243
|
+
* from it is already carried (message is copied, the failed claim is the subclass,
|
|
244
|
+
* `payload` is exposed where trustworthy), and jose nests the raw JWT payload into its own
|
|
245
|
+
* `cause`, which would otherwise survive ErrorObject serialization and leak into logs.
|
|
246
|
+
*/
|
|
247
|
+
export declare class JWTError extends AppError<JWTErrorData> {
|
|
248
|
+
constructor(message: string, data: JWTErrorData);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The token is well-formed and its signature is valid, but the `exp` claim check failed.
|
|
252
|
+
*
|
|
253
|
+
* `payload` carries the decoded token payload: signature is verified before claims are
|
|
254
|
+
* checked, so the payload is trustworthy - it's just expired.
|
|
255
|
+
* (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
|
|
256
|
+
*/
|
|
257
|
+
export declare class JWTExpiredError extends JWTError {
|
|
258
|
+
readonly payload: AnyObject;
|
|
259
|
+
constructor(message: string, data?: ErrorData, opt?: {
|
|
260
|
+
payload?: AnyObject;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The token is well-formed and its signature is valid, but the `nbf` claim check failed.
|
|
265
|
+
*
|
|
266
|
+
* `payload` carries the decoded token payload: signature is verified before claims are
|
|
267
|
+
* checked, so the payload is trustworthy - it's just not valid yet.
|
|
268
|
+
* (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
|
|
269
|
+
*/
|
|
270
|
+
export declare class JWTNotYetValidError extends JWTError {
|
|
271
|
+
readonly payload: AnyObject;
|
|
272
|
+
constructor(message: string, data?: ErrorData, opt?: {
|
|
273
|
+
payload?: AnyObject;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* The token could not be trusted: malformed token, wrong signature,
|
|
278
|
+
* or a claim mismatch other than exp/nbf (e.g issuer/audience).
|
|
279
|
+
* No payload is exposed - nothing from such a token should be used.
|
|
280
|
+
*/
|
|
281
|
+
export declare class JWTInvalidError extends JWTError {
|
|
282
|
+
constructor(message: string, data?: ErrorData);
|
|
65
283
|
}
|
package/dist/jwt/jwt.service.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey } from 'node:crypto';
|
|
1
2
|
import { _assert } from '@naturalcycles/js-lib/error/assert.js';
|
|
2
|
-
import { _errorDataAppend } from '@naturalcycles/js-lib/error/error.util.js';
|
|
3
|
-
import
|
|
4
|
-
export { jsonwebtoken };
|
|
5
|
-
// todo: define JWTError and list possible options
|
|
6
|
-
// jwt expired (TokenExpiredError)
|
|
7
|
-
// jwt invalid
|
|
8
|
-
// jwt token is empty
|
|
3
|
+
import { _errorDataAppend, AppError } from '@naturalcycles/js-lib/error/error.util.js';
|
|
4
|
+
import { decodeJwt, decodeProtectedHeader, errors, jwtVerify, SignJWT } from 'jose';
|
|
9
5
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* Wraps the `jose` library, exposing an implementation-agnostic API:
|
|
7
|
+
* no jose types, options or errors leak out of this service.
|
|
8
|
+
* All errors are normalized into JWTError subclasses
|
|
9
|
+
* (JWTExpiredError / JWTNotYetValidError / JWTInvalidError), matchable with `instanceof`.
|
|
10
|
+
*
|
|
11
|
+
* Successor of the legacy jsonwebtoken-based JWTService. Tokens are wire-compatible
|
|
12
|
+
* in both directions, so the two services can be swapped freely for the same key pair.
|
|
13
|
+
* Difference is that Sign and Verify are async (jose is WebCrypto-based, which is Promise-only),
|
|
14
|
+
* while Decode remains sync (pure base64url/JSON parsing, no crypto).
|
|
14
15
|
*
|
|
15
|
-
* Wraps popular `jsonwebtoken` library.
|
|
16
16
|
* You should create one instance of JWTService for each pair of private/public key.
|
|
17
|
+
* Providing cfg.schema types the service to its payload and validates it
|
|
18
|
+
* on every Sign/Verify/Decode; it can be overridden per-call via opt.schema.
|
|
17
19
|
*
|
|
18
20
|
* Generate key pair like this.
|
|
19
21
|
* Please note that parameters should be different for different algorithms.
|
|
@@ -24,47 +26,251 @@ export { jsonwebtoken };
|
|
|
24
26
|
*/
|
|
25
27
|
export class JWTService {
|
|
26
28
|
cfg;
|
|
29
|
+
privateKey;
|
|
30
|
+
publicKey;
|
|
27
31
|
constructor(cfg) {
|
|
28
32
|
this.cfg = cfg;
|
|
33
|
+
// KeyObjects are parsed synchronously via node:crypto (also accepts SEC1 PEMs,
|
|
34
|
+
// which jose's own async importers reject), keeping the constructor sync.
|
|
35
|
+
if (cfg.privateKey)
|
|
36
|
+
this.privateKey = createPrivateKey(cfg.privateKey);
|
|
37
|
+
if (cfg.publicKey)
|
|
38
|
+
this.publicKey = createPublicKey(cfg.publicKey);
|
|
29
39
|
}
|
|
30
|
-
sign(payload,
|
|
31
|
-
_assert(this.
|
|
32
|
-
schema
|
|
33
|
-
return jsonwebtoken.sign(payload, this.cfg.privateKey, {
|
|
34
|
-
algorithm: this.cfg.algorithm,
|
|
35
|
-
noTimestamp: true,
|
|
40
|
+
async sign(payload, opt) {
|
|
41
|
+
_assert(this.privateKey, 'JWTService: privateKey is required to be able to sign, but not provided', this.cfg.errorData);
|
|
42
|
+
const { expiresAt, notBefore, issuer, audience, subject, jwtid, issuedAt, kid, schema } = {
|
|
36
43
|
...this.cfg.signOptions,
|
|
37
44
|
...opt,
|
|
45
|
+
};
|
|
46
|
+
(schema || this.cfg.schema)?.validate(payload);
|
|
47
|
+
// `kid: undefined` is dropped by JSON serialization, keeping the header unchanged when not set
|
|
48
|
+
const jwt = new SignJWT(payload).setProtectedHeader({
|
|
49
|
+
alg: this.cfg.algorithm,
|
|
50
|
+
typ: 'JWT',
|
|
51
|
+
kid,
|
|
38
52
|
});
|
|
53
|
+
if (expiresAt !== null)
|
|
54
|
+
jwt.setExpirationTime(expiresAt);
|
|
55
|
+
if (notBefore !== undefined)
|
|
56
|
+
jwt.setNotBefore(notBefore);
|
|
57
|
+
if (issuer)
|
|
58
|
+
jwt.setIssuer(issuer);
|
|
59
|
+
if (audience)
|
|
60
|
+
jwt.setAudience(audience);
|
|
61
|
+
if (subject)
|
|
62
|
+
jwt.setSubject(subject);
|
|
63
|
+
if (jwtid)
|
|
64
|
+
jwt.setJti(jwtid);
|
|
65
|
+
if (issuedAt !== undefined)
|
|
66
|
+
jwt.setIssuedAt(issuedAt);
|
|
67
|
+
return await jwt.sign(this.privateKey);
|
|
39
68
|
}
|
|
40
|
-
verify(token,
|
|
41
|
-
|
|
69
|
+
async verify(token, opt = {}) {
|
|
70
|
+
const { now, schema, publicKey, ...joseOpt } = {
|
|
71
|
+
...this.cfg.verifyOptions,
|
|
72
|
+
...opt,
|
|
73
|
+
};
|
|
74
|
+
const key = publicKey ? createPublicKey(publicKey) : this.publicKey;
|
|
75
|
+
_assert(key, 'JWTService: publicKey is required to be able to verify, but not provided', this.cfg.errorData);
|
|
76
|
+
let data;
|
|
42
77
|
try {
|
|
43
|
-
const
|
|
44
|
-
algorithms: [this.cfg.algorithm],
|
|
45
|
-
...
|
|
46
|
-
|
|
78
|
+
const { payload } = await jwtVerify(token, key, {
|
|
79
|
+
algorithms: this.cfg.verifyAlgorithms || [this.cfg.algorithm],
|
|
80
|
+
...joseOpt,
|
|
81
|
+
currentDate: now === undefined ? undefined : new Date(now * 1000),
|
|
47
82
|
});
|
|
48
|
-
|
|
49
|
-
|
|
83
|
+
data = payload;
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
throw this.normalizeError(err);
|
|
87
|
+
}
|
|
88
|
+
this.validate(data, schema);
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Tries to Verify the token, returning an [error, payload] tuple instead of throwing:
|
|
93
|
+
* - [null, payload] - the token verified, payload can be trusted
|
|
94
|
+
* - [error, null] - verification failed
|
|
95
|
+
*
|
|
96
|
+
* Same contract as tryToVerifyOrDecode, but without the unverified-decode fallback:
|
|
97
|
+
* the payload is only returned when the token verified.
|
|
98
|
+
*/
|
|
99
|
+
async tryToVerify(token, opt = {}) {
|
|
100
|
+
try {
|
|
101
|
+
return [null, await this.verify(token, opt)];
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return [err, null];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Tries to Verify the token, falling back to unverified Decode on failure,
|
|
109
|
+
* so the caller can "peek" into the token's content even when it doesn't verify.
|
|
110
|
+
*
|
|
111
|
+
* Returns an [error, payload] tuple - ALWAYS check the error first:
|
|
112
|
+
* - [null, payload] - the token verified, payload can be trusted
|
|
113
|
+
* - [error, payload] - verification failed, but the token could still be decoded.
|
|
114
|
+
* When the error is JWTExpiredError/JWTNotYetValidError, the signature was already
|
|
115
|
+
* verified and only the time-claim check failed, so the payload is trustworthy
|
|
116
|
+
* (just expired/not yet valid). For any other error the payload is completely
|
|
117
|
+
* UNVERIFIED - never trust it, only peek (e.g to log/report the claimed identity).
|
|
118
|
+
* - [error, null] - the token could not even be decoded
|
|
119
|
+
*
|
|
120
|
+
* The decode fallback is raw: opt.schema/cfg.schema are only applied on the Verify path.
|
|
121
|
+
*/
|
|
122
|
+
async tryToVerifyOrDecode(token, opt = {}) {
|
|
123
|
+
const [verifyError, payload] = await this.tryToVerify(token, opt);
|
|
124
|
+
if (!verifyError)
|
|
125
|
+
return [null, payload];
|
|
126
|
+
// Expired/not-yet-valid errors already carry the (signature-verified) payload
|
|
127
|
+
if (verifyError instanceof JWTExpiredError || verifyError instanceof JWTNotYetValidError) {
|
|
128
|
+
return [verifyError, verifyError.payload];
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
return [verifyError, jwtDecode(token).payload];
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return [verifyError, null];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
decode(token, opt = {}) {
|
|
138
|
+
let decoded;
|
|
139
|
+
try {
|
|
140
|
+
decoded = jwtDecode(token);
|
|
50
141
|
}
|
|
51
142
|
catch (err) {
|
|
52
143
|
if (this.cfg.errorData) {
|
|
53
|
-
_errorDataAppend(err,
|
|
54
|
-
...this.cfg.errorData,
|
|
55
|
-
});
|
|
144
|
+
_errorDataAppend(err, this.cfg.errorData);
|
|
56
145
|
}
|
|
57
146
|
throw err;
|
|
58
147
|
}
|
|
148
|
+
this.validate(decoded.payload, opt.schema);
|
|
149
|
+
return decoded;
|
|
59
150
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Schema-validation errors on Verify/Decode are extended with cfg.errorData:
|
|
153
|
+
* a token with a non-conforming payload is as unauthorized as an invalid one.
|
|
154
|
+
* (On Sign the payload comes from own code, not from user input, so a validation
|
|
155
|
+
* error there indicates a programming error and is thrown as-is.)
|
|
156
|
+
*/
|
|
157
|
+
validate(payload, schema) {
|
|
158
|
+
try {
|
|
159
|
+
;
|
|
160
|
+
(schema || this.cfg.schema)?.validate(payload);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
if (this.cfg.errorData) {
|
|
164
|
+
_errorDataAppend(err, this.cfg.errorData);
|
|
165
|
+
}
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* jose errors are normalized into JWTError subclasses (extended with cfg.errorData).
|
|
171
|
+
* Non-jose errors (e.g TypeError from passing a key that doesn't fit the algorithm)
|
|
172
|
+
* indicate a programming error and are passed through as-is.
|
|
173
|
+
*/
|
|
174
|
+
normalizeError(err) {
|
|
175
|
+
const { errorData } = this.cfg;
|
|
176
|
+
if (err instanceof errors.JWTExpired) {
|
|
177
|
+
return new JWTExpiredError(err.message, errorData, { payload: err.payload });
|
|
178
|
+
}
|
|
179
|
+
if (err instanceof errors.JWTClaimValidationFailed && err.claim === 'nbf') {
|
|
180
|
+
return new JWTNotYetValidError(err.message, errorData, { payload: err.payload });
|
|
181
|
+
}
|
|
182
|
+
if (err instanceof errors.JOSEError) {
|
|
183
|
+
return new JWTInvalidError(err.message, errorData);
|
|
184
|
+
}
|
|
185
|
+
if (this.cfg.verifyAlgorithms && err instanceof TypeError) {
|
|
186
|
+
// With multiple verifyAlgorithms, a token/key algorithm mismatch is reachable
|
|
187
|
+
// by untrusted input, and jose reports it as TypeError - treat it as an invalid token
|
|
188
|
+
return new JWTInvalidError(err.message, errorData);
|
|
189
|
+
}
|
|
190
|
+
return err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Decodes a JWT without verifying its signature (no key needed) - never trust the result.
|
|
195
|
+
* Standalone version of JWTService.decode, for when there's no service instance at hand
|
|
196
|
+
* (no schema validation, no errorData extension).
|
|
197
|
+
*
|
|
198
|
+
* Throws JWTInvalidError if the token cannot be decoded.
|
|
199
|
+
*/
|
|
200
|
+
export function jwtDecode(token) {
|
|
201
|
+
let header;
|
|
202
|
+
let payload;
|
|
203
|
+
try {
|
|
204
|
+
header = decodeProtectedHeader(token);
|
|
205
|
+
payload = decodeJwt(token);
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
// The underlying message is folded in, as it carries the only specific detail
|
|
209
|
+
throw new JWTInvalidError(`invalid token, unable to decode: ${err.message}`);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
header,
|
|
213
|
+
payload,
|
|
214
|
+
signature: token.split('.')[2],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Thrown by JWTService on any Verify/Decode failure, always as one of its subclasses:
|
|
219
|
+
* - JWTExpiredError - `exp` claim check failed
|
|
220
|
+
* - JWTNotYetValidError - `nbf` claim check failed
|
|
221
|
+
* - JWTInvalidError - anything else (malformed token, wrong signature, other claim mismatches)
|
|
222
|
+
*
|
|
223
|
+
* Match errors with `instanceof`, e.g `err instanceof JWTExpiredError`.
|
|
224
|
+
* `data.code` carries the same distinction ('JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID'),
|
|
225
|
+
* for when the error crosses a serialization boundary (e.g ErrorObject over HTTP)
|
|
226
|
+
* where `instanceof` no longer works.
|
|
227
|
+
*
|
|
228
|
+
* The underlying jose error is deliberately NOT preserved in `cause`: everything useful
|
|
229
|
+
* from it is already carried (message is copied, the failed claim is the subclass,
|
|
230
|
+
* `payload` is exposed where trustworthy), and jose nests the raw JWT payload into its own
|
|
231
|
+
* `cause`, which would otherwise survive ErrorObject serialization and leak into logs.
|
|
232
|
+
*/
|
|
233
|
+
export class JWTError extends AppError {
|
|
234
|
+
constructor(message, data) {
|
|
235
|
+
// `new.target.name` makes subclasses report their own name
|
|
236
|
+
super(message, data, { name: new.target.name });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The token is well-formed and its signature is valid, but the `exp` claim check failed.
|
|
241
|
+
*
|
|
242
|
+
* `payload` carries the decoded token payload: signature is verified before claims are
|
|
243
|
+
* checked, so the payload is trustworthy - it's just expired.
|
|
244
|
+
* (Note: it's the raw JWT payload, incl. standard claims; cfg.schema is not applied to it.)
|
|
245
|
+
*/
|
|
246
|
+
export class JWTExpiredError extends JWTError {
|
|
247
|
+
payload;
|
|
248
|
+
constructor(message, data = {}, opt = {}) {
|
|
249
|
+
super(message, { ...data, code: 'JWT_EXPIRED' });
|
|
250
|
+
this.payload = opt.payload || {};
|
|
251
|
+
}
|
|
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 class JWTNotYetValidError extends JWTError {
|
|
261
|
+
payload;
|
|
262
|
+
constructor(message, data = {}, opt = {}) {
|
|
263
|
+
super(message, { ...data, code: 'JWT_NOT_YET_VALID' });
|
|
264
|
+
this.payload = opt.payload || {};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The token could not be trusted: malformed token, wrong signature,
|
|
269
|
+
* or a claim mismatch other than exp/nbf (e.g issuer/audience).
|
|
270
|
+
* No payload is exposed - nothing from such a token should be used.
|
|
271
|
+
*/
|
|
272
|
+
export class JWTInvalidError extends JWTError {
|
|
273
|
+
constructor(message, data = {}) {
|
|
274
|
+
super(message, { ...data, code: 'JWT_INVALID' });
|
|
69
275
|
}
|
|
70
276
|
}
|
|
@@ -271,7 +271,16 @@ export declare class JString<OUT extends string | undefined = string, Opt extend
|
|
|
271
271
|
ipv4(): this;
|
|
272
272
|
ipv6(): this;
|
|
273
273
|
slug(): this;
|
|
274
|
+
/**
|
|
275
|
+
* Validates the 3-part semver format: `major.minor.patch`, e.g `1.2.3`.
|
|
276
|
+
*/
|
|
274
277
|
semVer(): this;
|
|
278
|
+
/**
|
|
279
|
+
* Validates the 4-part version format: `major.minor.patch.build`, e.g `1.2.3.4`.
|
|
280
|
+
*
|
|
281
|
+
* Not a semver by the spec, but is used by e.g .NET and some mobile app versions.
|
|
282
|
+
*/
|
|
283
|
+
semVer4(): this;
|
|
275
284
|
languageTag(): this;
|
|
276
285
|
countryCode(): this;
|
|
277
286
|
currency(): this;
|