dms-middleware-auth 1.2.0 → 1.2.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.
@@ -22,7 +22,8 @@ export declare class AuthMiddleware implements NestMiddleware, OnModuleInit {
22
22
  private static shutdownTimer;
23
23
  private static licenceExpiryInterval;
24
24
  private static readonly licenceExpiredMessage;
25
- private static readonly CLOCK_TOLERANCE_MS;
25
+ private static readonly licenceValidationFailedMessage;
26
+ private static readonly LICENCE_MONITOR_INTERVAL_MS;
26
27
  private static readonly LIC_CACHE_TTL_SECONDS;
27
28
  private static readonly CLIENT_TOKEN_TTL_MAX_SECONDS;
28
29
  private static readonly CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS;
@@ -32,12 +33,12 @@ export declare class AuthMiddleware implements NestMiddleware, OnModuleInit {
32
33
  onModuleInit(): Promise<void>;
33
34
  use(req: any, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
34
35
  private enforceLicenceExpiry;
36
+ private monitorLicenceRuntime;
35
37
  private markLicenceExpired;
36
38
  private stopServer;
37
39
  private checkLicenceAndValidate;
38
40
  private validateLicence;
39
41
  private getLicencingTokenFromService;
40
- private getOrFetchClientAccessToken;
41
42
  private clientLogin;
42
43
  private getClientRoleAttributes;
43
44
  private verifyLicenceJwt;
@@ -45,5 +46,8 @@ export declare class AuthMiddleware implements NestMiddleware, OnModuleInit {
45
46
  private extractBearerToken;
46
47
  private normalizeEpochMs;
47
48
  private cacheSetSeconds;
49
+ private cacheGetIfFresh;
50
+ private isCacheEnvelope;
51
+ private normalizeTtlSeconds;
48
52
  }
49
53
  export {};
@@ -65,16 +65,17 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
65
65
  const { realm, publicKey } = this.options;
66
66
  // Validate once on boot
67
67
  await this.checkLicenceAndValidate(realm, publicKey);
68
- // Periodic expiry check (safe: small interval only)
68
+ this.monitorLicenceRuntime();
69
+ // Periodic expiry check (safe: short interval, no long timers)
69
70
  if (!AuthMiddleware_1.licenceExpiryInterval) {
70
71
  AuthMiddleware_1.licenceExpiryInterval = setInterval(() => {
71
72
  try {
72
- this.enforceLicenceExpiry();
73
+ this.monitorLicenceRuntime();
73
74
  }
74
75
  catch (e) {
75
76
  this.logger.warn(`Licence expiry interval error: ${e?.message || e}`);
76
77
  }
77
- }, 60 * 60 * 1000); // Check every hour
78
+ }, AuthMiddleware_1.LICENCE_MONITOR_INTERVAL_MS);
78
79
  }
79
80
  }
80
81
  // -------------------------
@@ -82,7 +83,9 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
82
83
  // -------------------------
83
84
  async use(req, res, next) {
84
85
  try {
85
- // Production-safe: enforce expiry by timestamp compare, not long setTimeout
86
+ // Validate licence for this request; fast path returns from in-memory state.
87
+ const licenceOk = await this.checkLicenceAndValidate(this.options.realm, this.options.publicKey);
88
+ // Production-safe: enforce expiry by timestamp compare, not long setTimeout
86
89
  this.enforceLicenceExpiry();
87
90
  if (AuthMiddleware_1.licenceExpired) {
88
91
  // In EKS, letting the pod terminate is fine; K8s restarts/rolls
@@ -95,39 +98,64 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
95
98
  : 'Unknown',
96
99
  });
97
100
  }
101
+ // Fail closed when licence could not be validated for this request.
102
+ if (!licenceOk || !AuthMiddleware_1.licenceValidatedUntilMs) {
103
+ return res.status(axios_1.HttpStatusCode.Forbidden).json({
104
+ message: AuthMiddleware_1.licenceValidationFailedMessage,
105
+ server_time: new Date().toISOString(),
106
+ });
107
+ }
108
+ if (req.originalUrl == this.options.bypassURL) {
109
+ return next();
110
+ }
98
111
  const authHeader = req.headers['authorization'];
99
112
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
100
113
  return res.status(401).json({ message: 'Bearer token required' });
101
114
  }
102
115
  const decoded = this.decodeAccessToken(authHeader, this.options.publicKey);
103
- const clientToken = await this.getOrFetchClientAccessToken();
116
+ // Read client token from app-managed cache envelope (no native store TTL timer).
117
+ let clientToken = await this.cacheGetIfFresh('client_access_token');
118
+ if (!clientToken) {
119
+ clientToken = await this.clientLogin();
120
+ const decodedToken = jwt.decode(clientToken);
121
+ const nowSec = Math.floor(Date.now() / 1000);
122
+ const expSec = Number(decodedToken?.exp || 0);
123
+ // Clamp to 1 hour max to avoid stale token usage.
124
+ let ttlSec = expSec - nowSec - AuthMiddleware_1.CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS;
125
+ if (!Number.isFinite(ttlSec) || ttlSec <= 0)
126
+ ttlSec = 60;
127
+ ttlSec = Math.min(Math.max(60, ttlSec), AuthMiddleware_1.CLIENT_TOKEN_TTL_MAX_SECONDS);
128
+ await this.cacheSetSeconds('client_access_token', clientToken, ttlSec);
129
+ }
104
130
  const role = decoded?.resource_access?.[this.options.clientId]?.roles?.[0];
105
131
  if (!role) {
106
132
  return res.status(403).json({ message: 'Role not found for client' });
107
133
  }
108
- const clientRoleCacheKey = `${this.options.clientId}${role}`;
109
- let clientAttributes = await this.cacheManager.get(clientRoleCacheKey);
134
+ const clientRoleName = `${this.options.clientId + role}`;
135
+ let clientAttributes = await this.cacheGetIfFresh(clientRoleName);
110
136
  if (!clientAttributes) {
111
- const roleObj = await this.getClientRoleAttributes(role, clientToken);
112
- clientAttributes = JSON.stringify(roleObj.attributes ?? {});
113
- // Cache with 24-hour TTL
114
- await this.cacheSetSeconds(clientRoleCacheKey, clientAttributes, AuthMiddleware_1.ROLE_ATTRIBUTES_TTL_SECONDS);
137
+ clientAttributes = await this.getClientRoleAttributes(role, clientToken);
138
+ clientAttributes = JSON.stringify(clientAttributes.attributes);
139
+ await this.cacheSetSeconds(clientRoleName, clientAttributes, AuthMiddleware_1.ROLE_ATTRIBUTES_TTL_SECONDS);
115
140
  }
116
- const attrs = JSON.parse(clientAttributes);
117
- if (!attrs[req.originalUrl]) {
141
+ // Check route access
142
+ clientAttributes = JSON.parse(clientAttributes);
143
+ if (!clientAttributes[req.originalUrl]) {
118
144
  return res.status(403).json({ message: 'Access denied for this route' });
119
145
  }
120
- const apiPermission = JSON.parse(attrs[req.originalUrl]);
121
- if (apiPermission?.params === 'true') {
122
- const event = req?.body?.event;
123
- const url = `${req.originalUrl}?action=${event}`;
124
- if (!attrs[url]) {
125
- return res.status(403).json({ message: 'Access denied for event' });
146
+ else {
147
+ const apiPermission = JSON.parse(clientAttributes[req.originalUrl]);
148
+ if (apiPermission?.params === "true") {
149
+ const event = req?.body?.event;
150
+ const url = req?.originalUrl + `?action=${event}`;
151
+ if (!clientAttributes[url]) {
152
+ return res.status(403).json({ message: 'Access denied for event' });
153
+ }
126
154
  }
127
155
  }
128
156
  req['user'] = {
129
- role,
130
- userName: decoded?.preferred_username,
157
+ role: role,
158
+ userName: decoded.preferred_username
131
159
  };
132
160
  return next();
133
161
  }
@@ -145,16 +173,27 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
145
173
  if (!AuthMiddleware_1.licenceValidatedUntilMs)
146
174
  return;
147
175
  const now = Date.now();
148
- if (now >= AuthMiddleware_1.licenceValidatedUntilMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) {
149
- this.markLicenceExpired();
176
+ if (now >= AuthMiddleware_1.licenceValidatedUntilMs) {
177
+ this.markLicenceExpired(AuthMiddleware_1.licenceValidatedUntilMs, 'Licence Expired');
178
+ }
179
+ }
180
+ monitorLicenceRuntime() {
181
+ this.enforceLicenceExpiry();
182
+ if (AuthMiddleware_1.licenceExpired) {
183
+ this.stopServer();
150
184
  }
151
185
  }
152
- markLicenceExpired() {
186
+ markLicenceExpired(expiredAtMs, reason) {
187
+ if (Number.isFinite(expiredAtMs) && Number(expiredAtMs) > 0) {
188
+ AuthMiddleware_1.licenceValidatedUntilMs = Number(expiredAtMs);
189
+ }
153
190
  if (!AuthMiddleware_1.licenceExpired) {
154
191
  AuthMiddleware_1.licenceExpired = true;
155
- this.logger.error(`Licence expired at ${AuthMiddleware_1.licenceValidatedUntilMs
192
+ const expiryText = AuthMiddleware_1.licenceValidatedUntilMs
156
193
  ? new Date(AuthMiddleware_1.licenceValidatedUntilMs).toISOString()
157
- : 'unknown'}`);
194
+ : 'unknown';
195
+ const reasonText = reason ? ` (reason: ${reason})` : '';
196
+ this.logger.error(`Licence expired at ${expiryText}${reasonText}`);
158
197
  }
159
198
  }
160
199
  // -------------------------
@@ -188,9 +227,13 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
188
227
  try {
189
228
  if (AuthMiddleware_1.licenceExpired)
190
229
  return false;
191
- // If we already have an expiry and it's still valid, no need to fetch again
192
- if (AuthMiddleware_1.licenceValidatedUntilMs &&
193
- Date.now() < AuthMiddleware_1.licenceValidatedUntilMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) {
230
+ // If we already have an expiry in memory, decide immediately.
231
+ if (AuthMiddleware_1.licenceValidatedUntilMs) {
232
+ const now = Date.now();
233
+ if (now >= AuthMiddleware_1.licenceValidatedUntilMs) {
234
+ this.markLicenceExpired(AuthMiddleware_1.licenceValidatedUntilMs, 'Licence Expired');
235
+ return false;
236
+ }
194
237
  return true;
195
238
  }
196
239
  // De-duplicate concurrent validations
@@ -199,7 +242,7 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
199
242
  }
200
243
  AuthMiddleware_1.licenceValidationPromise = (async () => {
201
244
  // 1) Try cache first (short TTL)
202
- const cachedToken = (await this.cacheManager.get('client_Licence_token'));
245
+ const cachedToken = await this.cacheGetIfFresh('client_Licence_token');
203
246
  if (cachedToken) {
204
247
  const ok = await this.validateLicence(cachedToken, publicKey, realm, this.options.keycloakUrl, false);
205
248
  if (ok) {
@@ -230,18 +273,29 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
230
273
  }
231
274
  async validateLicence(tokenJwt, publicKey, realm, keycloakUrl, updateCache) {
232
275
  try {
276
+ const isAuthoritativeToken = updateCache;
233
277
  const payload = this.verifyLicenceJwt(tokenJwt, publicKey, realm, keycloakUrl);
234
278
  // Your custom claim: lic_end
235
279
  const licEndMs = this.normalizeEpochMs(payload?.lic_end);
236
280
  const now = Date.now();
237
281
  if (!licEndMs) {
238
- this.logger.error('Licence token missing lic_end');
239
- this.markLicenceExpired();
282
+ if (isAuthoritativeToken) {
283
+ this.logger.error('Licence token missing lic_end');
284
+ this.markLicenceExpired(null, 'authoritative token missing lic_end');
285
+ }
286
+ else {
287
+ this.logger.warn('Cached licence token missing lic_end, refetching from service');
288
+ }
240
289
  return false;
241
290
  }
242
- if (now >= licEndMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) {
243
- this.logger.error(`Licence already expired. lic_end=${new Date(licEndMs).toISOString()} now=${new Date(now).toISOString()}`);
244
- this.markLicenceExpired();
291
+ if (now >= licEndMs) {
292
+ if (isAuthoritativeToken) {
293
+ this.logger.error(`Licence already expired. lic_end=${new Date(licEndMs).toISOString()} now=${new Date(now).toISOString()}`);
294
+ this.markLicenceExpired(licEndMs, 'authoritative token already expired');
295
+ }
296
+ else {
297
+ this.logger.warn(`Cached licence token expired. lic_end=${new Date(licEndMs).toISOString()} now=${new Date(now).toISOString()}`);
298
+ }
245
299
  return false;
246
300
  }
247
301
  // ✅ Save only the timestamp (supports 50 days / 500 days / years)
@@ -255,11 +309,17 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
255
309
  return true;
256
310
  }
257
311
  catch (e) {
312
+ const isAuthoritativeToken = updateCache;
258
313
  // If JWT invalid, expire
259
314
  const name = e?.name || '';
260
315
  if (name === 'JsonWebTokenError' || name === 'TokenExpiredError' || name === 'NotBeforeError') {
261
- this.logger.error(`Invalid licence token: ${e?.message || e}`);
262
- this.markLicenceExpired();
316
+ if (isAuthoritativeToken) {
317
+ this.logger.error(`Invalid licence token: ${e?.message || e}`);
318
+ this.markLicenceExpired(null, `authoritative token invalid (${name})`);
319
+ }
320
+ else {
321
+ this.logger.warn(`Cached licence token invalid, refetching from service: ${e?.message || e}`);
322
+ }
263
323
  return false;
264
324
  }
265
325
  // transient failures should not hard-expire immediately
@@ -287,25 +347,6 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
287
347
  throw e;
288
348
  }
289
349
  }
290
- // -------------------------
291
- // Keycloak client token caching (safe TTL)
292
- // -------------------------
293
- async getOrFetchClientAccessToken() {
294
- let token = (await this.cacheManager.get('client_access_token'));
295
- if (token)
296
- return token;
297
- token = await this.clientLogin();
298
- const decoded = jwt.decode(token);
299
- const nowSec = Math.floor(Date.now() / 1000);
300
- const expSec = Number(decoded?.exp || 0);
301
- // ttl seconds: refresh early, clamp to [60, 3600]
302
- let ttlSec = expSec - nowSec - AuthMiddleware_1.CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS;
303
- if (!Number.isFinite(ttlSec) || ttlSec <= 0)
304
- ttlSec = 60;
305
- ttlSec = Math.min(Math.max(60, ttlSec), AuthMiddleware_1.CLIENT_TOKEN_TTL_MAX_SECONDS);
306
- await this.cacheSetSeconds('client_access_token', token, ttlSec);
307
- return token;
308
- }
309
350
  async clientLogin() {
310
351
  const { keycloakUrl, realm, clientId, clientSecret } = this.options;
311
352
  const resp = await axios_1.default.post(`${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`, new URLSearchParams({
@@ -329,10 +370,8 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
329
370
  // -------------------------
330
371
  verifyLicenceJwt(token, publicKey, realm, keycloakUrl) {
331
372
  const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
332
- const issuer = `${keycloakUrl}/realms/${realm}`;
333
373
  return jwt.verify(token, publicKeys, {
334
374
  algorithms: ['RS256'],
335
- issuer,
336
375
  clockTolerance: 60,
337
376
  });
338
377
  }
@@ -363,18 +402,54 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
363
402
  // -------------------------
364
403
  // Cache helper with overflow protection
365
404
  // -------------------------
366
- async cacheSetSeconds(key, value, _ttlSeconds) {
405
+ async cacheSetSeconds(key, value, ttlSeconds) {
367
406
  try {
368
- // Force a consistent one-day TTL in seconds; clamp to avoid 32-bit timer overflow even if a store multiplies by 1000
369
- // Use a simple, safe 24-hour TTL; 86,400s (86,400,000ms if misread as ms) is far below the Node timer limit.
370
- const ttlSeconds = AuthMiddleware_1.LIC_CACHE_TTL_SECONDS; // 24 * 60 * 60
371
- await this.cacheManager.set(key, value, ttlSeconds);
372
- this.logger.debug(`Cache set: ${key} (TTL: ${ttlSeconds}s)`);
407
+ const safeTtlSeconds = this.normalizeTtlSeconds(ttlSeconds);
408
+ const envelope = {
409
+ value,
410
+ expiresAtMs: Date.now() + safeTtlSeconds * 1000,
411
+ };
412
+ // Do not use native store TTL; some stores map TTL to long setTimeout internally.
413
+ await this.cacheManager.set(key, envelope);
414
+ this.logger.debug(`Cache set: ${key} (TTL: ${safeTtlSeconds}s)`);
373
415
  }
374
416
  catch (e) {
375
417
  this.logger.error(`Cache set failed for key=${key}: ${e?.message || e}`, e?.stack);
376
418
  }
377
419
  }
420
+ async cacheGetIfFresh(key) {
421
+ try {
422
+ const cached = (await this.cacheManager.get(key));
423
+ if (!cached)
424
+ return undefined;
425
+ // Backward compatibility for entries written before envelope format.
426
+ if (!this.isCacheEnvelope(cached)) {
427
+ return cached;
428
+ }
429
+ if (Date.now() >= cached.expiresAtMs) {
430
+ await this.cacheManager.del(key);
431
+ this.logger.debug(`Cache expired: ${key}`);
432
+ return undefined;
433
+ }
434
+ return cached.value;
435
+ }
436
+ catch (e) {
437
+ this.logger.warn(`Cache get failed for key=${key}: ${e?.message || e}`);
438
+ return undefined;
439
+ }
440
+ }
441
+ isCacheEnvelope(value) {
442
+ if (!value || typeof value !== 'object')
443
+ return false;
444
+ const v = value;
445
+ return Number.isFinite(v.expiresAtMs) && Object.prototype.hasOwnProperty.call(v, 'value');
446
+ }
447
+ normalizeTtlSeconds(ttlSeconds) {
448
+ if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {
449
+ return AuthMiddleware_1.ONE_DAY_SECONDS;
450
+ }
451
+ return Math.floor(ttlSeconds);
452
+ }
378
453
  };
379
454
  exports.AuthMiddleware = AuthMiddleware;
380
455
  // ---- Global-ish state (process-wide) ----
@@ -386,8 +461,9 @@ AuthMiddleware.shutdownTimer = null;
386
461
  AuthMiddleware.licenceExpiryInterval = null;
387
462
  // ---- Constants ----
388
463
  AuthMiddleware.licenceExpiredMessage = 'Server Licence is expired! Please renew';
389
- // Allow small clock drift
390
- AuthMiddleware.CLOCK_TOLERANCE_MS = 60_000; // 1 minute
464
+ AuthMiddleware.licenceValidationFailedMessage = 'Server licence validation failed. Please try again.';
465
+ // Runtime monitor cadence for strict licence end enforcement.
466
+ AuthMiddleware.LICENCE_MONITOR_INTERVAL_MS = 10_000; // 10 seconds
391
467
  // Cache TTLs (safe by design; prevents timer overflow)
392
468
  AuthMiddleware.LIC_CACHE_TTL_SECONDS = 24 * 60 * 60; // 24 hours TTL for licence
393
469
  AuthMiddleware.CLIENT_TOKEN_TTL_MAX_SECONDS = 60 * 60; // 1 hour cap
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dms-middleware-auth",
3
- "version": "1.2.0",
3
+ "version": "1.2.3",
4
4
  "description": "Reusable middleware for authentication and authorization in NestJS applications.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,6 +24,11 @@ interface AuthMiddlewareOptions {
24
24
  licenceServiceUrl?: string;
25
25
  }
26
26
 
27
+ interface CacheEnvelope<T> {
28
+ value: T;
29
+ expiresAtMs: number;
30
+ }
31
+
27
32
  @Injectable()
28
33
  export class AuthMiddleware implements NestMiddleware, OnModuleInit {
29
34
  private readonly logger = new Logger('AuthMiddleware');
@@ -41,9 +46,11 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
41
46
  // ---- Constants ----
42
47
  private static readonly licenceExpiredMessage =
43
48
  'Server Licence is expired! Please renew';
49
+ private static readonly licenceValidationFailedMessage =
50
+ 'Server licence validation failed. Please try again.';
44
51
 
45
- // Allow small clock drift
46
- private static readonly CLOCK_TOLERANCE_MS = 60_000; // 1 minute
52
+ // Runtime monitor cadence for strict licence end enforcement.
53
+ private static readonly LICENCE_MONITOR_INTERVAL_MS = 10_000; // 10 seconds
47
54
 
48
55
  // Cache TTLs (safe by design; prevents timer overflow)
49
56
  private static readonly LIC_CACHE_TTL_SECONDS = 24 * 60 * 60; // 24 hours TTL for licence
@@ -67,16 +74,17 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
67
74
 
68
75
  // Validate once on boot
69
76
  await this.checkLicenceAndValidate(realm, publicKey);
77
+ this.monitorLicenceRuntime();
70
78
 
71
- // Periodic expiry check (safe: small interval only)
79
+ // Periodic expiry check (safe: short interval, no long timers)
72
80
  if (!AuthMiddleware.licenceExpiryInterval) {
73
81
  AuthMiddleware.licenceExpiryInterval = setInterval(() => {
74
82
  try {
75
- this.enforceLicenceExpiry();
83
+ this.monitorLicenceRuntime();
76
84
  } catch (e: any) {
77
85
  this.logger.warn(`Licence expiry interval error: ${e?.message || e}`);
78
86
  }
79
- }, 60 * 60 * 1000); // Check every hour
87
+ }, AuthMiddleware.LICENCE_MONITOR_INTERVAL_MS);
80
88
  }
81
89
  }
82
90
 
@@ -85,7 +93,13 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
85
93
  // -------------------------
86
94
  async use(req: any, res: Response, next: NextFunction) {
87
95
  try {
88
- // Production-safe: enforce expiry by timestamp compare, not long setTimeout
96
+ // Validate licence for this request; fast path returns from in-memory state.
97
+ const licenceOk = await this.checkLicenceAndValidate(
98
+ this.options.realm,
99
+ this.options.publicKey
100
+ );
101
+
102
+ // Production-safe: enforce expiry by timestamp compare, not long setTimeout
89
103
  this.enforceLicenceExpiry();
90
104
 
91
105
  if (AuthMiddleware.licenceExpired) {
@@ -100,6 +114,17 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
100
114
  });
101
115
  }
102
116
 
117
+ // Fail closed when licence could not be validated for this request.
118
+ if (!licenceOk || !AuthMiddleware.licenceValidatedUntilMs) {
119
+ return res.status(HttpStatusCode.Forbidden).json({
120
+ message: AuthMiddleware.licenceValidationFailedMessage,
121
+ server_time: new Date().toISOString(),
122
+ });
123
+ }
124
+
125
+ if (req.originalUrl == this.options.bypassURL) {
126
+ return next();
127
+ }
103
128
  const authHeader = req.headers['authorization'];
104
129
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
105
130
  return res.status(401).json({ message: 'Bearer token required' });
@@ -107,48 +132,63 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
107
132
 
108
133
  const decoded: any = this.decodeAccessToken(authHeader, this.options.publicKey);
109
134
 
110
- const clientToken = await this.getOrFetchClientAccessToken();
135
+ // Read client token from app-managed cache envelope (no native store TTL timer).
136
+ let clientToken = await this.cacheGetIfFresh<string>('client_access_token');
137
+ if (!clientToken) {
138
+ clientToken = await this.clientLogin();
139
+ const decodedToken: any = jwt.decode(clientToken);
140
+ const nowSec = Math.floor(Date.now() / 1000);
141
+ const expSec = Number(decodedToken?.exp || 0);
142
+
143
+ // Clamp to 1 hour max to avoid stale token usage.
144
+ let ttlSec = expSec - nowSec - AuthMiddleware.CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS;
145
+ if (!Number.isFinite(ttlSec) || ttlSec <= 0) ttlSec = 60;
146
+ ttlSec = Math.min(Math.max(60, ttlSec), AuthMiddleware.CLIENT_TOKEN_TTL_MAX_SECONDS);
147
+
148
+ await this.cacheSetSeconds('client_access_token', clientToken, ttlSec);
149
+ }
150
+
151
+
152
+
111
153
  const role = decoded?.resource_access?.[this.options.clientId]?.roles?.[0];
112
154
 
113
155
  if (!role) {
114
156
  return res.status(403).json({ message: 'Role not found for client' });
115
157
  }
116
158
 
117
- const clientRoleCacheKey = `${this.options.clientId}${role}`;
118
- let clientAttributes: any = await this.cacheManager.get(clientRoleCacheKey);
119
-
159
+ const clientRoleName = `${this.options.clientId + role}`;
160
+ let clientAttributes: any = await this.cacheGetIfFresh<string>(clientRoleName);
120
161
  if (!clientAttributes) {
121
- const roleObj = await this.getClientRoleAttributes(role, clientToken);
122
- clientAttributes = JSON.stringify(roleObj.attributes ?? {});
123
-
124
- // ✅ Cache with 24-hour TTL
162
+ clientAttributes = await this.getClientRoleAttributes(role, clientToken);
163
+ clientAttributes = JSON.stringify(clientAttributes.attributes);
125
164
  await this.cacheSetSeconds(
126
- clientRoleCacheKey,
165
+ clientRoleName,
127
166
  clientAttributes,
128
167
  AuthMiddleware.ROLE_ATTRIBUTES_TTL_SECONDS
129
168
  );
130
169
  }
131
-
132
- const attrs = JSON.parse(clientAttributes);
133
- if (!attrs[req.originalUrl]) {
170
+ // Check route access
171
+ clientAttributes = JSON.parse(clientAttributes);
172
+ if (!clientAttributes[req.originalUrl]) {
134
173
  return res.status(403).json({ message: 'Access denied for this route' });
135
- }
136
-
137
- const apiPermission = JSON.parse(attrs[req.originalUrl]);
138
- if (apiPermission?.params === 'true') {
139
- const event = req?.body?.event;
140
- const url = `${req.originalUrl}?action=${event}`;
141
- if (!attrs[url]) {
142
- return res.status(403).json({ message: 'Access denied for event' });
174
+ } else {
175
+ const apiPermission = JSON.parse(clientAttributes[req.originalUrl]);
176
+ if (apiPermission?.params === "true") {
177
+ const event = req?.body?.event;
178
+ const url = req?.originalUrl + `?action=${event}`;
179
+ if (!clientAttributes[url]) {
180
+ return res.status(403).json({ message: 'Access denied for event' });
181
+ }
143
182
  }
144
183
  }
145
-
184
+
146
185
  req['user'] = {
147
- role,
148
- userName: decoded?.preferred_username,
186
+ role: role,
187
+ userName: decoded.preferred_username
149
188
  };
150
189
 
151
190
  return next();
191
+
152
192
  } catch (error: any) {
153
193
  this.logger.error(`Middleware error: ${error?.message || error}`, error?.stack);
154
194
  return res.status(500).json({ message: error?.message || 'Internal error' });
@@ -163,18 +203,34 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
163
203
  if (!AuthMiddleware.licenceValidatedUntilMs) return;
164
204
 
165
205
  const now = Date.now();
166
- if (now >= AuthMiddleware.licenceValidatedUntilMs + AuthMiddleware.CLOCK_TOLERANCE_MS) {
167
- this.markLicenceExpired();
206
+ if (now >= AuthMiddleware.licenceValidatedUntilMs) {
207
+ this.markLicenceExpired(
208
+ AuthMiddleware.licenceValidatedUntilMs,
209
+ 'Licence Expired'
210
+ );
211
+ }
212
+ }
213
+
214
+ private monitorLicenceRuntime() {
215
+ this.enforceLicenceExpiry();
216
+ if (AuthMiddleware.licenceExpired) {
217
+ this.stopServer();
168
218
  }
169
219
  }
170
220
 
171
- private markLicenceExpired() {
221
+ private markLicenceExpired(expiredAtMs?: number | null, reason?: string) {
222
+ if (Number.isFinite(expiredAtMs) && Number(expiredAtMs) > 0) {
223
+ AuthMiddleware.licenceValidatedUntilMs = Number(expiredAtMs);
224
+ }
225
+
172
226
  if (!AuthMiddleware.licenceExpired) {
173
227
  AuthMiddleware.licenceExpired = true;
228
+ const expiryText = AuthMiddleware.licenceValidatedUntilMs
229
+ ? new Date(AuthMiddleware.licenceValidatedUntilMs).toISOString()
230
+ : 'unknown';
231
+ const reasonText = reason ? ` (reason: ${reason})` : '';
174
232
  this.logger.error(
175
- `Licence expired at ${AuthMiddleware.licenceValidatedUntilMs
176
- ? new Date(AuthMiddleware.licenceValidatedUntilMs).toISOString()
177
- : 'unknown'}`
233
+ `Licence expired at ${expiryText}${reasonText}`
178
234
  );
179
235
  }
180
236
  }
@@ -212,11 +268,16 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
212
268
  try {
213
269
  if (AuthMiddleware.licenceExpired) return false;
214
270
 
215
- // If we already have an expiry and it's still valid, no need to fetch again
216
- if (
217
- AuthMiddleware.licenceValidatedUntilMs &&
218
- Date.now() < AuthMiddleware.licenceValidatedUntilMs + AuthMiddleware.CLOCK_TOLERANCE_MS
219
- ) {
271
+ // If we already have an expiry in memory, decide immediately.
272
+ if (AuthMiddleware.licenceValidatedUntilMs) {
273
+ const now = Date.now();
274
+ if (now >= AuthMiddleware.licenceValidatedUntilMs) {
275
+ this.markLicenceExpired(
276
+ AuthMiddleware.licenceValidatedUntilMs,
277
+ 'Licence Expired'
278
+ );
279
+ return false;
280
+ }
220
281
  return true;
221
282
  }
222
283
 
@@ -227,7 +288,7 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
227
288
 
228
289
  AuthMiddleware.licenceValidationPromise = (async () => {
229
290
  // 1) Try cache first (short TTL)
230
- const cachedToken = (await this.cacheManager.get('client_Licence_token')) as string | undefined;
291
+ const cachedToken = await this.cacheGetIfFresh<string>('client_Licence_token');
231
292
  if (cachedToken) {
232
293
  const ok = await this.validateLicence(cachedToken, publicKey, realm, this.options.keycloakUrl, false);
233
294
  if (ok) {
@@ -267,6 +328,7 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
267
328
  updateCache: boolean
268
329
  ): Promise<boolean> {
269
330
  try {
331
+ const isAuthoritativeToken = updateCache;
270
332
  const payload: any = this.verifyLicenceJwt(tokenJwt, publicKey, realm, keycloakUrl);
271
333
 
272
334
  // Your custom claim: lic_end
@@ -274,16 +336,26 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
274
336
  const now = Date.now();
275
337
 
276
338
  if (!licEndMs) {
277
- this.logger.error('Licence token missing lic_end');
278
- this.markLicenceExpired();
339
+ if (isAuthoritativeToken) {
340
+ this.logger.error('Licence token missing lic_end');
341
+ this.markLicenceExpired(null, 'authoritative token missing lic_end');
342
+ } else {
343
+ this.logger.warn('Cached licence token missing lic_end, refetching from service');
344
+ }
279
345
  return false;
280
346
  }
281
347
 
282
- if (now >= licEndMs + AuthMiddleware.CLOCK_TOLERANCE_MS) {
283
- this.logger.error(
284
- `Licence already expired. lic_end=${new Date(licEndMs).toISOString()} now=${new Date(now).toISOString()}`
285
- );
286
- this.markLicenceExpired();
348
+ if (now >= licEndMs) {
349
+ if (isAuthoritativeToken) {
350
+ this.logger.error(
351
+ `Licence already expired. lic_end=${new Date(licEndMs).toISOString()} now=${new Date(now).toISOString()}`
352
+ );
353
+ this.markLicenceExpired(licEndMs, 'authoritative token already expired');
354
+ } else {
355
+ this.logger.warn(
356
+ `Cached licence token expired. lic_end=${new Date(licEndMs).toISOString()} now=${new Date(now).toISOString()}`
357
+ );
358
+ }
287
359
  return false;
288
360
  }
289
361
 
@@ -302,11 +374,16 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
302
374
 
303
375
  return true;
304
376
  } catch (e: any) {
377
+ const isAuthoritativeToken = updateCache;
305
378
  // If JWT invalid, expire
306
379
  const name = e?.name || '';
307
380
  if (name === 'JsonWebTokenError' || name === 'TokenExpiredError' || name === 'NotBeforeError') {
308
- this.logger.error(`Invalid licence token: ${e?.message || e}`);
309
- this.markLicenceExpired();
381
+ if (isAuthoritativeToken) {
382
+ this.logger.error(`Invalid licence token: ${e?.message || e}`);
383
+ this.markLicenceExpired(null, `authoritative token invalid (${name})`);
384
+ } else {
385
+ this.logger.warn(`Cached licence token invalid, refetching from service: ${e?.message || e}`);
386
+ }
310
387
  return false;
311
388
  }
312
389
 
@@ -342,27 +419,7 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
342
419
  }
343
420
  }
344
421
 
345
- // -------------------------
346
- // Keycloak client token caching (safe TTL)
347
- // -------------------------
348
- private async getOrFetchClientAccessToken(): Promise<string> {
349
- let token = (await this.cacheManager.get('client_access_token')) as string | undefined;
350
- if (token) return token;
351
-
352
- token = await this.clientLogin();
353
-
354
- const decoded: any = jwt.decode(token);
355
- const nowSec = Math.floor(Date.now() / 1000);
356
- const expSec = Number(decoded?.exp || 0);
357
-
358
- // ttl seconds: refresh early, clamp to [60, 3600]
359
- let ttlSec = expSec - nowSec - AuthMiddleware.CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS;
360
- if (!Number.isFinite(ttlSec) || ttlSec <= 0) ttlSec = 60;
361
- ttlSec = Math.min(Math.max(60, ttlSec), AuthMiddleware.CLIENT_TOKEN_TTL_MAX_SECONDS);
362
-
363
- await this.cacheSetSeconds('client_access_token', token, ttlSec);
364
- return token;
365
- }
422
+
366
423
 
367
424
  private async clientLogin(): Promise<string> {
368
425
  const { keycloakUrl, realm, clientId, clientSecret } = this.options;
@@ -401,11 +458,8 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
401
458
  private verifyLicenceJwt(token: string, publicKey: string, realm: string, keycloakUrl: string) {
402
459
  const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
403
460
 
404
- const issuer = `${keycloakUrl}/realms/${realm}`;
405
-
406
461
  return jwt.verify(token, publicKeys, {
407
462
  algorithms: ['RS256'],
408
- issuer,
409
463
  clockTolerance: 60,
410
464
  });
411
465
  }
@@ -440,15 +494,55 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
440
494
  // -------------------------
441
495
  // Cache helper with overflow protection
442
496
  // -------------------------
443
- private async cacheSetSeconds(key: string, value: any, _ttlSeconds: number) {
497
+ private async cacheSetSeconds(key: string, value: any, ttlSeconds: number) {
444
498
  try {
445
- // Force a consistent one-day TTL in seconds; clamp to avoid 32-bit timer overflow even if a store multiplies by 1000
446
- // Use a simple, safe 24-hour TTL; 86,400s (86,400,000ms if misread as ms) is far below the Node timer limit.
447
- const ttlSeconds = AuthMiddleware.LIC_CACHE_TTL_SECONDS; // 24 * 60 * 60
448
- await this.cacheManager.set(key, value, ttlSeconds);
449
- this.logger.debug(`Cache set: ${key} (TTL: ${ttlSeconds}s)`);
499
+ const safeTtlSeconds = this.normalizeTtlSeconds(ttlSeconds);
500
+ const envelope: CacheEnvelope<any> = {
501
+ value,
502
+ expiresAtMs: Date.now() + safeTtlSeconds * 1000,
503
+ };
504
+
505
+ // Do not use native store TTL; some stores map TTL to long setTimeout internally.
506
+ await this.cacheManager.set(key, envelope as any);
507
+ this.logger.debug(`Cache set: ${key} (TTL: ${safeTtlSeconds}s)`);
450
508
  } catch (e: any) {
451
509
  this.logger.error(`Cache set failed for key=${key}: ${e?.message || e}`, e?.stack);
452
510
  }
453
511
  }
512
+
513
+ private async cacheGetIfFresh<T>(key: string): Promise<T | undefined> {
514
+ try {
515
+ const cached = (await this.cacheManager.get(key)) as CacheEnvelope<T> | T | undefined;
516
+ if (!cached) return undefined;
517
+
518
+ // Backward compatibility for entries written before envelope format.
519
+ if (!this.isCacheEnvelope(cached)) {
520
+ return cached as T;
521
+ }
522
+
523
+ if (Date.now() >= cached.expiresAtMs) {
524
+ await this.cacheManager.del(key);
525
+ this.logger.debug(`Cache expired: ${key}`);
526
+ return undefined;
527
+ }
528
+
529
+ return cached.value;
530
+ } catch (e: any) {
531
+ this.logger.warn(`Cache get failed for key=${key}: ${e?.message || e}`);
532
+ return undefined;
533
+ }
534
+ }
535
+
536
+ private isCacheEnvelope<T>(value: unknown): value is CacheEnvelope<T> {
537
+ if (!value || typeof value !== 'object') return false;
538
+ const v = value as CacheEnvelope<T>;
539
+ return Number.isFinite(v.expiresAtMs) && Object.prototype.hasOwnProperty.call(v, 'value');
540
+ }
541
+
542
+ private normalizeTtlSeconds(ttlSeconds: number): number {
543
+ if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {
544
+ return AuthMiddleware.ONE_DAY_SECONDS;
545
+ }
546
+ return Math.floor(ttlSeconds);
547
+ }
454
548
  }