@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.
- package/.eslintrc.js +26 -0
- package/.prettierrc +20 -0
- package/README.md +299 -0
- package/dist/decorators/current-user.decorator.d.ts +3 -0
- package/dist/decorators/current-user.decorator.d.ts.map +1 -0
- package/dist/decorators/current-user.decorator.js +10 -0
- package/dist/decorators/current-user.decorator.js.map +1 -0
- package/dist/guards/keycloak-auth.guard.d.ts +18 -0
- package/dist/guards/keycloak-auth.guard.d.ts.map +1 -0
- package/dist/guards/keycloak-auth.guard.js +81 -0
- package/dist/guards/keycloak-auth.guard.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/keycloak-config.interface.d.ts +56 -0
- package/dist/interfaces/keycloak-config.interface.d.ts.map +1 -0
- package/dist/interfaces/keycloak-config.interface.js +3 -0
- package/dist/interfaces/keycloak-config.interface.js.map +1 -0
- package/dist/keycloak-auth.module.d.ts +5 -0
- package/dist/keycloak-auth.module.d.ts.map +1 -0
- package/dist/keycloak-auth.module.js +44 -0
- package/dist/keycloak-auth.module.js.map +1 -0
- package/dist/keycloak-auth.service.d.ts +16 -0
- package/dist/keycloak-auth.service.d.ts.map +1 -0
- package/dist/keycloak-auth.service.js +231 -0
- package/dist/keycloak-auth.service.js.map +1 -0
- package/examples/frontend-controlled-auth.ts +96 -0
- package/jest.config.js +14 -0
- package/package.json +52 -0
- package/src/decorators/current-user.decorator.ts +11 -0
- package/src/guards/keycloak-auth.guard.ts +93 -0
- package/src/index.ts +14 -0
- package/src/interfaces/keycloak-config.interface.ts +58 -0
- package/src/keycloak-auth.module.ts +32 -0
- package/src/keycloak-auth.service.ts +243 -0
- 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
|
+
}
|