dms-middleware-auth 1.1.5 → 1.1.7
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/auth.middleware.d.ts +3 -0
- package/dist/auth.middleware.js +134 -29
- package/package.json +1 -1
- package/src/auth.middleware.ts +160 -34
|
@@ -24,9 +24,12 @@ export declare class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
24
24
|
private static readonly licenceExpiredMessage;
|
|
25
25
|
private static readonly CLOCK_TOLERANCE_MS;
|
|
26
26
|
private static readonly LIC_EXPIRY_CHECK_INTERVAL_MS;
|
|
27
|
+
private static readonly MAX_SAFE_TTL_MS;
|
|
28
|
+
private static readonly MAX_SAFE_TTL_SECONDS;
|
|
27
29
|
private static readonly LIC_CACHE_TTL_SECONDS;
|
|
28
30
|
private static readonly CLIENT_TOKEN_TTL_MAX_SECONDS;
|
|
29
31
|
private static readonly CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS;
|
|
32
|
+
private static readonly ROLE_ATTRIBUTES_TTL_SECONDS;
|
|
30
33
|
constructor(cacheManager: Cache, options: AuthMiddlewareOptions);
|
|
31
34
|
onModuleInit(): Promise<void>;
|
|
32
35
|
use(req: any, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
|
package/dist/auth.middleware.js
CHANGED
|
@@ -92,7 +92,7 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
92
92
|
server_time: new Date().toISOString(),
|
|
93
93
|
licence_expiry: AuthMiddleware_1.licenceValidatedUntilMs
|
|
94
94
|
? new Date(AuthMiddleware_1.licenceValidatedUntilMs).toISOString()
|
|
95
|
-
: 'Unknown'
|
|
95
|
+
: 'Unknown',
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
// Allow bypass URL
|
|
@@ -118,8 +118,8 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
118
118
|
if (!clientAttributes) {
|
|
119
119
|
const roleObj = await this.getClientRoleAttributes(role, clientToken);
|
|
120
120
|
clientAttributes = JSON.stringify(roleObj.attributes ?? {});
|
|
121
|
-
//
|
|
122
|
-
await this.cacheSetSeconds(clientRoleCacheKey, clientAttributes,
|
|
121
|
+
// ✅ Cache with safe TTL
|
|
122
|
+
await this.cacheSetSeconds(clientRoleCacheKey, clientAttributes, AuthMiddleware_1.ROLE_ATTRIBUTES_TTL_SECONDS);
|
|
123
123
|
}
|
|
124
124
|
// Route access check
|
|
125
125
|
const attrs = JSON.parse(clientAttributes);
|
|
@@ -137,11 +137,12 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
137
137
|
}
|
|
138
138
|
req['user'] = {
|
|
139
139
|
role,
|
|
140
|
-
userName: decoded?.preferred_username
|
|
140
|
+
userName: decoded?.preferred_username,
|
|
141
141
|
};
|
|
142
142
|
return next();
|
|
143
143
|
}
|
|
144
144
|
catch (error) {
|
|
145
|
+
this.logger.error(`Middleware error: ${error?.message || error}`, error?.stack);
|
|
145
146
|
return res.status(500).json({ message: error?.message || 'Internal error' });
|
|
146
147
|
}
|
|
147
148
|
}
|
|
@@ -173,6 +174,7 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
173
174
|
if (AuthMiddleware_1.shutdownInitiated)
|
|
174
175
|
return;
|
|
175
176
|
AuthMiddleware_1.shutdownInitiated = true;
|
|
177
|
+
this.logger.warn('Initiating graceful shutdown due to licence expiry');
|
|
176
178
|
// immediate SIGTERM, then force exit after 5s
|
|
177
179
|
setTimeout(() => {
|
|
178
180
|
try {
|
|
@@ -182,7 +184,10 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
182
184
|
process.exit(1);
|
|
183
185
|
}
|
|
184
186
|
if (!AuthMiddleware_1.shutdownTimer) {
|
|
185
|
-
AuthMiddleware_1.shutdownTimer = setTimeout(() =>
|
|
187
|
+
AuthMiddleware_1.shutdownTimer = setTimeout(() => {
|
|
188
|
+
this.logger.error('Force exit after SIGTERM timeout');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}, 5000);
|
|
186
191
|
}
|
|
187
192
|
}, 0);
|
|
188
193
|
}
|
|
@@ -207,12 +212,18 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
207
212
|
const cachedToken = (await this.cacheManager.get('client_Licence_token'));
|
|
208
213
|
if (cachedToken) {
|
|
209
214
|
const ok = await this.validateLicence(cachedToken, publicKey, false);
|
|
210
|
-
if (ok)
|
|
215
|
+
if (ok) {
|
|
216
|
+
this.logger.log('Licence validated from cache');
|
|
211
217
|
return true;
|
|
218
|
+
}
|
|
212
219
|
}
|
|
213
220
|
// 2) Fetch from licensing service
|
|
221
|
+
this.logger.log('Fetching licence from service');
|
|
214
222
|
const token = await this.getLicencingTokenFromService(realm);
|
|
215
223
|
const ok = await this.validateLicence(token, publicKey, true);
|
|
224
|
+
if (ok) {
|
|
225
|
+
this.logger.log('Licence validated from service');
|
|
226
|
+
}
|
|
216
227
|
return ok;
|
|
217
228
|
})();
|
|
218
229
|
try {
|
|
@@ -223,7 +234,7 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
223
234
|
}
|
|
224
235
|
}
|
|
225
236
|
catch (e) {
|
|
226
|
-
this.logger.
|
|
237
|
+
this.logger.error(`checkLicenceAndValidate failed: ${e?.message || e}`, e?.stack);
|
|
227
238
|
return false;
|
|
228
239
|
}
|
|
229
240
|
}
|
|
@@ -245,6 +256,8 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
245
256
|
}
|
|
246
257
|
// ✅ Save only the timestamp (supports 50 days / 500 days / years)
|
|
247
258
|
AuthMiddleware_1.licenceValidatedUntilMs = licEndMs;
|
|
259
|
+
const daysUntilExpiry = Math.floor((licEndMs - now) / (1000 * 60 * 60 * 24));
|
|
260
|
+
this.logger.log(`Licence valid until ${new Date(licEndMs).toISOString()} (${daysUntilExpiry} days remaining)`);
|
|
248
261
|
// ✅ Cache only briefly (avoid long TTL -> avoids TimeoutOverflowWarning)
|
|
249
262
|
if (updateCache) {
|
|
250
263
|
await this.cacheSetSeconds('client_Licence_token', tokenJwt, AuthMiddleware_1.LIC_CACHE_TTL_SECONDS);
|
|
@@ -268,15 +281,21 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
268
281
|
const url = this.options.licenceServiceUrl ??
|
|
269
282
|
'https://iiuuckued274sisadalbpf7ivu0eiblk.lambda-url.ap-south-1.on.aws/';
|
|
270
283
|
const body = { client_name: realm };
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
284
|
+
try {
|
|
285
|
+
const resp = await axios_1.default.post(url, body, { timeout: 15_000 });
|
|
286
|
+
if (resp.status !== axios_1.HttpStatusCode.Ok) {
|
|
287
|
+
throw new Error(`Licencing service returned status ${resp.status}`);
|
|
288
|
+
}
|
|
289
|
+
const token = resp?.data?.token;
|
|
290
|
+
if (!token || typeof token !== 'string') {
|
|
291
|
+
throw new Error('Licencing service response missing token');
|
|
292
|
+
}
|
|
293
|
+
return token;
|
|
274
294
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
throw
|
|
295
|
+
catch (e) {
|
|
296
|
+
this.logger.error(`Failed to fetch licence from service: ${e?.message || e}`);
|
|
297
|
+
throw e;
|
|
278
298
|
}
|
|
279
|
-
return token;
|
|
280
299
|
}
|
|
281
300
|
// -------------------------
|
|
282
301
|
// Keycloak client token caching (safe TTL)
|
|
@@ -302,7 +321,7 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
302
321
|
const resp = await axios_1.default.post(`${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`, new URLSearchParams({
|
|
303
322
|
grant_type: 'client_credentials',
|
|
304
323
|
client_id: clientId,
|
|
305
|
-
client_secret: clientSecret
|
|
324
|
+
client_secret: clientSecret,
|
|
306
325
|
}).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 15_000 });
|
|
307
326
|
const token = resp?.data?.access_token;
|
|
308
327
|
if (!token) {
|
|
@@ -324,7 +343,7 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
324
343
|
return jwt.verify(token, publicKeys, {
|
|
325
344
|
algorithms: ['RS256'],
|
|
326
345
|
ignoreExpiration: true,
|
|
327
|
-
clockTolerance: 60
|
|
346
|
+
clockTolerance: 60,
|
|
328
347
|
});
|
|
329
348
|
}
|
|
330
349
|
decodeAccessToken(authHeader, publicKey) {
|
|
@@ -332,7 +351,7 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
332
351
|
const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
|
|
333
352
|
return jwt.verify(token, publicKeys, {
|
|
334
353
|
algorithms: ['RS256'],
|
|
335
|
-
clockTolerance: 60
|
|
354
|
+
clockTolerance: 60,
|
|
336
355
|
});
|
|
337
356
|
}
|
|
338
357
|
extractBearerToken(authHeader) {
|
|
@@ -352,19 +371,101 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
352
371
|
return n;
|
|
353
372
|
}
|
|
354
373
|
// -------------------------
|
|
355
|
-
// Cache helper
|
|
356
|
-
//
|
|
357
|
-
//
|
|
374
|
+
// ✅ ULTIMATE FIX: Cache helper with COMPLETE OVERFLOW PROTECTION
|
|
375
|
+
// -------------------------
|
|
376
|
+
// Works with ANY cache-manager version and ANY cache store
|
|
377
|
+
// Prevents cache stores from calculating TTL from JWT claims
|
|
358
378
|
// -------------------------
|
|
359
379
|
async cacheSetSeconds(key, value, ttlSeconds) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
380
|
+
try {
|
|
381
|
+
// ttlSeconds=0 => store forever (no expiry)
|
|
382
|
+
if (!ttlSeconds || ttlSeconds <= 0) {
|
|
383
|
+
await this.cacheManager.set(key, value);
|
|
384
|
+
this.logger.debug(`Cache set (no TTL): ${key}`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
// ✅ CRITICAL: Clamp TTL to prevent overflow
|
|
388
|
+
let safeTtlSeconds = ttlSeconds;
|
|
389
|
+
if (ttlSeconds > AuthMiddleware_1.MAX_SAFE_TTL_SECONDS) {
|
|
390
|
+
safeTtlSeconds = AuthMiddleware_1.MAX_SAFE_TTL_SECONDS;
|
|
391
|
+
this.logger.warn(`Cache TTL clamped: key=${key}, requested=${ttlSeconds}s (~${Math.floor(ttlSeconds / 86400)} days), using=${safeTtlSeconds}s (~24 days)`);
|
|
392
|
+
}
|
|
393
|
+
// Convert to milliseconds
|
|
394
|
+
const ttlMs = safeTtlSeconds * 1000;
|
|
395
|
+
// ✅ CRITICAL: Double-check we're not exceeding the limit
|
|
396
|
+
// This catches any edge cases where ttlMs might still overflow
|
|
397
|
+
const finalTtlMs = Math.min(ttlMs, AuthMiddleware_1.MAX_SAFE_TTL_MS);
|
|
398
|
+
if (finalTtlMs !== ttlMs) {
|
|
399
|
+
this.logger.warn(`TTL further clamped: ${ttlMs}ms -> ${finalTtlMs}ms`);
|
|
400
|
+
}
|
|
401
|
+
// ✅ Try multiple methods to ensure compatibility with all cache stores
|
|
402
|
+
let cacheSetSuccess = false;
|
|
403
|
+
let lastError = null;
|
|
404
|
+
// Method 1: Modern cache-manager v5+ (TTL in milliseconds as 3rd param)
|
|
405
|
+
if (!cacheSetSuccess) {
|
|
406
|
+
try {
|
|
407
|
+
await this.cacheManager.set(key, value, finalTtlMs);
|
|
408
|
+
cacheSetSuccess = true;
|
|
409
|
+
this.logger.debug(`Cache set (v5 format): ${key} (TTL: ${safeTtlSeconds}s / ${finalTtlMs}ms)`);
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
lastError = err;
|
|
413
|
+
// Try next method
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// Method 2: Object-based TTL (milliseconds) - some stores
|
|
417
|
+
if (!cacheSetSuccess) {
|
|
418
|
+
try {
|
|
419
|
+
await this.cacheManager.set(key, value, { ttl: finalTtlMs });
|
|
420
|
+
cacheSetSuccess = true;
|
|
421
|
+
this.logger.debug(`Cache set (object ms format): ${key} (TTL: ${finalTtlMs}ms)`);
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
lastError = err;
|
|
425
|
+
// Try next method
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Method 3: Object-based TTL (seconds) - Redis and others
|
|
429
|
+
if (!cacheSetSuccess) {
|
|
430
|
+
try {
|
|
431
|
+
await this.cacheManager.set(key, value, { ttl: safeTtlSeconds });
|
|
432
|
+
cacheSetSuccess = true;
|
|
433
|
+
this.logger.debug(`Cache set (object sec format): ${key} (TTL: ${safeTtlSeconds}s)`);
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
lastError = err;
|
|
437
|
+
// Try next method
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Method 4: Direct store access (for Redis stores)
|
|
441
|
+
if (!cacheSetSuccess) {
|
|
442
|
+
try {
|
|
443
|
+
const store = this.cacheManager.store;
|
|
444
|
+
const redisClient = store?.client || store?.getClient?.();
|
|
445
|
+
if (redisClient && typeof redisClient.set === 'function') {
|
|
446
|
+
// Redis native command: SET key value EX seconds
|
|
447
|
+
await redisClient.set(key, value, { EX: safeTtlSeconds });
|
|
448
|
+
cacheSetSuccess = true;
|
|
449
|
+
this.logger.debug(`Cache set (Redis direct): ${key} (EX: ${safeTtlSeconds}s)`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
lastError = err;
|
|
454
|
+
// Continue to fallback
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Method 5: Fallback - cache without TTL (better than failing)
|
|
458
|
+
if (!cacheSetSuccess) {
|
|
459
|
+
this.logger.warn(`All TTL methods failed for key=${key}, caching without TTL. Last error: ${lastError?.message}`);
|
|
460
|
+
await this.cacheManager.set(key, value);
|
|
461
|
+
this.logger.debug(`Cache set (no TTL fallback): ${key}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch (e) {
|
|
465
|
+
// Even if all methods fail, don't crash the app
|
|
466
|
+
this.logger.error(`Cache set completely failed for key=${key}: ${e?.message || e}`, e?.stack);
|
|
467
|
+
// Cache failures are non-fatal - log and continue
|
|
364
468
|
}
|
|
365
|
-
// cache-manager / Nest stores vary: some accept third param number, others accept { ttl }
|
|
366
|
-
// We use { ttl } for better compatibility, and TTL is small anyway.
|
|
367
|
-
await this.cacheManager.set(key, value, { ttl: ttlSeconds });
|
|
368
469
|
}
|
|
369
470
|
};
|
|
370
471
|
exports.AuthMiddleware = AuthMiddleware;
|
|
@@ -382,11 +483,15 @@ AuthMiddleware.CLOCK_TOLERANCE_MS = 60_000; // 1 min
|
|
|
382
483
|
// IMPORTANT:
|
|
383
484
|
// Node timers overflow above ~2,147,483,647ms (~24.8 days).
|
|
384
485
|
// We avoid scheduling long timeouts completely.
|
|
385
|
-
AuthMiddleware.LIC_EXPIRY_CHECK_INTERVAL_MS =
|
|
386
|
-
//
|
|
486
|
+
AuthMiddleware.LIC_EXPIRY_CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
487
|
+
// ✅ OVERFLOW PROTECTION: Max safe TTL for 32-bit timers
|
|
488
|
+
AuthMiddleware.MAX_SAFE_TTL_MS = 2_147_483_647; // ~24.8 days
|
|
489
|
+
AuthMiddleware.MAX_SAFE_TTL_SECONDS = Math.floor(2_147_483_647 / 1000); // ~24 days
|
|
490
|
+
// Cache TTLs (safe by design; prevents timer overflow)
|
|
387
491
|
AuthMiddleware.LIC_CACHE_TTL_SECONDS = 15 * 60; // 15 minutes
|
|
388
492
|
AuthMiddleware.CLIENT_TOKEN_TTL_MAX_SECONDS = 60 * 60; // 1 hour cap
|
|
389
493
|
AuthMiddleware.CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS = 30; // refresh a bit early
|
|
494
|
+
AuthMiddleware.ROLE_ATTRIBUTES_TTL_SECONDS = 24 * 60 * 60; // 24 hours
|
|
390
495
|
exports.AuthMiddleware = AuthMiddleware = AuthMiddleware_1 = __decorate([
|
|
391
496
|
(0, common_1.Injectable)(),
|
|
392
497
|
__param(0, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
|
package/package.json
CHANGED
package/src/auth.middleware.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
Injectable,
|
|
4
4
|
Logger,
|
|
5
5
|
NestMiddleware,
|
|
6
|
-
OnModuleInit
|
|
6
|
+
OnModuleInit,
|
|
7
7
|
} from '@nestjs/common';
|
|
8
8
|
import { Response, NextFunction } from 'express';
|
|
9
9
|
import axios, { HttpStatusCode } from 'axios';
|
|
@@ -52,18 +52,23 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
52
52
|
// IMPORTANT:
|
|
53
53
|
// Node timers overflow above ~2,147,483,647ms (~24.8 days).
|
|
54
54
|
// We avoid scheduling long timeouts completely.
|
|
55
|
-
private static readonly LIC_EXPIRY_CHECK_INTERVAL_MS =
|
|
55
|
+
private static readonly LIC_EXPIRY_CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
56
56
|
|
|
57
|
-
//
|
|
57
|
+
// ✅ OVERFLOW PROTECTION: Max safe TTL for 32-bit timers
|
|
58
|
+
private static readonly MAX_SAFE_TTL_MS = 2_147_483_647; // ~24.8 days
|
|
59
|
+
private static readonly MAX_SAFE_TTL_SECONDS = Math.floor(2_147_483_647 / 1000); // ~24 days
|
|
60
|
+
|
|
61
|
+
// Cache TTLs (safe by design; prevents timer overflow)
|
|
58
62
|
private static readonly LIC_CACHE_TTL_SECONDS = 15 * 60; // 15 minutes
|
|
59
63
|
private static readonly CLIENT_TOKEN_TTL_MAX_SECONDS = 60 * 60; // 1 hour cap
|
|
60
64
|
private static readonly CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS = 30; // refresh a bit early
|
|
65
|
+
private static readonly ROLE_ATTRIBUTES_TTL_SECONDS = 24 * 60 * 60; // 24 hours
|
|
61
66
|
|
|
62
67
|
constructor(
|
|
63
68
|
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
|
64
69
|
@Inject('AUTH_MIDDLEWARE_OPTIONS')
|
|
65
70
|
private readonly options: AuthMiddlewareOptions
|
|
66
|
-
) {}
|
|
71
|
+
) { }
|
|
67
72
|
|
|
68
73
|
// -------------------------
|
|
69
74
|
// Startup: validate licence once, then run a small periodic expiry check
|
|
@@ -102,7 +107,7 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
102
107
|
server_time: new Date().toISOString(),
|
|
103
108
|
licence_expiry: AuthMiddleware.licenceValidatedUntilMs
|
|
104
109
|
? new Date(AuthMiddleware.licenceValidatedUntilMs).toISOString()
|
|
105
|
-
: 'Unknown'
|
|
110
|
+
: 'Unknown',
|
|
106
111
|
});
|
|
107
112
|
}
|
|
108
113
|
|
|
@@ -135,8 +140,13 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
135
140
|
if (!clientAttributes) {
|
|
136
141
|
const roleObj = await this.getClientRoleAttributes(role, clientToken);
|
|
137
142
|
clientAttributes = JSON.stringify(roleObj.attributes ?? {});
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
|
|
144
|
+
// ✅ Cache with safe TTL
|
|
145
|
+
await this.cacheSetSeconds(
|
|
146
|
+
clientRoleCacheKey,
|
|
147
|
+
clientAttributes,
|
|
148
|
+
AuthMiddleware.ROLE_ATTRIBUTES_TTL_SECONDS
|
|
149
|
+
);
|
|
140
150
|
}
|
|
141
151
|
|
|
142
152
|
// Route access check
|
|
@@ -157,11 +167,12 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
157
167
|
|
|
158
168
|
req['user'] = {
|
|
159
169
|
role,
|
|
160
|
-
userName: decoded?.preferred_username
|
|
170
|
+
userName: decoded?.preferred_username,
|
|
161
171
|
};
|
|
162
172
|
|
|
163
173
|
return next();
|
|
164
174
|
} catch (error: any) {
|
|
175
|
+
this.logger.error(`Middleware error: ${error?.message || error}`, error?.stack);
|
|
165
176
|
return res.status(500).json({ message: error?.message || 'Internal error' });
|
|
166
177
|
}
|
|
167
178
|
}
|
|
@@ -197,6 +208,8 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
197
208
|
if (AuthMiddleware.shutdownInitiated) return;
|
|
198
209
|
AuthMiddleware.shutdownInitiated = true;
|
|
199
210
|
|
|
211
|
+
this.logger.warn('Initiating graceful shutdown due to licence expiry');
|
|
212
|
+
|
|
200
213
|
// immediate SIGTERM, then force exit after 5s
|
|
201
214
|
setTimeout(() => {
|
|
202
215
|
try {
|
|
@@ -206,7 +219,10 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
206
219
|
}
|
|
207
220
|
|
|
208
221
|
if (!AuthMiddleware.shutdownTimer) {
|
|
209
|
-
AuthMiddleware.shutdownTimer = setTimeout(() =>
|
|
222
|
+
AuthMiddleware.shutdownTimer = setTimeout(() => {
|
|
223
|
+
this.logger.error('Force exit after SIGTERM timeout');
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}, 5000);
|
|
210
226
|
}
|
|
211
227
|
}, 0);
|
|
212
228
|
}
|
|
@@ -236,12 +252,21 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
236
252
|
const cachedToken = (await this.cacheManager.get('client_Licence_token')) as string | undefined;
|
|
237
253
|
if (cachedToken) {
|
|
238
254
|
const ok = await this.validateLicence(cachedToken, publicKey, false);
|
|
239
|
-
if (ok)
|
|
255
|
+
if (ok) {
|
|
256
|
+
this.logger.log('Licence validated from cache');
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
240
259
|
}
|
|
241
260
|
|
|
242
261
|
// 2) Fetch from licensing service
|
|
262
|
+
this.logger.log('Fetching licence from service');
|
|
243
263
|
const token = await this.getLicencingTokenFromService(realm);
|
|
244
264
|
const ok = await this.validateLicence(token, publicKey, true);
|
|
265
|
+
|
|
266
|
+
if (ok) {
|
|
267
|
+
this.logger.log('Licence validated from service');
|
|
268
|
+
}
|
|
269
|
+
|
|
245
270
|
return ok;
|
|
246
271
|
})();
|
|
247
272
|
|
|
@@ -251,7 +276,7 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
251
276
|
AuthMiddleware.licenceValidationPromise = null;
|
|
252
277
|
}
|
|
253
278
|
} catch (e: any) {
|
|
254
|
-
this.logger.
|
|
279
|
+
this.logger.error(`checkLicenceAndValidate failed: ${e?.message || e}`, e?.stack);
|
|
255
280
|
return false;
|
|
256
281
|
}
|
|
257
282
|
}
|
|
@@ -281,6 +306,11 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
281
306
|
// ✅ Save only the timestamp (supports 50 days / 500 days / years)
|
|
282
307
|
AuthMiddleware.licenceValidatedUntilMs = licEndMs;
|
|
283
308
|
|
|
309
|
+
const daysUntilExpiry = Math.floor((licEndMs - now) / (1000 * 60 * 60 * 24));
|
|
310
|
+
this.logger.log(
|
|
311
|
+
`Licence valid until ${new Date(licEndMs).toISOString()} (${daysUntilExpiry} days remaining)`
|
|
312
|
+
);
|
|
313
|
+
|
|
284
314
|
// ✅ Cache only briefly (avoid long TTL -> avoids TimeoutOverflowWarning)
|
|
285
315
|
if (updateCache) {
|
|
286
316
|
await this.cacheSetSeconds('client_Licence_token', tokenJwt, AuthMiddleware.LIC_CACHE_TTL_SECONDS);
|
|
@@ -308,18 +338,24 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
308
338
|
'https://iiuuckued274sisadalbpf7ivu0eiblk.lambda-url.ap-south-1.on.aws/';
|
|
309
339
|
|
|
310
340
|
const body = { client_name: realm };
|
|
311
|
-
const resp = await axios.post(url, body, { timeout: 15_000 });
|
|
312
341
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
342
|
+
try {
|
|
343
|
+
const resp = await axios.post(url, body, { timeout: 15_000 });
|
|
316
344
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
345
|
+
if (resp.status !== HttpStatusCode.Ok) {
|
|
346
|
+
throw new Error(`Licencing service returned status ${resp.status}`);
|
|
347
|
+
}
|
|
321
348
|
|
|
322
|
-
|
|
349
|
+
const token = resp?.data?.token;
|
|
350
|
+
if (!token || typeof token !== 'string') {
|
|
351
|
+
throw new Error('Licencing service response missing token');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return token;
|
|
355
|
+
} catch (e: any) {
|
|
356
|
+
this.logger.error(`Failed to fetch licence from service: ${e?.message || e}`);
|
|
357
|
+
throw e;
|
|
358
|
+
}
|
|
323
359
|
}
|
|
324
360
|
|
|
325
361
|
// -------------------------
|
|
@@ -352,7 +388,7 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
352
388
|
new URLSearchParams({
|
|
353
389
|
grant_type: 'client_credentials',
|
|
354
390
|
client_id: clientId,
|
|
355
|
-
client_secret: clientSecret
|
|
391
|
+
client_secret: clientSecret,
|
|
356
392
|
}).toString(),
|
|
357
393
|
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 15_000 }
|
|
358
394
|
);
|
|
@@ -385,7 +421,7 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
385
421
|
return jwt.verify(token, publicKeys, {
|
|
386
422
|
algorithms: ['RS256'],
|
|
387
423
|
ignoreExpiration: true,
|
|
388
|
-
clockTolerance: 60
|
|
424
|
+
clockTolerance: 60,
|
|
389
425
|
});
|
|
390
426
|
}
|
|
391
427
|
|
|
@@ -395,7 +431,7 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
395
431
|
|
|
396
432
|
return jwt.verify(token, publicKeys, {
|
|
397
433
|
algorithms: ['RS256'],
|
|
398
|
-
clockTolerance: 60
|
|
434
|
+
clockTolerance: 60,
|
|
399
435
|
});
|
|
400
436
|
}
|
|
401
437
|
|
|
@@ -417,19 +453,109 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
417
453
|
}
|
|
418
454
|
|
|
419
455
|
// -------------------------
|
|
420
|
-
// Cache helper
|
|
421
|
-
//
|
|
422
|
-
//
|
|
456
|
+
// ✅ ULTIMATE FIX: Cache helper with COMPLETE OVERFLOW PROTECTION
|
|
457
|
+
// -------------------------
|
|
458
|
+
// Works with ANY cache-manager version and ANY cache store
|
|
459
|
+
// Prevents cache stores from calculating TTL from JWT claims
|
|
423
460
|
// -------------------------
|
|
424
461
|
private async cacheSetSeconds(key: string, value: any, ttlSeconds: number) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
462
|
+
try {
|
|
463
|
+
// ttlSeconds=0 => store forever (no expiry)
|
|
464
|
+
if (!ttlSeconds || ttlSeconds <= 0) {
|
|
465
|
+
await this.cacheManager.set(key, value);
|
|
466
|
+
this.logger.debug(`Cache set (no TTL): ${key}`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ✅ CRITICAL: Clamp TTL to prevent overflow
|
|
471
|
+
let safeTtlSeconds = ttlSeconds;
|
|
472
|
+
if (ttlSeconds > AuthMiddleware.MAX_SAFE_TTL_SECONDS) {
|
|
473
|
+
safeTtlSeconds = AuthMiddleware.MAX_SAFE_TTL_SECONDS;
|
|
474
|
+
this.logger.warn(
|
|
475
|
+
`Cache TTL clamped: key=${key}, requested=${ttlSeconds}s (~${Math.floor(ttlSeconds / 86400)} days), using=${safeTtlSeconds}s (~24 days)`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
430
478
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
479
|
+
// Convert to milliseconds
|
|
480
|
+
const ttlMs = safeTtlSeconds * 1000;
|
|
481
|
+
|
|
482
|
+
// ✅ CRITICAL: Double-check we're not exceeding the limit
|
|
483
|
+
// This catches any edge cases where ttlMs might still overflow
|
|
484
|
+
const finalTtlMs = Math.min(ttlMs, AuthMiddleware.MAX_SAFE_TTL_MS);
|
|
485
|
+
|
|
486
|
+
if (finalTtlMs !== ttlMs) {
|
|
487
|
+
this.logger.warn(`TTL further clamped: ${ttlMs}ms -> ${finalTtlMs}ms`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ✅ Try multiple methods to ensure compatibility with all cache stores
|
|
491
|
+
let cacheSetSuccess = false;
|
|
492
|
+
let lastError: any = null;
|
|
493
|
+
|
|
494
|
+
// Method 1: Modern cache-manager v5+ (TTL in milliseconds as 3rd param)
|
|
495
|
+
if (!cacheSetSuccess) {
|
|
496
|
+
try {
|
|
497
|
+
await this.cacheManager.set(key, value, finalTtlMs);
|
|
498
|
+
cacheSetSuccess = true;
|
|
499
|
+
this.logger.debug(`Cache set (v5 format): ${key} (TTL: ${safeTtlSeconds}s / ${finalTtlMs}ms)`);
|
|
500
|
+
} catch (err: any) {
|
|
501
|
+
lastError = err;
|
|
502
|
+
// Try next method
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Method 2: Object-based TTL (milliseconds) - some stores
|
|
507
|
+
if (!cacheSetSuccess) {
|
|
508
|
+
try {
|
|
509
|
+
await (this.cacheManager as any).set(key, value, { ttl: finalTtlMs });
|
|
510
|
+
cacheSetSuccess = true;
|
|
511
|
+
this.logger.debug(`Cache set (object ms format): ${key} (TTL: ${finalTtlMs}ms)`);
|
|
512
|
+
} catch (err: any) {
|
|
513
|
+
lastError = err;
|
|
514
|
+
// Try next method
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Method 3: Object-based TTL (seconds) - Redis and others
|
|
519
|
+
if (!cacheSetSuccess) {
|
|
520
|
+
try {
|
|
521
|
+
await (this.cacheManager as any).set(key, value, { ttl: safeTtlSeconds });
|
|
522
|
+
cacheSetSuccess = true;
|
|
523
|
+
this.logger.debug(`Cache set (object sec format): ${key} (TTL: ${safeTtlSeconds}s)`);
|
|
524
|
+
} catch (err: any) {
|
|
525
|
+
lastError = err;
|
|
526
|
+
// Try next method
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Method 4: Direct store access (for Redis stores)
|
|
531
|
+
if (!cacheSetSuccess) {
|
|
532
|
+
try {
|
|
533
|
+
const store = (this.cacheManager as any).store;
|
|
534
|
+
const redisClient = store?.client || store?.getClient?.();
|
|
535
|
+
|
|
536
|
+
if (redisClient && typeof redisClient.set === 'function') {
|
|
537
|
+
// Redis native command: SET key value EX seconds
|
|
538
|
+
await redisClient.set(key, value, { EX: safeTtlSeconds });
|
|
539
|
+
cacheSetSuccess = true;
|
|
540
|
+
this.logger.debug(`Cache set (Redis direct): ${key} (EX: ${safeTtlSeconds}s)`);
|
|
541
|
+
}
|
|
542
|
+
} catch (err: any) {
|
|
543
|
+
lastError = err;
|
|
544
|
+
// Continue to fallback
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Method 5: Fallback - cache without TTL (better than failing)
|
|
549
|
+
if (!cacheSetSuccess) {
|
|
550
|
+
this.logger.warn(`All TTL methods failed for key=${key}, caching without TTL. Last error: ${lastError?.message}`);
|
|
551
|
+
await this.cacheManager.set(key, value);
|
|
552
|
+
this.logger.debug(`Cache set (no TTL fallback): ${key}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
} catch (e: any) {
|
|
556
|
+
// Even if all methods fail, don't crash the app
|
|
557
|
+
this.logger.error(`Cache set completely failed for key=${key}: ${e?.message || e}`, e?.stack);
|
|
558
|
+
// Cache failures are non-fatal - log and continue
|
|
559
|
+
}
|
|
434
560
|
}
|
|
435
561
|
}
|