@monderks/nestjs-keycloak-auth 0.1.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.
Files changed (37) hide show
  1. package/.eslintrc.js +26 -0
  2. package/.prettierrc +20 -0
  3. package/README.md +299 -0
  4. package/dist/decorators/current-user.decorator.d.ts +3 -0
  5. package/dist/decorators/current-user.decorator.d.ts.map +1 -0
  6. package/dist/decorators/current-user.decorator.js +10 -0
  7. package/dist/decorators/current-user.decorator.js.map +1 -0
  8. package/dist/guards/keycloak-auth.guard.d.ts +18 -0
  9. package/dist/guards/keycloak-auth.guard.d.ts.map +1 -0
  10. package/dist/guards/keycloak-auth.guard.js +81 -0
  11. package/dist/guards/keycloak-auth.guard.js.map +1 -0
  12. package/dist/index.d.ts +6 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +28 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/interfaces/keycloak-config.interface.d.ts +56 -0
  17. package/dist/interfaces/keycloak-config.interface.d.ts.map +1 -0
  18. package/dist/interfaces/keycloak-config.interface.js +3 -0
  19. package/dist/interfaces/keycloak-config.interface.js.map +1 -0
  20. package/dist/keycloak-auth.module.d.ts +5 -0
  21. package/dist/keycloak-auth.module.d.ts.map +1 -0
  22. package/dist/keycloak-auth.module.js +44 -0
  23. package/dist/keycloak-auth.module.js.map +1 -0
  24. package/dist/keycloak-auth.service.d.ts +16 -0
  25. package/dist/keycloak-auth.service.d.ts.map +1 -0
  26. package/dist/keycloak-auth.service.js +231 -0
  27. package/dist/keycloak-auth.service.js.map +1 -0
  28. package/examples/frontend-controlled-auth.ts +96 -0
  29. package/jest.config.js +14 -0
  30. package/package.json +52 -0
  31. package/src/decorators/current-user.decorator.ts +11 -0
  32. package/src/guards/keycloak-auth.guard.ts +93 -0
  33. package/src/index.ts +14 -0
  34. package/src/interfaces/keycloak-config.interface.ts +58 -0
  35. package/src/keycloak-auth.module.ts +32 -0
  36. package/src/keycloak-auth.service.ts +243 -0
  37. package/tsconfig.json +40 -0
@@ -0,0 +1,16 @@
1
+ import { KeycloakConfig, DecodedToken, TokenValidationResult, RefreshTokenResult } from './interfaces/keycloak-config.interface';
2
+ export declare class KeycloakAuthService {
3
+ private config;
4
+ private readonly logger;
5
+ private readonly httpClient;
6
+ private publicKey;
7
+ constructor(config: KeycloakConfig);
8
+ validateToken(token: string): Promise<TokenValidationResult>;
9
+ getUserInfo(accessToken: string): Promise<DecodedToken>;
10
+ refreshToken(refreshToken: string): Promise<RefreshTokenResult>;
11
+ logout(refreshToken: string): Promise<boolean>;
12
+ hasRole(userId: string, roleName: string, clientId?: string): Promise<boolean>;
13
+ private loadPublicKey;
14
+ private getAdminToken;
15
+ }
16
+ //# sourceMappingURL=keycloak-auth.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keycloak-auth.service.d.ts","sourceRoot":"","sources":["../src/keycloak-auth.service.ts"],"names":[],"mappings":"AAIA,OAAO,EACH,cAAc,EACd,YAAY,EACZ,qBAAqB,EACrB,kBAAkB,EACrB,MAAM,wCAAwC,CAAC;AAEhD,qBACa,mBAAmB;IAKW,OAAO,CAAC,MAAM;IAJrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwC;IAC/D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;IAC3C,OAAO,CAAC,SAAS,CAAoB;gBAEU,MAAM,EAAE,cAAc;IA0B/D,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAwD5D,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAsBvD,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAiC/D,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA4B9C,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YA+BtE,aAAa;YAuBb,aAAa;CAM9B"}
@@ -0,0 +1,231 @@
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
+ var KeycloakAuthService_1;
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.KeycloakAuthService = void 0;
53
+ const common_1 = require("@nestjs/common");
54
+ const axios_1 = __importDefault(require("axios"));
55
+ const jwt = __importStar(require("jsonwebtoken"));
56
+ const jose_1 = require("jose");
57
+ let KeycloakAuthService = KeycloakAuthService_1 = class KeycloakAuthService {
58
+ constructor(config) {
59
+ this.config = config;
60
+ this.logger = new common_1.Logger(KeycloakAuthService_1.name);
61
+ this.publicKey = null;
62
+ this.httpClient = axios_1.default.create({
63
+ baseURL: config.serverUrl,
64
+ timeout: 10000,
65
+ });
66
+ this.httpClient.interceptors.request.use((config) => {
67
+ this.logger.debug(`Request: ${config.method?.toUpperCase()} ${config.url}`);
68
+ return config;
69
+ });
70
+ this.httpClient.interceptors.response.use((response) => {
71
+ this.logger.debug(`Response: ${response.status} ${response.config.url}`);
72
+ return response;
73
+ }, (error) => {
74
+ this.logger.error(`Error: ${error.response?.status} ${error.config?.url} - ${error.message}`);
75
+ return Promise.reject(error);
76
+ });
77
+ }
78
+ async validateToken(token) {
79
+ try {
80
+ const decoded = jwt.decode(token);
81
+ if (!decoded) {
82
+ return { valid: false, error: 'Token inválido' };
83
+ }
84
+ const now = Math.floor(Date.now() / 1000);
85
+ const tolerance = this.config.tokenExpirationTolerance || 0;
86
+ if (decoded.exp && decoded.exp < now - tolerance) {
87
+ return { valid: false, expired: true, error: 'Token expirado' };
88
+ }
89
+ if (!this.publicKey) {
90
+ await this.loadPublicKey(token);
91
+ }
92
+ if (this.publicKey) {
93
+ try {
94
+ const jwk = this.publicKey;
95
+ const keyLike = await (0, jose_1.importJWK)(jwk, 'RS256');
96
+ const jwtOptions = {};
97
+ if (this.config.verifyTokenIssuer) {
98
+ jwtOptions.issuer = `${this.config.serverUrl}/realms/${this.config.realm}`;
99
+ }
100
+ if (this.config.verifyTokenAudience) {
101
+ jwtOptions.audience = this.config.clientId;
102
+ }
103
+ await (0, jose_1.jwtVerify)(token, keyLike, jwtOptions);
104
+ }
105
+ catch (verifyError) {
106
+ return {
107
+ valid: false,
108
+ invalidSignature: true,
109
+ error: 'Firma del token inválida: ' + verifyError,
110
+ decoded,
111
+ };
112
+ }
113
+ }
114
+ return { valid: true, decoded };
115
+ }
116
+ catch (error) {
117
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
118
+ this.logger.error(`Error validando token: ${errorMessage}`);
119
+ return { valid: false, error: errorMessage };
120
+ }
121
+ }
122
+ async getUserInfo(accessToken) {
123
+ try {
124
+ const response = await this.httpClient.get(`/realms/${this.config.realm}/protocol/openid-connect/userinfo`, {
125
+ headers: {
126
+ Authorization: `Bearer ${accessToken}`,
127
+ },
128
+ });
129
+ return response.data;
130
+ }
131
+ catch (error) {
132
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
133
+ this.logger.error(`Error obteniendo información del usuario: ${errorMessage}`);
134
+ throw new Error('No se pudo obtener información del usuario');
135
+ }
136
+ }
137
+ async refreshToken(refreshToken) {
138
+ try {
139
+ const params = new URLSearchParams({
140
+ grant_type: 'refresh_token',
141
+ client_id: this.config.clientId,
142
+ refresh_token: refreshToken,
143
+ });
144
+ if (this.config.clientSecret) {
145
+ params.append('client_secret', this.config.clientSecret);
146
+ }
147
+ const response = await this.httpClient.post(`/realms/${this.config.realm}/protocol/openid-connect/token`, params, {
148
+ headers: {
149
+ 'Content-Type': 'application/x-www-form-urlencoded',
150
+ },
151
+ });
152
+ return { success: true, token: response.data };
153
+ }
154
+ catch (error) {
155
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
156
+ this.logger.error(`Error renovando token: ${errorMessage}`);
157
+ return { success: false, error: errorMessage };
158
+ }
159
+ }
160
+ async logout(refreshToken) {
161
+ try {
162
+ const params = new URLSearchParams({
163
+ client_id: this.config.clientId,
164
+ refresh_token: refreshToken,
165
+ });
166
+ if (this.config.clientSecret) {
167
+ params.append('client_secret', this.config.clientSecret);
168
+ }
169
+ await this.httpClient.post(`/realms/${this.config.realm}/protocol/openid-connect/logout`, params, {
170
+ headers: {
171
+ 'Content-Type': 'application/x-www-form-urlencoded',
172
+ },
173
+ });
174
+ return true;
175
+ }
176
+ catch (error) {
177
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
178
+ this.logger.error(`Error en logout: ${errorMessage}`);
179
+ return false;
180
+ }
181
+ }
182
+ async hasRole(userId, roleName, clientId) {
183
+ try {
184
+ const response = await this.httpClient.get(`/admin/realms/${this.config.realm}/users/${userId}/role-mappings`, {
185
+ headers: {
186
+ Authorization: `Bearer ${await this.getAdminToken()}`,
187
+ },
188
+ });
189
+ const realmRoles = response.data.realmMappings || [];
190
+ const clientRoles = response.data.clientMappings || {};
191
+ if (clientId) {
192
+ const clientRoleMappings = clientRoles[clientId]?.mappings || [];
193
+ return clientRoleMappings.some((role) => role.name === roleName);
194
+ }
195
+ else {
196
+ return realmRoles.some((role) => role.name === roleName);
197
+ }
198
+ }
199
+ catch (error) {
200
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
201
+ this.logger.error(`Error verificando rol: ${errorMessage}`);
202
+ return false;
203
+ }
204
+ }
205
+ async loadPublicKey(token) {
206
+ try {
207
+ const response = await this.httpClient.get(`/realms/${this.config.realm}/protocol/openid-connect/certs`);
208
+ const keys = response.data.keys;
209
+ const decodedHeader = JSON.parse(Buffer.from(token.split('.')[0], 'base64').toString());
210
+ const kid = decodedHeader.kid;
211
+ const matchingKey = keys.find((k) => k.kid === kid);
212
+ if (!matchingKey)
213
+ throw new Error(`No se encontró clave con kid: ${kid}`);
214
+ this.publicKey = matchingKey;
215
+ }
216
+ catch (error) {
217
+ this.logger.error(`Error cargando clave pública: ${error instanceof Error ? error.message : 'Error desconocido'}`);
218
+ throw new Error('No se pudo cargar la clave pública del realm');
219
+ }
220
+ }
221
+ async getAdminToken() {
222
+ throw new Error('Credenciales de administrador no configuradas');
223
+ }
224
+ };
225
+ exports.KeycloakAuthService = KeycloakAuthService;
226
+ exports.KeycloakAuthService = KeycloakAuthService = KeycloakAuthService_1 = __decorate([
227
+ (0, common_1.Injectable)(),
228
+ __param(0, (0, common_1.Inject)('KEYCLOAK_CONFIG')),
229
+ __metadata("design:paramtypes", [Object])
230
+ ], KeycloakAuthService);
231
+ //# sourceMappingURL=keycloak-auth.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keycloak-auth.service.js","sourceRoot":"","sources":["../src/keycloak-auth.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { KeycloakAuthModule } from '../src';
3
+
4
+ // Configuración para validación de tokens (frontend controla login)
5
+ const keycloakConfig = {
6
+ serverUrl: 'http://localhost:8080',
7
+ realm: 'my-realm',
8
+ clientId: 'my-client',
9
+ clientSecret: 'my-client-secret', // Necesario para refresh y logout
10
+ verifyTokenAudience: true,
11
+ verifyTokenIssuer: true,
12
+ tokenExpirationTolerance: 30, // segundos
13
+ };
14
+
15
+ @Module({
16
+ imports: [
17
+ KeycloakAuthModule.forRoot({
18
+ config: keycloakConfig,
19
+ }),
20
+ ],
21
+ })
22
+ export class AppModule {}
23
+
24
+ // Ejemplo de uso en Next.js frontend
25
+ export const nextjsExample = `
26
+ // En tu aplicación Next.js, el flujo sería así:
27
+
28
+ // 1. Instalar keycloak-js
29
+ // npm install keycloak-js
30
+
31
+ // 2. Configurar Keycloak en el frontend
32
+ import Keycloak from 'keycloak-js';
33
+
34
+ const keycloak = new Keycloak({
35
+ url: 'http://localhost:8080',
36
+ realm: 'my-realm',
37
+ clientId: 'my-client'
38
+ });
39
+
40
+ // 3. Inicializar y hacer login
41
+ await keycloak.init({
42
+ onLoad: 'login-required'
43
+ });
44
+
45
+ // 4. Obtener el token
46
+ const token = keycloak.token;
47
+
48
+ // 5. Enviar requests al backend con el token
49
+ const response = await fetch('/api/auth/protected', {
50
+ headers: {
51
+ 'Authorization': \`Bearer \${token}\`
52
+ }
53
+ });
54
+
55
+ // 6. Para validar el token en el backend
56
+ const validateResponse = await fetch('/api/auth/validate', {
57
+ method: 'POST',
58
+ headers: {
59
+ 'Content-Type': 'application/json'
60
+ },
61
+ body: JSON.stringify({ token })
62
+ });
63
+
64
+ // 7. Para renovar el token
65
+ const refreshResponse = await fetch('/api/auth/refresh', {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json'
69
+ },
70
+ body: JSON.stringify({ refreshToken: keycloak.refreshToken })
71
+ });
72
+
73
+ // 8. Para logout
74
+ const logoutResponse = await fetch('/api/auth/logout', {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json'
78
+ },
79
+ body: JSON.stringify({ refreshToken: keycloak.refreshToken })
80
+ });
81
+ `;
82
+
83
+ // Ejemplo de controlador personalizado
84
+ export class CustomControllerExample {
85
+ constructor(private keycloakService: any) {}
86
+
87
+ // Endpoint que solo valida el token
88
+ async validateTokenFromFrontend(token: string) {
89
+ return await this.keycloakService.validateToken(token);
90
+ }
91
+
92
+ // Endpoint que obtiene información del usuario
93
+ async getUserFromToken(token: string) {
94
+ return await this.keycloakService.getUserInfo(token);
95
+ }
96
+ }
package/jest.config.js ADDED
@@ -0,0 +1,14 @@
1
+ module.exports = {
2
+ moduleFileExtensions: ['js', 'json', 'ts'],
3
+ rootDir: 'src',
4
+ testRegex: '.*\\.spec\\.ts$',
5
+ transform: {
6
+ '^.+\\.(t|j)s$': 'ts-jest',
7
+ },
8
+ collectCoverageFrom: ['**/*.(t|j)s'],
9
+ coverageDirectory: '../coverage',
10
+ testEnvironment: 'node',
11
+ moduleNameMapping: {
12
+ '^@/(.*)$': '<rootDir>/$1',
13
+ },
14
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@monderks/nestjs-keycloak-auth",
3
+ "version": "0.1.0",
4
+ "description": "Librería para validación de tokens Keycloak en NestJS (frontend controla login)",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "test": "jest",
11
+ "lint": "eslint src/**/*.ts",
12
+ "format": "prettier --write src/**/*.ts",
13
+ "prepare": "npm run build",
14
+ "publish": "npm publish --access public"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/AlejoClifton/nestjs-keycloak-auth.git"
19
+ },
20
+ "keywords": [
21
+ "keycloak",
22
+ "authentication",
23
+ "nestjs",
24
+ "jwt",
25
+ "token-validation",
26
+ "frontend-auth"
27
+ ],
28
+ "author": "Alejo Tomás Clifton Goldney",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@nestjs/common": "^11.1.3",
32
+ "@nestjs/config": "^4.0.2",
33
+ "@nestjs/core": "^11.1.3",
34
+ "axios": "^1.10.0",
35
+ "jose": "^6.0.11",
36
+ "jsonwebtoken": "^9.0.2"
37
+ },
38
+ "devDependencies": {
39
+ "@types/jsonwebtoken": "^9.0.10",
40
+ "@types/node": "^24.0.4",
41
+ "@typescript-eslint/eslint-plugin": "^8.35.0",
42
+ "@typescript-eslint/parser": "^8.35.0",
43
+ "eslint": "^9.29.0",
44
+ "jest": "^30.0.2",
45
+ "prettier": "^3.6.0",
46
+ "typescript": "^5.8.3"
47
+ },
48
+ "peerDependencies": {
49
+ "@nestjs/common": "^11.1.3",
50
+ "@nestjs/core": "^11.1.3"
51
+ }
52
+ }
@@ -0,0 +1,11 @@
1
+ import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2
+ import { DecodedToken } from '../interfaces/keycloak-config.interface';
3
+
4
+ export const CurrentUser = createParamDecorator(
5
+ (data: keyof DecodedToken | undefined, ctx: ExecutionContext): DecodedToken | any => {
6
+ const request = ctx.switchToHttp().getRequest();
7
+ const user = request.user;
8
+
9
+ return data ? user?.[data] : user;
10
+ },
11
+ );
@@ -0,0 +1,93 @@
1
+ import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
2
+ import { Reflector } from '@nestjs/core';
3
+ import { KeycloakAuthService } from '../keycloak-auth.service';
4
+
5
+ export interface KeycloakAuthOptions {
6
+ roles?: string[];
7
+ clientRoles?: Record<string, string[]>;
8
+ requireAllRoles?: boolean;
9
+ optional?: boolean; // Si es true, permite acceso sin token
10
+ }
11
+
12
+ export const KEYCLOAK_AUTH_KEY = 'keycloak_auth';
13
+
14
+ export const KeycloakAuth = (options: KeycloakAuthOptions = {}) => {
15
+ return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
16
+ Reflect.defineMetadata(KEYCLOAK_AUTH_KEY, options, descriptor.value);
17
+ return descriptor;
18
+ };
19
+ };
20
+
21
+ @Injectable()
22
+ export class KeycloakAuthGuard implements CanActivate {
23
+ constructor(
24
+ private reflector: Reflector,
25
+ private keycloakService: KeycloakAuthService,
26
+ ) {}
27
+
28
+ async canActivate(context: ExecutionContext): Promise<boolean> {
29
+ const request = context.switchToHttp().getRequest();
30
+ const options = this.reflector.getAllAndOverride<KeycloakAuthOptions>(KEYCLOAK_AUTH_KEY, [
31
+ context.getHandler(),
32
+ context.getClass(),
33
+ ]);
34
+
35
+ // Extraer el token del header Authorization
36
+ const authHeader = request.headers.authorization;
37
+
38
+ // Si es opcional y no hay token, permitir acceso
39
+ if (options?.optional && (!authHeader || !authHeader.startsWith('Bearer '))) {
40
+ return true;
41
+ }
42
+
43
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
44
+ throw new UnauthorizedException('Token de autorización requerido');
45
+ }
46
+
47
+ const token = authHeader.substring(7);
48
+
49
+ // Validar el token
50
+ const validationResult = await this.keycloakService.validateToken(token);
51
+ if (!validationResult.valid) {
52
+ throw new UnauthorizedException(validationResult.error || 'Token inválido');
53
+ }
54
+
55
+ // Agregar el usuario decodificado a la request
56
+ request.user = validationResult.decoded;
57
+
58
+ // Si no hay opciones de roles, solo validar que el token sea válido
59
+ if (!options || (!options.roles && !options.clientRoles)) {
60
+ return true;
61
+ }
62
+
63
+ // Verificar roles del realm
64
+ if (options.roles && options.roles.length > 0) {
65
+ const userRoles = validationResult.decoded?.realm_access?.roles || [];
66
+ const hasRealmRole = options.requireAllRoles
67
+ ? options.roles.every((role) => userRoles.includes(role))
68
+ : options.roles.some((role) => userRoles.includes(role));
69
+
70
+ if (!hasRealmRole) {
71
+ throw new UnauthorizedException('Roles insuficientes');
72
+ }
73
+ }
74
+
75
+ // Verificar roles de cliente
76
+ if (options.clientRoles) {
77
+ const userClientRoles = validationResult.decoded?.resource_access || {};
78
+
79
+ for (const [clientId, requiredRoles] of Object.entries(options.clientRoles)) {
80
+ const userRoles = userClientRoles[clientId]?.roles || [];
81
+ const hasClientRole = options.requireAllRoles
82
+ ? requiredRoles.every((role) => userRoles.includes(role))
83
+ : requiredRoles.some((role) => userRoles.includes(role));
84
+
85
+ if (!hasClientRole) {
86
+ throw new UnauthorizedException(`Roles insuficientes para el cliente ${clientId}`);
87
+ }
88
+ }
89
+ }
90
+
91
+ return true;
92
+ }
93
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ // Módulo principal
2
+ export { KeycloakAuthModule } from './keycloak-auth.module';
3
+
4
+ // Servicio principal
5
+ export { KeycloakAuthService } from './keycloak-auth.service';
6
+
7
+ // Guard de autenticación
8
+ export { KeycloakAuthGuard, KeycloakAuth } from './guards/keycloak-auth.guard';
9
+
10
+ // Decorador para obtener usuario actual
11
+ export { CurrentUser } from './decorators/current-user.decorator';
12
+
13
+ // Interfaces y tipos
14
+ export * from './interfaces/keycloak-config.interface';
@@ -0,0 +1,58 @@
1
+ export interface KeycloakConfig {
2
+ serverUrl: string; // http://localhost:8080
3
+ realm: string; // the-name-of-the-realm
4
+ clientId: string; // the-name-of-the-client
5
+ clientSecret?: string; // opcional para clientes públicos
6
+ publicKey?: string; // clave pública del realm para validar JWT
7
+ verifyTokenAudience?: boolean; // verificar audience del token
8
+ verifyTokenIssuer?: boolean; // verificar issuer del token
9
+ tokenExpirationTolerance?: number; // tolerancia en segundos para expiración
10
+ }
11
+
12
+ export interface DecodedToken {
13
+ sub: string; // subject (user ID)
14
+ iss: string; // issuer
15
+ aud: string | string[]; // audience
16
+ exp: number; // expiration time
17
+ iat: number; // issued at
18
+ jti?: string; // JWT ID
19
+ azp?: string; // authorized party
20
+ scope?: string; // scopes
21
+ realm_access?: {
22
+ roles: string[];
23
+ };
24
+ resource_access?: {
25
+ [clientId: string]: {
26
+ roles: string[];
27
+ };
28
+ };
29
+ preferred_username?: string;
30
+ email?: string;
31
+ email_verified?: boolean;
32
+ name?: string;
33
+ given_name?: string;
34
+ family_name?: string;
35
+ }
36
+
37
+ export interface TokenValidationResult {
38
+ valid: boolean;
39
+ decoded?: DecodedToken;
40
+ error?: string;
41
+ expired?: boolean;
42
+ invalidSignature?: boolean;
43
+ invalidAudience?: boolean;
44
+ invalidIssuer?: boolean;
45
+ }
46
+
47
+ export interface RefreshTokenResult {
48
+ success: boolean;
49
+ token?: {
50
+ access_token: string;
51
+ refresh_token?: string;
52
+ token_type: string;
53
+ expires_in: number;
54
+ scope?: string;
55
+ id_token?: string;
56
+ };
57
+ error?: string;
58
+ }
@@ -0,0 +1,32 @@
1
+ import { Module, DynamicModule } from '@nestjs/common';
2
+ import { ConfigModule, ConfigService } from '@nestjs/config';
3
+ import { KeycloakConfig } from './interfaces/keycloak-config.interface';
4
+ import { KeycloakAuthService } from './keycloak-auth.service';
5
+ import { KeycloakAuthGuard } from './guards/keycloak-auth.guard';
6
+
7
+ @Module({})
8
+ export class KeycloakAuthModule {
9
+ static forRootFromEnv(): DynamicModule {
10
+ return {
11
+ module: KeycloakAuthModule,
12
+ imports: [ConfigModule],
13
+ providers: [
14
+ {
15
+ provide: 'KEYCLOAK_CONFIG',
16
+ useFactory: (configService: ConfigService): KeycloakConfig => ({
17
+ serverUrl: configService.get<string>('KEYCLOAK_SERVER_URL', 'http://localhost:8080'),
18
+ realm: configService.get<string>('KEYCLOAK_REALM', 'my-realm'),
19
+ clientId: configService.get<string>('KEYCLOAK_CLIENT_ID', 'backend'),
20
+ clientSecret: configService.get<string>('KEYCLOAK_CLIENT_SECRET'),
21
+ verifyTokenIssuer: configService.get<string>('KEYCLOAK_VERIFY_ISSUER', 'true') === 'true',
22
+ verifyTokenAudience: configService.get<string>('KEYCLOAK_VERIFY_AUDIENCE', 'true') === 'true',
23
+ }),
24
+ inject: [ConfigService],
25
+ },
26
+ KeycloakAuthService,
27
+ KeycloakAuthGuard,
28
+ ],
29
+ exports: [KeycloakAuthService, KeycloakAuthGuard],
30
+ };
31
+ }
32
+ }