dms-middleware-auth 1.1.0 → 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 +31 -43
- package/package.json +2 -2
- package/src/auth.middleware.ts +86 -103
|
@@ -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
|
@@ -111,29 +111,13 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
// Cache interface details
|
|
115
|
-
/*const userName = decoded.preferred_username;
|
|
116
|
-
let userAttributes: any = await this.cacheManager.get(userName);
|
|
117
|
-
if (!userAttributes) {
|
|
118
|
-
userAttributes = await this.getUserDetails(userName, clientToken);
|
|
119
|
-
userAttributes = JSON.stringify(userAttributes.attributes);
|
|
120
|
-
await this.cacheManager.set(userName, userAttributes, 0);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Attach attributes to request
|
|
124
|
-
userAttributes = JSON.parse(userAttributes);*/
|
|
125
114
|
req['user'] = {
|
|
126
115
|
role: role,
|
|
127
116
|
userName: decoded.preferred_username
|
|
128
117
|
};
|
|
129
|
-
// req['attributes'] = {
|
|
130
|
-
// client: clientAttributes[req.originalUrl],
|
|
131
|
-
// // user: userAttributes,
|
|
132
|
-
// };
|
|
133
118
|
return next();
|
|
134
119
|
}
|
|
135
120
|
catch (error) {
|
|
136
|
-
// console.error('AuthMiddleware error:', error);
|
|
137
121
|
return res.status(500).json({ message: error.message });
|
|
138
122
|
}
|
|
139
123
|
}
|
|
@@ -176,19 +160,29 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
176
160
|
if (AuthMiddleware_1.licenceExpired) {
|
|
177
161
|
return false;
|
|
178
162
|
}
|
|
179
|
-
|
|
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()) {
|
|
180
173
|
return true;
|
|
181
174
|
}
|
|
182
175
|
if (AuthMiddleware_1.licenceValidationPromise) {
|
|
183
176
|
return await AuthMiddleware_1.licenceValidationPromise;
|
|
184
177
|
}
|
|
178
|
+
// 2. if the licence details not available in cache get the data from another server and validate.
|
|
185
179
|
AuthMiddleware_1.licenceValidationPromise = (async () => {
|
|
186
180
|
const response = await this.getLicencingDetails(realm);
|
|
187
181
|
if (response.code === axios_1.HttpStatusCode.InternalServerError) {
|
|
188
182
|
return false;
|
|
189
183
|
}
|
|
190
184
|
else {
|
|
191
|
-
const validate = await this.validateLicence(response.data, publicKey);
|
|
185
|
+
const validate = await this.validateLicence(response.data, publicKey, true);
|
|
192
186
|
return validate.status;
|
|
193
187
|
}
|
|
194
188
|
})();
|
|
@@ -228,57 +222,49 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
228
222
|
};
|
|
229
223
|
}
|
|
230
224
|
}
|
|
231
|
-
async validateLicence(lic_data, publicKey) {
|
|
225
|
+
async validateLicence(lic_data, publicKey, updateCache = true) {
|
|
232
226
|
try {
|
|
233
227
|
const token = await this.verifyJwt(lic_data, publicKey);
|
|
234
228
|
const licEndMs = this.normalizeEpochMs(token?.lic_end);
|
|
235
229
|
// Reject when licence end (epoch) is missing or already in the past.
|
|
236
|
-
if (!licEndMs) {
|
|
230
|
+
if (!licEndMs || licEndMs <= this.getUtcNowMs()) {
|
|
237
231
|
this.markLicenceExpired();
|
|
238
|
-
return {
|
|
239
|
-
status: false
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
if (licEndMs <= Date.now()) {
|
|
243
|
-
this.markLicenceExpired();
|
|
244
|
-
return {
|
|
245
|
-
status: false
|
|
246
|
-
};
|
|
232
|
+
return { status: false };
|
|
247
233
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
};
|
|
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);
|
|
255
240
|
}
|
|
241
|
+
return {
|
|
242
|
+
status: true,
|
|
243
|
+
};
|
|
256
244
|
}
|
|
257
245
|
catch (error) {
|
|
258
246
|
this.markLicenceExpired();
|
|
259
|
-
return {
|
|
260
|
-
status: false
|
|
261
|
-
};
|
|
247
|
+
return { status: false };
|
|
262
248
|
}
|
|
263
249
|
}
|
|
264
250
|
markLicenceExpired() {
|
|
265
251
|
if (!AuthMiddleware_1.licenceExpired) {
|
|
266
252
|
AuthMiddleware_1.licenceExpired = true;
|
|
267
253
|
}
|
|
268
|
-
// this.stopServer();
|
|
269
254
|
}
|
|
270
255
|
scheduleLicenceShutdown(licEndMs) {
|
|
271
256
|
if (!licEndMs) {
|
|
272
257
|
return;
|
|
273
258
|
}
|
|
274
|
-
const delay = licEndMs -
|
|
259
|
+
const delay = licEndMs - this.getUtcNowMs();
|
|
275
260
|
if (delay <= 0) {
|
|
276
261
|
this.markLicenceExpired();
|
|
277
262
|
return;
|
|
278
263
|
}
|
|
279
264
|
if (AuthMiddleware_1.licenceExpiryTimer) {
|
|
280
|
-
|
|
265
|
+
clearTimeout(AuthMiddleware_1.licenceExpiryTimer);
|
|
281
266
|
}
|
|
267
|
+
// and then shutdown for till next user request to know the user to respond and shutdown the server.
|
|
282
268
|
AuthMiddleware_1.licenceExpiryTimer = setTimeout(() => {
|
|
283
269
|
this.markLicenceExpired();
|
|
284
270
|
}, delay);
|
|
@@ -306,12 +292,14 @@ let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
|
306
292
|
if (!epoch || Number.isNaN(epoch)) {
|
|
307
293
|
return null;
|
|
308
294
|
}
|
|
309
|
-
// Accept seconds or milliseconds since Unix epoch and normalize to ms for Date.now() comparison.
|
|
310
295
|
if (epoch < 1_000_000_000_000) {
|
|
311
296
|
return epoch * 1000;
|
|
312
297
|
}
|
|
313
298
|
return epoch;
|
|
314
299
|
}
|
|
300
|
+
getUtcNowMs() {
|
|
301
|
+
return Date.now();
|
|
302
|
+
}
|
|
315
303
|
verifyJwt(token, publicKey) {
|
|
316
304
|
const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
|
|
317
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.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
|
@@ -5,6 +5,7 @@ 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;
|
|
@@ -28,51 +29,49 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
28
29
|
constructor(
|
|
29
30
|
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
|
30
31
|
@Inject('AUTH_MIDDLEWARE_OPTIONS') private readonly options: AuthMiddlewareOptions
|
|
31
|
-
) {}
|
|
32
|
+
) { }
|
|
32
33
|
|
|
33
34
|
async onModuleInit() {
|
|
34
35
|
const { publicKey, realm } = this.options;
|
|
35
36
|
const isValid = await this.checkLicenceAndValidate(realm, publicKey);
|
|
36
|
-
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
async use(req: any, res: Response, next: NextFunction) {
|
|
40
|
-
|
|
41
|
-
|
|
42
40
|
const { publicKey, clientId, realm, bypassURL } = this.options;
|
|
43
41
|
|
|
44
42
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
if (AuthMiddleware.licenceExpired) {
|
|
47
44
|
this.stopServer();
|
|
48
|
-
|
|
49
45
|
return res.status(HttpStatusCode.Forbidden).json({ message: AuthMiddleware.licenceExpiredMessage });
|
|
50
46
|
}
|
|
51
47
|
|
|
52
48
|
if (!AuthMiddleware.licenceValidatedUntilMs) {
|
|
53
49
|
return res.status(HttpStatusCode.Forbidden).json({ message: AuthMiddleware.licenceExpiredMessage });
|
|
54
50
|
}
|
|
51
|
+
|
|
55
52
|
if (req.originalUrl == bypassURL) {
|
|
56
53
|
return next();
|
|
57
54
|
}
|
|
58
|
-
const authHeader = req.headers['authorization'];
|
|
59
55
|
|
|
56
|
+
const authHeader = req.headers['authorization'];
|
|
60
57
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
61
58
|
return res.status(401).json({ message: 'Bearer token required' });
|
|
62
59
|
}
|
|
63
|
-
|
|
60
|
+
|
|
61
|
+
const decoded: any = this.decodeAccessToken(authHeader, publicKey);
|
|
62
|
+
|
|
64
63
|
// Cache the client token
|
|
65
64
|
let clientToken: any = await this.cacheManager.get('client_access_token');
|
|
66
65
|
if (!clientToken) {
|
|
67
66
|
clientToken = await this.clientLogin();
|
|
68
67
|
const decodedToken: any = jwt.decode(clientToken);
|
|
69
68
|
const ttl = (decodedToken.exp - Math.floor(Date.now() / 1000)) * 1000;
|
|
70
|
-
await this.cacheManager.set('client_access_token', clientToken, ttl
|
|
69
|
+
await this.cacheManager.set('client_access_token', clientToken, ttl);
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
// Cache client role attributes
|
|
74
73
|
const role = decoded.resource_access[clientId]?.roles?.[0];
|
|
75
|
-
const clientRoleName = `${clientId+role}`;
|
|
74
|
+
const clientRoleName = `${clientId + role}`;
|
|
76
75
|
let clientAttributes: any = await this.cacheManager.get(clientRoleName);
|
|
77
76
|
if (!clientAttributes) {
|
|
78
77
|
clientAttributes = await this.getClientRoleAttributes(role, clientToken);
|
|
@@ -82,54 +81,32 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
82
81
|
|
|
83
82
|
// Check route access
|
|
84
83
|
clientAttributes = JSON.parse(clientAttributes);
|
|
85
|
-
|
|
86
84
|
if (!clientAttributes[req.originalUrl]) {
|
|
87
|
-
|
|
88
85
|
return res.status(403).json({ message: 'Access denied for this route' });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (apiPermission?.params === "true")
|
|
93
|
-
{
|
|
86
|
+
} else {
|
|
87
|
+
const apiPermission = JSON.parse(clientAttributes[req.originalUrl]);
|
|
88
|
+
if (apiPermission?.params === "true") {
|
|
94
89
|
const event = req?.body?.event;
|
|
95
90
|
const url = req?.originalUrl + `?action=${event}`;
|
|
96
|
-
if (!clientAttributes[url]){
|
|
91
|
+
if (!clientAttributes[url]) {
|
|
97
92
|
return res.status(403).json({ message: 'Access denied for event' });
|
|
98
93
|
}
|
|
99
94
|
}
|
|
100
95
|
}
|
|
101
96
|
|
|
102
|
-
// Cache interface details
|
|
103
|
-
/*const userName = decoded.preferred_username;
|
|
104
|
-
let userAttributes: any = await this.cacheManager.get(userName);
|
|
105
|
-
if (!userAttributes) {
|
|
106
|
-
userAttributes = await this.getUserDetails(userName, clientToken);
|
|
107
|
-
userAttributes = JSON.stringify(userAttributes.attributes);
|
|
108
|
-
await this.cacheManager.set(userName, userAttributes, 0);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Attach attributes to request
|
|
112
|
-
userAttributes = JSON.parse(userAttributes);*/
|
|
113
97
|
req['user'] = {
|
|
114
98
|
role: role,
|
|
115
|
-
userName:decoded.preferred_username
|
|
99
|
+
userName: decoded.preferred_username
|
|
116
100
|
};
|
|
117
101
|
|
|
118
|
-
// req['attributes'] = {
|
|
119
|
-
// client: clientAttributes[req.originalUrl],
|
|
120
|
-
// // user: userAttributes,
|
|
121
|
-
// };
|
|
122
|
-
|
|
123
102
|
return next();
|
|
124
|
-
} catch (error:any) {
|
|
125
|
-
// console.error('AuthMiddleware error:', error);
|
|
103
|
+
} catch (error: any) {
|
|
126
104
|
return res.status(500).json({ message: error.message });
|
|
127
105
|
}
|
|
128
106
|
}
|
|
129
107
|
|
|
130
108
|
private async clientLogin() {
|
|
131
109
|
const { keycloakUrl, realm, clientId, clientSecret } = this.options;
|
|
132
|
-
|
|
133
110
|
try {
|
|
134
111
|
const response = await axios.post(
|
|
135
112
|
`${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`,
|
|
@@ -141,130 +118,133 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
141
118
|
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
|
142
119
|
);
|
|
143
120
|
return response.data.access_token;
|
|
144
|
-
} catch (error:any) {
|
|
121
|
+
} catch (error: any) {
|
|
145
122
|
throw new Error(`Failed to obtain client token: ${error.response?.data?.error_description || error.message}`);
|
|
146
123
|
}
|
|
147
124
|
}
|
|
148
125
|
|
|
149
126
|
private async getUserDetails(username: string, token: string) {
|
|
150
127
|
const { keycloakUrl, realm } = this.options;
|
|
151
|
-
|
|
152
128
|
try {
|
|
153
129
|
const response = await axios.get(
|
|
154
130
|
`${keycloakUrl}/admin/realms/${realm}/users?username=${username}`,
|
|
155
131
|
{ headers: { Authorization: `Bearer ${token}` } }
|
|
156
132
|
);
|
|
157
133
|
return response.data[0];
|
|
158
|
-
} catch (error:any) {
|
|
134
|
+
} catch (error: any) {
|
|
159
135
|
throw new Error(`Failed to fetch user details: ${error.response?.data?.error_description || error.message}`);
|
|
160
136
|
}
|
|
161
137
|
}
|
|
162
138
|
|
|
163
139
|
private async getClientRoleAttributes(role: string, token: string) {
|
|
164
140
|
const { keycloakUrl, realm, clientUuid } = this.options;
|
|
165
|
-
|
|
166
141
|
try {
|
|
167
142
|
const response = await axios.get(
|
|
168
143
|
`${keycloakUrl}/admin/realms/${realm}/clients/${clientUuid}/roles/${role}`,
|
|
169
144
|
{ headers: { Authorization: `Bearer ${token}` } }
|
|
170
145
|
);
|
|
171
146
|
return response.data;
|
|
172
|
-
} catch (error:any) {
|
|
147
|
+
} catch (error: any) {
|
|
173
148
|
throw new Error(`Failed to fetch client role attributes: ${error.response?.data?.error_description || error.message}`);
|
|
174
149
|
}
|
|
175
150
|
}
|
|
176
151
|
|
|
177
|
-
private async checkLicenceAndValidate(realm: string, publicKey:string) {
|
|
152
|
+
private async checkLicenceAndValidate(realm: string, publicKey: string) {
|
|
178
153
|
try {
|
|
179
154
|
if (AuthMiddleware.licenceExpired) {
|
|
180
155
|
return false;
|
|
181
156
|
}
|
|
182
|
-
|
|
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
|
+
}
|
|
167
|
+
|
|
168
|
+
if (AuthMiddleware.licenceValidatedUntilMs && AuthMiddleware.licenceValidatedUntilMs > this.getUtcNowMs()) {
|
|
183
169
|
return true;
|
|
184
170
|
}
|
|
171
|
+
|
|
185
172
|
if (AuthMiddleware.licenceValidationPromise) {
|
|
186
173
|
return await AuthMiddleware.licenceValidationPromise;
|
|
187
174
|
}
|
|
175
|
+
|
|
176
|
+
// 2. if the licence details not available in cache get the data from another server and validate.
|
|
188
177
|
AuthMiddleware.licenceValidationPromise = (async () => {
|
|
189
|
-
const response:any = await
|
|
178
|
+
const response: any = await this.getLicencingDetails(realm);
|
|
190
179
|
if (response.code === HttpStatusCode.InternalServerError) {
|
|
191
|
-
return false
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const validate = await this.validateLicence(response.data, publicKey);
|
|
180
|
+
return false;
|
|
181
|
+
} else {
|
|
182
|
+
const validate = await this.validateLicence(response.data, publicKey, true);
|
|
195
183
|
return validate.status;
|
|
196
184
|
}
|
|
197
185
|
})();
|
|
186
|
+
|
|
198
187
|
try {
|
|
199
188
|
return await AuthMiddleware.licenceValidationPromise;
|
|
200
189
|
} finally {
|
|
201
190
|
AuthMiddleware.licenceValidationPromise = null;
|
|
202
191
|
}
|
|
203
|
-
}
|
|
204
|
-
catch(error:any){
|
|
192
|
+
} catch (error: any) {
|
|
205
193
|
return false;
|
|
206
|
-
|
|
207
194
|
}
|
|
208
195
|
}
|
|
209
|
-
private async getLicencingDetails(realm: string) {
|
|
210
|
-
try{
|
|
211
196
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
}catch(error:any){
|
|
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) {
|
|
232
214
|
return {
|
|
233
215
|
code: HttpStatusCode.InternalServerError,
|
|
234
216
|
data: error?.response?.data || error?.message || 'Licencing service error'
|
|
235
217
|
};
|
|
236
218
|
}
|
|
237
219
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
220
|
+
|
|
221
|
+
private async validateLicence(lic_data: any, publicKey: any, updateCache = true) {
|
|
222
|
+
try {
|
|
223
|
+
const token: any = await this.verifyJwt(lic_data, publicKey);
|
|
241
224
|
const licEndMs = this.normalizeEpochMs(token?.lic_end);
|
|
225
|
+
|
|
242
226
|
// Reject when licence end (epoch) is missing or already in the past.
|
|
243
|
-
if (!licEndMs) {
|
|
227
|
+
if (!licEndMs || licEndMs <= this.getUtcNowMs()) {
|
|
244
228
|
this.markLicenceExpired();
|
|
245
|
-
return{
|
|
246
|
-
status: false
|
|
247
|
-
}
|
|
229
|
+
return { status: false };
|
|
248
230
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.
|
|
257
|
-
return {
|
|
258
|
-
status: true,
|
|
259
|
-
ttl: (licEndMs - Date.now()) > 0 ? (licEndMs - Date.now()) : null
|
|
260
|
-
}
|
|
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);
|
|
261
239
|
}
|
|
262
|
-
|
|
263
|
-
catch(error:any){
|
|
264
|
-
this.markLicenceExpired();
|
|
240
|
+
|
|
265
241
|
return {
|
|
266
|
-
status:
|
|
242
|
+
status: true,
|
|
243
|
+
|
|
267
244
|
};
|
|
245
|
+
} catch (error: any) {
|
|
246
|
+
this.markLicenceExpired();
|
|
247
|
+
return { status: false };
|
|
268
248
|
}
|
|
269
249
|
}
|
|
270
250
|
|
|
@@ -272,21 +252,21 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
272
252
|
if (!AuthMiddleware.licenceExpired) {
|
|
273
253
|
AuthMiddleware.licenceExpired = true;
|
|
274
254
|
}
|
|
275
|
-
// this.stopServer();
|
|
276
255
|
}
|
|
277
256
|
|
|
278
257
|
private scheduleLicenceShutdown(licEndMs: number) {
|
|
279
258
|
if (!licEndMs) {
|
|
280
259
|
return;
|
|
281
260
|
}
|
|
282
|
-
const delay = licEndMs -
|
|
261
|
+
const delay = licEndMs - this.getUtcNowMs();
|
|
283
262
|
if (delay <= 0) {
|
|
284
263
|
this.markLicenceExpired();
|
|
285
264
|
return;
|
|
286
265
|
}
|
|
287
266
|
if (AuthMiddleware.licenceExpiryTimer) {
|
|
288
|
-
|
|
267
|
+
clearTimeout(AuthMiddleware.licenceExpiryTimer);
|
|
289
268
|
}
|
|
269
|
+
// and then shutdown for till next user request to know the user to respond and shutdown the server.
|
|
290
270
|
AuthMiddleware.licenceExpiryTimer = setTimeout(() => {
|
|
291
271
|
this.markLicenceExpired();
|
|
292
272
|
}, delay);
|
|
@@ -315,13 +295,16 @@ export class AuthMiddleware implements NestMiddleware, OnModuleInit {
|
|
|
315
295
|
if (!epoch || Number.isNaN(epoch)) {
|
|
316
296
|
return null;
|
|
317
297
|
}
|
|
318
|
-
// Accept seconds or milliseconds since Unix epoch and normalize to ms for Date.now() comparison.
|
|
319
298
|
if (epoch < 1_000_000_000_000) {
|
|
320
299
|
return epoch * 1000;
|
|
321
300
|
}
|
|
322
301
|
return epoch;
|
|
323
302
|
}
|
|
324
303
|
|
|
304
|
+
private getUtcNowMs() {
|
|
305
|
+
return Date.now();
|
|
306
|
+
}
|
|
307
|
+
|
|
325
308
|
private verifyJwt(token: string, publicKey: string) {
|
|
326
309
|
const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
|
|
327
310
|
return jwt.verify(token, publicKeys, { algorithms: ['RS256'] });
|