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.
- package/dist/auth.middleware.d.ts +21 -0
- package/dist/auth.middleware.js +155 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +17 -0
- package/dist/keycloak.service.d.ts +6 -0
- package/dist/keycloak.service.js +97 -0
- package/package.json +29 -0
- package/src/auth.middleware.ts +135 -0
- package/src/index.ts +1 -0
- package/tsconfig.json +22 -0
- package/types/lru-cache.d.ts +5 -0
|
@@ -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);
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
|