dms-middleware-auth 1.1.3 → 1.1.5

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.
@@ -1,6 +1,6 @@
1
1
  import { NestMiddleware, OnModuleInit } from '@nestjs/common';
2
2
  import { Response, NextFunction } from 'express';
3
- import { Cache } from 'cache-manager';
3
+ import type { Cache } from 'cache-manager';
4
4
  interface AuthMiddlewareOptions {
5
5
  publicKey: string;
6
6
  keycloakUrl: string;
@@ -9,35 +9,40 @@ interface AuthMiddlewareOptions {
9
9
  clientSecret: string;
10
10
  clientUuid: string;
11
11
  bypassURL: string;
12
+ licenceServiceUrl?: string;
12
13
  }
13
14
  export declare class AuthMiddleware implements NestMiddleware, OnModuleInit {
14
- private cacheManager;
15
+ private readonly cacheManager;
15
16
  private readonly options;
17
+ private readonly logger;
16
18
  private static licenceExpired;
17
- private static shutdownTimer;
18
- private static licenceExpiryTimer;
19
19
  private static licenceValidatedUntilMs;
20
20
  private static licenceValidationPromise;
21
21
  private static shutdownInitiated;
22
+ private static shutdownTimer;
23
+ private static licenceExpiryInterval;
22
24
  private static readonly licenceExpiredMessage;
23
25
  private static readonly CLOCK_TOLERANCE_MS;
24
- private readonly logger;
26
+ private static readonly LIC_EXPIRY_CHECK_INTERVAL_MS;
27
+ private static readonly LIC_CACHE_TTL_SECONDS;
28
+ private static readonly CLIENT_TOKEN_TTL_MAX_SECONDS;
29
+ private static readonly CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS;
25
30
  constructor(cacheManager: Cache, options: AuthMiddlewareOptions);
26
31
  onModuleInit(): Promise<void>;
27
32
  use(req: any, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
28
- private clientLogin;
29
- private getUserDetails;
30
- private getClientRoleAttributes;
31
- private checkLicenceAndValidate;
32
- private getLicencingDetails;
33
- private validateLicence;
33
+ private enforceLicenceExpiry;
34
34
  private markLicenceExpired;
35
- private scheduleLicenceShutdown;
36
35
  private stopServer;
37
- private normalizeEpochMs;
38
- private getUtcNowMs;
39
- private verifyJwt;
36
+ private checkLicenceAndValidate;
37
+ private validateLicence;
38
+ private getLicencingTokenFromService;
39
+ private getOrFetchClientAccessToken;
40
+ private clientLogin;
41
+ private getClientRoleAttributes;
42
+ private verifyLicenceJwt;
40
43
  private decodeAccessToken;
41
44
  private extractBearerToken;
45
+ private normalizeEpochMs;
46
+ private cacheSetSeconds;
42
47
  }
43
48
  export {};
@@ -48,8 +48,8 @@ var AuthMiddleware_1;
48
48
  Object.defineProperty(exports, "__esModule", { value: true });
49
49
  exports.AuthMiddleware = void 0;
50
50
  const common_1 = require("@nestjs/common");
51
- const jwt = __importStar(require("jsonwebtoken"));
52
51
  const axios_1 = __importStar(require("axios"));
52
+ const jwt = __importStar(require("jsonwebtoken"));
53
53
  const cache_manager_1 = require("@nestjs/cache-manager");
54
54
  const process = __importStar(require("node:process"));
55
55
  let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
@@ -58,139 +58,162 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
58
58
  this.options = options;
59
59
  this.logger = new common_1.Logger('AuthMiddleware');
60
60
  }
61
+ // -------------------------
62
+ // Startup: validate licence once, then run a small periodic expiry check
63
+ // -------------------------
61
64
  async onModuleInit() {
62
- const { publicKey, realm } = this.options;
63
- const isValid = await this.checkLicenceAndValidate(realm, publicKey);
65
+ const { realm, publicKey } = this.options;
66
+ // Validate once on boot
67
+ await this.checkLicenceAndValidate(realm, publicKey);
68
+ // Periodic expiry check (safe: small interval only)
69
+ if (!AuthMiddleware_1.licenceExpiryInterval) {
70
+ AuthMiddleware_1.licenceExpiryInterval = setInterval(() => {
71
+ try {
72
+ this.enforceLicenceExpiry();
73
+ }
74
+ catch (e) {
75
+ this.logger.warn(`Licence expiry interval error: ${e?.message || e}`);
76
+ }
77
+ }, AuthMiddleware_1.LIC_EXPIRY_CHECK_INTERVAL_MS);
78
+ }
64
79
  }
80
+ // -------------------------
81
+ // Main middleware
82
+ // -------------------------
65
83
  async use(req, res, next) {
66
- const { publicKey, clientId, realm, bypassURL } = this.options;
67
84
  try {
85
+ // ✅ Production-safe: enforce expiry by timestamp compare, not long setTimeout
86
+ this.enforceLicenceExpiry();
68
87
  if (AuthMiddleware_1.licenceExpired) {
88
+ // In EKS, letting the pod terminate is fine; K8s restarts/rolls
69
89
  this.stopServer();
70
90
  return res.status(axios_1.HttpStatusCode.Forbidden).json({
71
91
  message: AuthMiddleware_1.licenceExpiredMessage,
72
92
  server_time: new Date().toISOString(),
73
- licence_expiry: AuthMiddleware_1.licenceValidatedUntilMs ? new Date(AuthMiddleware_1.licenceValidatedUntilMs).toISOString() : 'Unknown',
74
- drift_ms: AuthMiddleware_1.licenceValidatedUntilMs ? (AuthMiddleware_1.licenceValidatedUntilMs - Date.now()) : 0
93
+ licence_expiry: AuthMiddleware_1.licenceValidatedUntilMs
94
+ ? new Date(AuthMiddleware_1.licenceValidatedUntilMs).toISOString()
95
+ : 'Unknown'
75
96
  });
76
97
  }
77
- if (!AuthMiddleware_1.licenceValidatedUntilMs) {
78
- return res.status(axios_1.HttpStatusCode.Forbidden).json({ message: AuthMiddleware_1.licenceExpiredMessage });
79
- }
80
- if (req.originalUrl == bypassURL) {
98
+ // Allow bypass URL
99
+ if (req.originalUrl === this.options.bypassURL) {
81
100
  return next();
82
101
  }
102
+ // Authorization header
83
103
  const authHeader = req.headers['authorization'];
84
104
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
85
105
  return res.status(401).json({ message: 'Bearer token required' });
86
106
  }
87
- const decoded = this.decodeAccessToken(authHeader, publicKey);
88
- // Cache the client token
89
- let clientToken = await this.cacheManager.get('client_access_token');
90
- if (!clientToken) {
91
- clientToken = await this.clientLogin();
92
- const decodedToken = jwt.decode(clientToken);
93
- const ttl = (decodedToken.exp - Math.floor(Date.now() / 1000)) * 1000;
94
- await this.cacheManager.set('client_access_token', clientToken, ttl);
107
+ // Decode + verify caller token
108
+ const decoded = this.decodeAccessToken(authHeader, this.options.publicKey);
109
+ // Fetch (or cache) Keycloak client token
110
+ const clientToken = await this.getOrFetchClientAccessToken();
111
+ // Fetch (or cache) role attributes
112
+ const role = decoded?.resource_access?.[this.options.clientId]?.roles?.[0];
113
+ if (!role) {
114
+ return res.status(403).json({ message: 'Role not found for client' });
95
115
  }
96
- // Cache client role attributes
97
- const role = decoded.resource_access[clientId]?.roles?.[0];
98
- const clientRoleName = `${clientId + role}`;
99
- let clientAttributes = await this.cacheManager.get(clientRoleName);
116
+ const clientRoleCacheKey = `${this.options.clientId}${role}`;
117
+ let clientAttributes = await this.cacheManager.get(clientRoleCacheKey);
100
118
  if (!clientAttributes) {
101
- clientAttributes = await this.getClientRoleAttributes(role, clientToken);
102
- clientAttributes = JSON.stringify(clientAttributes.attributes);
103
- await this.cacheManager.set(clientRoleName, clientAttributes, 0);
119
+ const roleObj = await this.getClientRoleAttributes(role, clientToken);
120
+ clientAttributes = JSON.stringify(roleObj.attributes ?? {});
121
+ // store indefinitely (or you can choose a short TTL)
122
+ await this.cacheSetSeconds(clientRoleCacheKey, clientAttributes, 0);
104
123
  }
105
- // Check route access
106
- clientAttributes = JSON.parse(clientAttributes);
107
- if (!clientAttributes[req.originalUrl]) {
124
+ // Route access check
125
+ const attrs = JSON.parse(clientAttributes);
126
+ if (!attrs[req.originalUrl]) {
108
127
  return res.status(403).json({ message: 'Access denied for this route' });
109
128
  }
110
- else {
111
- const apiPermission = JSON.parse(clientAttributes[req.originalUrl]);
112
- if (apiPermission?.params === "true") {
113
- const event = req?.body?.event;
114
- const url = req?.originalUrl + `?action=${event}`;
115
- if (!clientAttributes[url]) {
116
- return res.status(403).json({ message: 'Access denied for event' });
117
- }
129
+ // Optional event/params check
130
+ const apiPermission = JSON.parse(attrs[req.originalUrl]);
131
+ if (apiPermission?.params === 'true') {
132
+ const event = req?.body?.event;
133
+ const url = `${req.originalUrl}?action=${event}`;
134
+ if (!attrs[url]) {
135
+ return res.status(403).json({ message: 'Access denied for event' });
118
136
  }
119
137
  }
120
138
  req['user'] = {
121
- role: role,
122
- userName: decoded.preferred_username
139
+ role,
140
+ userName: decoded?.preferred_username
123
141
  };
124
142
  return next();
125
143
  }
126
144
  catch (error) {
127
- return res.status(500).json({ message: error.message });
145
+ return res.status(500).json({ message: error?.message || 'Internal error' });
128
146
  }
129
147
  }
130
- async clientLogin() {
131
- const { keycloakUrl, realm, clientId, clientSecret } = this.options;
132
- try {
133
- const response = await axios_1.default.post(`${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`, new URLSearchParams({
134
- grant_type: 'client_credentials',
135
- client_id: clientId,
136
- client_secret: clientSecret,
137
- }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
138
- return response.data.access_token;
139
- }
140
- catch (error) {
141
- throw new Error(`Failed to obtain client token: ${error.response?.data?.error_description || error.message}`);
148
+ // -------------------------
149
+ // Licence expiry enforcement (NO LONG TIMERS)
150
+ // -------------------------
151
+ enforceLicenceExpiry() {
152
+ if (AuthMiddleware_1.licenceExpired)
153
+ return;
154
+ if (!AuthMiddleware_1.licenceValidatedUntilMs)
155
+ return;
156
+ const now = Date.now();
157
+ if (now >= AuthMiddleware_1.licenceValidatedUntilMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) {
158
+ this.markLicenceExpired();
142
159
  }
143
160
  }
144
- async getUserDetails(username, token) {
145
- const { keycloakUrl, realm } = this.options;
146
- try {
147
- const response = await axios_1.default.get(`${keycloakUrl}/admin/realms/${realm}/users?username=${username}`, { headers: { Authorization: `Bearer ${token}` } });
148
- return response.data[0];
149
- }
150
- catch (error) {
151
- throw new Error(`Failed to fetch user details: ${error.response?.data?.error_description || error.message}`);
161
+ markLicenceExpired() {
162
+ if (!AuthMiddleware_1.licenceExpired) {
163
+ AuthMiddleware_1.licenceExpired = true;
164
+ this.logger.error(`Licence expired at ${AuthMiddleware_1.licenceValidatedUntilMs
165
+ ? new Date(AuthMiddleware_1.licenceValidatedUntilMs).toISOString()
166
+ : 'unknown'}`);
152
167
  }
153
168
  }
154
- async getClientRoleAttributes(role, token) {
155
- const { keycloakUrl, realm, clientUuid } = this.options;
156
- try {
157
- const response = await axios_1.default.get(`${keycloakUrl}/admin/realms/${realm}/clients/${clientUuid}/roles/${role}`, { headers: { Authorization: `Bearer ${token}` } });
158
- return response.data;
159
- }
160
- catch (error) {
161
- throw new Error(`Failed to fetch client role attributes: ${error.response?.data?.error_description || error.message}`);
162
- }
169
+ // -------------------------
170
+ // Graceful shutdown for EKS
171
+ // -------------------------
172
+ stopServer() {
173
+ if (AuthMiddleware_1.shutdownInitiated)
174
+ return;
175
+ AuthMiddleware_1.shutdownInitiated = true;
176
+ // immediate SIGTERM, then force exit after 5s
177
+ setTimeout(() => {
178
+ try {
179
+ process.kill(process.pid, 'SIGTERM');
180
+ }
181
+ catch {
182
+ process.exit(1);
183
+ }
184
+ if (!AuthMiddleware_1.shutdownTimer) {
185
+ AuthMiddleware_1.shutdownTimer = setTimeout(() => process.exit(1), 5000);
186
+ }
187
+ }, 0);
163
188
  }
189
+ // -------------------------
190
+ // Licence validate flow (called at boot / can be called again if needed)
191
+ // -------------------------
164
192
  async checkLicenceAndValidate(realm, publicKey) {
165
193
  try {
166
- if (AuthMiddleware_1.licenceExpired) {
194
+ if (AuthMiddleware_1.licenceExpired)
167
195
  return false;
168
- }
169
- // 1. check the licence is available in cache if available then check the expiry
170
- const cachedToken = await this.cacheManager.get('client_Licence_token');
171
- if (cachedToken) {
172
- const validate = await this.validateLicence(cachedToken, publicKey, false);
173
- if (validate.status) {
174
- return true;
175
- }
176
- // If cached token is invalid/expired, we'll try to get a new one from the server.
177
- }
178
- if (AuthMiddleware_1.licenceValidatedUntilMs && AuthMiddleware_1.licenceValidatedUntilMs > this.getUtcNowMs()) {
196
+ // If we already have an expiry and it's still valid, no need to fetch again
197
+ if (AuthMiddleware_1.licenceValidatedUntilMs &&
198
+ Date.now() < AuthMiddleware_1.licenceValidatedUntilMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) {
179
199
  return true;
180
200
  }
201
+ // De-duplicate concurrent validations
181
202
  if (AuthMiddleware_1.licenceValidationPromise) {
182
203
  return await AuthMiddleware_1.licenceValidationPromise;
183
204
  }
184
- // 2. if the licence details not available in cache get the data from another server and validate.
185
205
  AuthMiddleware_1.licenceValidationPromise = (async () => {
186
- const response = await this.getLicencingDetails(realm);
187
- if (response.code === axios_1.HttpStatusCode.InternalServerError) {
188
- return false;
189
- }
190
- else {
191
- const validate = await this.validateLicence(response.data, publicKey, true);
192
- return validate.status;
206
+ // 1) Try cache first (short TTL)
207
+ const cachedToken = (await this.cacheManager.get('client_Licence_token'));
208
+ if (cachedToken) {
209
+ const ok = await this.validateLicence(cachedToken, publicKey, false);
210
+ if (ok)
211
+ return true;
193
212
  }
213
+ // 2) Fetch from licensing service
214
+ const token = await this.getLicencingTokenFromService(realm);
215
+ const ok = await this.validateLicence(token, publicKey, true);
216
+ return ok;
194
217
  })();
195
218
  try {
196
219
  return await AuthMiddleware_1.licenceValidationPromise;
@@ -199,142 +222,105 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
199
222
  AuthMiddleware_1.licenceValidationPromise = null;
200
223
  }
201
224
  }
202
- catch (error) {
225
+ catch (e) {
226
+ this.logger.warn(`checkLicenceAndValidate failed: ${e?.message || e}`);
203
227
  return false;
204
228
  }
205
229
  }
206
- async getLicencingDetails(realm) {
230
+ async validateLicence(tokenJwt, publicKey, updateCache) {
207
231
  try {
208
- const url = 'https://iiuuckued274sisadalbpf7ivu0eiblk.lambda-url.ap-south-1.on.aws/';
209
- const body = { "client_name": realm };
210
- const response = await axios_1.default.post(url, body);
211
- if (response.status === axios_1.HttpStatusCode.Ok) {
212
- return {
213
- code: axios_1.HttpStatusCode.Ok,
214
- data: response.data.token
215
- };
216
- }
217
- else {
218
- return {
219
- code: axios_1.HttpStatusCode.InternalServerError,
220
- message: "InternalServer error"
221
- };
232
+ const payload = this.verifyLicenceJwt(tokenJwt, publicKey);
233
+ // Your custom claim: lic_end
234
+ const licEndMs = this.normalizeEpochMs(payload?.lic_end);
235
+ const now = Date.now();
236
+ if (!licEndMs) {
237
+ this.logger.error('Licence token missing lic_end');
238
+ this.markLicenceExpired();
239
+ return false;
222
240
  }
223
- }
224
- catch (error) {
225
- return {
226
- code: axios_1.HttpStatusCode.InternalServerError,
227
- data: error?.response?.data || error?.message || 'Licencing service error'
228
- };
229
- }
230
- }
231
- async validateLicence(lic_data, publicKey, updateCache = true) {
232
- try {
233
- const token = await this.verifyJwt(lic_data, publicKey);
234
- const licEndMs = this.normalizeEpochMs(token?.lic_end);
235
- const now = this.getUtcNowMs();
236
- // Reject when licence end (epoch) is missing or already in the past (with tolerance).
237
- if (!licEndMs || (licEndMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) <= now) {
238
- this.logger.error(`Licence expired. Expiry: ${new Date(licEndMs || 0).toISOString()}, Server Time: ${new Date(now).toISOString()}`);
241
+ if (now >= licEndMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) {
242
+ this.logger.error(`Licence already expired. lic_end=${new Date(licEndMs).toISOString()} now=${new Date(now).toISOString()}`);
239
243
  this.markLicenceExpired();
240
- return { status: false };
244
+ return false;
241
245
  }
242
- // 3. if the licencing server have the future expiry time wait till the expiry time
246
+ // Save only the timestamp (supports 50 days / 500 days / years)
243
247
  AuthMiddleware_1.licenceValidatedUntilMs = licEndMs;
244
- this.scheduleLicenceShutdown(licEndMs);
248
+ // ✅ Cache only briefly (avoid long TTL -> avoids TimeoutOverflowWarning)
245
249
  if (updateCache) {
246
- // Cap TTL to avoid timeout overflow in cache managers (32-bit signed int max is ~24.8 days)
247
- const MAX_TTL = 2147483647;
248
- const calculatedTtl = licEndMs - now;
249
- const ttl = Math.min(Math.max(0, calculatedTtl), MAX_TTL);
250
- await this.cacheManager.set('client_Licence_token', lic_data, ttl);
250
+ await this.cacheSetSeconds('client_Licence_token', tokenJwt, AuthMiddleware_1.LIC_CACHE_TTL_SECONDS);
251
251
  }
252
- return {
253
- status: true,
254
- };
252
+ return true;
255
253
  }
256
- catch (error) {
257
- if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
258
- this.logger.error(`Invalid Licence Token: ${error.message}`);
254
+ catch (e) {
255
+ // If JWT invalid, expire
256
+ const name = e?.name || '';
257
+ if (name === 'JsonWebTokenError' || name === 'TokenExpiredError' || name === 'NotBeforeError') {
258
+ this.logger.error(`Invalid licence token: ${e?.message || e}`);
259
259
  this.markLicenceExpired();
260
+ return false;
260
261
  }
261
- else {
262
- this.logger.warn(`Transient error during licence validation: ${error.message}`);
263
- }
264
- return { status: false };
262
+ // transient failures should not hard-expire immediately
263
+ this.logger.warn(`Transient licence validation error: ${e?.message || e}`);
264
+ return false;
265
265
  }
266
266
  }
267
- markLicenceExpired() {
268
- if (!AuthMiddleware_1.licenceExpired) {
269
- AuthMiddleware_1.licenceExpired = true;
267
+ async getLicencingTokenFromService(realm) {
268
+ const url = this.options.licenceServiceUrl ??
269
+ 'https://iiuuckued274sisadalbpf7ivu0eiblk.lambda-url.ap-south-1.on.aws/';
270
+ const body = { client_name: realm };
271
+ const resp = await axios_1.default.post(url, body, { timeout: 15_000 });
272
+ if (resp.status !== axios_1.HttpStatusCode.Ok) {
273
+ throw new Error(`Licencing service returned status ${resp.status}`);
270
274
  }
271
- }
272
- scheduleLicenceShutdown(licEndMs) {
273
- if (!licEndMs) {
274
- return;
275
+ const token = resp?.data?.token;
276
+ if (!token || typeof token !== 'string') {
277
+ throw new Error('Licencing service response missing token');
275
278
  }
276
- const now = this.getUtcNowMs();
277
- const delay = (licEndMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) - now;
278
- if (delay <= 0) {
279
- this.logger.warn('Licence expired immediately (including tolerance).');
280
- this.markLicenceExpired();
281
- return;
282
- }
283
- if (AuthMiddleware_1.licenceExpiryTimer) {
284
- clearTimeout(AuthMiddleware_1.licenceExpiryTimer);
285
- }
286
- // setTimeout uses a 32-bit signed integer. Max value is 2147483647 ms (approx 24.8 days).
287
- // If the delay is larger than this, setTimeout will fire immediately (overflow).
288
- // so we cap the delay and re-check when it fires.
289
- const MAX_TIMEOUT_VAL = 2147483647;
290
- const safeDelay = Math.min(delay, MAX_TIMEOUT_VAL);
291
- AuthMiddleware_1.licenceExpiryTimer = setTimeout(() => {
292
- const remaining = (licEndMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) - this.getUtcNowMs();
293
- if (remaining <= 0) {
294
- this.markLicenceExpired();
295
- }
296
- else {
297
- // Reschedule if we still have time left
298
- this.scheduleLicenceShutdown(licEndMs);
299
- }
300
- }, safeDelay);
279
+ return token;
301
280
  }
302
- stopServer() {
303
- if (AuthMiddleware_1.shutdownInitiated) {
304
- return;
305
- }
306
- AuthMiddleware_1.shutdownInitiated = true;
307
- setTimeout(() => {
308
- try {
309
- process.kill(process.pid, 'SIGTERM');
310
- }
311
- catch {
312
- process.exit(1);
313
- }
314
- if (!AuthMiddleware_1.shutdownTimer) {
315
- AuthMiddleware_1.shutdownTimer = setTimeout(() => {
316
- process.exit(1);
317
- }, 5000);
318
- }
319
- }, 0);
281
+ // -------------------------
282
+ // Keycloak client token caching (safe TTL)
283
+ // -------------------------
284
+ async getOrFetchClientAccessToken() {
285
+ let token = (await this.cacheManager.get('client_access_token'));
286
+ if (token)
287
+ return token;
288
+ token = await this.clientLogin();
289
+ const decoded = jwt.decode(token);
290
+ const nowSec = Math.floor(Date.now() / 1000);
291
+ const expSec = Number(decoded?.exp || 0);
292
+ // ttl seconds: refresh early, clamp to [60, 3600]
293
+ let ttlSec = expSec - nowSec - AuthMiddleware_1.CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS;
294
+ if (!Number.isFinite(ttlSec) || ttlSec <= 0)
295
+ ttlSec = 60;
296
+ ttlSec = Math.min(Math.max(60, ttlSec), AuthMiddleware_1.CLIENT_TOKEN_TTL_MAX_SECONDS);
297
+ await this.cacheSetSeconds('client_access_token', token, ttlSec);
298
+ return token;
320
299
  }
321
- normalizeEpochMs(epoch) {
322
- const epochNum = Number(epoch);
323
- if (!epochNum || Number.isNaN(epochNum)) {
324
- return null;
325
- }
326
- if (epochNum < 1_000_000_000_000) {
327
- return epochNum * 1000;
300
+ async clientLogin() {
301
+ const { keycloakUrl, realm, clientId, clientSecret } = this.options;
302
+ const resp = await axios_1.default.post(`${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`, new URLSearchParams({
303
+ grant_type: 'client_credentials',
304
+ client_id: clientId,
305
+ client_secret: clientSecret
306
+ }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 15_000 });
307
+ const token = resp?.data?.access_token;
308
+ if (!token) {
309
+ throw new Error('Keycloak did not return access_token');
328
310
  }
329
- return epochNum;
311
+ return token;
330
312
  }
331
- getUtcNowMs() {
332
- return Date.now();
313
+ async getClientRoleAttributes(role, token) {
314
+ const { keycloakUrl, realm, clientUuid } = this.options;
315
+ const resp = await axios_1.default.get(`${keycloakUrl}/admin/realms/${realm}/clients/${clientUuid}/roles/${role}`, { headers: { Authorization: `Bearer ${token}` }, timeout: 15_000 });
316
+ return resp.data;
333
317
  }
334
- verifyJwt(token, publicKey) {
318
+ // -------------------------
319
+ // JWT helpers
320
+ // -------------------------
321
+ verifyLicenceJwt(token, publicKey) {
335
322
  const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
336
- // We ignore the standard 'exp' claim and rely on our custom 'lic_end' field.
337
- // Also include a small clock tolerance for the library itself.
323
+ // We ignore standard 'exp' and rely on custom 'lic_end'
338
324
  return jwt.verify(token, publicKeys, {
339
325
  algorithms: ['RS256'],
340
326
  ignoreExpiration: true,
@@ -343,21 +329,64 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
343
329
  }
344
330
  decodeAccessToken(authHeader, publicKey) {
345
331
  const token = this.extractBearerToken(authHeader);
346
- return this.verifyJwt(token, publicKey);
332
+ const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
333
+ return jwt.verify(token, publicKeys, {
334
+ algorithms: ['RS256'],
335
+ clockTolerance: 60
336
+ });
347
337
  }
348
338
  extractBearerToken(authHeader) {
349
339
  return authHeader.split(' ')[1];
350
340
  }
341
+ // -------------------------
342
+ // Time helpers
343
+ // -------------------------
344
+ normalizeEpochMs(epoch) {
345
+ const n = Number(epoch);
346
+ if (!Number.isFinite(n) || n <= 0)
347
+ return null;
348
+ // If it's seconds (10 digits), convert to ms
349
+ // If it's ms (13 digits), keep
350
+ if (n < 1_000_000_000_000)
351
+ return n * 1000;
352
+ return n;
353
+ }
354
+ // -------------------------
355
+ // Cache helper (seconds-based, safe across stores)
356
+ // - Using seconds avoids ms/sec mismatch across cache-manager stores
357
+ // - TTL is kept small so no overflow even if a store uses Node timers
358
+ // -------------------------
359
+ async cacheSetSeconds(key, value, ttlSeconds) {
360
+ // ttlSeconds=0 => store forever (supported by many stores); if not, we just set without ttl
361
+ if (!ttlSeconds || ttlSeconds <= 0) {
362
+ await this.cacheManager.set(key, value);
363
+ return;
364
+ }
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
+ }
351
369
  };
352
370
  exports.AuthMiddleware = AuthMiddleware;
371
+ // ---- Global-ish state (process-wide) ----
353
372
  AuthMiddleware.licenceExpired = false;
354
- AuthMiddleware.shutdownTimer = null;
355
- AuthMiddleware.licenceExpiryTimer = null;
356
373
  AuthMiddleware.licenceValidatedUntilMs = null;
357
374
  AuthMiddleware.licenceValidationPromise = null;
358
375
  AuthMiddleware.shutdownInitiated = false;
359
- AuthMiddleware.licenceExpiredMessage = "server Licence is expired!. please renew";
360
- AuthMiddleware.CLOCK_TOLERANCE_MS = 60000; // 1 minute grace period
376
+ AuthMiddleware.shutdownTimer = null;
377
+ AuthMiddleware.licenceExpiryInterval = null;
378
+ // ---- Constants ----
379
+ AuthMiddleware.licenceExpiredMessage = 'server Licence is expired!. please renew';
380
+ // allow small clock drift
381
+ AuthMiddleware.CLOCK_TOLERANCE_MS = 60_000; // 1 min
382
+ // IMPORTANT:
383
+ // Node timers overflow above ~2,147,483,647ms (~24.8 days).
384
+ // We avoid scheduling long timeouts completely.
385
+ AuthMiddleware.LIC_EXPIRY_CHECK_INTERVAL_MS = 60_000; // 1 min
386
+ // Cache TTLs (short by design; safe for any store, avoids timer overflow)
387
+ AuthMiddleware.LIC_CACHE_TTL_SECONDS = 15 * 60; // 15 minutes
388
+ AuthMiddleware.CLIENT_TOKEN_TTL_MAX_SECONDS = 60 * 60; // 1 hour cap
389
+ AuthMiddleware.CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS = 30; // refresh a bit early
361
390
  exports.AuthMiddleware = AuthMiddleware = AuthMiddleware_1 = __decorate([
362
391
  (0, common_1.Injectable)(),
363
392
  __param(0, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dms-middleware-auth",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
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",
@@ -1,194 +1,248 @@
1
- import { Inject, Injectable, Logger, NestMiddleware, OnModuleInit } from '@nestjs/common';
1
+ import {
2
+ Inject,
3
+ Injectable,
4
+ Logger,
5
+ NestMiddleware,
6
+ OnModuleInit
7
+ } from '@nestjs/common';
2
8
  import { Response, NextFunction } from 'express';
3
- import * as jwt from 'jsonwebtoken';
4
9
  import axios, { HttpStatusCode } from 'axios';
10
+ import * as jwt from 'jsonwebtoken';
5
11
  import { CACHE_MANAGER } from '@nestjs/cache-manager';
6
- import { Cache } from 'cache-manager';
7
- import * as process from "node:process";
12
+ import type { Cache } from 'cache-manager';
13
+ import * as process from 'node:process';
8
14
 
9
15
  interface AuthMiddlewareOptions {
10
16
  publicKey: string;
17
+
11
18
  keycloakUrl: string;
12
19
  realm: string;
20
+
13
21
  clientId: string;
14
22
  clientSecret: string;
15
23
  clientUuid: string;
24
+
16
25
  bypassURL: string;
26
+
27
+ // Optional: licensing service URL (better than hardcoding in code)
28
+ licenceServiceUrl?: string;
17
29
  }
18
30
 
19
31
  @Injectable()
20
32
  export class AuthMiddleware implements NestMiddleware, OnModuleInit {
33
+ private readonly logger = new Logger('AuthMiddleware');
34
+
35
+ // ---- Global-ish state (process-wide) ----
21
36
  private static licenceExpired = false;
22
- private static shutdownTimer: NodeJS.Timeout | null = null;
23
- private static licenceExpiryTimer: NodeJS.Timeout | null = null;
24
37
  private static licenceValidatedUntilMs: number | null = null;
25
38
  private static licenceValidationPromise: Promise<boolean> | null = null;
39
+
26
40
  private static shutdownInitiated = false;
27
- private static readonly licenceExpiredMessage = "server Licence is expired!. please renew";
28
- private static readonly CLOCK_TOLERANCE_MS = 60000; // 1 minute grace period
29
- private readonly logger = new Logger('AuthMiddleware');
41
+ private static shutdownTimer: NodeJS.Timeout | null = null;
30
42
 
31
- constructor(
32
- @Inject(CACHE_MANAGER) private cacheManager: Cache,
33
- @Inject('AUTH_MIDDLEWARE_OPTIONS') private readonly options: AuthMiddlewareOptions
34
- ) { }
43
+ private static licenceExpiryInterval: NodeJS.Timeout | null = null;
44
+
45
+ // ---- Constants ----
46
+ private static readonly licenceExpiredMessage =
47
+ 'server Licence is expired!. please renew';
48
+
49
+ // allow small clock drift
50
+ private static readonly CLOCK_TOLERANCE_MS = 60_000; // 1 min
51
+
52
+ // IMPORTANT:
53
+ // Node timers overflow above ~2,147,483,647ms (~24.8 days).
54
+ // We avoid scheduling long timeouts completely.
55
+ private static readonly LIC_EXPIRY_CHECK_INTERVAL_MS = 60_000; // 1 min
56
+
57
+ // Cache TTLs (short by design; safe for any store, avoids timer overflow)
58
+ private static readonly LIC_CACHE_TTL_SECONDS = 15 * 60; // 15 minutes
59
+ private static readonly CLIENT_TOKEN_TTL_MAX_SECONDS = 60 * 60; // 1 hour cap
60
+ private static readonly CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS = 30; // refresh a bit early
35
61
 
62
+ constructor(
63
+ @Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
64
+ @Inject('AUTH_MIDDLEWARE_OPTIONS')
65
+ private readonly options: AuthMiddlewareOptions
66
+ ) {}
67
+
68
+ // -------------------------
69
+ // Startup: validate licence once, then run a small periodic expiry check
70
+ // -------------------------
36
71
  async onModuleInit() {
37
- const { publicKey, realm } = this.options;
38
- const isValid = await this.checkLicenceAndValidate(realm, publicKey);
72
+ const { realm, publicKey } = this.options;
73
+
74
+ // Validate once on boot
75
+ await this.checkLicenceAndValidate(realm, publicKey);
76
+
77
+ // Periodic expiry check (safe: small interval only)
78
+ if (!AuthMiddleware.licenceExpiryInterval) {
79
+ AuthMiddleware.licenceExpiryInterval = setInterval(() => {
80
+ try {
81
+ this.enforceLicenceExpiry();
82
+ } catch (e: any) {
83
+ this.logger.warn(`Licence expiry interval error: ${e?.message || e}`);
84
+ }
85
+ }, AuthMiddleware.LIC_EXPIRY_CHECK_INTERVAL_MS);
86
+ }
39
87
  }
40
88
 
89
+ // -------------------------
90
+ // Main middleware
91
+ // -------------------------
41
92
  async use(req: any, res: Response, next: NextFunction) {
42
- const { publicKey, clientId, realm, bypassURL } = this.options;
43
-
44
93
  try {
94
+ // ✅ Production-safe: enforce expiry by timestamp compare, not long setTimeout
95
+ this.enforceLicenceExpiry();
96
+
45
97
  if (AuthMiddleware.licenceExpired) {
98
+ // In EKS, letting the pod terminate is fine; K8s restarts/rolls
46
99
  this.stopServer();
47
100
  return res.status(HttpStatusCode.Forbidden).json({
48
101
  message: AuthMiddleware.licenceExpiredMessage,
49
102
  server_time: new Date().toISOString(),
50
- licence_expiry: AuthMiddleware.licenceValidatedUntilMs ? new Date(AuthMiddleware.licenceValidatedUntilMs).toISOString() : 'Unknown',
51
- drift_ms: AuthMiddleware.licenceValidatedUntilMs ? (AuthMiddleware.licenceValidatedUntilMs - Date.now()) : 0
103
+ licence_expiry: AuthMiddleware.licenceValidatedUntilMs
104
+ ? new Date(AuthMiddleware.licenceValidatedUntilMs).toISOString()
105
+ : 'Unknown'
52
106
  });
53
107
  }
54
108
 
55
- if (!AuthMiddleware.licenceValidatedUntilMs) {
56
- return res.status(HttpStatusCode.Forbidden).json({ message: AuthMiddleware.licenceExpiredMessage });
57
- }
58
-
59
- if (req.originalUrl == bypassURL) {
109
+ // Allow bypass URL
110
+ if (req.originalUrl === this.options.bypassURL) {
60
111
  return next();
61
112
  }
62
113
 
114
+ // Authorization header
63
115
  const authHeader = req.headers['authorization'];
64
116
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
65
117
  return res.status(401).json({ message: 'Bearer token required' });
66
118
  }
67
119
 
68
- const decoded: any = this.decodeAccessToken(authHeader, publicKey);
120
+ // Decode + verify caller token
121
+ const decoded: any = this.decodeAccessToken(authHeader, this.options.publicKey);
69
122
 
70
- // Cache the client token
71
- let clientToken: any = await this.cacheManager.get('client_access_token');
72
- if (!clientToken) {
73
- clientToken = await this.clientLogin();
74
- const decodedToken: any = jwt.decode(clientToken);
75
- const ttl = (decodedToken.exp - Math.floor(Date.now() / 1000)) * 1000;
76
- await this.cacheManager.set('client_access_token', clientToken, ttl);
123
+ // Fetch (or cache) Keycloak client token
124
+ const clientToken = await this.getOrFetchClientAccessToken();
125
+
126
+ // Fetch (or cache) role attributes
127
+ const role = decoded?.resource_access?.[this.options.clientId]?.roles?.[0];
128
+ if (!role) {
129
+ return res.status(403).json({ message: 'Role not found for client' });
77
130
  }
78
131
 
79
- // Cache client role attributes
80
- const role = decoded.resource_access[clientId]?.roles?.[0];
81
- const clientRoleName = `${clientId + role}`;
82
- let clientAttributes: any = await this.cacheManager.get(clientRoleName);
132
+ const clientRoleCacheKey = `${this.options.clientId}${role}`;
133
+ let clientAttributes: any = await this.cacheManager.get(clientRoleCacheKey);
134
+
83
135
  if (!clientAttributes) {
84
- clientAttributes = await this.getClientRoleAttributes(role, clientToken);
85
- clientAttributes = JSON.stringify(clientAttributes.attributes);
86
- await this.cacheManager.set(clientRoleName, clientAttributes, 0);
136
+ const roleObj = await this.getClientRoleAttributes(role, clientToken);
137
+ clientAttributes = JSON.stringify(roleObj.attributes ?? {});
138
+ // store indefinitely (or you can choose a short TTL)
139
+ await this.cacheSetSeconds(clientRoleCacheKey, clientAttributes, 0);
87
140
  }
88
141
 
89
- // Check route access
90
- clientAttributes = JSON.parse(clientAttributes);
91
- if (!clientAttributes[req.originalUrl]) {
142
+ // Route access check
143
+ const attrs = JSON.parse(clientAttributes);
144
+ if (!attrs[req.originalUrl]) {
92
145
  return res.status(403).json({ message: 'Access denied for this route' });
93
- } else {
94
- const apiPermission = JSON.parse(clientAttributes[req.originalUrl]);
95
- if (apiPermission?.params === "true") {
96
- const event = req?.body?.event;
97
- const url = req?.originalUrl + `?action=${event}`;
98
- if (!clientAttributes[url]) {
99
- return res.status(403).json({ message: 'Access denied for event' });
100
- }
146
+ }
147
+
148
+ // Optional event/params check
149
+ const apiPermission = JSON.parse(attrs[req.originalUrl]);
150
+ if (apiPermission?.params === 'true') {
151
+ const event = req?.body?.event;
152
+ const url = `${req.originalUrl}?action=${event}`;
153
+ if (!attrs[url]) {
154
+ return res.status(403).json({ message: 'Access denied for event' });
101
155
  }
102
156
  }
103
157
 
104
158
  req['user'] = {
105
- role: role,
106
- userName: decoded.preferred_username
159
+ role,
160
+ userName: decoded?.preferred_username
107
161
  };
108
162
 
109
163
  return next();
110
164
  } catch (error: any) {
111
- return res.status(500).json({ message: error.message });
165
+ return res.status(500).json({ message: error?.message || 'Internal error' });
112
166
  }
113
167
  }
114
168
 
115
- private async clientLogin() {
116
- const { keycloakUrl, realm, clientId, clientSecret } = this.options;
117
- try {
118
- const response = await axios.post(
119
- `${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`,
120
- new URLSearchParams({
121
- grant_type: 'client_credentials',
122
- client_id: clientId,
123
- client_secret: clientSecret,
124
- }).toString(),
125
- { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
126
- );
127
- return response.data.access_token;
128
- } catch (error: any) {
129
- throw new Error(`Failed to obtain client token: ${error.response?.data?.error_description || error.message}`);
130
- }
131
- }
169
+ // -------------------------
170
+ // Licence expiry enforcement (NO LONG TIMERS)
171
+ // -------------------------
172
+ private enforceLicenceExpiry() {
173
+ if (AuthMiddleware.licenceExpired) return;
174
+ if (!AuthMiddleware.licenceValidatedUntilMs) return;
132
175
 
133
- private async getUserDetails(username: string, token: string) {
134
- const { keycloakUrl, realm } = this.options;
135
- try {
136
- const response = await axios.get(
137
- `${keycloakUrl}/admin/realms/${realm}/users?username=${username}`,
138
- { headers: { Authorization: `Bearer ${token}` } }
139
- );
140
- return response.data[0];
141
- } catch (error: any) {
142
- throw new Error(`Failed to fetch user details: ${error.response?.data?.error_description || error.message}`);
176
+ const now = Date.now();
177
+ if (now >= AuthMiddleware.licenceValidatedUntilMs + AuthMiddleware.CLOCK_TOLERANCE_MS) {
178
+ this.markLicenceExpired();
143
179
  }
144
180
  }
145
181
 
146
- private async getClientRoleAttributes(role: string, token: string) {
147
- const { keycloakUrl, realm, clientUuid } = this.options;
148
- try {
149
- const response = await axios.get(
150
- `${keycloakUrl}/admin/realms/${realm}/clients/${clientUuid}/roles/${role}`,
151
- { headers: { Authorization: `Bearer ${token}` } }
182
+ private markLicenceExpired() {
183
+ if (!AuthMiddleware.licenceExpired) {
184
+ AuthMiddleware.licenceExpired = true;
185
+ this.logger.error(
186
+ `Licence expired at ${AuthMiddleware.licenceValidatedUntilMs
187
+ ? new Date(AuthMiddleware.licenceValidatedUntilMs).toISOString()
188
+ : 'unknown'}`
152
189
  );
153
- return response.data;
154
- } catch (error: any) {
155
- throw new Error(`Failed to fetch client role attributes: ${error.response?.data?.error_description || error.message}`);
156
190
  }
157
191
  }
158
192
 
159
- private async checkLicenceAndValidate(realm: string, publicKey: string) {
160
- try {
161
- if (AuthMiddleware.licenceExpired) {
162
- return false;
193
+ // -------------------------
194
+ // Graceful shutdown for EKS
195
+ // -------------------------
196
+ private stopServer() {
197
+ if (AuthMiddleware.shutdownInitiated) return;
198
+ AuthMiddleware.shutdownInitiated = true;
199
+
200
+ // immediate SIGTERM, then force exit after 5s
201
+ setTimeout(() => {
202
+ try {
203
+ process.kill(process.pid, 'SIGTERM');
204
+ } catch {
205
+ process.exit(1);
163
206
  }
164
207
 
165
- // 1. check the licence is available in cache if available then check the expiry
166
- const cachedToken: string | undefined = await this.cacheManager.get('client_Licence_token');
167
- if (cachedToken) {
168
- const validate = await this.validateLicence(cachedToken, publicKey, false);
169
- if (validate.status) {
170
- return true;
171
- }
172
- // If cached token is invalid/expired, we'll try to get a new one from the server.
208
+ if (!AuthMiddleware.shutdownTimer) {
209
+ AuthMiddleware.shutdownTimer = setTimeout(() => process.exit(1), 5000);
173
210
  }
211
+ }, 0);
212
+ }
213
+
214
+ // -------------------------
215
+ // Licence validate flow (called at boot / can be called again if needed)
216
+ // -------------------------
217
+ private async checkLicenceAndValidate(realm: string, publicKey: string): Promise<boolean> {
218
+ try {
219
+ if (AuthMiddleware.licenceExpired) return false;
174
220
 
175
- if (AuthMiddleware.licenceValidatedUntilMs && AuthMiddleware.licenceValidatedUntilMs > this.getUtcNowMs()) {
221
+ // If we already have an expiry and it's still valid, no need to fetch again
222
+ if (
223
+ AuthMiddleware.licenceValidatedUntilMs &&
224
+ Date.now() < AuthMiddleware.licenceValidatedUntilMs + AuthMiddleware.CLOCK_TOLERANCE_MS
225
+ ) {
176
226
  return true;
177
227
  }
178
228
 
229
+ // De-duplicate concurrent validations
179
230
  if (AuthMiddleware.licenceValidationPromise) {
180
231
  return await AuthMiddleware.licenceValidationPromise;
181
232
  }
182
233
 
183
- // 2. if the licence details not available in cache get the data from another server and validate.
184
234
  AuthMiddleware.licenceValidationPromise = (async () => {
185
- const response: any = await this.getLicencingDetails(realm);
186
- if (response.code === HttpStatusCode.InternalServerError) {
187
- return false;
188
- } else {
189
- const validate = await this.validateLicence(response.data, publicKey, true);
190
- return validate.status;
235
+ // 1) Try cache first (short TTL)
236
+ const cachedToken = (await this.cacheManager.get('client_Licence_token')) as string | undefined;
237
+ if (cachedToken) {
238
+ const ok = await this.validateLicence(cachedToken, publicKey, false);
239
+ if (ok) return true;
191
240
  }
241
+
242
+ // 2) Fetch from licensing service
243
+ const token = await this.getLicencingTokenFromService(realm);
244
+ const ok = await this.validateLicence(token, publicKey, true);
245
+ return ok;
192
246
  })();
193
247
 
194
248
  try {
@@ -196,152 +250,138 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
196
250
  } finally {
197
251
  AuthMiddleware.licenceValidationPromise = null;
198
252
  }
199
- } catch (error: any) {
253
+ } catch (e: any) {
254
+ this.logger.warn(`checkLicenceAndValidate failed: ${e?.message || e}`);
200
255
  return false;
201
256
  }
202
257
  }
203
258
 
204
- private async getLicencingDetails(realm: string) {
259
+ private async validateLicence(tokenJwt: string, publicKey: string, updateCache: boolean): Promise<boolean> {
205
260
  try {
206
- const url = 'https://iiuuckued274sisadalbpf7ivu0eiblk.lambda-url.ap-south-1.on.aws/';
207
- const body = { "client_name": realm };
208
- const response = await axios.post(url, body);
209
- if (response.status === HttpStatusCode.Ok) {
210
- return {
211
- code: HttpStatusCode.Ok,
212
- data: response.data.token
213
- };
214
- } else {
215
- return {
216
- code: HttpStatusCode.InternalServerError,
217
- message: "InternalServer error"
218
- };
219
- }
220
- } catch (error: any) {
221
- return {
222
- code: HttpStatusCode.InternalServerError,
223
- data: error?.response?.data || error?.message || 'Licencing service error'
224
- };
225
- }
226
- }
261
+ const payload: any = this.verifyLicenceJwt(tokenJwt, publicKey);
227
262
 
228
- private async validateLicence(lic_data: any, publicKey: any, updateCache = true) {
229
- try {
230
- const token: any = await this.verifyJwt(lic_data, publicKey);
231
- const licEndMs = this.normalizeEpochMs(token?.lic_end);
232
- const now = this.getUtcNowMs();
263
+ // Your custom claim: lic_end
264
+ const licEndMs = this.normalizeEpochMs(payload?.lic_end);
265
+ const now = Date.now();
233
266
 
234
- // Reject when licence end (epoch) is missing or already in the past (with tolerance).
235
- if (!licEndMs || (licEndMs + AuthMiddleware.CLOCK_TOLERANCE_MS) <= now) {
236
- this.logger.error(`Licence expired. Expiry: ${new Date(licEndMs || 0).toISOString()}, Server Time: ${new Date(now).toISOString()}`);
267
+ if (!licEndMs) {
268
+ this.logger.error('Licence token missing lic_end');
237
269
  this.markLicenceExpired();
238
- return { status: false };
270
+ return false;
239
271
  }
240
272
 
241
- // 3. if the licencing server have the future expiry time wait till the expiry time
273
+ if (now >= licEndMs + AuthMiddleware.CLOCK_TOLERANCE_MS) {
274
+ this.logger.error(
275
+ `Licence already expired. lic_end=${new Date(licEndMs).toISOString()} now=${new Date(now).toISOString()}`
276
+ );
277
+ this.markLicenceExpired();
278
+ return false;
279
+ }
280
+
281
+ // ✅ Save only the timestamp (supports 50 days / 500 days / years)
242
282
  AuthMiddleware.licenceValidatedUntilMs = licEndMs;
243
- this.scheduleLicenceShutdown(licEndMs);
244
283
 
284
+ // ✅ Cache only briefly (avoid long TTL -> avoids TimeoutOverflowWarning)
245
285
  if (updateCache) {
246
- // Cap TTL to avoid timeout overflow in cache managers (32-bit signed int max is ~24.8 days)
247
- const MAX_TTL = 2147483647;
248
- const calculatedTtl = licEndMs - now;
249
- const ttl = Math.min(Math.max(0, calculatedTtl), MAX_TTL);
250
- await this.cacheManager.set('client_Licence_token', lic_data, ttl);
286
+ await this.cacheSetSeconds('client_Licence_token', tokenJwt, AuthMiddleware.LIC_CACHE_TTL_SECONDS);
251
287
  }
252
288
 
253
- return {
254
- status: true,
255
- };
256
- } catch (error: any) {
257
- if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
258
- this.logger.error(`Invalid Licence Token: ${error.message}`);
289
+ return true;
290
+ } catch (e: any) {
291
+ // If JWT invalid, expire
292
+ const name = e?.name || '';
293
+ if (name === 'JsonWebTokenError' || name === 'TokenExpiredError' || name === 'NotBeforeError') {
294
+ this.logger.error(`Invalid licence token: ${e?.message || e}`);
259
295
  this.markLicenceExpired();
260
- } else {
261
- this.logger.warn(`Transient error during licence validation: ${error.message}`);
296
+ return false;
262
297
  }
263
- return { status: false };
264
- }
265
- }
266
298
 
267
- private markLicenceExpired() {
268
- if (!AuthMiddleware.licenceExpired) {
269
- AuthMiddleware.licenceExpired = true;
299
+ // transient failures should not hard-expire immediately
300
+ this.logger.warn(`Transient licence validation error: ${e?.message || e}`);
301
+ return false;
270
302
  }
271
303
  }
272
304
 
273
- private scheduleLicenceShutdown(licEndMs: number) {
274
- if (!licEndMs) {
275
- return;
276
- }
277
- const now = this.getUtcNowMs();
278
- const delay = (licEndMs + AuthMiddleware.CLOCK_TOLERANCE_MS) - now;
305
+ private async getLicencingTokenFromService(realm: string): Promise<string> {
306
+ const url =
307
+ this.options.licenceServiceUrl ??
308
+ 'https://iiuuckued274sisadalbpf7ivu0eiblk.lambda-url.ap-south-1.on.aws/';
279
309
 
280
- if (delay <= 0) {
281
- this.logger.warn('Licence expired immediately (including tolerance).');
282
- this.markLicenceExpired();
283
- return;
284
- }
310
+ const body = { client_name: realm };
311
+ const resp = await axios.post(url, body, { timeout: 15_000 });
285
312
 
286
- if (AuthMiddleware.licenceExpiryTimer) {
287
- clearTimeout(AuthMiddleware.licenceExpiryTimer);
313
+ if (resp.status !== HttpStatusCode.Ok) {
314
+ throw new Error(`Licencing service returned status ${resp.status}`);
288
315
  }
289
316
 
290
- // setTimeout uses a 32-bit signed integer. Max value is 2147483647 ms (approx 24.8 days).
291
- // If the delay is larger than this, setTimeout will fire immediately (overflow).
292
- // so we cap the delay and re-check when it fires.
293
- const MAX_TIMEOUT_VAL = 2147483647;
294
- const safeDelay = Math.min(delay, MAX_TIMEOUT_VAL);
317
+ const token = resp?.data?.token;
318
+ if (!token || typeof token !== 'string') {
319
+ throw new Error('Licencing service response missing token');
320
+ }
295
321
 
296
- AuthMiddleware.licenceExpiryTimer = setTimeout(() => {
297
- const remaining = (licEndMs + AuthMiddleware.CLOCK_TOLERANCE_MS) - this.getUtcNowMs();
298
- if (remaining <= 0) {
299
- this.markLicenceExpired();
300
- } else {
301
- // Reschedule if we still have time left
302
- this.scheduleLicenceShutdown(licEndMs);
303
- }
304
- }, safeDelay);
322
+ return token;
305
323
  }
306
324
 
307
- private stopServer() {
308
- if (AuthMiddleware.shutdownInitiated) {
309
- return;
310
- }
311
- AuthMiddleware.shutdownInitiated = true;
312
- setTimeout(() => {
313
- try {
314
- process.kill(process.pid, 'SIGTERM');
315
- } catch {
316
- process.exit(1);
317
- }
318
- if (!AuthMiddleware.shutdownTimer) {
319
- AuthMiddleware.shutdownTimer = setTimeout(() => {
320
- process.exit(1);
321
- }, 5000);
322
- }
323
- }, 0);
325
+ // -------------------------
326
+ // Keycloak client token caching (safe TTL)
327
+ // -------------------------
328
+ private async getOrFetchClientAccessToken(): Promise<string> {
329
+ let token = (await this.cacheManager.get('client_access_token')) as string | undefined;
330
+ if (token) return token;
331
+
332
+ token = await this.clientLogin();
333
+
334
+ const decoded: any = jwt.decode(token);
335
+ const nowSec = Math.floor(Date.now() / 1000);
336
+ const expSec = Number(decoded?.exp || 0);
337
+
338
+ // ttl seconds: refresh early, clamp to [60, 3600]
339
+ let ttlSec = expSec - nowSec - AuthMiddleware.CLIENT_TOKEN_TTL_SAFETY_SKEW_SECONDS;
340
+ if (!Number.isFinite(ttlSec) || ttlSec <= 0) ttlSec = 60;
341
+ ttlSec = Math.min(Math.max(60, ttlSec), AuthMiddleware.CLIENT_TOKEN_TTL_MAX_SECONDS);
342
+
343
+ await this.cacheSetSeconds('client_access_token', token, ttlSec);
344
+ return token;
324
345
  }
325
346
 
326
- private normalizeEpochMs(epoch: string | number): number | null {
327
- const epochNum = Number(epoch);
328
- if (!epochNum || Number.isNaN(epochNum)) {
329
- return null;
330
- }
331
- if (epochNum < 1_000_000_000_000) {
332
- return epochNum * 1000;
347
+ private async clientLogin(): Promise<string> {
348
+ const { keycloakUrl, realm, clientId, clientSecret } = this.options;
349
+
350
+ const resp = await axios.post(
351
+ `${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`,
352
+ new URLSearchParams({
353
+ grant_type: 'client_credentials',
354
+ client_id: clientId,
355
+ client_secret: clientSecret
356
+ }).toString(),
357
+ { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 15_000 }
358
+ );
359
+
360
+ const token = resp?.data?.access_token;
361
+ if (!token) {
362
+ throw new Error('Keycloak did not return access_token');
333
363
  }
334
- return epochNum;
364
+ return token;
335
365
  }
336
366
 
337
- private getUtcNowMs() {
338
- return Date.now();
367
+ private async getClientRoleAttributes(role: string, token: string) {
368
+ const { keycloakUrl, realm, clientUuid } = this.options;
369
+
370
+ const resp = await axios.get(
371
+ `${keycloakUrl}/admin/realms/${realm}/clients/${clientUuid}/roles/${role}`,
372
+ { headers: { Authorization: `Bearer ${token}` }, timeout: 15_000 }
373
+ );
374
+
375
+ return resp.data;
339
376
  }
340
377
 
341
- private verifyJwt(token: string, publicKey: string) {
378
+ // -------------------------
379
+ // JWT helpers
380
+ // -------------------------
381
+ private verifyLicenceJwt(token: string, publicKey: string) {
342
382
  const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
343
- // We ignore the standard 'exp' claim and rely on our custom 'lic_end' field.
344
- // Also include a small clock tolerance for the library itself.
383
+
384
+ // We ignore standard 'exp' and rely on custom 'lic_end'
345
385
  return jwt.verify(token, publicKeys, {
346
386
  algorithms: ['RS256'],
347
387
  ignoreExpiration: true,
@@ -351,10 +391,45 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
351
391
 
352
392
  private decodeAccessToken(authHeader: string, publicKey: string) {
353
393
  const token = this.extractBearerToken(authHeader);
354
- return this.verifyJwt(token, publicKey);
394
+ const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
395
+
396
+ return jwt.verify(token, publicKeys, {
397
+ algorithms: ['RS256'],
398
+ clockTolerance: 60
399
+ });
355
400
  }
356
401
 
357
402
  private extractBearerToken(authHeader: string) {
358
403
  return authHeader.split(' ')[1];
359
404
  }
405
+
406
+ // -------------------------
407
+ // Time helpers
408
+ // -------------------------
409
+ private normalizeEpochMs(epoch: string | number): number | null {
410
+ const n = Number(epoch);
411
+ if (!Number.isFinite(n) || n <= 0) return null;
412
+
413
+ // If it's seconds (10 digits), convert to ms
414
+ // If it's ms (13 digits), keep
415
+ if (n < 1_000_000_000_000) return n * 1000;
416
+ return n;
417
+ }
418
+
419
+ // -------------------------
420
+ // Cache helper (seconds-based, safe across stores)
421
+ // - Using seconds avoids ms/sec mismatch across cache-manager stores
422
+ // - TTL is kept small so no overflow even if a store uses Node timers
423
+ // -------------------------
424
+ private async cacheSetSeconds(key: string, value: any, ttlSeconds: number) {
425
+ // ttlSeconds=0 => store forever (supported by many stores); if not, we just set without ttl
426
+ if (!ttlSeconds || ttlSeconds <= 0) {
427
+ await (this.cacheManager.set as any)(key, value);
428
+ return;
429
+ }
430
+
431
+ // cache-manager / Nest stores vary: some accept third param number, others accept { ttl }
432
+ // We use { ttl } for better compatibility, and TTL is small anyway.
433
+ await (this.cacheManager.set as any)(key, value, { ttl: ttlSeconds });
434
+ }
360
435
  }