dms-middleware-auth 1.1.1 → 1.1.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.
@@ -87,7 +87,7 @@ describe('AuthMiddleware licensing', () => {
87
87
  const result = await (middleware as any).checkLicenceAndValidate('my-realm', 'PUBLIC_KEY');
88
88
 
89
89
  expect(result).toBe(true);
90
- expect(cache.set).toHaveBeenCalledWith('client_Licence_token', 'server-token', 0);
90
+ expect(cache.set).toHaveBeenCalledWith('client_Licence_token', 'server-token', expect.any(Number));
91
91
  });
92
92
 
93
93
  it('schedules licence expiration correctly but DOES NOT shutdown immediately', async () => {
@@ -103,14 +103,49 @@ describe('AuthMiddleware licensing', () => {
103
103
 
104
104
  await (middleware as any).checkLicenceAndValidate('my-realm', 'PUBLIC_KEY');
105
105
 
106
- // Fast forward to expiry
107
- jest.advanceTimersByTime(1001);
106
+ // Fast forward past expiry + tolerance
107
+ jest.advanceTimersByTime(1001 + 60000);
108
108
  expect((AuthMiddleware as any).licenceExpired).toBe(true);
109
109
 
110
110
  // Should NOT have initiated shutdown yet (no request made after expiry)
111
111
  expect((AuthMiddleware as any).shutdownInitiated).toBe(false);
112
112
  });
113
113
 
114
+ it('allows a 60-second clock tolerance for expiration', async () => {
115
+ jest.useFakeTimers();
116
+ cache.get.mockResolvedValueOnce(null);
117
+ jest.spyOn(middleware as any, 'getLicencingDetails').mockResolvedValue({
118
+ code: HttpStatusCode.Ok,
119
+ data: 'server-token',
120
+ });
121
+ jest.spyOn(middleware as any, 'verifyJwt').mockReturnValue({
122
+ lic_end: Date.now() - 30000, // 30 seconds ago
123
+ });
124
+
125
+ const result = await (middleware as any).checkLicenceAndValidate('my-realm', 'PUBLIC_KEY');
126
+
127
+ // Even though it's 30s in the past, tolerance (60s) should keep it valid.
128
+ expect(result).toBe(true);
129
+ expect((AuthMiddleware as any).licenceExpired).toBe(false);
130
+ });
131
+
132
+ it('fails after the 60-second grace period', async () => {
133
+ jest.useFakeTimers();
134
+ cache.get.mockResolvedValueOnce(null);
135
+ jest.spyOn(middleware as any, 'getLicencingDetails').mockResolvedValue({
136
+ code: HttpStatusCode.Ok,
137
+ data: 'server-token',
138
+ });
139
+ jest.spyOn(middleware as any, 'verifyJwt').mockReturnValue({
140
+ lic_end: Date.now() - 61000, // 61 seconds ago
141
+ });
142
+
143
+ const result = await (middleware as any).checkLicenceAndValidate('my-realm', 'PUBLIC_KEY');
144
+
145
+ expect(result).toBe(false);
146
+ expect((AuthMiddleware as any).licenceExpired).toBe(true);
147
+ });
148
+
114
149
  it('shuts down on the next request after licence expiry', async () => {
115
150
  jest.useFakeTimers();
116
151
  (AuthMiddleware as any).licenceExpired = true;
@@ -123,7 +158,11 @@ describe('AuthMiddleware licensing', () => {
123
158
  await middleware.use(req, res as any, next);
124
159
 
125
160
  expect(res.status).toHaveBeenCalledWith(HttpStatusCode.Forbidden);
126
- expect(res.json).toHaveBeenCalledWith({ message: (AuthMiddleware as any).licenceExpiredMessage });
161
+ const responseJson = res.json.mock.calls[0][0];
162
+ expect(responseJson.message).toBe((AuthMiddleware as any).licenceExpiredMessage);
163
+ expect(responseJson).toHaveProperty('server_time');
164
+ expect(responseJson).toHaveProperty('licence_expiry');
165
+ expect(responseJson).toHaveProperty('drift_ms');
127
166
  expect(stopSpy).toHaveBeenCalled();
128
167
  });
129
168
 
@@ -20,6 +20,8 @@ export declare class AuthMiddleware implements NestMiddleware, OnModuleInit {
20
20
  private static licenceValidationPromise;
21
21
  private static shutdownInitiated;
22
22
  private static readonly licenceExpiredMessage;
23
+ private static readonly CLOCK_TOLERANCE_MS;
24
+ private readonly logger;
23
25
  constructor(cacheManager: Cache, options: AuthMiddlewareOptions);
24
26
  onModuleInit(): Promise<void>;
25
27
  use(req: any, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
@@ -56,6 +56,7 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
56
56
  constructor(cacheManager, options) {
57
57
  this.cacheManager = cacheManager;
58
58
  this.options = options;
59
+ this.logger = new common_1.Logger('AuthMiddleware');
59
60
  }
60
61
  async onModuleInit() {
61
62
  const { publicKey, realm } = this.options;
@@ -66,7 +67,12 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
66
67
  try {
67
68
  if (AuthMiddleware_1.licenceExpired) {
68
69
  this.stopServer();
69
- return res.status(axios_1.HttpStatusCode.Forbidden).json({ message: AuthMiddleware_1.licenceExpiredMessage });
70
+ return res.status(axios_1.HttpStatusCode.Forbidden).json({
71
+ message: AuthMiddleware_1.licenceExpiredMessage,
72
+ 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
75
+ });
70
76
  }
71
77
  if (!AuthMiddleware_1.licenceValidatedUntilMs) {
72
78
  return res.status(axios_1.HttpStatusCode.Forbidden).json({ message: AuthMiddleware_1.licenceExpiredMessage });
@@ -226,8 +232,10 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
226
232
  try {
227
233
  const token = await this.verifyJwt(lic_data, publicKey);
228
234
  const licEndMs = this.normalizeEpochMs(token?.lic_end);
229
- // Reject when licence end (epoch) is missing or already in the past.
230
- if (!licEndMs || licEndMs <= this.getUtcNowMs()) {
235
+ // Reject when licence end (epoch) is missing or already in the past (with tolerance).
236
+ const now = this.getUtcNowMs();
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()}`);
231
239
  this.markLicenceExpired();
232
240
  return { status: false };
233
241
  }
@@ -256,18 +264,31 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
256
264
  if (!licEndMs) {
257
265
  return;
258
266
  }
259
- const delay = licEndMs - this.getUtcNowMs();
267
+ const now = this.getUtcNowMs();
268
+ const delay = (licEndMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) - now;
260
269
  if (delay <= 0) {
270
+ this.logger.warn('Licence expired immediately (including tolerance).');
261
271
  this.markLicenceExpired();
262
272
  return;
263
273
  }
264
274
  if (AuthMiddleware_1.licenceExpiryTimer) {
265
275
  clearTimeout(AuthMiddleware_1.licenceExpiryTimer);
266
276
  }
267
- // and then shutdown for till next user request to know the user to respond and shutdown the server.
277
+ // setTimeout uses a 32-bit signed integer. Max value is 2147483647 ms (approx 24.8 days).
278
+ // If the delay is larger than this, setTimeout will fire immediately (overflow).
279
+ // so we cap the delay and re-check when it fires.
280
+ const MAX_TIMEOUT_VAL = 2147483647;
281
+ const safeDelay = Math.min(delay, MAX_TIMEOUT_VAL);
268
282
  AuthMiddleware_1.licenceExpiryTimer = setTimeout(() => {
269
- this.markLicenceExpired();
270
- }, delay);
283
+ const remaining = (licEndMs + AuthMiddleware_1.CLOCK_TOLERANCE_MS) - this.getUtcNowMs();
284
+ if (remaining <= 0) {
285
+ this.markLicenceExpired();
286
+ }
287
+ else {
288
+ // Reschedule if we still have time left
289
+ this.scheduleLicenceShutdown(licEndMs);
290
+ }
291
+ }, safeDelay);
271
292
  }
272
293
  stopServer() {
273
294
  if (AuthMiddleware_1.shutdownInitiated) {
@@ -289,20 +310,27 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
289
310
  }, 0);
290
311
  }
291
312
  normalizeEpochMs(epoch) {
292
- if (!epoch || Number.isNaN(epoch)) {
313
+ const epochNum = Number(epoch);
314
+ if (!epochNum || Number.isNaN(epochNum)) {
293
315
  return null;
294
316
  }
295
- if (epoch < 1_000_000_000_000) {
296
- return epoch * 1000;
317
+ if (epochNum < 1_000_000_000_000) {
318
+ return epochNum * 1000;
297
319
  }
298
- return epoch;
320
+ return epochNum;
299
321
  }
300
322
  getUtcNowMs() {
301
323
  return Date.now();
302
324
  }
303
325
  verifyJwt(token, publicKey) {
304
326
  const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
305
- return jwt.verify(token, publicKeys, { algorithms: ['RS256'] });
327
+ // We ignore the standard 'exp' claim and rely on our custom 'lic_end' field.
328
+ // Also include a small clock tolerance for the library itself.
329
+ return jwt.verify(token, publicKeys, {
330
+ algorithms: ['RS256'],
331
+ ignoreExpiration: true,
332
+ clockTolerance: 60
333
+ });
306
334
  }
307
335
  decodeAccessToken(authHeader, publicKey) {
308
336
  const token = this.extractBearerToken(authHeader);
@@ -320,6 +348,7 @@ AuthMiddleware.licenceValidatedUntilMs = null;
320
348
  AuthMiddleware.licenceValidationPromise = null;
321
349
  AuthMiddleware.shutdownInitiated = false;
322
350
  AuthMiddleware.licenceExpiredMessage = "server Licence is expired!. please renew";
351
+ AuthMiddleware.CLOCK_TOLERANCE_MS = 60000; // 1 minute grace period
323
352
  exports.AuthMiddleware = AuthMiddleware = AuthMiddleware_1 = __decorate([
324
353
  (0, common_1.Injectable)(),
325
354
  __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.1",
3
+ "version": "1.1.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",
@@ -29,4 +29,4 @@
29
29
  "ts-jest": "^29.3.4",
30
30
  "typescript": "^5.7.2"
31
31
  }
32
- }
32
+ }
package/repro_bug.ts ADDED
@@ -0,0 +1,37 @@
1
+
2
+ const licEndString = "1774981740000";
3
+ const CLOCK_TOLERANCE_MS = 60000;
4
+
5
+ function normalizeEpochMs(epoch: any): number | null {
6
+ if (!epoch || (typeof epoch === 'number' && Number.isNaN(epoch))) {
7
+ return null;
8
+ }
9
+ // Buggy logic: relying on implicit conversion but returning original type
10
+ if (epoch < 1_000_000_000_000) {
11
+ return epoch * 1000;
12
+ }
13
+ return epoch;
14
+ }
15
+
16
+ const now = Date.now();
17
+ const licEndMs = normalizeEpochMs(licEndString);
18
+ console.log(`licEndMs Type: ${typeof licEndMs}, Value: ${licEndMs}`);
19
+
20
+ // Simulate the logic in auth.middleware.ts
21
+ const delay = ((licEndMs as any) + CLOCK_TOLERANCE_MS) - now;
22
+ console.log(`Delay: ${delay}`);
23
+
24
+ if (delay > 2147483647) {
25
+ console.log("Delay exceeds 32-bit signed integer limit! This often causes setTimeout to fire immediately (overflow to 1).");
26
+ }
27
+
28
+ console.log("Setting timeout...");
29
+ const timer = setTimeout(() => {
30
+ console.log("BUG REPRODUCED: Timeout fired immediately despite huge delay!");
31
+ }, delay);
32
+
33
+ // Keep alive for a bit to see if it fires immediately
34
+ setTimeout(() => {
35
+ console.log("Finished waiting 2s");
36
+ clearTimeout(timer);
37
+ }, 2000);
@@ -1,4 +1,4 @@
1
- import { Inject, Injectable, NestMiddleware, OnModuleInit } from '@nestjs/common';
1
+ import { Inject, Injectable, Logger, NestMiddleware, OnModuleInit } from '@nestjs/common';
2
2
  import { Response, NextFunction } from 'express';
3
3
  import * as jwt from 'jsonwebtoken';
4
4
  import axios, { HttpStatusCode } from 'axios';
@@ -25,6 +25,8 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
25
25
  private static licenceValidationPromise: Promise<boolean> | null = null;
26
26
  private static shutdownInitiated = false;
27
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');
28
30
 
29
31
  constructor(
30
32
  @Inject(CACHE_MANAGER) private cacheManager: Cache,
@@ -42,7 +44,12 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
42
44
  try {
43
45
  if (AuthMiddleware.licenceExpired) {
44
46
  this.stopServer();
45
- return res.status(HttpStatusCode.Forbidden).json({ message: AuthMiddleware.licenceExpiredMessage });
47
+ return res.status(HttpStatusCode.Forbidden).json({
48
+ message: AuthMiddleware.licenceExpiredMessage,
49
+ 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
52
+ });
46
53
  }
47
54
 
48
55
  if (!AuthMiddleware.licenceValidatedUntilMs) {
@@ -223,8 +230,10 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
223
230
  const token: any = await this.verifyJwt(lic_data, publicKey);
224
231
  const licEndMs = this.normalizeEpochMs(token?.lic_end);
225
232
 
226
- // Reject when licence end (epoch) is missing or already in the past.
227
- if (!licEndMs || licEndMs <= this.getUtcNowMs()) {
233
+ // Reject when licence end (epoch) is missing or already in the past (with tolerance).
234
+ const now = this.getUtcNowMs();
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()}`);
228
237
  this.markLicenceExpired();
229
238
  return { status: false };
230
239
  }
@@ -258,18 +267,34 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
258
267
  if (!licEndMs) {
259
268
  return;
260
269
  }
261
- const delay = licEndMs - this.getUtcNowMs();
270
+ const now = this.getUtcNowMs();
271
+ const delay = (licEndMs + AuthMiddleware.CLOCK_TOLERANCE_MS) - now;
272
+
262
273
  if (delay <= 0) {
274
+ this.logger.warn('Licence expired immediately (including tolerance).');
263
275
  this.markLicenceExpired();
264
276
  return;
265
277
  }
278
+
266
279
  if (AuthMiddleware.licenceExpiryTimer) {
267
280
  clearTimeout(AuthMiddleware.licenceExpiryTimer);
268
281
  }
269
- // and then shutdown for till next user request to know the user to respond and shutdown the server.
282
+
283
+ // setTimeout uses a 32-bit signed integer. Max value is 2147483647 ms (approx 24.8 days).
284
+ // If the delay is larger than this, setTimeout will fire immediately (overflow).
285
+ // so we cap the delay and re-check when it fires.
286
+ const MAX_TIMEOUT_VAL = 2147483647;
287
+ const safeDelay = Math.min(delay, MAX_TIMEOUT_VAL);
288
+
270
289
  AuthMiddleware.licenceExpiryTimer = setTimeout(() => {
271
- this.markLicenceExpired();
272
- }, delay);
290
+ const remaining = (licEndMs + AuthMiddleware.CLOCK_TOLERANCE_MS) - this.getUtcNowMs();
291
+ if (remaining <= 0) {
292
+ this.markLicenceExpired();
293
+ } else {
294
+ // Reschedule if we still have time left
295
+ this.scheduleLicenceShutdown(licEndMs);
296
+ }
297
+ }, safeDelay);
273
298
  }
274
299
 
275
300
  private stopServer() {
@@ -291,14 +316,15 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
291
316
  }, 0);
292
317
  }
293
318
 
294
- private normalizeEpochMs(epoch: number): number | null {
295
- if (!epoch || Number.isNaN(epoch)) {
319
+ private normalizeEpochMs(epoch: string | number): number | null {
320
+ const epochNum = Number(epoch);
321
+ if (!epochNum || Number.isNaN(epochNum)) {
296
322
  return null;
297
323
  }
298
- if (epoch < 1_000_000_000_000) {
299
- return epoch * 1000;
324
+ if (epochNum < 1_000_000_000_000) {
325
+ return epochNum * 1000;
300
326
  }
301
- return epoch;
327
+ return epochNum;
302
328
  }
303
329
 
304
330
  private getUtcNowMs() {
@@ -307,7 +333,13 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
307
333
 
308
334
  private verifyJwt(token: string, publicKey: string) {
309
335
  const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
310
- return jwt.verify(token, publicKeys, { algorithms: ['RS256'] });
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.
338
+ return jwt.verify(token, publicKeys, {
339
+ algorithms: ['RS256'],
340
+ ignoreExpiration: true,
341
+ clockTolerance: 60
342
+ });
311
343
  }
312
344
 
313
345
  private decodeAccessToken(authHeader: string, publicKey: string) {
@@ -0,0 +1,84 @@
1
+
2
+ const CLOCK_TOLERANCE_MS = 60000;
3
+ const MAX_TIMEOUT_VAL = 2147483647;
4
+
5
+ let licenceExpiryTimer: NodeJS.Timeout | null = null;
6
+ let licenceExpired = false;
7
+
8
+ function getUtcNowMs() {
9
+ return Date.now();
10
+ }
11
+
12
+ function normalizeEpochMs(epoch: number | string): number | null {
13
+ const epochNum = Number(epoch);
14
+ if (!epochNum || Number.isNaN(epochNum)) {
15
+ return null;
16
+ }
17
+ if (epochNum < 1_000_000_000_000) {
18
+ return epochNum * 1000;
19
+ }
20
+ return epochNum;
21
+ }
22
+
23
+ function markLicenceExpired() {
24
+ licenceExpired = true;
25
+ console.log("Licence marked as expired!");
26
+ }
27
+
28
+ function scheduleLicenceShutdown(licEndMs: number) {
29
+ if (!licEndMs) {
30
+ return;
31
+ }
32
+ const now = getUtcNowMs();
33
+ const delay = (licEndMs + CLOCK_TOLERANCE_MS) - now;
34
+
35
+ console.log(`Scheduling shutdown. LicEnd: ${licEndMs}, Now: ${now}, Full Delay: ${delay}`);
36
+
37
+ if (delay <= 0) {
38
+ console.log('Licence expired immediately (including tolerance).');
39
+ markLicenceExpired();
40
+ return;
41
+ }
42
+
43
+ if (licenceExpiryTimer) {
44
+ clearTimeout(licenceExpiryTimer);
45
+ }
46
+
47
+ // Check if delay fits in 32-bit int
48
+ const safeDelay = Math.min(delay, MAX_TIMEOUT_VAL);
49
+
50
+ console.log(`Setting timeout for ${safeDelay}ms`);
51
+
52
+ // In a real test we can't wait 24 days, so we'll just assert the logic is sound.
53
+ // If safeDelay < delay, it means we need to reschedule.
54
+
55
+ if (safeDelay < delay) {
56
+ console.log("Delay exceeds max timeout, scheduling recursive check...");
57
+ }
58
+
59
+ licenceExpiryTimer = setTimeout(() => {
60
+ const remaining = (licEndMs + CLOCK_TOLERANCE_MS) - getUtcNowMs();
61
+ console.log(`Timeout fired. Remaining: ${remaining}`);
62
+ if (remaining <= 0) {
63
+ markLicenceExpired();
64
+ } else {
65
+ scheduleLicenceShutdown(licEndMs);
66
+ }
67
+ }, Math.min(safeDelay, 1000)); // Override delay to 1s for test execution
68
+ }
69
+
70
+ // Test Case
71
+ const licEndString = "1774981740000"; // Future date (approx 50 days from 2026-02-09)
72
+ const licEndMs = normalizeEpochMs(licEndString);
73
+
74
+ if (licEndMs) {
75
+ console.log(`Normalized LicEnd: ${licEndMs}`);
76
+ scheduleLicenceShutdown(licEndMs);
77
+ } else {
78
+ console.error("Failed to normalize epoch");
79
+ }
80
+
81
+ // Wait for the test simulation
82
+ setTimeout(() => {
83
+ console.log("Test finished.");
84
+ }, 2000);