dms-middleware-auth 1.2.0 → 1.2.2

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