@naturalcycles/nodejs-lib 15.116.1 → 15.116.3

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.
@@ -27,8 +27,23 @@ export interface JWTService2Cfg<T extends AnyObject = AnyObject> {
27
27
  * Recommended: ES256
28
28
  * Keys (private/public) should be generated using proper settings
29
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.
30
33
  */
31
34
  algorithm: JWTAlgorithm;
35
+ /**
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 JWT_INVALID.
43
+ * (All JWTAlgorithms are asymmetric, so the classic algorithm-confusion attacks
44
+ * don't apply regardless.)
45
+ */
46
+ verifyAlgorithms?: JWTAlgorithm[];
32
47
  /**
33
48
  * If provided - payloads are validated against it on every Sign/Verify/Decode.
34
49
  * Can be overridden per-call via `opt.schema`.
@@ -78,6 +93,10 @@ export interface JWTSignOptions<T extends AnyObject = AnyObject> {
78
93
  * By default `iat` is NOT set (same as legacy JWTService with its `noTimestamp: true` default).
79
94
  */
80
95
  issuedAt?: UnixTimestamp;
96
+ /**
97
+ * Sets the `kid` (key id) header, required by some APIs (e.g Apple App Store Connect).
98
+ */
99
+ kid?: string;
81
100
  /**
82
101
  * Overrides cfg.schema for this call.
83
102
  */
@@ -170,6 +189,14 @@ export declare class JWTService2<T extends AnyObject = AnyObject> {
170
189
  */
171
190
  private normalizeError;
172
191
  }
192
+ /**
193
+ * Decodes a JWT without verifying its signature (no key needed) - never trust the result.
194
+ * Standalone version of JWTService2.decode, for when there's no service instance at hand
195
+ * (no schema validation, no errorData extension).
196
+ *
197
+ * Throws JWTError with code JWT_INVALID if the token cannot be decoded.
198
+ */
199
+ export declare function jwtDecode<T extends AnyObject>(token: JWTString): JWTDecoded<T>;
173
200
  export type JWTErrorCode = 'JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID';
174
201
  export interface JWTErrorData extends ErrorData {
175
202
  code: JWTErrorCode;
@@ -38,12 +38,17 @@ export class JWTService2 {
38
38
  }
39
39
  async sign(payload, opt) {
40
40
  _assert(this.privateKey, 'JWTService2: privateKey is required to be able to sign, but not provided', this.cfg.errorData);
41
- const { expiresAt, notBefore, issuer, audience, subject, jwtid, issuedAt, schema } = {
41
+ const { expiresAt, notBefore, issuer, audience, subject, jwtid, issuedAt, kid, schema } = {
42
42
  ...this.cfg.signOptions,
43
43
  ...opt,
44
44
  };
45
45
  (schema || this.cfg.schema)?.validate(payload);
46
- const jwt = new SignJWT(payload).setProtectedHeader({ alg: this.cfg.algorithm, typ: 'JWT' });
46
+ // `kid: undefined` is dropped by JSON serialization, keeping the header unchanged when not set
47
+ const jwt = new SignJWT(payload).setProtectedHeader({
48
+ alg: this.cfg.algorithm,
49
+ typ: 'JWT',
50
+ kid,
51
+ });
47
52
  if (expiresAt !== null)
48
53
  jwt.setExpirationTime(expiresAt);
49
54
  if (notBefore !== undefined)
@@ -70,7 +75,7 @@ export class JWTService2 {
70
75
  let data;
71
76
  try {
72
77
  const { payload } = await jwtVerify(token, key, {
73
- algorithms: [this.cfg.algorithm],
78
+ algorithms: this.cfg.verifyAlgorithms || [this.cfg.algorithm],
74
79
  ...joseOpt,
75
80
  currentDate: now === undefined ? undefined : new Date(now * 1000),
76
81
  });
@@ -83,24 +88,18 @@ export class JWTService2 {
83
88
  return data;
84
89
  }
85
90
  decode(token, opt = {}) {
86
- let header;
87
- let payload;
91
+ let decoded;
88
92
  try {
89
- header = decodeProtectedHeader(token);
90
- payload = decodeJwt(token);
93
+ decoded = jwtDecode(token);
91
94
  }
92
95
  catch (err) {
93
- throw new JWTError('invalid token, unable to decode', {
94
- ...this.cfg.errorData,
95
- code: 'JWT_INVALID',
96
- }, { cause: err });
96
+ if (this.cfg.errorData) {
97
+ _errorDataAppend(err, this.cfg.errorData);
98
+ }
99
+ throw err;
97
100
  }
98
- this.validate(payload, opt.schema);
99
- return {
100
- header,
101
- payload,
102
- signature: token.split('.')[2],
103
- };
101
+ this.validate(decoded.payload, opt.schema);
102
+ return decoded;
104
103
  }
105
104
  /**
106
105
  * Schema-validation errors on Verify/Decode are extended with cfg.errorData:
@@ -136,6 +135,11 @@ export class JWTService2 {
136
135
  else if (err instanceof errors.JOSEError) {
137
136
  code = 'JWT_INVALID';
138
137
  }
138
+ else if (this.cfg.verifyAlgorithms && err instanceof TypeError) {
139
+ // With multiple verifyAlgorithms, a token/key algorithm mismatch is reachable
140
+ // by untrusted input, and jose reports it as TypeError - treat it as an invalid token
141
+ code = 'JWT_INVALID';
142
+ }
139
143
  else {
140
144
  return err;
141
145
  }
@@ -145,6 +149,29 @@ export class JWTService2 {
145
149
  }, { cause: err });
146
150
  }
147
151
  }
152
+ /**
153
+ * Decodes a JWT without verifying its signature (no key needed) - never trust the result.
154
+ * Standalone version of JWTService2.decode, for when there's no service instance at hand
155
+ * (no schema validation, no errorData extension).
156
+ *
157
+ * Throws JWTError with code JWT_INVALID if the token cannot be decoded.
158
+ */
159
+ export function jwtDecode(token) {
160
+ let header;
161
+ let payload;
162
+ try {
163
+ header = decodeProtectedHeader(token);
164
+ payload = decodeJwt(token);
165
+ }
166
+ catch (err) {
167
+ throw new JWTError('invalid token, unable to decode', { code: 'JWT_INVALID' }, { cause: err });
168
+ }
169
+ return {
170
+ header,
171
+ payload,
172
+ signature: token.split('.')[2],
173
+ };
174
+ }
148
175
  /**
149
176
  * Thrown by JWTService2 on any Verify/Decode failure.
150
177
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.116.1",
4
+ "version": "15.116.3",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -49,9 +49,25 @@ export interface JWTService2Cfg<T extends AnyObject = AnyObject> {
49
49
  * Recommended: ES256
50
50
  * Keys (private/public) should be generated using proper settings
51
51
  * that fit the used Algorithm.
52
+ *
53
+ * Used for Sign, and (unless `verifyAlgorithms` is set) it's also
54
+ * the only algorithm accepted on Verify.
52
55
  */
53
56
  algorithm: JWTAlgorithm
54
57
 
58
+ /**
59
+ * JWS algorithms accepted on Verify. Defaults to `[algorithm]`.
60
+ *
61
+ * Only needed when one verifier must accept keys of different types
62
+ * (e.g a per-kid key set mixing EC and RSA keys). Keep the list as narrow as possible.
63
+ *
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 JWT_INVALID.
66
+ * (All JWTAlgorithms are asymmetric, so the classic algorithm-confusion attacks
67
+ * don't apply regardless.)
68
+ */
69
+ verifyAlgorithms?: JWTAlgorithm[]
70
+
55
71
  /**
56
72
  * If provided - payloads are validated against it on every Sign/Verify/Decode.
57
73
  * Can be overridden per-call via `opt.schema`.
@@ -105,6 +121,10 @@ export interface JWTSignOptions<T extends AnyObject = AnyObject> {
105
121
  * By default `iat` is NOT set (same as legacy JWTService with its `noTimestamp: true` default).
106
122
  */
107
123
  issuedAt?: UnixTimestamp
124
+ /**
125
+ * Sets the `kid` (key id) header, required by some APIs (e.g Apple App Store Connect).
126
+ */
127
+ kid?: string
108
128
  /**
109
129
  * Overrides cfg.schema for this call.
110
130
  */
@@ -198,14 +218,19 @@ export class JWTService2<T extends AnyObject = AnyObject> {
198
218
  this.cfg.errorData,
199
219
  )
200
220
 
201
- const { expiresAt, notBefore, issuer, audience, subject, jwtid, issuedAt, schema } = {
221
+ const { expiresAt, notBefore, issuer, audience, subject, jwtid, issuedAt, kid, schema } = {
202
222
  ...this.cfg.signOptions,
203
223
  ...opt,
204
224
  }
205
225
 
206
226
  ;(schema || this.cfg.schema)?.validate(payload)
207
227
 
208
- const jwt = new SignJWT(payload).setProtectedHeader({ alg: this.cfg.algorithm, typ: 'JWT' })
228
+ // `kid: undefined` is dropped by JSON serialization, keeping the header unchanged when not set
229
+ const jwt = new SignJWT(payload).setProtectedHeader({
230
+ alg: this.cfg.algorithm,
231
+ typ: 'JWT',
232
+ kid,
233
+ })
209
234
  if (expiresAt !== null) jwt.setExpirationTime(expiresAt)
210
235
  if (notBefore !== undefined) jwt.setNotBefore(notBefore)
211
236
  if (issuer) jwt.setIssuer(issuer)
@@ -234,7 +259,7 @@ export class JWTService2<T extends AnyObject = AnyObject> {
234
259
 
235
260
  try {
236
261
  const { payload } = await jwtVerify(token, key, {
237
- algorithms: [this.cfg.algorithm],
262
+ algorithms: this.cfg.verifyAlgorithms || [this.cfg.algorithm],
238
263
  ...joseOpt,
239
264
  currentDate: now === undefined ? undefined : new Date(now * 1000),
240
265
  })
@@ -249,30 +274,20 @@ export class JWTService2<T extends AnyObject = AnyObject> {
249
274
  }
250
275
 
251
276
  decode<TT extends T = T>(token: JWTString, opt: JWTDecodeOptions<TT> = {}): JWTDecoded<TT> {
252
- let header: JWTHeader
253
- let payload: TT
277
+ let decoded: JWTDecoded<TT>
254
278
 
255
279
  try {
256
- header = decodeProtectedHeader(token) as JWTHeader
257
- payload = decodeJwt(token) as TT
280
+ decoded = jwtDecode<TT>(token)
258
281
  } catch (err) {
259
- throw new JWTError(
260
- 'invalid token, unable to decode',
261
- {
262
- ...this.cfg.errorData,
263
- code: 'JWT_INVALID',
264
- },
265
- { cause: err },
266
- )
282
+ if (this.cfg.errorData) {
283
+ _errorDataAppend(err, this.cfg.errorData)
284
+ }
285
+ throw err
267
286
  }
268
287
 
269
- this.validate(payload, opt.schema)
288
+ this.validate(decoded.payload, opt.schema)
270
289
 
271
- return {
272
- header,
273
- payload,
274
- signature: token.split('.')[2]!,
275
- }
290
+ return decoded
276
291
  }
277
292
 
278
293
  /**
@@ -306,6 +321,10 @@ export class JWTService2<T extends AnyObject = AnyObject> {
306
321
  code = 'JWT_NOT_YET_VALID'
307
322
  } else if (err instanceof errors.JOSEError) {
308
323
  code = 'JWT_INVALID'
324
+ } else if (this.cfg.verifyAlgorithms && err instanceof TypeError) {
325
+ // With multiple verifyAlgorithms, a token/key algorithm mismatch is reachable
326
+ // by untrusted input, and jose reports it as TypeError - treat it as an invalid token
327
+ code = 'JWT_INVALID'
309
328
  } else {
310
329
  return err as Error
311
330
  }
@@ -321,6 +340,31 @@ export class JWTService2<T extends AnyObject = AnyObject> {
321
340
  }
322
341
  }
323
342
 
343
+ /**
344
+ * Decodes a JWT without verifying its signature (no key needed) - never trust the result.
345
+ * Standalone version of JWTService2.decode, for when there's no service instance at hand
346
+ * (no schema validation, no errorData extension).
347
+ *
348
+ * Throws JWTError with code JWT_INVALID if the token cannot be decoded.
349
+ */
350
+ export function jwtDecode<T extends AnyObject>(token: JWTString): JWTDecoded<T> {
351
+ let header: JWTHeader
352
+ let payload: T
353
+
354
+ try {
355
+ header = decodeProtectedHeader(token) as JWTHeader
356
+ payload = decodeJwt(token) as T
357
+ } catch (err) {
358
+ throw new JWTError('invalid token, unable to decode', { code: 'JWT_INVALID' }, { cause: err })
359
+ }
360
+
361
+ return {
362
+ header,
363
+ payload,
364
+ signature: token.split('.')[2]!,
365
+ }
366
+ }
367
+
324
368
  export type JWTErrorCode = 'JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_INVALID'
325
369
 
326
370
  export interface JWTErrorData extends ErrorData {