dms-middleware-auth 1.0.7 → 1.0.9
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 +131 -0
- package/dist/auth.middleware.d.ts +12 -0
- package/dist/auth.middleware.js +138 -15
- package/jest.config.js +14 -0
- package/package.json +4 -1
- package/src/auth.middleware.ts +151 -16
- package/tsconfig.jest.json +7 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { AuthMiddleware } from '../src/auth.middleware';
|
|
2
|
+
import { HttpStatusCode } from 'axios';
|
|
3
|
+
|
|
4
|
+
const baseOptions = {
|
|
5
|
+
publicKey: 'PUBLIC_KEY',
|
|
6
|
+
keycloakUrl: '',
|
|
7
|
+
realm: '',
|
|
8
|
+
clientId: 'client',
|
|
9
|
+
clientSecret: '',
|
|
10
|
+
clientUuid: '',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const createCacheMock = (initial: Record<string, any> = {}) => {
|
|
14
|
+
const store = { ...initial };
|
|
15
|
+
return {
|
|
16
|
+
get: jest.fn(async (key: string) => store[key]),
|
|
17
|
+
set: jest.fn(async (key: string, value: any) => {
|
|
18
|
+
store[key] = value;
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const createRes = () => {
|
|
24
|
+
const res: any = {
|
|
25
|
+
status: jest.fn().mockReturnThis(),
|
|
26
|
+
json: jest.fn(),
|
|
27
|
+
};
|
|
28
|
+
return res;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const createReq = (override: Record<string, any> = {}) =>
|
|
32
|
+
({
|
|
33
|
+
headers: { authorization: 'Bearer access-token' },
|
|
34
|
+
originalUrl: '/route',
|
|
35
|
+
body: {},
|
|
36
|
+
...override,
|
|
37
|
+
} as any);
|
|
38
|
+
|
|
39
|
+
describe('AuthMiddleware licensing', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
jest.restoreAllMocks();
|
|
42
|
+
jest.clearAllMocks();
|
|
43
|
+
jest.useRealTimers();
|
|
44
|
+
// Reset static flags/timers between tests
|
|
45
|
+
const cls: any = AuthMiddleware;
|
|
46
|
+
if (cls.shutdownTimer) {
|
|
47
|
+
clearTimeout(cls.shutdownTimer);
|
|
48
|
+
}
|
|
49
|
+
cls.licenceExpired = false;
|
|
50
|
+
cls.shutdownTimer = null;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('allows requests when licence is valid (supports seconds epoch)', async () => {
|
|
54
|
+
const cache = createCacheMock({ client_access_token: 'cached-client-token' });
|
|
55
|
+
const middleware = new AuthMiddleware(cache as any, baseOptions as any);
|
|
56
|
+
|
|
57
|
+
jest.spyOn(middleware as any, 'getLicencingDetails').mockResolvedValue({
|
|
58
|
+
code: HttpStatusCode.Ok,
|
|
59
|
+
data: 'licence-token',
|
|
60
|
+
});
|
|
61
|
+
jest.spyOn(middleware as any, 'verifyJwt').mockReturnValue({
|
|
62
|
+
lic_end: Math.floor(Date.now() / 1000) + 60, // seconds epoch
|
|
63
|
+
});
|
|
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
|
+
|
|
78
|
+
await middleware.use(req, res as any, next);
|
|
79
|
+
|
|
80
|
+
expect(res.status).not.toHaveBeenCalled();
|
|
81
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(cache.set).toHaveBeenCalledWith('client_Licence_token', 'licence-token');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('rejects and schedules shutdown when licence is expired', async () => {
|
|
86
|
+
jest.useFakeTimers();
|
|
87
|
+
const cache = createCacheMock({ client_access_token: 'cached-client-token' });
|
|
88
|
+
const middleware = new AuthMiddleware(cache as any, baseOptions as any);
|
|
89
|
+
|
|
90
|
+
jest.spyOn(middleware as any, 'getLicencingDetails').mockResolvedValue({
|
|
91
|
+
code: HttpStatusCode.Ok,
|
|
92
|
+
data: 'licence-token',
|
|
93
|
+
});
|
|
94
|
+
jest.spyOn(middleware as any, 'verifyJwt').mockReturnValue({
|
|
95
|
+
lic_end: Date.now() - 1, // already expired (ms epoch)
|
|
96
|
+
});
|
|
97
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as any);
|
|
98
|
+
|
|
99
|
+
const req = createReq();
|
|
100
|
+
const res = createRes();
|
|
101
|
+
const next = jest.fn();
|
|
102
|
+
|
|
103
|
+
await middleware.use(req, res as any, next);
|
|
104
|
+
|
|
105
|
+
expect(res.status).toHaveBeenCalledWith(HttpStatusCode.Forbidden);
|
|
106
|
+
expect(res.json).toHaveBeenCalled();
|
|
107
|
+
expect(next).not.toHaveBeenCalled();
|
|
108
|
+
expect((AuthMiddleware as any).licenceExpired).toBe(true);
|
|
109
|
+
|
|
110
|
+
// advance time to trigger shutdown timer
|
|
111
|
+
jest.advanceTimersByTime(2 * 60 * 1000);
|
|
112
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('immediately rejects when already marked expired', async () => {
|
|
116
|
+
const cache = createCacheMock();
|
|
117
|
+
(AuthMiddleware as any).licenceExpired = true;
|
|
118
|
+
const middleware = new AuthMiddleware(cache as any, baseOptions as any);
|
|
119
|
+
const checkSpy = jest.spyOn(middleware as any, 'checkLicenceAndValidate');
|
|
120
|
+
|
|
121
|
+
const req = createReq();
|
|
122
|
+
const res = createRes();
|
|
123
|
+
const next = jest.fn();
|
|
124
|
+
|
|
125
|
+
await middleware.use(req, res as any, next);
|
|
126
|
+
|
|
127
|
+
expect(checkSpy).not.toHaveBeenCalled();
|
|
128
|
+
expect(res.status).toHaveBeenCalledWith(HttpStatusCode.Forbidden);
|
|
129
|
+
expect(next).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -8,14 +8,26 @@ interface AuthMiddlewareOptions {
|
|
|
8
8
|
clientId: string;
|
|
9
9
|
clientSecret: string;
|
|
10
10
|
clientUuid: string;
|
|
11
|
+
bypassURL: string;
|
|
11
12
|
}
|
|
12
13
|
export declare class AuthMiddleware implements NestMiddleware {
|
|
13
14
|
private cacheManager;
|
|
14
15
|
private readonly options;
|
|
16
|
+
private static licenceExpired;
|
|
17
|
+
private static shutdownTimer;
|
|
18
|
+
private static readonly licenceExpiredMessage;
|
|
15
19
|
constructor(cacheManager: Cache, options: AuthMiddlewareOptions);
|
|
16
20
|
use(req: any, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
|
|
17
21
|
private clientLogin;
|
|
18
22
|
private getUserDetails;
|
|
19
23
|
private getClientRoleAttributes;
|
|
24
|
+
private checkLicenceAndValidate;
|
|
25
|
+
private getLicencingDetails;
|
|
26
|
+
private validateLicence;
|
|
27
|
+
private markLicenceExpired;
|
|
28
|
+
private normalizeEpochMs;
|
|
29
|
+
private verifyJwt;
|
|
30
|
+
private decodeAccessToken;
|
|
31
|
+
private extractBearerToken;
|
|
20
32
|
}
|
|
21
33
|
export {};
|
package/dist/auth.middleware.js
CHANGED
|
@@ -44,30 +44,38 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
44
44
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
45
45
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
46
46
|
};
|
|
47
|
-
var
|
|
48
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
|
-
};
|
|
47
|
+
var AuthMiddleware_1;
|
|
50
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
49
|
exports.AuthMiddleware = void 0;
|
|
52
50
|
const common_1 = require("@nestjs/common");
|
|
53
51
|
const jwt = __importStar(require("jsonwebtoken"));
|
|
54
|
-
const axios_1 =
|
|
52
|
+
const axios_1 = __importStar(require("axios"));
|
|
55
53
|
const cache_manager_1 = require("@nestjs/cache-manager");
|
|
56
|
-
|
|
54
|
+
const process = __importStar(require("node:process"));
|
|
55
|
+
let AuthMiddleware = AuthMiddleware_1 = class AuthMiddleware {
|
|
57
56
|
constructor(cacheManager, options) {
|
|
58
57
|
this.cacheManager = cacheManager;
|
|
59
58
|
this.options = options;
|
|
60
59
|
}
|
|
61
60
|
async use(req, res, next) {
|
|
62
|
-
const
|
|
63
|
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
64
|
-
return res.status(401).json({ message: 'Bearer token required' });
|
|
65
|
-
}
|
|
66
|
-
const { publicKey, clientId } = this.options;
|
|
61
|
+
const { publicKey, clientId, realm, bypassURL } = this.options;
|
|
67
62
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
if (AuthMiddleware_1.licenceExpired) {
|
|
64
|
+
return res.status(axios_1.HttpStatusCode.Forbidden).json({ message: AuthMiddleware_1.licenceExpiredMessage });
|
|
65
|
+
}
|
|
66
|
+
// fetching the licence and validating.
|
|
67
|
+
const validate_lic = await this.checkLicenceAndValidate(realm, publicKey);
|
|
68
|
+
if (!validate_lic) {
|
|
69
|
+
return res.status(axios_1.HttpStatusCode.Forbidden).json({ message: AuthMiddleware_1.licenceExpiredMessage });
|
|
70
|
+
}
|
|
71
|
+
else if (req.originalUrl == bypassURL && validate_lic) {
|
|
72
|
+
return next();
|
|
73
|
+
}
|
|
74
|
+
const authHeader = req.headers['authorization'];
|
|
75
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
76
|
+
return res.status(401).json({ message: 'Bearer token required' });
|
|
77
|
+
}
|
|
78
|
+
const decoded = this.decodeAccessToken(authHeader, publicKey);
|
|
71
79
|
// Cache the client token
|
|
72
80
|
let clientToken = await this.cacheManager.get('client_access_token');
|
|
73
81
|
if (!clientToken) {
|
|
@@ -94,7 +102,7 @@ let AuthMiddleware = class AuthMiddleware {
|
|
|
94
102
|
const apiPermission = JSON.parse(clientAttributes[req.originalUrl]);
|
|
95
103
|
if (apiPermission?.params === "true") {
|
|
96
104
|
const event = req?.body?.event;
|
|
97
|
-
const url = req?.originalUrl + `?
|
|
105
|
+
const url = req?.originalUrl + `?action=${event}`;
|
|
98
106
|
if (!clientAttributes[url]) {
|
|
99
107
|
return res.status(403).json({ message: 'Access denied for event' });
|
|
100
108
|
}
|
|
@@ -160,9 +168,124 @@ let AuthMiddleware = class AuthMiddleware {
|
|
|
160
168
|
throw new Error(`Failed to fetch client role attributes: ${error.response?.data?.error_description || error.message}`);
|
|
161
169
|
}
|
|
162
170
|
}
|
|
171
|
+
async checkLicenceAndValidate(realm, publicKey) {
|
|
172
|
+
try {
|
|
173
|
+
// Reuse cached licence when possible; short-circuit if already marked expired.
|
|
174
|
+
if (AuthMiddleware_1.licenceExpired) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
const lic_token = await this.cacheManager.get('client_Licence_token');
|
|
178
|
+
if (!lic_token) {
|
|
179
|
+
const response = await this.getLicencingDetails(realm);
|
|
180
|
+
if (response.code === axios_1.HttpStatusCode.InternalServerError) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
const validate = await this.validateLicence(response.data, publicKey);
|
|
185
|
+
if (validate.status && validate.ttl) {
|
|
186
|
+
await this.cacheManager.set('client_Licence_token', response.data, validate.ttl);
|
|
187
|
+
}
|
|
188
|
+
return validate.status;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
return await this.validateLicence(lic_token, publicKey);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async getLicencingDetails(realm) {
|
|
200
|
+
try {
|
|
201
|
+
const url = 'https://iiuuckued274sisadalbpf7ivu0eiblk.lambda-url.ap-south-1.on.aws/';
|
|
202
|
+
const body = { "client_name": realm };
|
|
203
|
+
const response = await axios_1.default.post(url, body);
|
|
204
|
+
if (response.status === axios_1.HttpStatusCode.Ok) {
|
|
205
|
+
return {
|
|
206
|
+
code: axios_1.HttpStatusCode.Ok,
|
|
207
|
+
data: response.data.token
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
return {
|
|
212
|
+
code: axios_1.HttpStatusCode.InternalServerError,
|
|
213
|
+
message: "InternalServer error"
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
return {
|
|
219
|
+
code: axios_1.HttpStatusCode.InternalServerError,
|
|
220
|
+
data: error?.response?.data || error?.message || 'Licencing service error'
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async validateLicence(lic_data, publicKey) {
|
|
225
|
+
try {
|
|
226
|
+
const token = await this.verifyJwt(lic_data, publicKey);
|
|
227
|
+
const licEndMs = this.normalizeEpochMs(token?.lic_end);
|
|
228
|
+
// Reject when licence end (epoch) is missing or already in the past.
|
|
229
|
+
if (!licEndMs) {
|
|
230
|
+
return {
|
|
231
|
+
status: false
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (licEndMs <= Date.now()) {
|
|
235
|
+
this.markLicenceExpired();
|
|
236
|
+
return {
|
|
237
|
+
status: false
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
return {
|
|
242
|
+
status: true,
|
|
243
|
+
ttl: (licEndMs - Date.now()) > 0 ? (licEndMs - Date.now()) : null
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
this.markLicenceExpired();
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
markLicenceExpired() {
|
|
253
|
+
if (!AuthMiddleware_1.licenceExpired) {
|
|
254
|
+
AuthMiddleware_1.licenceExpired = true;
|
|
255
|
+
}
|
|
256
|
+
if (!AuthMiddleware_1.shutdownTimer) {
|
|
257
|
+
AuthMiddleware_1.shutdownTimer = setTimeout(() => {
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}, 2 * 60 * 1000); // waiting two minutes after shutdowning server.
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
normalizeEpochMs(epoch) {
|
|
263
|
+
if (!epoch || Number.isNaN(epoch)) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
// Accept seconds or milliseconds since Unix epoch and normalize to ms for Date.now() comparison.
|
|
267
|
+
if (epoch < 1_000_000_000_000) {
|
|
268
|
+
return epoch * 1000;
|
|
269
|
+
}
|
|
270
|
+
return epoch;
|
|
271
|
+
}
|
|
272
|
+
verifyJwt(token, publicKey) {
|
|
273
|
+
const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
|
|
274
|
+
return jwt.verify(token, publicKeys, { algorithms: ['RS256'] });
|
|
275
|
+
}
|
|
276
|
+
decodeAccessToken(authHeader, publicKey) {
|
|
277
|
+
const token = this.extractBearerToken(authHeader);
|
|
278
|
+
return this.verifyJwt(token, publicKey);
|
|
279
|
+
}
|
|
280
|
+
extractBearerToken(authHeader) {
|
|
281
|
+
return authHeader.split(' ')[1];
|
|
282
|
+
}
|
|
163
283
|
};
|
|
164
284
|
exports.AuthMiddleware = AuthMiddleware;
|
|
165
|
-
|
|
285
|
+
AuthMiddleware.licenceExpired = false;
|
|
286
|
+
AuthMiddleware.shutdownTimer = null;
|
|
287
|
+
AuthMiddleware.licenceExpiredMessage = "server Licence is expired!. please renew";
|
|
288
|
+
exports.AuthMiddleware = AuthMiddleware = AuthMiddleware_1 = __decorate([
|
|
166
289
|
(0, common_1.Injectable)(),
|
|
167
290
|
__param(0, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
|
|
168
291
|
__param(1, (0, common_1.Inject)('AUTH_MIDDLEWARE_OPTIONS')),
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
preset: 'ts-jest',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
testMatch: ['**/__tests__/**/*.spec.ts'],
|
|
6
|
+
clearMocks: true,
|
|
7
|
+
roots: ['<rootDir>'],
|
|
8
|
+
moduleFileExtensions: ['ts', 'js', 'json'],
|
|
9
|
+
globals: {
|
|
10
|
+
'ts-jest': {
|
|
11
|
+
tsconfig: '<rootDir>/tsconfig.jest.json',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dms-middleware-auth",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
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",
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/axios": "^0.14.4",
|
|
25
25
|
"@types/express": "^5.0.0",
|
|
26
|
+
"@types/jest": "^29.5.14",
|
|
26
27
|
"@types/jsonwebtoken": "^9.0.7",
|
|
28
|
+
"jest": "^29.7.0",
|
|
29
|
+
"ts-jest": "^29.3.4",
|
|
27
30
|
"typescript": "^5.7.2"
|
|
28
31
|
}
|
|
29
32
|
}
|
package/src/auth.middleware.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Inject, Injectable, NestMiddleware
|
|
2
|
-
import {
|
|
1
|
+
import { Inject, Injectable, NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import { Response, NextFunction } from 'express';
|
|
3
3
|
import * as jwt from 'jsonwebtoken';
|
|
4
|
-
import axios from 'axios';
|
|
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
|
interface AuthMiddlewareOptions {
|
|
9
9
|
publicKey: string;
|
|
10
10
|
keycloakUrl: string;
|
|
@@ -12,38 +12,50 @@ interface AuthMiddlewareOptions {
|
|
|
12
12
|
clientId: string;
|
|
13
13
|
clientSecret: string;
|
|
14
14
|
clientUuid: string;
|
|
15
|
+
bypassURL: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
@Injectable()
|
|
18
19
|
export class AuthMiddleware implements NestMiddleware {
|
|
20
|
+
private static licenceExpired = false;
|
|
21
|
+
private static shutdownTimer: NodeJS.Timeout | null = null;
|
|
22
|
+
private static readonly licenceExpiredMessage = "server Licence is expired!. please renew";
|
|
23
|
+
|
|
19
24
|
constructor(
|
|
20
25
|
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
|
21
26
|
@Inject('AUTH_MIDDLEWARE_OPTIONS') private readonly options: AuthMiddlewareOptions
|
|
22
27
|
) {}
|
|
23
28
|
|
|
24
29
|
async use(req: any, res: Response, next: NextFunction) {
|
|
25
|
-
const authHeader = req.headers['authorization'];
|
|
26
30
|
|
|
27
|
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
28
|
-
return res.status(401).json({ message: 'Bearer token required' });
|
|
29
|
-
}
|
|
30
31
|
|
|
31
|
-
const { publicKey, clientId } = this.options;
|
|
32
|
+
const { publicKey, clientId, realm, bypassURL } = this.options;
|
|
32
33
|
|
|
33
34
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
if (AuthMiddleware.licenceExpired) {
|
|
36
|
+
return res.status(HttpStatusCode.Forbidden).json({ message: AuthMiddleware.licenceExpiredMessage });
|
|
37
|
+
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
// fetching the licence and validating.
|
|
40
|
+
const validate_lic = await this.checkLicenceAndValidate(realm, publicKey);
|
|
41
|
+
if (!validate_lic) {
|
|
42
|
+
return res.status(HttpStatusCode.Forbidden).json({message: AuthMiddleware.licenceExpiredMessage});
|
|
43
|
+
}
|
|
44
|
+
else if (req.originalUrl == bypassURL && validate_lic ) {
|
|
45
|
+
return next();
|
|
46
|
+
}
|
|
47
|
+
const authHeader = req.headers['authorization'];
|
|
38
48
|
|
|
49
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
50
|
+
return res.status(401).json({ message: 'Bearer token required' });
|
|
51
|
+
}
|
|
52
|
+
const decoded: any = this.decodeAccessToken(authHeader, publicKey)
|
|
39
53
|
// Cache the client token
|
|
40
54
|
let clientToken: any = await this.cacheManager.get('client_access_token');
|
|
41
55
|
if (!clientToken) {
|
|
42
56
|
clientToken = await this.clientLogin();
|
|
43
57
|
const decodedToken: any = jwt.decode(clientToken);
|
|
44
58
|
const ttl = (decodedToken.exp - Math.floor(Date.now() / 1000)) * 1000;
|
|
45
|
-
|
|
46
|
-
|
|
47
59
|
await this.cacheManager.set('client_access_token', clientToken, ttl );
|
|
48
60
|
}
|
|
49
61
|
|
|
@@ -69,14 +81,13 @@ export class AuthMiddleware implements NestMiddleware {
|
|
|
69
81
|
if (apiPermission?.params === "true")
|
|
70
82
|
{
|
|
71
83
|
const event = req?.body?.event;
|
|
72
|
-
const url = req?.originalUrl + `?
|
|
84
|
+
const url = req?.originalUrl + `?action=${event}`;
|
|
73
85
|
if (!clientAttributes[url]){
|
|
74
86
|
return res.status(403).json({ message: 'Access denied for event' });
|
|
75
87
|
}
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
90
|
|
|
79
|
-
|
|
80
91
|
// Cache interface details
|
|
81
92
|
/*const userName = decoded.preferred_username;
|
|
82
93
|
let userAttributes: any = await this.cacheManager.get(userName);
|
|
@@ -151,4 +162,128 @@ export class AuthMiddleware implements NestMiddleware {
|
|
|
151
162
|
throw new Error(`Failed to fetch client role attributes: ${error.response?.data?.error_description || error.message}`);
|
|
152
163
|
}
|
|
153
164
|
}
|
|
165
|
+
|
|
166
|
+
private async checkLicenceAndValidate(realm: string, publicKey:string) {
|
|
167
|
+
try {
|
|
168
|
+
// Reuse cached licence when possible; short-circuit if already marked expired.
|
|
169
|
+
if (AuthMiddleware.licenceExpired) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const lic_token = await this.cacheManager.get( 'client_Licence_token' );
|
|
173
|
+
if (!lic_token) {
|
|
174
|
+
const response:any = await this.getLicencingDetails(realm);
|
|
175
|
+
if (response.code === HttpStatusCode.InternalServerError) {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
else{
|
|
179
|
+
const validate:any = await this.validateLicence(response.data, publicKey);
|
|
180
|
+
if (validate.status && validate.ttl) {
|
|
181
|
+
await this.cacheManager.set('client_Licence_token', response.data, validate.ttl);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return validate.status;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
return await this.validateLicence(lic_token, publicKey);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch(error:any){
|
|
192
|
+
return false;
|
|
193
|
+
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
private async getLicencingDetails(realm: string) {
|
|
197
|
+
try{
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
const url = 'https://iiuuckued274sisadalbpf7ivu0eiblk.lambda-url.ap-south-1.on.aws/'
|
|
202
|
+
const body = { "client_name": realm };
|
|
203
|
+
|
|
204
|
+
const response = await axios.post( url , body );
|
|
205
|
+
if (response.status === HttpStatusCode.Ok) {
|
|
206
|
+
return {
|
|
207
|
+
code: HttpStatusCode.Ok,
|
|
208
|
+
data: response.data.token
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
return {
|
|
213
|
+
code: HttpStatusCode.InternalServerError,
|
|
214
|
+
message: "InternalServer error"
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
}catch(error:any){
|
|
219
|
+
return {
|
|
220
|
+
code: HttpStatusCode.InternalServerError,
|
|
221
|
+
data: error?.response?.data || error?.message || 'Licencing service error'
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
private async validateLicence(lic_data:any, publicKey:any){
|
|
226
|
+
try{
|
|
227
|
+
const token:any = await this.verifyJwt(lic_data, publicKey);
|
|
228
|
+
const licEndMs = this.normalizeEpochMs(token?.lic_end);
|
|
229
|
+
// Reject when licence end (epoch) is missing or already in the past.
|
|
230
|
+
if (!licEndMs) {
|
|
231
|
+
return{
|
|
232
|
+
status: false
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (licEndMs <= Date.now()) {
|
|
236
|
+
this.markLicenceExpired();
|
|
237
|
+
return {
|
|
238
|
+
status: false
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
status: true,
|
|
244
|
+
ttl: (licEndMs - Date.now()) > 0 ? (licEndMs - Date.now()) : null
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch(error:any){
|
|
249
|
+
this.markLicenceExpired();
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private markLicenceExpired() {
|
|
255
|
+
if (!AuthMiddleware.licenceExpired) {
|
|
256
|
+
AuthMiddleware.licenceExpired = true;
|
|
257
|
+
}
|
|
258
|
+
if (!AuthMiddleware.shutdownTimer) {
|
|
259
|
+
AuthMiddleware.shutdownTimer = setTimeout(() => {
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}, 2 * 60 * 1000); // waiting two minutes after shutdowning server.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private normalizeEpochMs(epoch: number): number | null {
|
|
266
|
+
if (!epoch || Number.isNaN(epoch)) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
// Accept seconds or milliseconds since Unix epoch and normalize to ms for Date.now() comparison.
|
|
270
|
+
if (epoch < 1_000_000_000_000) {
|
|
271
|
+
return epoch * 1000;
|
|
272
|
+
}
|
|
273
|
+
return epoch;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private verifyJwt(token: string, publicKey: string) {
|
|
277
|
+
const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
|
|
278
|
+
return jwt.verify(token, publicKeys, { algorithms: ['RS256'] });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private decodeAccessToken(authHeader: string, publicKey: string) {
|
|
282
|
+
const token = this.extractBearerToken(authHeader);
|
|
283
|
+
return this.verifyJwt(token, publicKey);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private extractBearerToken(authHeader: string) {
|
|
287
|
+
return authHeader.split(' ')[1];
|
|
288
|
+
}
|
|
154
289
|
}
|