dms-middleware-auth 1.0.0

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.
@@ -0,0 +1,21 @@
1
+ import { NestMiddleware } from '@nestjs/common';
2
+ import { Response, NextFunction } from 'express';
3
+ import { Cache } from 'cache-manager';
4
+ interface AuthMiddlewareOptions {
5
+ publicKey: string;
6
+ keycloakUrl: string;
7
+ realm: string;
8
+ clientId: string;
9
+ clientSecret: string;
10
+ clientUuid: string;
11
+ }
12
+ export declare class AuthMiddleware implements NestMiddleware {
13
+ private cacheManager;
14
+ private readonly options;
15
+ constructor(cacheManager: Cache, options: AuthMiddlewareOptions);
16
+ use(req: any, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
17
+ private clientLogin;
18
+ private getUserDetails;
19
+ private getClientRoleAttributes;
20
+ }
21
+ export {};
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __metadata = (this && this.__metadata) || function (k, v) {
42
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
+ };
44
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
45
+ return function (target, key) { decorator(target, key, paramIndex); }
46
+ };
47
+ var __importDefault = (this && this.__importDefault) || function (mod) {
48
+ return (mod && mod.__esModule) ? mod : { "default": mod };
49
+ };
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.AuthMiddleware = void 0;
52
+ const common_1 = require("@nestjs/common");
53
+ const jwt = __importStar(require("jsonwebtoken"));
54
+ const axios_1 = __importDefault(require("axios"));
55
+ const cache_manager_1 = require("@nestjs/cache-manager");
56
+ let AuthMiddleware = class AuthMiddleware {
57
+ constructor(cacheManager, options) {
58
+ this.cacheManager = cacheManager;
59
+ this.options = options;
60
+ }
61
+ async use(req, res, next) {
62
+ const authHeader = req.headers['authorization'];
63
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
64
+ return res.status(401).json({ message: 'Bearer token required' });
65
+ }
66
+ const { publicKey, clientId } = this.options;
67
+ try {
68
+ const token = authHeader.split(' ')[1];
69
+ const publicKeys = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
70
+ const decoded = jwt.verify(token, publicKeys, { algorithms: ['RS256'] });
71
+ // Cache the client token
72
+ let clientToken = await this.cacheManager.get('client_access_token');
73
+ if (!clientToken) {
74
+ clientToken = await this.clientLogin();
75
+ const decodedToken = jwt.decode(clientToken);
76
+ const ttl = (decodedToken.exp - Math.floor(Date.now() / 1000)) * 1000;
77
+ await this.cacheManager.set('client_access_token', clientToken, ttl);
78
+ }
79
+ // Cache client role attributes
80
+ const role = decoded.resource_access[clientId]?.roles?.[0];
81
+ const clientRoleName = `${clientId + role}`;
82
+ let clientAttributes = await this.cacheManager.get(clientRoleName);
83
+ if (!clientAttributes) {
84
+ clientAttributes = await this.getClientRoleAttributes(role, clientToken);
85
+ clientAttributes = JSON.stringify(clientAttributes.attributes);
86
+ await this.cacheManager.set(clientRoleName, clientAttributes, 0);
87
+ }
88
+ // Check route access
89
+ clientAttributes = JSON.parse(clientAttributes);
90
+ if (!clientAttributes[req.originalUrl]) {
91
+ return res.status(403).json({ message: 'Access denied for this route' });
92
+ }
93
+ // Cache user details
94
+ const userName = decoded.preferred_username;
95
+ let userAttributes = await this.cacheManager.get(userName);
96
+ if (!userAttributes) {
97
+ userAttributes = await this.getUserDetails(userName, clientToken);
98
+ userAttributes = JSON.stringify(userAttributes.attributes);
99
+ await this.cacheManager.set(userName, userAttributes, 0);
100
+ }
101
+ // Attach attributes to request
102
+ userAttributes = JSON.parse(userAttributes);
103
+ req['attributes'] = {
104
+ client: clientAttributes[req.originalUrl],
105
+ user: userAttributes,
106
+ };
107
+ return next();
108
+ }
109
+ catch (error) {
110
+ // console.error('AuthMiddleware error:', error);
111
+ return res.status(500).json({ message: error.message });
112
+ }
113
+ }
114
+ async clientLogin() {
115
+ const { keycloakUrl, realm, clientId, clientSecret } = this.options;
116
+ try {
117
+ const response = await axios_1.default.post(`${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`, new URLSearchParams({
118
+ grant_type: 'client_credentials',
119
+ client_id: clientId,
120
+ client_secret: clientSecret,
121
+ }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
122
+ return response.data.access_token;
123
+ }
124
+ catch (error) {
125
+ throw new Error(`Failed to obtain client token: ${error.response?.data?.error_description || error.message}`);
126
+ }
127
+ }
128
+ async getUserDetails(username, token) {
129
+ const { keycloakUrl, realm } = this.options;
130
+ try {
131
+ const response = await axios_1.default.get(`${keycloakUrl}/admin/realms/${realm}/users?username=${username}`, { headers: { Authorization: `Bearer ${token}` } });
132
+ return response.data[0];
133
+ }
134
+ catch (error) {
135
+ throw new Error(`Failed to fetch user details: ${error.response?.data?.error_description || error.message}`);
136
+ }
137
+ }
138
+ async getClientRoleAttributes(role, token) {
139
+ const { keycloakUrl, realm, clientUuid } = this.options;
140
+ try {
141
+ const response = await axios_1.default.get(`${keycloakUrl}/admin/realms/${realm}/clients/${clientUuid}/roles/${role}`, { headers: { Authorization: `Bearer ${token}` } });
142
+ return response.data;
143
+ }
144
+ catch (error) {
145
+ throw new Error(`Failed to fetch client role attributes: ${error.response?.data?.error_description || error.message}`);
146
+ }
147
+ }
148
+ };
149
+ exports.AuthMiddleware = AuthMiddleware;
150
+ exports.AuthMiddleware = AuthMiddleware = __decorate([
151
+ (0, common_1.Injectable)(),
152
+ __param(0, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
153
+ __param(1, (0, common_1.Inject)('AUTH_MIDDLEWARE_OPTIONS')),
154
+ __metadata("design:paramtypes", [Object, Object])
155
+ ], AuthMiddleware);
@@ -0,0 +1 @@
1
+ export * from './auth.middleware';
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./auth.middleware"), exports);
@@ -0,0 +1,6 @@
1
+ export declare class KeycloakService {
2
+ decodeToken(token: string): any;
3
+ clientLogin(options: any): Promise<any>;
4
+ getUserDetails(username: string, token: string, options: any): Promise<any>;
5
+ getClientRoleAttributes(role: string, token: string, options: any): Promise<any>;
6
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __importDefault = (this && this.__importDefault) || function (mod) {
42
+ return (mod && mod.__esModule) ? mod : { "default": mod };
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.KeycloakService = void 0;
46
+ const common_1 = require("@nestjs/common");
47
+ const jwt = __importStar(require("jsonwebtoken"));
48
+ const axios_1 = __importDefault(require("axios"));
49
+ let KeycloakService = class KeycloakService {
50
+ decodeToken(token) {
51
+ try {
52
+ return jwt.decode(token);
53
+ }
54
+ catch (error) {
55
+ throw new Error('Invalid token');
56
+ }
57
+ }
58
+ // Verify the token's validity
59
+ async clientLogin(options) {
60
+ const { keycloakUrl, realm, clientId, clientSecret } = options;
61
+ try {
62
+ const response = await axios_1.default.post(`${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`, new URLSearchParams({
63
+ grant_type: 'client_credentials',
64
+ client_id: clientId,
65
+ client_secret: clientSecret,
66
+ }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
67
+ return response.data.access_token;
68
+ }
69
+ catch (error) {
70
+ throw new Error(`Failed to obtain client token: ${error.response?.data?.error_description || error.message}`);
71
+ }
72
+ }
73
+ async getUserDetails(username, token, options) {
74
+ const { keycloakUrl, realm } = options;
75
+ try {
76
+ const response = await axios_1.default.get(`${keycloakUrl}/admin/realms/${realm}/users?username=${username}`, { headers: { Authorization: `Bearer ${token}` } });
77
+ return response.data[0];
78
+ }
79
+ catch (error) {
80
+ throw new Error(`Failed to fetch user details: ${error.response?.data?.error_description || error.message}`);
81
+ }
82
+ }
83
+ async getClientRoleAttributes(role, token, options) {
84
+ const { keycloakUrl, realm, clientUuid } = options;
85
+ try {
86
+ const response = await axios_1.default.get(`${keycloakUrl}/admin/realms/${realm}/clients/${clientUuid}/roles/${role}`, { headers: { Authorization: `Bearer ${token}` } });
87
+ return response.data;
88
+ }
89
+ catch (error) {
90
+ throw new Error(`Failed to fetch client role attributes: ${error.response?.data?.error_description || error.message}`);
91
+ }
92
+ }
93
+ };
94
+ exports.KeycloakService = KeycloakService;
95
+ exports.KeycloakService = KeycloakService = __decorate([
96
+ (0, common_1.Injectable)()
97
+ ], KeycloakService);
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "dms-middleware-auth",
3
+ "version": "1.0.0",
4
+ "description": "Reusable middleware for authentication and authorization in NestJS applications.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "jest"
10
+ },
11
+ "author": "dms-team",
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "@nestjs/cache-manager": "^2.3.0",
15
+ "@nestjs/common": "^10.4.13",
16
+ "@nestjs/core": "^10.4.13",
17
+ "axios": "^1.7.9",
18
+ "cache-manager": "^5.7.6",
19
+ "express": "^4.21.1",
20
+ "jsonwebtoken": "^9.0.2",
21
+ "lru-cache": "^11.0.2"
22
+ },
23
+ "devDependencies": {
24
+ "@types/axios": "^0.14.4",
25
+ "@types/express": "^5.0.0",
26
+ "@types/jsonwebtoken": "^9.0.7",
27
+ "typescript": "^5.7.2"
28
+ }
29
+ }
@@ -0,0 +1,135 @@
1
+ import { Inject, Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
2
+ import { Request, Response, NextFunction } from 'express';
3
+ import * as jwt from 'jsonwebtoken';
4
+ import axios from 'axios';
5
+ import { CACHE_MANAGER } from '@nestjs/cache-manager';
6
+ import { Cache } from 'cache-manager';
7
+
8
+ interface AuthMiddlewareOptions {
9
+ publicKey: string;
10
+ keycloakUrl: string;
11
+ realm: string;
12
+ clientId: string;
13
+ clientSecret: string;
14
+ clientUuid: string;
15
+ }
16
+
17
+ @Injectable()
18
+ export class AuthMiddleware implements NestMiddleware {
19
+ constructor(
20
+ @Inject(CACHE_MANAGER) private cacheManager: Cache,
21
+ @Inject('AUTH_MIDDLEWARE_OPTIONS') private readonly options: AuthMiddlewareOptions
22
+ ) {}
23
+
24
+ async use(req: any, res: Response, next: NextFunction) {
25
+ const authHeader = req.headers['authorization'];
26
+
27
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
28
+ return res.status(401).json({ message: 'Bearer token required' });
29
+ }
30
+
31
+ const { publicKey, clientId } = this.options;
32
+
33
+ try {
34
+ const token = authHeader.split(' ')[1];
35
+ const publicKeys: string = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`;
36
+
37
+ const decoded: any = jwt.verify(token, publicKeys, { algorithms: ['RS256'] });
38
+
39
+ // Cache the client token
40
+ let clientToken: any = await this.cacheManager.get('client_access_token');
41
+ if (!clientToken) {
42
+ clientToken = await this.clientLogin();
43
+ const decodedToken: any = jwt.decode(clientToken);
44
+ const ttl = (decodedToken.exp - Math.floor(Date.now() / 1000)) * 1000;
45
+
46
+
47
+ await this.cacheManager.set('client_access_token', clientToken, ttl );
48
+ }
49
+
50
+ // Cache client role attributes
51
+ const role = decoded.resource_access[clientId]?.roles?.[0];
52
+ const clientRoleName = `${clientId+role}`;
53
+ let clientAttributes: any = await this.cacheManager.get(clientRoleName);
54
+ if (!clientAttributes) {
55
+ clientAttributes = await this.getClientRoleAttributes(role, clientToken);
56
+ clientAttributes = JSON.stringify(clientAttributes.attributes);
57
+ await this.cacheManager.set(clientRoleName, clientAttributes, 0);
58
+ }
59
+
60
+ // Check route access
61
+ clientAttributes = JSON.parse(clientAttributes);
62
+ if (!clientAttributes[req.originalUrl]) {
63
+ return res.status(403).json({ message: 'Access denied for this route' });
64
+ }
65
+
66
+ // Cache user details
67
+ const userName = decoded.preferred_username;
68
+ let userAttributes: any = await this.cacheManager.get(userName);
69
+ if (!userAttributes) {
70
+ userAttributes = await this.getUserDetails(userName, clientToken);
71
+ userAttributes = JSON.stringify(userAttributes.attributes);
72
+ await this.cacheManager.set(userName, userAttributes, 0);
73
+ }
74
+
75
+ // Attach attributes to request
76
+ userAttributes = JSON.parse(userAttributes);
77
+ req['attributes'] = {
78
+ client: clientAttributes[req.originalUrl],
79
+ user: userAttributes,
80
+ };
81
+
82
+ return next();
83
+ } catch (error:any) {
84
+ // console.error('AuthMiddleware error:', error);
85
+ return res.status(500).json({ message: error.message });
86
+ }
87
+ }
88
+
89
+ private async clientLogin() {
90
+ const { keycloakUrl, realm, clientId, clientSecret } = this.options;
91
+
92
+ try {
93
+ const response = await axios.post(
94
+ `${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`,
95
+ new URLSearchParams({
96
+ grant_type: 'client_credentials',
97
+ client_id: clientId,
98
+ client_secret: clientSecret,
99
+ }).toString(),
100
+ { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
101
+ );
102
+ return response.data.access_token;
103
+ } catch (error:any) {
104
+ throw new Error(`Failed to obtain client token: ${error.response?.data?.error_description || error.message}`);
105
+ }
106
+ }
107
+
108
+ private async getUserDetails(username: string, token: string) {
109
+ const { keycloakUrl, realm } = this.options;
110
+
111
+ try {
112
+ const response = await axios.get(
113
+ `${keycloakUrl}/admin/realms/${realm}/users?username=${username}`,
114
+ { headers: { Authorization: `Bearer ${token}` } }
115
+ );
116
+ return response.data[0];
117
+ } catch (error:any) {
118
+ throw new Error(`Failed to fetch user details: ${error.response?.data?.error_description || error.message}`);
119
+ }
120
+ }
121
+
122
+ private async getClientRoleAttributes(role: string, token: string) {
123
+ const { keycloakUrl, realm, clientUuid } = this.options;
124
+
125
+ try {
126
+ const response = await axios.get(
127
+ `${keycloakUrl}/admin/realms/${realm}/clients/${clientUuid}/roles/${role}`,
128
+ { headers: { Authorization: `Bearer ${token}` } }
129
+ );
130
+ return response.data;
131
+ } catch (error:any) {
132
+ throw new Error(`Failed to fetch client role attributes: ${error.response?.data?.error_description || error.message}`);
133
+ }
134
+ }
135
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './auth.middleware';
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "target": "ES2021",
5
+ "declaration": true,
6
+ "outDir": "./dist",
7
+ "strict": true, // Keep strict mode for better type safety
8
+ "esModuleInterop": true,
9
+ "emitDecoratorMetadata": true,
10
+ "experimentalDecorators": true,
11
+ "skipLibCheck": true, // Skip type checking for node_modules
12
+ "typeRoots": ["node_modules/@types", "./types"],
13
+ "types": ["node", "lru-cache"],
14
+ "baseUrl": ".",
15
+ "paths": {
16
+ "lru-cache": ["types/lru-cache.d.ts"]
17
+ }
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": ["node_modules", "dist", "**/*.spec.ts"]
21
+ }
22
+
@@ -0,0 +1,5 @@
1
+ declare module 'lru-cache' {
2
+ import { LRUCache } from 'lru-cache/dist/cjs/index.js';
3
+ export = LRUCache;
4
+ }
5
+