@devilsdev/rag-pipeline-utils 2.2.1 → 2.3.1
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/README.md +260 -19
- package/contracts/contract-schema.json +189 -0
- package/dist/index.cjs +19 -0
- package/dist/index.mjs +15 -1
- package/package.json +3 -2
- package/src/cli/commands/wizard.js +627 -0
- package/src/cli/input-sanitization-middleware.js +356 -0
- package/src/cli/templates/development.template.json +64 -0
- package/src/cli/templates/minimal.template.json +33 -0
- package/src/cli/templates/production.template.json +104 -0
- package/src/cli/templates/testing.template.json +45 -0
- package/src/config/connection-pool-config.json +259 -0
- package/src/config/memory-optimization.js +414 -0
- package/src/config/redaction-patterns.json +150 -0
- package/src/config/sanitization-rules.json +323 -0
- package/src/core/plugin-registry.js +266 -20
- package/src/dag/core/error-context.js +391 -0
- package/src/dag/core/execution-scheduler.js +350 -0
- package/src/dag/core/topology-validation.js +415 -0
- package/src/dag/dag-engine.js +26 -424
- package/src/dev/README.md +126 -0
- package/src/dev/dev-server.js +356 -0
- package/src/dev/examples/hot-reload-usage.js +355 -0
- package/src/dev/hot-reload.js +807 -0
- package/src/enterprise/sso-integration.js +67 -25
- package/src/index.js +19 -0
- package/src/observability/event-logger.js +62 -2
- package/src/plugins/optimized-openai-embedder.js +169 -0
- package/src/security/jwt-config.js +431 -0
- package/src/security/jwt-validator.js +701 -0
- package/src/utils/api-client-pool-integration.js +436 -0
- package/src/utils/batch-processor.js +710 -0
- package/src/utils/connection-pool.js +605 -0
- package/src/utils/contract-validator.js +460 -0
- package/src/utils/error-formatter.js +603 -0
- package/src/utils/examples/error-formatter-usage.js +351 -0
- package/src/utils/input-sanitizer.js +967 -0
- package/src/utils/memory-monitor.js +800 -0
- package/src/utils/secure-logger-config.js +352 -0
- package/src/utils/secure-logger.js +576 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Secure JWT Validator
|
|
5
|
+
*
|
|
6
|
+
* Provides hardened JWT validation with protection against common attacks:
|
|
7
|
+
* - Algorithm confusion attacks (none algorithm, algorithm switching)
|
|
8
|
+
* - Token substitution attacks
|
|
9
|
+
* - Weak signing algorithms
|
|
10
|
+
* - Missing or invalid claims
|
|
11
|
+
* - Replay attacks (with JTI tracking)
|
|
12
|
+
* - Token expiration and timing issues
|
|
13
|
+
*
|
|
14
|
+
* @module security/jwt-validator
|
|
15
|
+
* @since 2.3.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const jwt = require('jsonwebtoken');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const { EventEmitter } = require('events');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Secure JWT signing and validation algorithms
|
|
24
|
+
* Explicitly disallow 'none' algorithm and weak algorithms
|
|
25
|
+
*/
|
|
26
|
+
const SECURE_ALGORITHMS = {
|
|
27
|
+
// HMAC-based (symmetric)
|
|
28
|
+
HS256: 'HS256',
|
|
29
|
+
HS384: 'HS384',
|
|
30
|
+
HS512: 'HS512',
|
|
31
|
+
|
|
32
|
+
// RSA-based (asymmetric) - preferred for production
|
|
33
|
+
RS256: 'RS256',
|
|
34
|
+
RS384: 'RS384',
|
|
35
|
+
RS512: 'RS512',
|
|
36
|
+
|
|
37
|
+
// ECDSA-based (asymmetric) - preferred for performance
|
|
38
|
+
ES256: 'ES256',
|
|
39
|
+
ES384: 'ES384',
|
|
40
|
+
ES512: 'ES512',
|
|
41
|
+
|
|
42
|
+
// PSS (asymmetric)
|
|
43
|
+
PS256: 'PS256',
|
|
44
|
+
PS384: 'PS384',
|
|
45
|
+
PS512: 'PS512',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Minimum key sizes for secure JWT signing
|
|
50
|
+
*/
|
|
51
|
+
const MIN_KEY_SIZES = {
|
|
52
|
+
HS256: 256, // bits
|
|
53
|
+
HS384: 384,
|
|
54
|
+
HS512: 512,
|
|
55
|
+
RS256: 2048,
|
|
56
|
+
RS384: 2048,
|
|
57
|
+
RS512: 2048,
|
|
58
|
+
ES256: 256,
|
|
59
|
+
ES384: 384,
|
|
60
|
+
ES512: 521,
|
|
61
|
+
PS256: 2048,
|
|
62
|
+
PS384: 2048,
|
|
63
|
+
PS512: 2048,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Required JWT claims for validation
|
|
68
|
+
*/
|
|
69
|
+
const REQUIRED_CLAIMS = ['iat', 'exp', 'iss', 'aud'];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* JWT Validator with hardened security
|
|
73
|
+
*/
|
|
74
|
+
class JWTValidator extends EventEmitter {
|
|
75
|
+
constructor(options = {}) {
|
|
76
|
+
super();
|
|
77
|
+
|
|
78
|
+
this.config = {
|
|
79
|
+
// Algorithm configuration
|
|
80
|
+
algorithm: options.algorithm || 'HS256',
|
|
81
|
+
allowedAlgorithms: options.allowedAlgorithms || ['HS256'],
|
|
82
|
+
|
|
83
|
+
// Secret/Key configuration
|
|
84
|
+
secret: options.secret,
|
|
85
|
+
publicKey: options.publicKey,
|
|
86
|
+
privateKey: options.privateKey,
|
|
87
|
+
|
|
88
|
+
// Issuer and audience validation
|
|
89
|
+
issuer: options.issuer,
|
|
90
|
+
audience: options.audience,
|
|
91
|
+
|
|
92
|
+
// Timing configuration
|
|
93
|
+
clockTolerance: options.clockTolerance || 30, // seconds
|
|
94
|
+
maxTokenAge: options.maxTokenAge || 3600, // 1 hour in seconds
|
|
95
|
+
notBeforeTolerance: options.notBeforeTolerance || 30, // seconds
|
|
96
|
+
|
|
97
|
+
// Security options
|
|
98
|
+
requireJti: options.requireJti !== false, // Require JWT ID by default
|
|
99
|
+
requireSub: options.requireSub !== false, // Require subject by default
|
|
100
|
+
allowNoneAlgorithm: false, // Never allow 'none' algorithm
|
|
101
|
+
strictValidation: options.strictValidation !== false, // Strict by default
|
|
102
|
+
|
|
103
|
+
// JTI tracking for replay protection
|
|
104
|
+
enableJtiTracking: options.enableJtiTracking !== false,
|
|
105
|
+
jtiExpirationMs: options.jtiExpirationMs || 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
106
|
+
|
|
107
|
+
// Additional validation
|
|
108
|
+
validateClaims: options.validateClaims || {},
|
|
109
|
+
|
|
110
|
+
// Audit logging
|
|
111
|
+
enableAuditLog: options.enableAuditLog !== false,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Validate configuration
|
|
115
|
+
this._validateConfig();
|
|
116
|
+
|
|
117
|
+
// Initialize JTI tracking
|
|
118
|
+
if (this.config.enableJtiTracking) {
|
|
119
|
+
this.usedJtis = new Map(); // jti -> expiration timestamp (for external tokens)
|
|
120
|
+
this.selfSignedJtis = new Set(); // JTIs of tokens we signed ourselves
|
|
121
|
+
this._startJtiCleanup();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Statistics
|
|
125
|
+
this.stats = {
|
|
126
|
+
tokensGenerated: 0,
|
|
127
|
+
tokensValidated: 0,
|
|
128
|
+
validationFailures: 0,
|
|
129
|
+
algorithmMismatch: 0,
|
|
130
|
+
expiredTokens: 0,
|
|
131
|
+
invalidSignature: 0,
|
|
132
|
+
replayAttempts: 0,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validate configuration on initialization
|
|
138
|
+
* @private
|
|
139
|
+
*/
|
|
140
|
+
_validateConfig() {
|
|
141
|
+
const {
|
|
142
|
+
algorithm,
|
|
143
|
+
allowedAlgorithms,
|
|
144
|
+
secret,
|
|
145
|
+
publicKey,
|
|
146
|
+
privateKey,
|
|
147
|
+
issuer,
|
|
148
|
+
audience,
|
|
149
|
+
strictValidation,
|
|
150
|
+
} = this.config;
|
|
151
|
+
|
|
152
|
+
// Validate required claims configuration
|
|
153
|
+
if (strictValidation !== false) {
|
|
154
|
+
if (!issuer) {
|
|
155
|
+
throw new Error('issuer is required');
|
|
156
|
+
}
|
|
157
|
+
if (!audience) {
|
|
158
|
+
throw new Error('audience is required');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Validate algorithm
|
|
163
|
+
if (!SECURE_ALGORITHMS[algorithm]) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Unsupported algorithm: ${algorithm}. Use one of: ${Object.keys(SECURE_ALGORITHMS).join(', ')}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate allowed algorithms
|
|
170
|
+
if (!Array.isArray(allowedAlgorithms) || allowedAlgorithms.length === 0) {
|
|
171
|
+
throw new Error('allowedAlgorithms must be a non-empty array');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const alg of allowedAlgorithms) {
|
|
175
|
+
if (!SECURE_ALGORITHMS[alg]) {
|
|
176
|
+
throw new Error(`Invalid algorithm in allowedAlgorithms: ${alg}`);
|
|
177
|
+
}
|
|
178
|
+
if (alg === 'none') {
|
|
179
|
+
throw new Error('Algorithm "none" is not allowed for security reasons');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Validate secret/key configuration
|
|
184
|
+
const isSymmetric = algorithm.startsWith('HS');
|
|
185
|
+
const isAsymmetric =
|
|
186
|
+
algorithm.startsWith('RS') ||
|
|
187
|
+
algorithm.startsWith('ES') ||
|
|
188
|
+
algorithm.startsWith('PS');
|
|
189
|
+
|
|
190
|
+
if (isSymmetric && !secret) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
'secret is required for HMAC algorithms (HS256, HS384, HS512)',
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (isAsymmetric) {
|
|
197
|
+
if (!publicKey) {
|
|
198
|
+
throw new Error('publicKey is required for asymmetric algorithms');
|
|
199
|
+
}
|
|
200
|
+
if (!privateKey) {
|
|
201
|
+
console.warn(
|
|
202
|
+
'[JWT_VALIDATOR] Warning: privateKey not provided. Token signing will not be available.',
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Validate secret strength for HMAC
|
|
208
|
+
if (isSymmetric && secret) {
|
|
209
|
+
const minBits = MIN_KEY_SIZES[algorithm];
|
|
210
|
+
const secretBits = Buffer.byteLength(secret, 'utf8') * 8;
|
|
211
|
+
|
|
212
|
+
if (secretBits < minBits) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Secret is too weak for ${algorithm}. Minimum ${minBits} bits required, got ${secretBits} bits. ` +
|
|
215
|
+
`Use a secret of at least ${Math.ceil(minBits / 8)} bytes.`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Generate a secure JWT token
|
|
223
|
+
*
|
|
224
|
+
* @param {Object} payload - Token payload
|
|
225
|
+
* @param {Object} options - Additional signing options
|
|
226
|
+
* @returns {string} Signed JWT token
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* const token = validator.sign({
|
|
230
|
+
* sub: 'user-123',
|
|
231
|
+
* roles: ['admin'],
|
|
232
|
+
* tenant: 'acme-corp'
|
|
233
|
+
* });
|
|
234
|
+
*/
|
|
235
|
+
sign(payload, options = {}) {
|
|
236
|
+
try {
|
|
237
|
+
// Validate payload
|
|
238
|
+
if (!payload || typeof payload !== 'object') {
|
|
239
|
+
throw new Error('Payload must be an object');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Prevent algorithm override in payload
|
|
243
|
+
if (payload.alg) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
'Algorithm cannot be specified in payload (security risk)',
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Generate JTI if required
|
|
250
|
+
const jti = this.config.requireJti
|
|
251
|
+
? options.jti || crypto.randomUUID()
|
|
252
|
+
: options.jti;
|
|
253
|
+
|
|
254
|
+
// Build complete payload with required claims
|
|
255
|
+
const now = Math.floor(Date.now() / 1000);
|
|
256
|
+
const expiresIn = options.expiresIn || this.config.maxTokenAge;
|
|
257
|
+
|
|
258
|
+
const completePayload = {
|
|
259
|
+
...payload,
|
|
260
|
+
iss: this.config.issuer,
|
|
261
|
+
aud: this.config.audience,
|
|
262
|
+
iat: now,
|
|
263
|
+
exp: now + expiresIn,
|
|
264
|
+
nbf: options.notBefore || now,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Add JTI if required
|
|
268
|
+
if (jti) {
|
|
269
|
+
completePayload.jti = jti;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Add subject if required
|
|
273
|
+
if (this.config.requireSub && !completePayload.sub) {
|
|
274
|
+
throw new Error('Subject (sub) claim is required but not provided');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Determine signing key
|
|
278
|
+
const signingKey = this.config.algorithm.startsWith('HS')
|
|
279
|
+
? this.config.secret
|
|
280
|
+
: this.config.privateKey;
|
|
281
|
+
|
|
282
|
+
if (!signingKey) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
'No signing key available. Ensure secret or privateKey is configured.',
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Sign token with explicit algorithm
|
|
289
|
+
const token = jwt.sign(completePayload, signingKey, {
|
|
290
|
+
algorithm: this.config.algorithm,
|
|
291
|
+
// Do not allow algorithm to be overridden
|
|
292
|
+
header: {
|
|
293
|
+
alg: this.config.algorithm,
|
|
294
|
+
typ: 'JWT',
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Track self-signed JTIs with special handling to allow multiple verifications
|
|
299
|
+
// Self-signed tokens are tracked to prevent JTI collisions with external tokens
|
|
300
|
+
// but they can be verified multiple times (marked as reusable)
|
|
301
|
+
if (this.config.enableJtiTracking && jti) {
|
|
302
|
+
this.selfSignedJtis.add(jti);
|
|
303
|
+
// Also track in usedJtis with reusable flag to prevent external tokens from using same JTI
|
|
304
|
+
const expirationTime = (now + expiresIn) * 1000;
|
|
305
|
+
this.usedJtis.set(jti, { expirationTime, reusable: true });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Update statistics
|
|
309
|
+
this.stats.tokensGenerated++;
|
|
310
|
+
|
|
311
|
+
// Emit audit event
|
|
312
|
+
if (this.config.enableAuditLog) {
|
|
313
|
+
this.emit('token_generated', {
|
|
314
|
+
jti,
|
|
315
|
+
sub: completePayload.sub,
|
|
316
|
+
iss: completePayload.iss,
|
|
317
|
+
exp: completePayload.exp,
|
|
318
|
+
timestamp: new Date().toISOString(),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return token;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
this.emit('signing_error', {
|
|
325
|
+
error: error.message,
|
|
326
|
+
timestamp: new Date().toISOString(),
|
|
327
|
+
});
|
|
328
|
+
throw new Error(`JWT signing failed: ${error.message}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Verify and decode a JWT token with hardened validation
|
|
334
|
+
*
|
|
335
|
+
* @param {string} token - JWT token to verify
|
|
336
|
+
* @param {Object} options - Verification options
|
|
337
|
+
* @returns {Object} Decoded and validated payload
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* try {
|
|
341
|
+
* const payload = validator.verify(token);
|
|
342
|
+
* console.log('Valid token for user:', payload.sub);
|
|
343
|
+
* } catch (error) {
|
|
344
|
+
* console.error('Invalid token:', error.message);
|
|
345
|
+
* }
|
|
346
|
+
*/
|
|
347
|
+
verify(token, options = {}) {
|
|
348
|
+
try {
|
|
349
|
+
// Basic validation
|
|
350
|
+
if (!token || typeof token !== 'string') {
|
|
351
|
+
throw new Error('Token must be a non-empty string');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Prevent header manipulation - decode header first to check algorithm
|
|
355
|
+
const header = this._decodeTokenHeader(token);
|
|
356
|
+
|
|
357
|
+
// CRITICAL: Explicitly reject 'none' algorithm FIRST (before other checks)
|
|
358
|
+
if (header.alg === 'none' || header.alg.toLowerCase() === 'none') {
|
|
359
|
+
this.stats.validationFailures++;
|
|
360
|
+
this.emit('none_algorithm_rejected', {
|
|
361
|
+
timestamp: new Date().toISOString(),
|
|
362
|
+
});
|
|
363
|
+
throw new Error('Algorithm "none" is not allowed');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// CRITICAL: Verify algorithm matches expected algorithm
|
|
367
|
+
if (!this.config.allowedAlgorithms.includes(header.alg)) {
|
|
368
|
+
this.stats.algorithmMismatch++;
|
|
369
|
+
this.emit('algorithm_mismatch', {
|
|
370
|
+
expected: this.config.allowedAlgorithms,
|
|
371
|
+
received: header.alg,
|
|
372
|
+
timestamp: new Date().toISOString(),
|
|
373
|
+
});
|
|
374
|
+
throw new Error(
|
|
375
|
+
`Algorithm mismatch: expected one of [${this.config.allowedAlgorithms.join(', ')}], got ${header.alg}`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Determine verification key
|
|
380
|
+
const verificationKey = header.alg.startsWith('HS')
|
|
381
|
+
? this.config.secret
|
|
382
|
+
: this.config.publicKey;
|
|
383
|
+
|
|
384
|
+
if (!verificationKey) {
|
|
385
|
+
throw new Error('No verification key available');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Verify token with strict options
|
|
389
|
+
const verifyOptions = {
|
|
390
|
+
algorithms: this.config.allowedAlgorithms,
|
|
391
|
+
clockTolerance: this.config.clockTolerance,
|
|
392
|
+
// Complete validation checks
|
|
393
|
+
complete: false, // We want payload only after validation
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// Only validate issuer/audience if strictValidation is enabled
|
|
397
|
+
// This prevents duplicate validation (jsonwebtoken validates these if provided)
|
|
398
|
+
if (this.config.strictValidation) {
|
|
399
|
+
verifyOptions.issuer = this.config.issuer;
|
|
400
|
+
verifyOptions.audience = this.config.audience;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Only add maxAge if explicitly requested (optional validation)
|
|
404
|
+
if (options.maxAge || this.config.maxTokenAge < Infinity) {
|
|
405
|
+
verifyOptions.maxAge = options.maxAge || `${this.config.maxTokenAge}s`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const payload = jwt.verify(token, verificationKey, verifyOptions);
|
|
409
|
+
|
|
410
|
+
// Additional claim validation
|
|
411
|
+
this._validateRequiredClaims(payload);
|
|
412
|
+
|
|
413
|
+
// Check for replay attacks using JTI
|
|
414
|
+
// NOTE: This uses a check-then-set pattern which is safe for this use case:
|
|
415
|
+
// - Self-signed tokens (reusable) can be verified multiple times by design
|
|
416
|
+
// - External tokens (non-reusable): if race condition occurs, both requests succeed on first use
|
|
417
|
+
// (acceptable) but subsequent replays are blocked (secure)
|
|
418
|
+
if (this.config.enableJtiTracking && payload.jti) {
|
|
419
|
+
// Check if this is a self-signed (reusable) token
|
|
420
|
+
const isSelfSigned = this.selfSignedJtis.has(payload.jti);
|
|
421
|
+
|
|
422
|
+
if (!this.usedJtis.has(payload.jti)) {
|
|
423
|
+
// First time seeing this JTI - atomically track it
|
|
424
|
+
const expirationTime = payload.exp * 1000;
|
|
425
|
+
this.usedJtis.set(payload.jti, {
|
|
426
|
+
expirationTime,
|
|
427
|
+
reusable: isSelfSigned,
|
|
428
|
+
});
|
|
429
|
+
} else {
|
|
430
|
+
// JTI has been seen before
|
|
431
|
+
const jtiEntry = this.usedJtis.get(payload.jti);
|
|
432
|
+
|
|
433
|
+
// Self-signed tokens are reusable - allow multiple verifications
|
|
434
|
+
if (jtiEntry.reusable && isSelfSigned) {
|
|
435
|
+
// This is valid for refresh flows and load balancer retries
|
|
436
|
+
} else {
|
|
437
|
+
// External token being replayed - this is a security violation
|
|
438
|
+
this.stats.replayAttempts++;
|
|
439
|
+
this.emit('replay_detected', {
|
|
440
|
+
jti: payload.jti,
|
|
441
|
+
sub: payload.sub,
|
|
442
|
+
isSelfSigned,
|
|
443
|
+
timestamp: new Date().toISOString(),
|
|
444
|
+
});
|
|
445
|
+
throw new Error('Token replay detected: JTI has already been used');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Validate custom claims if specified
|
|
451
|
+
if (
|
|
452
|
+
this.config.validateClaims &&
|
|
453
|
+
Object.keys(this.config.validateClaims).length > 0
|
|
454
|
+
) {
|
|
455
|
+
this._validateCustomClaims(payload, this.config.validateClaims);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Update statistics
|
|
459
|
+
this.stats.tokensValidated++;
|
|
460
|
+
|
|
461
|
+
// Emit audit event
|
|
462
|
+
if (this.config.enableAuditLog) {
|
|
463
|
+
this.emit('token_validated', {
|
|
464
|
+
jti: payload.jti,
|
|
465
|
+
sub: payload.sub,
|
|
466
|
+
iss: payload.iss,
|
|
467
|
+
exp: payload.exp,
|
|
468
|
+
timestamp: new Date().toISOString(),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return payload;
|
|
473
|
+
} catch (error) {
|
|
474
|
+
// Update statistics based on error type
|
|
475
|
+
if (error.name === 'TokenExpiredError') {
|
|
476
|
+
this.stats.expiredTokens++;
|
|
477
|
+
} else if (error.name === 'JsonWebTokenError') {
|
|
478
|
+
this.stats.invalidSignature++;
|
|
479
|
+
} else {
|
|
480
|
+
this.stats.validationFailures++;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Emit audit event for validation failure
|
|
484
|
+
if (this.config.enableAuditLog) {
|
|
485
|
+
this.emit('validation_failed', {
|
|
486
|
+
error: error.message,
|
|
487
|
+
errorType: error.name,
|
|
488
|
+
timestamp: new Date().toISOString(),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Re-throw with context
|
|
493
|
+
if (error.name === 'TokenExpiredError') {
|
|
494
|
+
throw new Error('JWT token has expired');
|
|
495
|
+
} else if (error.name === 'JsonWebTokenError') {
|
|
496
|
+
throw new Error('Invalid JWT token signature or structure');
|
|
497
|
+
} else if (error.name === 'NotBeforeError') {
|
|
498
|
+
throw new Error('JWT token not yet valid (nbf claim)');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
throw new Error(`JWT verification failed: ${error.message}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Decode token header without verification (for algorithm checking)
|
|
507
|
+
* @private
|
|
508
|
+
*/
|
|
509
|
+
_decodeTokenHeader(token) {
|
|
510
|
+
try {
|
|
511
|
+
const parts = token.split('.');
|
|
512
|
+
if (parts.length !== 3) {
|
|
513
|
+
throw new Error('Invalid token structure');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const header = JSON.parse(
|
|
517
|
+
Buffer.from(parts[0], 'base64url').toString('utf8'),
|
|
518
|
+
);
|
|
519
|
+
return header;
|
|
520
|
+
} catch (error) {
|
|
521
|
+
throw new Error(`Failed to decode token header: ${error.message}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Validate required claims are present
|
|
527
|
+
* @private
|
|
528
|
+
*/
|
|
529
|
+
_validateRequiredClaims(payload) {
|
|
530
|
+
const missingClaims = [];
|
|
531
|
+
|
|
532
|
+
// Always require iat and exp (timing claims)
|
|
533
|
+
for (const claim of ['iat', 'exp']) {
|
|
534
|
+
if (!payload[claim]) {
|
|
535
|
+
missingClaims.push(claim);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Note: iss/aud validation is handled by jwt.verify() when strictValidation is enabled
|
|
540
|
+
// (see verifyOptions configuration in verify() method)
|
|
541
|
+
// No need to duplicate that validation here
|
|
542
|
+
|
|
543
|
+
if (this.config.requireJti && !payload.jti) {
|
|
544
|
+
missingClaims.push('jti');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (this.config.requireSub && !payload.sub) {
|
|
548
|
+
missingClaims.push('sub');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (missingClaims.length > 0) {
|
|
552
|
+
throw new Error(`Missing required claims: ${missingClaims.join(', ')}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Validate custom claims
|
|
558
|
+
* @private
|
|
559
|
+
*/
|
|
560
|
+
_validateCustomClaims(payload, expectedClaims) {
|
|
561
|
+
for (const [claim, expectedValue] of Object.entries(expectedClaims)) {
|
|
562
|
+
if (typeof expectedValue === 'function') {
|
|
563
|
+
// Custom validation function
|
|
564
|
+
if (!expectedValue(payload[claim], payload)) {
|
|
565
|
+
throw new Error(`Custom claim validation failed for: ${claim}`);
|
|
566
|
+
}
|
|
567
|
+
} else if (Array.isArray(expectedValue)) {
|
|
568
|
+
// Value must be in array
|
|
569
|
+
if (!expectedValue.includes(payload[claim])) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
`Claim ${claim} must be one of: ${expectedValue.join(', ')}`,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
// Exact match
|
|
576
|
+
if (payload[claim] !== expectedValue) {
|
|
577
|
+
throw new Error(
|
|
578
|
+
`Claim ${claim} mismatch: expected ${expectedValue}, got ${payload[claim]}`,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Start periodic cleanup of expired JTIs
|
|
587
|
+
* @private
|
|
588
|
+
*/
|
|
589
|
+
_startJtiCleanup() {
|
|
590
|
+
this.jtiCleanupInterval = setInterval(
|
|
591
|
+
() => {
|
|
592
|
+
const now = Date.now();
|
|
593
|
+
let cleanedCount = 0;
|
|
594
|
+
|
|
595
|
+
for (const [jti, jtiEntry] of this.usedJtis.entries()) {
|
|
596
|
+
// Handle both old format (number) and new format (object)
|
|
597
|
+
const expirationTime =
|
|
598
|
+
typeof jtiEntry === 'number' ? jtiEntry : jtiEntry.expirationTime;
|
|
599
|
+
if (expirationTime < now) {
|
|
600
|
+
this.usedJtis.delete(jti);
|
|
601
|
+
// Also remove from selfSignedJtis if it's there
|
|
602
|
+
this.selfSignedJtis.delete(jti);
|
|
603
|
+
cleanedCount++;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (cleanedCount > 0 && this.config.enableAuditLog) {
|
|
608
|
+
this.emit('jti_cleanup', {
|
|
609
|
+
cleanedCount,
|
|
610
|
+
remainingCount: this.usedJtis.size,
|
|
611
|
+
timestamp: new Date().toISOString(),
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
60 * 60 * 1000,
|
|
616
|
+
); // Run every hour
|
|
617
|
+
|
|
618
|
+
// Don't block process exit
|
|
619
|
+
if (this.jtiCleanupInterval.unref) {
|
|
620
|
+
this.jtiCleanupInterval.unref();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Decode token without verification (use with caution)
|
|
626
|
+
*
|
|
627
|
+
* @param {string} token - JWT token to decode
|
|
628
|
+
* @returns {Object} Decoded payload (unverified)
|
|
629
|
+
*/
|
|
630
|
+
decode(token) {
|
|
631
|
+
try {
|
|
632
|
+
return jwt.decode(token, { complete: false });
|
|
633
|
+
} catch (error) {
|
|
634
|
+
throw new Error(`Failed to decode token: ${error.message}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Get validator statistics
|
|
640
|
+
*
|
|
641
|
+
* @returns {Object} Statistics object
|
|
642
|
+
*/
|
|
643
|
+
getStats() {
|
|
644
|
+
return {
|
|
645
|
+
...this.stats,
|
|
646
|
+
trackedJtis: this.config.enableJtiTracking ? this.usedJtis.size : 0,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Reset statistics
|
|
652
|
+
*/
|
|
653
|
+
resetStats() {
|
|
654
|
+
this.stats = {
|
|
655
|
+
tokensGenerated: 0,
|
|
656
|
+
tokensValidated: 0,
|
|
657
|
+
validationFailures: 0,
|
|
658
|
+
algorithmMismatch: 0,
|
|
659
|
+
expiredTokens: 0,
|
|
660
|
+
invalidSignature: 0,
|
|
661
|
+
replayAttempts: 0,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Clear JTI tracking
|
|
667
|
+
*/
|
|
668
|
+
clearJtiTracking() {
|
|
669
|
+
if (this.config.enableJtiTracking) {
|
|
670
|
+
this.usedJtis.clear();
|
|
671
|
+
this.selfSignedJtis.clear();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Destroy validator and cleanup resources
|
|
677
|
+
*/
|
|
678
|
+
destroy() {
|
|
679
|
+
if (this.jtiCleanupInterval) {
|
|
680
|
+
clearInterval(this.jtiCleanupInterval);
|
|
681
|
+
this.jtiCleanupInterval = null;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (this.usedJtis) {
|
|
685
|
+
this.usedJtis.clear();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (this.selfSignedJtis) {
|
|
689
|
+
this.selfSignedJtis.clear();
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
this.removeAllListeners();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
module.exports = {
|
|
697
|
+
JWTValidator,
|
|
698
|
+
SECURE_ALGORITHMS,
|
|
699
|
+
MIN_KEY_SIZES,
|
|
700
|
+
REQUIRED_CLAIMS,
|
|
701
|
+
};
|