dms-middleware-auth 1.0.10 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/auth.middleware.spec.ts +64 -56
- package/dist/auth.middleware.d.ts +1 -0
- package/dist/auth.middleware.js +32 -46
- package/package.json +2 -2
- package/src/auth.middleware.ts +177 -193
|
@@ -4,7 +4,7 @@ import { HttpStatusCode } from 'axios';
|
|
|
4
4
|
const baseOptions = {
|
|
5
5
|
publicKey: 'PUBLIC_KEY',
|
|
6
6
|
keycloakUrl: '',
|
|
7
|
-
realm: '',
|
|
7
|
+
realm: 'my-realm',
|
|
8
8
|
clientId: 'client',
|
|
9
9
|
clientSecret: '',
|
|
10
10
|
clientUuid: '',
|
|
@@ -29,94 +29,92 @@ const createRes = () => {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
const createReq = (override: Record<string, any> = {}) =>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
({
|
|
33
|
+
headers: { authorization: 'Bearer access-token' },
|
|
34
|
+
originalUrl: '/route',
|
|
35
|
+
body: {},
|
|
36
|
+
...override,
|
|
37
|
+
} as any);
|
|
38
38
|
|
|
39
39
|
describe('AuthMiddleware licensing', () => {
|
|
40
|
+
let cache: any;
|
|
41
|
+
let middleware: AuthMiddleware;
|
|
42
|
+
|
|
40
43
|
beforeEach(() => {
|
|
41
44
|
jest.restoreAllMocks();
|
|
42
45
|
jest.clearAllMocks();
|
|
43
46
|
jest.useRealTimers();
|
|
47
|
+
|
|
44
48
|
// Reset static flags/timers between tests
|
|
45
49
|
const cls: any = AuthMiddleware;
|
|
46
|
-
if (cls.shutdownTimer)
|
|
47
|
-
|
|
48
|
-
}
|
|
50
|
+
if (cls.shutdownTimer) clearTimeout(cls.shutdownTimer);
|
|
51
|
+
if (cls.licenceExpiryTimer) clearTimeout(cls.licenceExpiryTimer);
|
|
49
52
|
cls.licenceExpired = false;
|
|
50
53
|
cls.shutdownTimer = null;
|
|
54
|
+
cls.licenceExpiryTimer = null;
|
|
55
|
+
cls.licenceValidatedUntilMs = null;
|
|
56
|
+
cls.shutdownInitiated = false;
|
|
57
|
+
|
|
58
|
+
cache = createCacheMock();
|
|
59
|
+
middleware = new AuthMiddleware(cache as any, baseOptions as any);
|
|
51
60
|
});
|
|
52
61
|
|
|
53
|
-
it('
|
|
54
|
-
|
|
55
|
-
const
|
|
62
|
+
it('checks the licence in cache first', async () => {
|
|
63
|
+
cache.get.mockResolvedValueOnce('cached-token');
|
|
64
|
+
const verifySpy = jest.spyOn(middleware as any, 'verifyJwt').mockReturnValue({
|
|
65
|
+
lic_end: Math.floor(Date.now() / 1000) + 3600, // Valid for 1 hour
|
|
66
|
+
});
|
|
67
|
+
const fetchSpy = jest.spyOn(middleware as any, 'getLicencingDetails');
|
|
68
|
+
|
|
69
|
+
const result = await (middleware as any).checkLicenceAndValidate('my-realm', 'PUBLIC_KEY');
|
|
70
|
+
|
|
71
|
+
expect(result).toBe(true);
|
|
72
|
+
expect(cache.get).toHaveBeenCalledWith('client_Licence_token');
|
|
73
|
+
expect(verifySpy).toHaveBeenCalledWith('cached-token', 'PUBLIC_KEY');
|
|
74
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
56
76
|
|
|
77
|
+
it('fetches licence from server if not in cache', async () => {
|
|
78
|
+
cache.get.mockResolvedValueOnce(null);
|
|
57
79
|
jest.spyOn(middleware as any, 'getLicencingDetails').mockResolvedValue({
|
|
58
80
|
code: HttpStatusCode.Ok,
|
|
59
|
-
data: '
|
|
81
|
+
data: 'server-token',
|
|
60
82
|
});
|
|
61
83
|
jest.spyOn(middleware as any, 'verifyJwt').mockReturnValue({
|
|
62
|
-
lic_end: Math.floor(Date.now() / 1000) +
|
|
84
|
+
lic_end: Math.floor(Date.now() / 1000) + 3600,
|
|
63
85
|
});
|
|
64
|
-
jest
|
|
65
|
-
.spyOn(middleware as any, 'decodeAccessToken')
|
|
66
|
-
.mockReturnValue({
|
|
67
|
-
resource_access: { client: { roles: ['admin'] } },
|
|
68
|
-
preferred_username: 'user',
|
|
69
|
-
});
|
|
70
|
-
jest.spyOn(middleware as any, 'getClientRoleAttributes').mockResolvedValue({
|
|
71
|
-
attributes: { '/route': '{"params":"false"}' },
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const req = createReq();
|
|
75
|
-
const res = createRes();
|
|
76
|
-
const next = jest.fn();
|
|
77
86
|
|
|
78
|
-
await middleware
|
|
87
|
+
const result = await (middleware as any).checkLicenceAndValidate('my-realm', 'PUBLIC_KEY');
|
|
79
88
|
|
|
80
|
-
expect(
|
|
81
|
-
expect(
|
|
82
|
-
expect(cache.set).toHaveBeenCalledWith('client_Licence_token', 'licence-token');
|
|
89
|
+
expect(result).toBe(true);
|
|
90
|
+
expect(cache.set).toHaveBeenCalledWith('client_Licence_token', 'server-token', 0);
|
|
83
91
|
});
|
|
84
92
|
|
|
85
|
-
it('
|
|
93
|
+
it('schedules licence expiration correctly but DOES NOT shutdown immediately', async () => {
|
|
86
94
|
jest.useFakeTimers();
|
|
87
|
-
|
|
88
|
-
const middleware = new AuthMiddleware(cache as any, baseOptions as any);
|
|
89
|
-
|
|
95
|
+
cache.get.mockResolvedValueOnce(null);
|
|
90
96
|
jest.spyOn(middleware as any, 'getLicencingDetails').mockResolvedValue({
|
|
91
97
|
code: HttpStatusCode.Ok,
|
|
92
|
-
data: '
|
|
98
|
+
data: 'server-token',
|
|
93
99
|
});
|
|
94
100
|
jest.spyOn(middleware as any, 'verifyJwt').mockReturnValue({
|
|
95
|
-
lic_end: Date.now()
|
|
101
|
+
lic_end: Date.now() + 1000, // Expires in 1 second
|
|
96
102
|
});
|
|
97
|
-
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as any);
|
|
98
103
|
|
|
99
|
-
|
|
100
|
-
const res = createRes();
|
|
101
|
-
const next = jest.fn();
|
|
104
|
+
await (middleware as any).checkLicenceAndValidate('my-realm', 'PUBLIC_KEY');
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
expect(res.status).toHaveBeenCalledWith(HttpStatusCode.Forbidden);
|
|
106
|
-
expect(res.json).toHaveBeenCalled();
|
|
107
|
-
expect(next).not.toHaveBeenCalled();
|
|
106
|
+
// Fast forward to expiry
|
|
107
|
+
jest.advanceTimersByTime(1001);
|
|
108
108
|
expect((AuthMiddleware as any).licenceExpired).toBe(true);
|
|
109
109
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
110
|
+
// Should NOT have initiated shutdown yet (no request made after expiry)
|
|
111
|
+
expect((AuthMiddleware as any).shutdownInitiated).toBe(false);
|
|
113
112
|
});
|
|
114
113
|
|
|
115
|
-
it('
|
|
116
|
-
|
|
114
|
+
it('shuts down on the next request after licence expiry', async () => {
|
|
115
|
+
jest.useFakeTimers();
|
|
117
116
|
(AuthMiddleware as any).licenceExpired = true;
|
|
118
|
-
const
|
|
119
|
-
const checkSpy = jest.spyOn(middleware as any, 'checkLicenceAndValidate');
|
|
117
|
+
const stopSpy = jest.spyOn(middleware as any, 'stopServer');
|
|
120
118
|
|
|
121
119
|
const req = createReq();
|
|
122
120
|
const res = createRes();
|
|
@@ -124,8 +122,18 @@ describe('AuthMiddleware licensing', () => {
|
|
|
124
122
|
|
|
125
123
|
await middleware.use(req, res as any, next);
|
|
126
124
|
|
|
127
|
-
expect(checkSpy).not.toHaveBeenCalled();
|
|
128
125
|
expect(res.status).toHaveBeenCalledWith(HttpStatusCode.Forbidden);
|
|
129
|
-
expect(
|
|
126
|
+
expect(res.json).toHaveBeenCalledWith({ message: (AuthMiddleware as any).licenceExpiredMessage });
|
|
127
|
+
expect(stopSpy).toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('fails if server is invalid and no cache', async () => {
|
|
131
|
+
cache.get.mockResolvedValueOnce(null);
|
|
132
|
+
jest.spyOn(middleware as any, 'getLicencingDetails').mockResolvedValue({
|
|
133
|
+
code: HttpStatusCode.InternalServerError,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = await (middleware as any).checkLicenceAndValidate('my-realm', 'PUBLIC_KEY');
|
|
137
|
+
expect(result).toBe(false);
|
|
130
138
|
});
|
|
131
139
|
});
|
|
@@ -33,6 +33,7 @@ export declare class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
33
33
|
private scheduleLicenceShutdown;
|
|
34
34
|
private stopServer;
|
|
35
35
|
private normalizeEpochMs;
|
|
36
|
+
private getUtcNowMs;
|
|
36
37
|
private verifyJwt;
|
|
37
38
|
private decodeAccessToken;
|
|
38
39
|
private extractBearerToken;
|
package/dist/auth.middleware.js
CHANGED
|
@@ -60,14 +60,12 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
60
60
|
async onModuleInit() {
|
|
61
61
|
const { publicKey, realm } = this.options;
|
|
62
62
|
const isValid = await this.checkLicenceAndValidate(realm, publicKey);
|
|
63
|
-
if (!isValid) {
|
|
64
|
-
this.stopServer();
|
|
65
|
-
}
|
|
66
63
|
}
|
|
67
64
|
async use(req, res, next) {
|
|
68
65
|
const { publicKey, clientId, realm, bypassURL } = this.options;
|
|
69
66
|
try {
|
|
70
67
|
if (AuthMiddleware_1.licenceExpired) {
|
|
68
|
+
this.stopServer();
|
|
71
69
|
return res.status(axios_1.HttpStatusCode.Forbidden).json({ message: AuthMiddleware_1.licenceExpiredMessage });
|
|
72
70
|
}
|
|
73
71
|
if (!AuthMiddleware_1.licenceValidatedUntilMs) {
|
|
@@ -113,29 +111,13 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
113
111
|
}
|
|
114
112
|
}
|
|
115
113
|
}
|
|
116
|
-
// Cache interface details
|
|
117
|
-
/*const userName = decoded.preferred_username;
|
|
118
|
-
let userAttributes: any = await this.cacheManager.get(userName);
|
|
119
|
-
if (!userAttributes) {
|
|
120
|
-
userAttributes = await this.getUserDetails(userName, clientToken);
|
|
121
|
-
userAttributes = JSON.stringify(userAttributes.attributes);
|
|
122
|
-
await this.cacheManager.set(userName, userAttributes, 0);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Attach attributes to request
|
|
126
|
-
userAttributes = JSON.parse(userAttributes);*/
|
|
127
114
|
req['user'] = {
|
|
128
115
|
role: role,
|
|
129
116
|
userName: decoded.preferred_username
|
|
130
117
|
};
|
|
131
|
-
// req['attributes'] = {
|
|
132
|
-
// client: clientAttributes[req.originalUrl],
|
|
133
|
-
// // user: userAttributes,
|
|
134
|
-
// };
|
|
135
118
|
return next();
|
|
136
119
|
}
|
|
137
120
|
catch (error) {
|
|
138
|
-
// console.error('AuthMiddleware error:', error);
|
|
139
121
|
return res.status(500).json({ message: error.message });
|
|
140
122
|
}
|
|
141
123
|
}
|
|
@@ -178,19 +160,29 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
178
160
|
if (AuthMiddleware_1.licenceExpired) {
|
|
179
161
|
return false;
|
|
180
162
|
}
|
|
181
|
-
|
|
163
|
+
// 1. check the licence is available in cache if available then check the expiry
|
|
164
|
+
const cachedToken = await this.cacheManager.get('client_Licence_token');
|
|
165
|
+
if (cachedToken) {
|
|
166
|
+
const validate = await this.validateLicence(cachedToken, publicKey, false);
|
|
167
|
+
if (validate.status) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
// If cached token is invalid/expired, we'll try to get a new one from the server.
|
|
171
|
+
}
|
|
172
|
+
if (AuthMiddleware_1.licenceValidatedUntilMs && AuthMiddleware_1.licenceValidatedUntilMs > this.getUtcNowMs()) {
|
|
182
173
|
return true;
|
|
183
174
|
}
|
|
184
175
|
if (AuthMiddleware_1.licenceValidationPromise) {
|
|
185
176
|
return await AuthMiddleware_1.licenceValidationPromise;
|
|
186
177
|
}
|
|
178
|
+
// 2. if the licence details not available in cache get the data from another server and validate.
|
|
187
179
|
AuthMiddleware_1.licenceValidationPromise = (async () => {
|
|
188
180
|
const response = await this.getLicencingDetails(realm);
|
|
189
181
|
if (response.code === axios_1.HttpStatusCode.InternalServerError) {
|
|
190
182
|
return false;
|
|
191
183
|
}
|
|
192
184
|
else {
|
|
193
|
-
const validate = await this.validateLicence(response.data, publicKey);
|
|
185
|
+
const validate = await this.validateLicence(response.data, publicKey, true);
|
|
194
186
|
return validate.status;
|
|
195
187
|
}
|
|
196
188
|
})();
|
|
@@ -230,57 +222,49 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
230
222
|
};
|
|
231
223
|
}
|
|
232
224
|
}
|
|
233
|
-
async validateLicence(lic_data, publicKey) {
|
|
225
|
+
async validateLicence(lic_data, publicKey, updateCache = true) {
|
|
234
226
|
try {
|
|
235
227
|
const token = await this.verifyJwt(lic_data, publicKey);
|
|
236
228
|
const licEndMs = this.normalizeEpochMs(token?.lic_end);
|
|
237
229
|
// Reject when licence end (epoch) is missing or already in the past.
|
|
238
|
-
if (!licEndMs) {
|
|
230
|
+
if (!licEndMs || licEndMs <= this.getUtcNowMs()) {
|
|
239
231
|
this.markLicenceExpired();
|
|
240
|
-
return {
|
|
241
|
-
status: false
|
|
242
|
-
};
|
|
232
|
+
return { status: false };
|
|
243
233
|
}
|
|
244
|
-
if
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
else {
|
|
251
|
-
AuthMiddleware_1.licenceValidatedUntilMs = licEndMs;
|
|
252
|
-
this.scheduleLicenceShutdown(licEndMs);
|
|
253
|
-
return {
|
|
254
|
-
status: true,
|
|
255
|
-
ttl: (licEndMs - Date.now()) > 0 ? (licEndMs - Date.now()) : null
|
|
256
|
-
};
|
|
234
|
+
// 3. if the licencing server have the future expiry time wait till the expiry time
|
|
235
|
+
AuthMiddleware_1.licenceValidatedUntilMs = licEndMs;
|
|
236
|
+
this.scheduleLicenceShutdown(licEndMs);
|
|
237
|
+
if (updateCache) {
|
|
238
|
+
const ttl = (licEndMs - this.getUtcNowMs()) > 0 ? (licEndMs - this.getUtcNowMs()) : null;
|
|
239
|
+
await this.cacheManager.set('client_Licence_token', lic_data, ttl);
|
|
257
240
|
}
|
|
241
|
+
return {
|
|
242
|
+
status: true,
|
|
243
|
+
};
|
|
258
244
|
}
|
|
259
245
|
catch (error) {
|
|
260
246
|
this.markLicenceExpired();
|
|
261
|
-
return {
|
|
262
|
-
status: false
|
|
263
|
-
};
|
|
247
|
+
return { status: false };
|
|
264
248
|
}
|
|
265
249
|
}
|
|
266
250
|
markLicenceExpired() {
|
|
267
251
|
if (!AuthMiddleware_1.licenceExpired) {
|
|
268
252
|
AuthMiddleware_1.licenceExpired = true;
|
|
269
253
|
}
|
|
270
|
-
this.stopServer();
|
|
271
254
|
}
|
|
272
255
|
scheduleLicenceShutdown(licEndMs) {
|
|
273
256
|
if (!licEndMs) {
|
|
274
257
|
return;
|
|
275
258
|
}
|
|
276
|
-
const delay = licEndMs -
|
|
259
|
+
const delay = licEndMs - this.getUtcNowMs();
|
|
277
260
|
if (delay <= 0) {
|
|
278
261
|
this.markLicenceExpired();
|
|
279
262
|
return;
|
|
280
263
|
}
|
|
281
264
|
if (AuthMiddleware_1.licenceExpiryTimer) {
|
|
282
|
-
|
|
265
|
+
clearTimeout(AuthMiddleware_1.licenceExpiryTimer);
|
|
283
266
|
}
|
|
267
|
+
// and then shutdown for till next user request to know the user to respond and shutdown the server.
|
|
284
268
|
AuthMiddleware_1.licenceExpiryTimer = setTimeout(() => {
|
|
285
269
|
this.markLicenceExpired();
|
|
286
270
|
}, delay);
|
|
@@ -308,12 +292,14 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
308
292
|
if (!epoch || Number.isNaN(epoch)) {
|
|
309
293
|
return null;
|
|
310
294
|
}
|
|
311
|
-
// Accept seconds or milliseconds since Unix epoch and normalize to ms for Date.now() comparison.
|
|
312
295
|
if (epoch < 1_000_000_000_000) {
|
|
313
296
|
return epoch * 1000;
|
|
314
297
|
}
|
|
315
298
|
return epoch;
|
|
316
299
|
}
|
|
300
|
+
getUtcNowMs() {
|
|
301
|
+
return Date.now();
|
|
302
|
+
}
|
|
317
303
|
verifyJwt(token, publicKey) {
|
|
318
304
|
const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
|
|
319
305
|
return jwt.verify(token, publicKeys, { algorithms: ['RS256'] });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dms-middleware-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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/src/auth.middleware.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { Inject, Injectable, NestMiddleware, OnModuleInit } from '@nestjs/common';
|
|
1
|
+
import { Inject, Injectable, 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';
|
|
5
5
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
|
6
6
|
import { Cache } from 'cache-manager';
|
|
7
7
|
import * as process from "node:process";
|
|
8
|
+
|
|
8
9
|
interface AuthMiddlewareOptions {
|
|
9
10
|
publicKey: string;
|
|
10
11
|
keycloakUrl: string;
|
|
@@ -16,62 +17,61 @@ interface AuthMiddlewareOptions {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
@Injectable()
|
|
19
|
-
export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
20
|
-
private static licenceExpired = false;
|
|
21
|
-
private static shutdownTimer: NodeJS.Timeout | null = null;
|
|
22
|
-
private static licenceExpiryTimer: NodeJS.Timeout | null = null;
|
|
23
|
-
private static licenceValidatedUntilMs: number | null = null;
|
|
24
|
-
private static licenceValidationPromise: Promise<boolean> | null = null;
|
|
25
|
-
private static shutdownInitiated = false;
|
|
26
|
-
private static readonly licenceExpiredMessage = "server Licence is expired!. please renew";
|
|
27
|
-
|
|
28
|
-
constructor(
|
|
29
|
-
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
|
30
|
-
@Inject('AUTH_MIDDLEWARE_OPTIONS') private readonly options: AuthMiddlewareOptions
|
|
31
|
-
) {}
|
|
32
|
-
|
|
33
|
-
async onModuleInit() {
|
|
34
|
-
const { publicKey, realm } = this.options;
|
|
35
|
-
const isValid = await this.checkLicenceAndValidate(realm, publicKey);
|
|
36
|
-
if (!isValid) {
|
|
37
|
-
this.stopServer();
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async use(req: any, res: Response, next: NextFunction) {
|
|
20
|
+
export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
21
|
+
private static licenceExpired = false;
|
|
22
|
+
private static shutdownTimer: NodeJS.Timeout | null = null;
|
|
23
|
+
private static licenceExpiryTimer: NodeJS.Timeout | null = null;
|
|
24
|
+
private static licenceValidatedUntilMs: number | null = null;
|
|
25
|
+
private static licenceValidationPromise: Promise<boolean> | null = null;
|
|
26
|
+
private static shutdownInitiated = false;
|
|
27
|
+
private static readonly licenceExpiredMessage = "server Licence is expired!. please renew";
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
|
31
|
+
@Inject('AUTH_MIDDLEWARE_OPTIONS') private readonly options: AuthMiddlewareOptions
|
|
32
|
+
) { }
|
|
42
33
|
|
|
34
|
+
async onModuleInit() {
|
|
35
|
+
const { publicKey, realm } = this.options;
|
|
36
|
+
const isValid = await this.checkLicenceAndValidate(realm, publicKey);
|
|
37
|
+
}
|
|
43
38
|
|
|
39
|
+
async use(req: any, res: Response, next: NextFunction) {
|
|
44
40
|
const { publicKey, clientId, realm, bypassURL } = this.options;
|
|
45
41
|
|
|
46
42
|
try {
|
|
47
|
-
|
|
43
|
+
if (AuthMiddleware.licenceExpired) {
|
|
44
|
+
this.stopServer();
|
|
48
45
|
return res.status(HttpStatusCode.Forbidden).json({ message: AuthMiddleware.licenceExpiredMessage });
|
|
49
46
|
}
|
|
50
47
|
|
|
51
|
-
if (!AuthMiddleware.licenceValidatedUntilMs) {
|
|
52
|
-
return res.status(HttpStatusCode.Forbidden).json({ message: AuthMiddleware.licenceExpiredMessage });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
if (!AuthMiddleware.licenceValidatedUntilMs) {
|
|
49
|
+
return res.status(HttpStatusCode.Forbidden).json({ message: AuthMiddleware.licenceExpiredMessage });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (req.originalUrl == bypassURL) {
|
|
53
|
+
return next();
|
|
54
|
+
}
|
|
58
55
|
|
|
56
|
+
const authHeader = req.headers['authorization'];
|
|
59
57
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
60
58
|
return res.status(401).json({ message: 'Bearer token required' });
|
|
61
59
|
}
|
|
62
|
-
|
|
60
|
+
|
|
61
|
+
const decoded: any = this.decodeAccessToken(authHeader, publicKey);
|
|
62
|
+
|
|
63
63
|
// Cache the client token
|
|
64
64
|
let clientToken: any = await this.cacheManager.get('client_access_token');
|
|
65
65
|
if (!clientToken) {
|
|
66
66
|
clientToken = await this.clientLogin();
|
|
67
67
|
const decodedToken: any = jwt.decode(clientToken);
|
|
68
68
|
const ttl = (decodedToken.exp - Math.floor(Date.now() / 1000)) * 1000;
|
|
69
|
-
await this.cacheManager.set('client_access_token', clientToken, ttl
|
|
69
|
+
await this.cacheManager.set('client_access_token', clientToken, ttl);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// Cache client role attributes
|
|
73
73
|
const role = decoded.resource_access[clientId]?.roles?.[0];
|
|
74
|
-
const clientRoleName = `${clientId+role}`;
|
|
74
|
+
const clientRoleName = `${clientId + role}`;
|
|
75
75
|
let clientAttributes: any = await this.cacheManager.get(clientRoleName);
|
|
76
76
|
if (!clientAttributes) {
|
|
77
77
|
clientAttributes = await this.getClientRoleAttributes(role, clientToken);
|
|
@@ -81,54 +81,32 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
81
81
|
|
|
82
82
|
// Check route access
|
|
83
83
|
clientAttributes = JSON.parse(clientAttributes);
|
|
84
|
-
|
|
85
84
|
if (!clientAttributes[req.originalUrl]) {
|
|
86
|
-
|
|
87
85
|
return res.status(403).json({ message: 'Access denied for this route' });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (apiPermission?.params === "true")
|
|
92
|
-
{
|
|
86
|
+
} else {
|
|
87
|
+
const apiPermission = JSON.parse(clientAttributes[req.originalUrl]);
|
|
88
|
+
if (apiPermission?.params === "true") {
|
|
93
89
|
const event = req?.body?.event;
|
|
94
90
|
const url = req?.originalUrl + `?action=${event}`;
|
|
95
|
-
if (!clientAttributes[url]){
|
|
91
|
+
if (!clientAttributes[url]) {
|
|
96
92
|
return res.status(403).json({ message: 'Access denied for event' });
|
|
97
93
|
}
|
|
98
94
|
}
|
|
99
95
|
}
|
|
100
96
|
|
|
101
|
-
// Cache interface details
|
|
102
|
-
/*const userName = decoded.preferred_username;
|
|
103
|
-
let userAttributes: any = await this.cacheManager.get(userName);
|
|
104
|
-
if (!userAttributes) {
|
|
105
|
-
userAttributes = await this.getUserDetails(userName, clientToken);
|
|
106
|
-
userAttributes = JSON.stringify(userAttributes.attributes);
|
|
107
|
-
await this.cacheManager.set(userName, userAttributes, 0);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Attach attributes to request
|
|
111
|
-
userAttributes = JSON.parse(userAttributes);*/
|
|
112
97
|
req['user'] = {
|
|
113
98
|
role: role,
|
|
114
|
-
userName:decoded.preferred_username
|
|
99
|
+
userName: decoded.preferred_username
|
|
115
100
|
};
|
|
116
101
|
|
|
117
|
-
// req['attributes'] = {
|
|
118
|
-
// client: clientAttributes[req.originalUrl],
|
|
119
|
-
// // user: userAttributes,
|
|
120
|
-
// };
|
|
121
|
-
|
|
122
102
|
return next();
|
|
123
|
-
} catch (error:any) {
|
|
124
|
-
// console.error('AuthMiddleware error:', error);
|
|
103
|
+
} catch (error: any) {
|
|
125
104
|
return res.status(500).json({ message: error.message });
|
|
126
105
|
}
|
|
127
106
|
}
|
|
128
107
|
|
|
129
108
|
private async clientLogin() {
|
|
130
109
|
const { keycloakUrl, realm, clientId, clientSecret } = this.options;
|
|
131
|
-
|
|
132
110
|
try {
|
|
133
111
|
const response = await axios.post(
|
|
134
112
|
`${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`,
|
|
@@ -140,187 +118,193 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
140
118
|
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
|
141
119
|
);
|
|
142
120
|
return response.data.access_token;
|
|
143
|
-
} catch (error:any) {
|
|
121
|
+
} catch (error: any) {
|
|
144
122
|
throw new Error(`Failed to obtain client token: ${error.response?.data?.error_description || error.message}`);
|
|
145
123
|
}
|
|
146
124
|
}
|
|
147
125
|
|
|
148
126
|
private async getUserDetails(username: string, token: string) {
|
|
149
127
|
const { keycloakUrl, realm } = this.options;
|
|
150
|
-
|
|
151
128
|
try {
|
|
152
129
|
const response = await axios.get(
|
|
153
130
|
`${keycloakUrl}/admin/realms/${realm}/users?username=${username}`,
|
|
154
131
|
{ headers: { Authorization: `Bearer ${token}` } }
|
|
155
132
|
);
|
|
156
133
|
return response.data[0];
|
|
157
|
-
} catch (error:any) {
|
|
134
|
+
} catch (error: any) {
|
|
158
135
|
throw new Error(`Failed to fetch user details: ${error.response?.data?.error_description || error.message}`);
|
|
159
136
|
}
|
|
160
137
|
}
|
|
161
138
|
|
|
162
139
|
private async getClientRoleAttributes(role: string, token: string) {
|
|
163
140
|
const { keycloakUrl, realm, clientUuid } = this.options;
|
|
164
|
-
|
|
165
141
|
try {
|
|
166
142
|
const response = await axios.get(
|
|
167
143
|
`${keycloakUrl}/admin/realms/${realm}/clients/${clientUuid}/roles/${role}`,
|
|
168
144
|
{ headers: { Authorization: `Bearer ${token}` } }
|
|
169
145
|
);
|
|
170
146
|
return response.data;
|
|
171
|
-
} catch (error:any) {
|
|
147
|
+
} catch (error: any) {
|
|
172
148
|
throw new Error(`Failed to fetch client role attributes: ${error.response?.data?.error_description || error.message}`);
|
|
173
149
|
}
|
|
174
150
|
}
|
|
175
151
|
|
|
176
|
-
private async checkLicenceAndValidate(realm: string, publicKey:string) {
|
|
177
|
-
try {
|
|
178
|
-
if (AuthMiddleware.licenceExpired) {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
if (AuthMiddleware.licenceValidatedUntilMs && AuthMiddleware.licenceValidatedUntilMs > Date.now()) {
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
if (AuthMiddleware.licenceValidationPromise) {
|
|
185
|
-
return await AuthMiddleware.licenceValidationPromise;
|
|
186
|
-
}
|
|
187
|
-
AuthMiddleware.licenceValidationPromise = (async () => {
|
|
188
|
-
const response:any = await this.getLicencingDetails(realm);
|
|
189
|
-
if (response.code === HttpStatusCode.InternalServerError) {
|
|
190
|
-
return false
|
|
191
|
-
}
|
|
192
|
-
else{
|
|
193
|
-
const validate = await this.validateLicence(response.data, publicKey);
|
|
194
|
-
return validate.status;
|
|
195
|
-
}
|
|
196
|
-
})();
|
|
197
|
-
try {
|
|
198
|
-
return await AuthMiddleware.licenceValidationPromise;
|
|
199
|
-
} finally {
|
|
200
|
-
AuthMiddleware.licenceValidationPromise = null;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
catch(error:any){
|
|
204
|
-
return false;
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
private async getLicencingDetails(realm: string) {
|
|
209
|
-
try{
|
|
152
|
+
private async checkLicenceAndValidate(realm: string, publicKey: string) {
|
|
153
|
+
try {
|
|
154
|
+
if (AuthMiddleware.licenceExpired) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
210
157
|
|
|
158
|
+
// 1. check the licence is available in cache if available then check the expiry
|
|
159
|
+
const cachedToken: string | undefined = await this.cacheManager.get('client_Licence_token');
|
|
160
|
+
if (cachedToken) {
|
|
161
|
+
const validate = await this.validateLicence(cachedToken, publicKey, false);
|
|
162
|
+
if (validate.status) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
// If cached token is invalid/expired, we'll try to get a new one from the server.
|
|
166
|
+
}
|
|
211
167
|
|
|
168
|
+
if (AuthMiddleware.licenceValidatedUntilMs && AuthMiddleware.licenceValidatedUntilMs > this.getUtcNowMs()) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
212
171
|
|
|
213
|
-
|
|
214
|
-
|
|
172
|
+
if (AuthMiddleware.licenceValidationPromise) {
|
|
173
|
+
return await AuthMiddleware.licenceValidationPromise;
|
|
174
|
+
}
|
|
215
175
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
code: HttpStatusCode.InternalServerError,
|
|
226
|
-
message: "InternalServer error"
|
|
227
|
-
};
|
|
176
|
+
// 2. if the licence details not available in cache get the data from another server and validate.
|
|
177
|
+
AuthMiddleware.licenceValidationPromise = (async () => {
|
|
178
|
+
const response: any = await this.getLicencingDetails(realm);
|
|
179
|
+
if (response.code === HttpStatusCode.InternalServerError) {
|
|
180
|
+
return false;
|
|
181
|
+
} else {
|
|
182
|
+
const validate = await this.validateLicence(response.data, publicKey, true);
|
|
183
|
+
return validate.status;
|
|
228
184
|
}
|
|
185
|
+
})();
|
|
229
186
|
|
|
230
|
-
|
|
187
|
+
try {
|
|
188
|
+
return await AuthMiddleware.licenceValidationPromise;
|
|
189
|
+
} finally {
|
|
190
|
+
AuthMiddleware.licenceValidationPromise = null;
|
|
191
|
+
}
|
|
192
|
+
} catch (error: any) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private async getLicencingDetails(realm: string) {
|
|
198
|
+
try {
|
|
199
|
+
const url = 'https://iiuuckued274sisadalbpf7ivu0eiblk.lambda-url.ap-south-1.on.aws/';
|
|
200
|
+
const body = { "client_name": realm };
|
|
201
|
+
const response = await axios.post(url, body);
|
|
202
|
+
if (response.status === HttpStatusCode.Ok) {
|
|
203
|
+
return {
|
|
204
|
+
code: HttpStatusCode.Ok,
|
|
205
|
+
data: response.data.token
|
|
206
|
+
};
|
|
207
|
+
} else {
|
|
208
|
+
return {
|
|
209
|
+
code: HttpStatusCode.InternalServerError,
|
|
210
|
+
message: "InternalServer error"
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
} catch (error: any) {
|
|
231
214
|
return {
|
|
232
215
|
code: HttpStatusCode.InternalServerError,
|
|
233
216
|
data: error?.response?.data || error?.message || 'Licencing service error'
|
|
234
217
|
};
|
|
235
218
|
}
|
|
236
219
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
catch(error:any){
|
|
263
|
-
this.markLicenceExpired();
|
|
264
|
-
return {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
process.
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}, 0);
|
|
311
|
-
}
|
|
220
|
+
|
|
221
|
+
private async validateLicence(lic_data: any, publicKey: any, updateCache = true) {
|
|
222
|
+
try {
|
|
223
|
+
const token: any = await this.verifyJwt(lic_data, publicKey);
|
|
224
|
+
const licEndMs = this.normalizeEpochMs(token?.lic_end);
|
|
225
|
+
|
|
226
|
+
// Reject when licence end (epoch) is missing or already in the past.
|
|
227
|
+
if (!licEndMs || licEndMs <= this.getUtcNowMs()) {
|
|
228
|
+
this.markLicenceExpired();
|
|
229
|
+
return { status: false };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 3. if the licencing server have the future expiry time wait till the expiry time
|
|
233
|
+
AuthMiddleware.licenceValidatedUntilMs = licEndMs;
|
|
234
|
+
this.scheduleLicenceShutdown(licEndMs);
|
|
235
|
+
|
|
236
|
+
if (updateCache) {
|
|
237
|
+
const ttl: any = (licEndMs - this.getUtcNowMs()) > 0 ? (licEndMs - this.getUtcNowMs()) : null
|
|
238
|
+
await this.cacheManager.set('client_Licence_token', lic_data, ttl);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
status: true,
|
|
243
|
+
|
|
244
|
+
};
|
|
245
|
+
} catch (error: any) {
|
|
246
|
+
this.markLicenceExpired();
|
|
247
|
+
return { status: false };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private markLicenceExpired() {
|
|
252
|
+
if (!AuthMiddleware.licenceExpired) {
|
|
253
|
+
AuthMiddleware.licenceExpired = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private scheduleLicenceShutdown(licEndMs: number) {
|
|
258
|
+
if (!licEndMs) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const delay = licEndMs - this.getUtcNowMs();
|
|
262
|
+
if (delay <= 0) {
|
|
263
|
+
this.markLicenceExpired();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (AuthMiddleware.licenceExpiryTimer) {
|
|
267
|
+
clearTimeout(AuthMiddleware.licenceExpiryTimer);
|
|
268
|
+
}
|
|
269
|
+
// and then shutdown for till next user request to know the user to respond and shutdown the server.
|
|
270
|
+
AuthMiddleware.licenceExpiryTimer = setTimeout(() => {
|
|
271
|
+
this.markLicenceExpired();
|
|
272
|
+
}, delay);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private stopServer() {
|
|
276
|
+
if (AuthMiddleware.shutdownInitiated) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
AuthMiddleware.shutdownInitiated = true;
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
try {
|
|
282
|
+
process.kill(process.pid, 'SIGTERM');
|
|
283
|
+
} catch {
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
if (!AuthMiddleware.shutdownTimer) {
|
|
287
|
+
AuthMiddleware.shutdownTimer = setTimeout(() => {
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}, 5000);
|
|
290
|
+
}
|
|
291
|
+
}, 0);
|
|
292
|
+
}
|
|
312
293
|
|
|
313
294
|
private normalizeEpochMs(epoch: number): number | null {
|
|
314
295
|
if (!epoch || Number.isNaN(epoch)) {
|
|
315
296
|
return null;
|
|
316
297
|
}
|
|
317
|
-
// Accept seconds or milliseconds since Unix epoch and normalize to ms for Date.now() comparison.
|
|
318
298
|
if (epoch < 1_000_000_000_000) {
|
|
319
299
|
return epoch * 1000;
|
|
320
300
|
}
|
|
321
301
|
return epoch;
|
|
322
302
|
}
|
|
323
303
|
|
|
304
|
+
private getUtcNowMs() {
|
|
305
|
+
return Date.now();
|
|
306
|
+
}
|
|
307
|
+
|
|
324
308
|
private verifyJwt(token: string, publicKey: string) {
|
|
325
309
|
const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
|
|
326
310
|
return jwt.verify(token, publicKeys, { algorithms: ['RS256'] });
|