jerkjs 2.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.
Files changed (177) hide show
  1. package/LICENSE +200 -0
  2. package/README.md +171 -0
  3. package/doc/EXTENSION_MANUAL.md +958 -0
  4. package/doc/FIREWALL_MANUAL.md +419 -0
  5. package/doc/HOOKS_REFERENCE_IMPROVED.md +599 -0
  6. package/doc/MANUAL_API_SDK.md +539 -0
  7. package/doc/MANUAL_MVC.md +397 -0
  8. package/doc/MARIADB_TOKENS_IMPLEMENTATION.md +113 -0
  9. package/doc/MIDDLEWARE_MANUAL.md +521 -0
  10. package/doc/OAUTH2_GOOGLE_MANUAL.md +408 -0
  11. package/doc/frontend-and-sessions.md +356 -0
  12. package/examples/advanced/controllers/productController.js +64 -0
  13. package/examples/advanced/controllers/userController.js +85 -0
  14. package/examples/advanced/routes.json +51 -0
  15. package/examples/advanced_example.js +93 -0
  16. package/examples/basic/controllers/userController.js +85 -0
  17. package/examples/basic_example.js +72 -0
  18. package/examples/frontend/README.md +71 -0
  19. package/examples/frontend/app.js +71 -0
  20. package/examples/frontend/controllers/apiController.js +39 -0
  21. package/examples/frontend/controllers/authController.js +220 -0
  22. package/examples/frontend/controllers/formController.js +47 -0
  23. package/examples/frontend/controllers/messageController.js +96 -0
  24. package/examples/frontend/controllers/pageController.js +178 -0
  25. package/examples/frontend/controllers/staticController.js +167 -0
  26. package/examples/frontend/routes.json +90 -0
  27. package/examples/mvc_example/app.js +138 -0
  28. package/examples/mvc_example/views/home/index.html +26 -0
  29. package/examples/mvc_example/views/home/simple.html +3 -0
  30. package/examples/mvc_example/views/layout.html +23 -0
  31. package/examples/mvc_example/views/test.html +3 -0
  32. package/examples/mvc_example/views/user/invalid.html +6 -0
  33. package/examples/mvc_example/views/user/list.html +36 -0
  34. package/examples/mvc_example/views/user/notfound.html +6 -0
  35. package/examples/mvc_example/views/user/profile.html +11 -0
  36. package/examples/mvc_routes_example/app.js +34 -0
  37. package/examples/mvc_routes_example/controllers/mainController.js +27 -0
  38. package/examples/mvc_routes_example/controllers/productController.js +47 -0
  39. package/examples/mvc_routes_example/controllers/userController.js +76 -0
  40. package/examples/mvc_routes_example/routes.json +30 -0
  41. package/examples/mvc_routes_example/views/layout.html +31 -0
  42. package/examples/mvc_routes_example/views/main/index.html +11 -0
  43. package/examples/mvc_routes_example/views/product/catalog.html +24 -0
  44. package/examples/mvc_routes_example/views/user/invalid.html +6 -0
  45. package/examples/mvc_routes_example/views/user/list.html +40 -0
  46. package/examples/mvc_routes_example/views/user/notfound.html +6 -0
  47. package/examples/mvc_routes_example/views/user/profile.html +18 -0
  48. package/examples/public/README.md +92 -0
  49. package/examples/public/app.js +72 -0
  50. package/examples/public/controllers/healthController.js +20 -0
  51. package/examples/public/controllers/mainController.js +22 -0
  52. package/examples/public/controllers/userController.js +139 -0
  53. package/examples/public/routes.json +51 -0
  54. package/examples/v2/README.md +72 -0
  55. package/examples/v2/app.js +74 -0
  56. package/examples/v2/app_fixed.js +74 -0
  57. package/examples/v2/controllers/authController.js +64 -0
  58. package/examples/v2/controllers/mainController.js +24 -0
  59. package/examples/v2/controllers/protectedController.js +12 -0
  60. package/examples/v2/controllers/userController.js +16 -0
  61. package/examples/v2/package.json +27 -0
  62. package/examples/v2/routes.json +30 -0
  63. package/examples/v2/test_api.sh +47 -0
  64. package/examples/v2/tokens_example.sqlite +0 -0
  65. package/examples/v2.1_firewall_demo/README.md +113 -0
  66. package/examples/v2.1_firewall_demo/app.js +182 -0
  67. package/examples/v2.1_firewall_demo/package.json +27 -0
  68. package/examples/v2.1_hooks_demo/README.md +85 -0
  69. package/examples/v2.1_hooks_demo/app.js +101 -0
  70. package/examples/v2.1_hooks_demo/controllers/hooksController.js +29 -0
  71. package/examples/v2.1_hooks_demo/controllers/mainController.js +18 -0
  72. package/examples/v2.1_hooks_demo/package.json +27 -0
  73. package/examples/v2.1_hooks_demo/routes.json +16 -0
  74. package/examples/v2.1_openapi_demo/README.md +82 -0
  75. package/examples/v2.1_openapi_demo/app.js +296 -0
  76. package/examples/v2.1_openapi_demo/package.json +26 -0
  77. package/examples/v2_cors/README.md +82 -0
  78. package/examples/v2_cors/app.js +108 -0
  79. package/examples/v2_cors/package.json +23 -0
  80. package/examples/v2_json_auth/README.md +83 -0
  81. package/examples/v2_json_auth/app.js +72 -0
  82. package/examples/v2_json_auth/controllers/authController.js +67 -0
  83. package/examples/v2_json_auth/controllers/mainController.js +16 -0
  84. package/examples/v2_json_auth/controllers/protectedController.js +12 -0
  85. package/examples/v2_json_auth/controllers/tokenController.js +28 -0
  86. package/examples/v2_json_auth/controllers/userController.js +15 -0
  87. package/examples/v2_json_auth/package.json +26 -0
  88. package/examples/v2_json_auth/routes.json +37 -0
  89. package/examples/v2_json_auth/tokens.json +20 -0
  90. package/examples/v2_mariadb_auth/README.md +94 -0
  91. package/examples/v2_mariadb_auth/app.js +81 -0
  92. package/examples/v2_mariadb_auth/controllers/authController.js +95 -0
  93. package/examples/v2_mariadb_auth/controllers/mainController.js +31 -0
  94. package/examples/v2_mariadb_auth/controllers/protectedController.js +12 -0
  95. package/examples/v2_mariadb_auth/controllers/userController.js +17 -0
  96. package/examples/v2_mariadb_auth/package.json +27 -0
  97. package/examples/v2_mariadb_auth/routes.json +37 -0
  98. package/examples/v2_no_auth/README.md +75 -0
  99. package/examples/v2_no_auth/app.js +72 -0
  100. package/examples/v2_no_auth/controllers/healthController.js +14 -0
  101. package/examples/v2_no_auth/controllers/mainController.js +19 -0
  102. package/examples/v2_no_auth/controllers/productController.js +31 -0
  103. package/examples/v2_no_auth/controllers/publicController.js +16 -0
  104. package/examples/v2_no_auth/package.json +22 -0
  105. package/examples/v2_no_auth/routes.json +37 -0
  106. package/examples/v2_oauth/README.md +70 -0
  107. package/examples/v2_oauth/app.js +90 -0
  108. package/examples/v2_oauth/controllers/mainController.js +45 -0
  109. package/examples/v2_oauth/controllers/oauthController.js +247 -0
  110. package/examples/v2_oauth/controllers/protectedController.js +13 -0
  111. package/examples/v2_oauth/controllers/userController.js +17 -0
  112. package/examples/v2_oauth/package.json +26 -0
  113. package/examples/v2_oauth/routes.json +44 -0
  114. package/examples/v2_openapi/README.md +77 -0
  115. package/examples/v2_openapi/app.js +222 -0
  116. package/examples/v2_openapi/controllers/authController.js +52 -0
  117. package/examples/v2_openapi/controllers/mainController.js +26 -0
  118. package/examples/v2_openapi/controllers/productController.js +17 -0
  119. package/examples/v2_openapi/controllers/userController.js +27 -0
  120. package/examples/v2_openapi/package.json +26 -0
  121. package/examples/v2_openapi/routes.json +37 -0
  122. package/generate_token.js +10 -0
  123. package/index.js +85 -0
  124. package/jerk.jpg +0 -0
  125. package/lib/core/handler.js +86 -0
  126. package/lib/core/hooks.js +224 -0
  127. package/lib/core/router.js +204 -0
  128. package/lib/core/securityEnhancedServer.js +752 -0
  129. package/lib/core/server.js +369 -0
  130. package/lib/loader/controllerLoader.js +175 -0
  131. package/lib/loader/routeLoader.js +341 -0
  132. package/lib/middleware/auditLogger.js +208 -0
  133. package/lib/middleware/authenticator.js +565 -0
  134. package/lib/middleware/compressor.js +218 -0
  135. package/lib/middleware/cors.js +135 -0
  136. package/lib/middleware/firewall.js +443 -0
  137. package/lib/middleware/rateLimiter.js +210 -0
  138. package/lib/middleware/session.js +301 -0
  139. package/lib/middleware/validator.js +193 -0
  140. package/lib/mvc/controllerBase.js +207 -0
  141. package/lib/mvc/viewEngine.js +752 -0
  142. package/lib/utils/configParser.js +223 -0
  143. package/lib/utils/logger.js +145 -0
  144. package/lib/utils/mariadbTokenAdapter.js +226 -0
  145. package/lib/utils/openapiGenerator.js +140 -0
  146. package/lib/utils/sqliteTokenAdapter.js +224 -0
  147. package/lib/utils/tokenManager.js +254 -0
  148. package/package.json +47 -0
  149. package/v2examplle/v2_json_auth/README.md +83 -0
  150. package/v2examplle/v2_json_auth/app.js +72 -0
  151. package/v2examplle/v2_json_auth/controllers/authController.js +67 -0
  152. package/v2examplle/v2_json_auth/controllers/mainController.js +16 -0
  153. package/v2examplle/v2_json_auth/controllers/protectedController.js +12 -0
  154. package/v2examplle/v2_json_auth/controllers/tokenController.js +28 -0
  155. package/v2examplle/v2_json_auth/controllers/userController.js +15 -0
  156. package/v2examplle/v2_json_auth/package.json +26 -0
  157. package/v2examplle/v2_json_auth/routes.json +37 -0
  158. package/v2examplle/v2_json_auth/tokens.json +20 -0
  159. package/v2examplle/v2_mariadb_auth/README.md +94 -0
  160. package/v2examplle/v2_mariadb_auth/app.js +81 -0
  161. package/v2examplle/v2_mariadb_auth/controllers/authController.js +95 -0
  162. package/v2examplle/v2_mariadb_auth/controllers/mainController.js +31 -0
  163. package/v2examplle/v2_mariadb_auth/controllers/protectedController.js +12 -0
  164. package/v2examplle/v2_mariadb_auth/controllers/userController.js +17 -0
  165. package/v2examplle/v2_mariadb_auth/package.json +27 -0
  166. package/v2examplle/v2_mariadb_auth/routes.json +37 -0
  167. package/v2examplle/v2_sqlite_auth/README.md +72 -0
  168. package/v2examplle/v2_sqlite_auth/app.js +74 -0
  169. package/v2examplle/v2_sqlite_auth/app_fixed.js +74 -0
  170. package/v2examplle/v2_sqlite_auth/controllers/authController.js +64 -0
  171. package/v2examplle/v2_sqlite_auth/controllers/mainController.js +24 -0
  172. package/v2examplle/v2_sqlite_auth/controllers/protectedController.js +12 -0
  173. package/v2examplle/v2_sqlite_auth/controllers/userController.js +16 -0
  174. package/v2examplle/v2_sqlite_auth/package.json +27 -0
  175. package/v2examplle/v2_sqlite_auth/routes.json +30 -0
  176. package/v2examplle/v2_sqlite_auth/test_api.sh +47 -0
  177. package/v2examplle/v2_sqlite_auth/tokens_example.sqlite +0 -0
@@ -0,0 +1,565 @@
1
+ /**
2
+ * Middleware de autenticación para el framework API SDK
3
+ * Implementación extendida del componente middleware/authenticator.js
4
+ * Incluye soporte para OAuth2 y OpenID Connect
5
+ */
6
+
7
+ class Authenticator {
8
+ /**
9
+ * Constructor del autenticador
10
+ * @param {Object} options - Opciones de configuración
11
+ * @param {Object} options.logger - Instancia de logger para auditoría de seguridad
12
+ */
13
+ constructor(options = {}) {
14
+ this.strategies = new Map();
15
+ this.logger = options.logger || console;
16
+ this.failedAttempts = new Map(); // Para seguimiento de intentos fallidos
17
+ this.blockThreshold = options.blockThreshold || 5; // Número de intentos fallidos antes de bloquear
18
+ this.blockDuration = options.blockDuration || 900000; // Duración del bloqueo en ms (15 min por defecto)
19
+ }
20
+
21
+ /**
22
+ * Método para registrar una estrategia de autenticación
23
+ * @param {string} name - Nombre de la estrategia
24
+ * @param {Function} strategy - Función de estrategia de autenticación
25
+ */
26
+ use(name, strategy) {
27
+ this.strategies.set(name, strategy);
28
+ }
29
+
30
+ /**
31
+ * Método para crear middleware de autenticación por nombre
32
+ * @param {string} strategyName - Nombre de la estrategia a usar
33
+ * @param {Object} options - Opciones para la estrategia
34
+ * @returns {Function} - Middleware de autenticación
35
+ */
36
+ authenticate(strategyName, options = {}) {
37
+ const strategy = this.strategies.get(strategyName);
38
+
39
+ if (!strategy) {
40
+ throw new Error(`Estrategia de autenticación '${strategyName}' no encontrada`);
41
+ }
42
+
43
+ return async (req, res, next) => {
44
+ // Obtener IP del cliente para seguimiento
45
+ const clientIP = req.headers['x-forwarded-for'] ||
46
+ req.connection.remoteAddress ||
47
+ req.socket.remoteAddress ||
48
+ (req.connection.socket ? req.connection.socket.remoteAddress : null);
49
+
50
+ // Verificar si la IP está bloqueada
51
+ if (this.isBlocked(clientIP)) {
52
+ this.logger.warn(`Intento de autenticación bloqueado desde IP: ${clientIP}`);
53
+ res.writeHead(403, { 'Content-Type': 'application/json' });
54
+ res.end(JSON.stringify({ error: 'Acceso bloqueado temporalmente por múltiples intentos fallidos' }));
55
+ return false;
56
+ }
57
+
58
+ try {
59
+ const isAuthenticated = await strategy(req, options);
60
+
61
+ if (isAuthenticated) {
62
+ // Registrar evento de autenticación exitosa
63
+ this.logger.info(`Autenticación exitosa para IP: ${clientIP}, estrategia: ${strategyName}`);
64
+
65
+ // Reiniciar contador de intentos fallidos para esta IP
66
+ this.resetFailedAttempts(clientIP);
67
+
68
+ if (next) {
69
+ next();
70
+ }
71
+ return true;
72
+ } else {
73
+ // Incrementar contador de intentos fallidos
74
+ this.incrementFailedAttempts(clientIP);
75
+
76
+ // Registrar evento de autenticación fallida
77
+ this.logger.warn(`Autenticación fallida para IP: ${clientIP}, estrategia: ${strategyName}`);
78
+
79
+ res.writeHead(401, { 'Content-Type': 'application/json' });
80
+ res.end(JSON.stringify({ error: 'No autorizado' }));
81
+ return false;
82
+ }
83
+ } catch (error) {
84
+ // Incrementar contador de intentos fallidos
85
+ this.incrementFailedAttempts(clientIP);
86
+
87
+ // Registrar evento de error en autenticación
88
+ this.logger.error(`Error en autenticación para IP: ${clientIP}, estrategia: ${strategyName}`, error.message);
89
+
90
+ res.writeHead(500, { 'Content-Type': 'application/json' });
91
+ res.end(JSON.stringify({ error: 'Error en la autenticación' }));
92
+ return false;
93
+ }
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Estrategia de autenticación por API Key
99
+ * @param {string} headerName - Nombre del header que contiene la API Key
100
+ * @param {Array} validKeys - Array de claves válidas
101
+ * @param {Object} tenantConfig - Configuración por tenant
102
+ * @returns {Function} - Estrategia de autenticación por API Key
103
+ */
104
+ apiKeyStrategy(headerName = 'X-API-Key', validKeys = [], tenantConfig = {}) {
105
+ return (req, options = {}) => {
106
+ const providedKey = req.headers[headerName.toLowerCase()];
107
+ const tenantId = req.headers['x-tenant-id'] || req.headers['tenant']; // Identificador del tenant
108
+
109
+ // Si se proporciona un tenantId y existe configuración específica para ese tenant
110
+ if (tenantId && tenantConfig[tenantId]) {
111
+ const tenantKeys = tenantConfig[tenantId].keys || [];
112
+ return new Promise((resolve) => {
113
+ resolve(tenantKeys.includes(providedKey));
114
+ });
115
+ }
116
+
117
+ const keys = options.validKeys || validKeys;
118
+
119
+ return new Promise((resolve) => {
120
+ resolve(keys.includes(providedKey));
121
+ });
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Estrategia de autenticación por JWT
127
+ * @param {string} secret - Secreto para verificar el token
128
+ * @param {Object} options - Opciones adicionales
129
+ * @param {string} options.refreshSecret - Secreto para verificar refresh tokens
130
+ * @param {string} options.storage - Tipo de almacenamiento para tokens ('memory', 'json')
131
+ * @param {string} options.tokenFile - Ruta al archivo JSON para almacenamiento
132
+ * @returns {Function} - Estrategia de autenticación por JWT
133
+ */
134
+ jwtStrategy(secret, options = {}) {
135
+ const refreshSecret = options.refreshSecret || secret; // Usar mismo secreto si no se proporciona uno diferente
136
+ const tokenManager = new (require('../utils/tokenManager'))({
137
+ storage: options.storage || 'memory',
138
+ tokenFile: options.tokenFile
139
+ });
140
+
141
+ // Verificar que se haya proporcionado un secreto
142
+ if (!secret) {
143
+ throw new Error('Se requiere un secreto para la estrategia JWT');
144
+ }
145
+
146
+ // Importar jsonwebtoken si está disponible
147
+ const jwt = require('jsonwebtoken');
148
+
149
+ return async (req, options = {}) => {
150
+ const authHeader = req.headers.authorization;
151
+ const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
152
+ const jwtSecret = options.secret || secret;
153
+
154
+ if (!token) {
155
+ // Verificar si hay un refresh token para renovar el access token
156
+ const refreshToken = req.headers['x-refresh-token'];
157
+ if (refreshToken) {
158
+ try {
159
+ // Usar el token manager para renovar el token
160
+ const renewedTokens = tokenManager.refreshToken(
161
+ refreshToken,
162
+ jwtSecret,
163
+ refreshSecret,
164
+ options.accessTokenExpiration || '15m'
165
+ );
166
+
167
+ if (renewedTokens) {
168
+ // Agregar nuevo token a la respuesta para que el cliente lo actualice
169
+ if (req.res) {
170
+ req.res.setHeader('X-New-Access-Token', renewedTokens.accessToken);
171
+ }
172
+
173
+ // Decodificar el nuevo token para obtener la información del usuario
174
+ const decoded = jwt.verify(renewedTokens.accessToken, jwtSecret);
175
+ req.user = decoded;
176
+ return Promise.resolve(true);
177
+ }
178
+ } catch (refreshError) {
179
+ return Promise.resolve(false);
180
+ }
181
+ }
182
+
183
+ return Promise.resolve(false);
184
+ }
185
+
186
+ return new Promise((resolve) => {
187
+ jwt.verify(token, jwtSecret, (err, decoded) => {
188
+ if (err) {
189
+ // Si el token ha expirado, intentar usar refresh token
190
+ if (err.name === 'TokenExpiredError') {
191
+ const refreshToken = req.headers['x-refresh-token'];
192
+ if (refreshToken) {
193
+ const renewedTokens = tokenManager.refreshToken(
194
+ refreshToken,
195
+ jwtSecret,
196
+ refreshSecret,
197
+ options.accessTokenExpiration || '15m'
198
+ );
199
+
200
+ if (renewedTokens) {
201
+ // Agregar nuevo token a la respuesta para que el cliente lo actualice
202
+ if (req.res) {
203
+ req.res.setHeader('X-New-Access-Token', renewedTokens.accessToken);
204
+ }
205
+
206
+ // Decodificar el nuevo token para obtener la información del usuario
207
+ const freshDecoded = jwt.verify(renewedTokens.accessToken, jwtSecret);
208
+ req.user = freshDecoded;
209
+ resolve(true);
210
+ } else {
211
+ resolve(false);
212
+ }
213
+ } else {
214
+ resolve(false);
215
+ }
216
+ } else {
217
+ resolve(false);
218
+ }
219
+ } else {
220
+ // Agregar el payload decodificado a la solicitud
221
+ req.user = decoded;
222
+ resolve(true);
223
+ }
224
+ });
225
+ });
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Estrategia de autenticación básica
231
+ * @param {Object} credentials - Credenciales válidas { username: password }
232
+ * @returns {Function} - Estrategia de autenticación básica
233
+ */
234
+ basicStrategy(credentials = {}) {
235
+ return (req, options = {}) => {
236
+ const authHeader = req.headers.authorization;
237
+ if (!authHeader || !authHeader.startsWith('Basic ')) {
238
+ return Promise.resolve(false);
239
+ }
240
+
241
+ const base64Credentials = authHeader.split(' ')[1];
242
+ const credentialsString = Buffer.from(base64Credentials, 'base64').toString('ascii');
243
+ const [username, password] = credentialsString.split(':');
244
+
245
+ const validCredentials = options.credentials || credentials;
246
+
247
+ return new Promise((resolve) => {
248
+ if (validCredentials[username] && validCredentials[username] === password) {
249
+ req.user = { username }; // Agregar usuario a la solicitud
250
+ resolve(true);
251
+ } else {
252
+ resolve(false);
253
+ }
254
+ });
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Verifica si una IP está bloqueada por exceso de intentos fallidos
260
+ * @param {string} ip - Dirección IP a verificar
261
+ * @returns {boolean} - Verdadero si la IP está bloqueada
262
+ */
263
+ isBlocked(ip) {
264
+ const record = this.failedAttempts.get(ip);
265
+ if (!record) {
266
+ return false;
267
+ }
268
+
269
+ // Verificar si aún está dentro del período de bloqueo
270
+ if (Date.now() - record.firstAttemptTime < this.blockDuration) {
271
+ return record.count >= this.blockThreshold;
272
+ } else {
273
+ // El período de bloqueo ha expirado, eliminar el registro
274
+ this.failedAttempts.delete(ip);
275
+ return false;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Incrementa el contador de intentos fallidos para una IP
281
+ * @param {string} ip - Dirección IP
282
+ */
283
+ incrementFailedAttempts(ip) {
284
+ const record = this.failedAttempts.get(ip);
285
+ if (record) {
286
+ record.count++;
287
+ record.lastAttemptTime = Date.now();
288
+ } else {
289
+ this.failedAttempts.set(ip, {
290
+ count: 1,
291
+ firstAttemptTime: Date.now(),
292
+ lastAttemptTime: Date.now()
293
+ });
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Reinicia el contador de intentos fallidos para una IP
299
+ * @param {string} ip - Dirección IP
300
+ */
301
+ resetFailedAttempts(ip) {
302
+ this.failedAttempts.delete(ip);
303
+ }
304
+
305
+ /**
306
+ * Obtiene estadísticas de seguridad para una IP
307
+ * @param {string} ip - Dirección IP
308
+ * @returns {Object} - Estadísticas de intentos de autenticación
309
+ */
310
+ getSecurityStats(ip) {
311
+ return this.failedAttempts.get(ip) || { count: 0, blocked: false };
312
+ }
313
+
314
+ /**
315
+ * Estrategia de autenticación OAuth2
316
+ * @param {Object} options - Opciones para la estrategia OAuth2
317
+ * @param {string} options.clientId - ID del cliente OAuth2
318
+ * @param {string} options.clientSecret - Secreto del cliente OAuth2
319
+ * @param {string} options.callbackURL - URL de callback para OAuth2
320
+ * @param {string} options.authorizationURL - URL de autorización
321
+ * @param {string} options.tokenURL - URL para obtener token
322
+ * @returns {Function} - Estrategia de autenticación OAuth2
323
+ */
324
+ oauth2Strategy(options = {}) {
325
+ const { clientId, clientSecret, callbackURL, authorizationURL, tokenURL } = options;
326
+
327
+ if (!clientId || !clientSecret || !callbackURL || !authorizationURL || !tokenURL) {
328
+ throw new Error('Opciones requeridas para OAuth2: clientId, clientSecret, callbackURL, authorizationURL, tokenURL');
329
+ }
330
+
331
+ return async (req, options = {}) => {
332
+ // Verificar si req.session existe (requiere middleware de sesión externo)
333
+ if (!req.session) {
334
+ // Si no hay sesión, verificar si se proporciona token en header
335
+ const authHeader = req.headers.authorization;
336
+ if (authHeader && authHeader.startsWith('Bearer ')) {
337
+ const token = authHeader.substring(7);
338
+
339
+ // En una implementación real, aquí verificaríamos el token con el proveedor OAuth2
340
+ // Por ahora, simplemente simulamos la verificación
341
+ try {
342
+ // Simular verificación del token con el proveedor OAuth2
343
+ // En una implementación real, haríamos una solicitud HTTP al proveedor
344
+ // para verificar la validez del token
345
+ req.oauth2User = { token: token, verified: true };
346
+ return Promise.resolve(true);
347
+ } catch (error) {
348
+ return Promise.resolve(false);
349
+ }
350
+ }
351
+
352
+ // Si no hay sesión ni token en header, la autenticación falla
353
+ return Promise.resolve(false);
354
+ }
355
+
356
+ // Si hay sesión, verificar si ya tenemos un token de acceso
357
+ const accessToken = req.session.accessToken;
358
+
359
+ if (accessToken) {
360
+ // Verificar validez del token si es posible
361
+ try {
362
+ // Aquí normalmente haríamos una llamada para verificar el token
363
+ // Por simplicidad en este ejemplo, asumiremos que es válido si existe
364
+ return Promise.resolve(true);
365
+ } catch (error) {
366
+ return Promise.resolve(false);
367
+ }
368
+ }
369
+
370
+ // Si no hay token, verificar si estamos en el proceso de callback
371
+ const parsedUrl = require('url').parse(req.url, true);
372
+ const { query } = parsedUrl;
373
+
374
+ if (query.code) {
375
+ // Este es el callback con el código de autorización
376
+ try {
377
+ // Intercambiar el código por un token de acceso
378
+ const tokenResponse = await this.exchangeCodeForToken(
379
+ query.code,
380
+ clientId,
381
+ clientSecret,
382
+ callbackURL,
383
+ tokenURL
384
+ );
385
+
386
+ // Almacenar el token en la sesión si existe
387
+ if (req.session) {
388
+ req.session.accessToken = tokenResponse.access_token;
389
+ req.session.refreshToken = tokenResponse.refresh_token;
390
+ }
391
+
392
+ // Agregar información del usuario a la solicitud
393
+ req.oauth2User = {
394
+ accessToken: tokenResponse.access_token,
395
+ tokenType: tokenResponse.token_type
396
+ };
397
+
398
+ return Promise.resolve(true);
399
+ } catch (error) {
400
+ console.error('Error en el proceso de OAuth2:', error);
401
+ return Promise.resolve(false);
402
+ }
403
+ }
404
+
405
+ // Si no hay token ni código de autorización, la autenticación falla
406
+ return Promise.resolve(false);
407
+ };
408
+ }
409
+
410
+ /**
411
+ * Método auxiliar para intercambiar código por token
412
+ * @private
413
+ */
414
+ async exchangeCodeForToken(code, clientId, clientSecret, callbackURL, tokenURL) {
415
+ const https = require('https');
416
+ const querystring = require('querystring');
417
+
418
+ const postData = querystring.stringify({
419
+ grant_type: 'authorization_code',
420
+ client_id: clientId,
421
+ client_secret: clientSecret,
422
+ redirect_uri: callbackURL,
423
+ code: code
424
+ });
425
+
426
+ const options = {
427
+ method: 'POST',
428
+ headers: {
429
+ 'Content-Type': 'application/x-www-form-urlencoded',
430
+ 'Content-Length': Buffer.byteLength(postData)
431
+ }
432
+ };
433
+
434
+ return new Promise((resolve, reject) => {
435
+ const req = https.request(tokenURL, options, (res) => {
436
+ let data = '';
437
+
438
+ res.on('data', (chunk) => {
439
+ data += chunk;
440
+ });
441
+
442
+ res.on('end', () => {
443
+ try {
444
+ const tokenResponse = JSON.parse(data);
445
+ resolve(tokenResponse);
446
+ } catch (error) {
447
+ reject(error);
448
+ }
449
+ });
450
+ });
451
+
452
+ req.on('error', (error) => {
453
+ reject(error);
454
+ });
455
+
456
+ req.write(postData);
457
+ req.end();
458
+ });
459
+ }
460
+
461
+ /**
462
+ * Estrategia de autenticación OpenID Connect
463
+ * @param {Object} options - Opciones para la estrategia OIDC
464
+ * @param {string} options.issuer - URL del proveedor OIDC
465
+ * @param {string} options.clientId - ID del cliente OIDC
466
+ * @param {string} options.clientSecret - Secreto del cliente OIDC
467
+ * @param {string} options.callbackURL - URL de callback para OIDC
468
+ * @returns {Function} - Estrategia de autenticación OIDC
469
+ */
470
+ oidcStrategy(options = {}) {
471
+ const { issuer, clientId, clientSecret, callbackURL } = options;
472
+
473
+ if (!issuer || !clientId || !clientSecret || !callbackURL) {
474
+ throw new Error('Opciones requeridas para OIDC: issuer, clientId, clientSecret, callbackURL');
475
+ }
476
+
477
+ return async (req, options = {}) => {
478
+ // Verificar si ya tenemos un token de ID
479
+ const idToken = req.session ? req.session.idToken : null;
480
+
481
+ if (idToken) {
482
+ try {
483
+ // Verificar el token de ID con la biblioteca adecuada
484
+ // Para esta implementación, necesitaríamos jsonwebtoken y jwks-rsa
485
+ const jwt = require('jsonwebtoken');
486
+ const jwksClient = require('jwks-rsa');
487
+
488
+ // Obtener la clave pública del proveedor OIDC
489
+ const client = jwksClient({
490
+ jwksUri: `${issuer}/.well-known/jwks.json`
491
+ });
492
+
493
+ const getKey = (header, callback) => {
494
+ client.getSigningKey(header.kid, (err, key) => {
495
+ const signingKey = key.publicKey || key.rsaPublicKey;
496
+ callback(null, signingKey);
497
+ });
498
+ };
499
+
500
+ return new Promise((resolve) => {
501
+ jwt.verify(idToken, getKey, {
502
+ audience: clientId,
503
+ issuer: issuer
504
+ }, (err, decoded) => {
505
+ if (err) {
506
+ console.error('Error verificando token OIDC:', err);
507
+ resolve(false);
508
+ } else {
509
+ // Agregar información del usuario a la solicitud
510
+ req.oidcUser = decoded;
511
+ resolve(true);
512
+ }
513
+ });
514
+ });
515
+ } catch (error) {
516
+ console.error('Error en la verificación OIDC:', error);
517
+ return Promise.resolve(false);
518
+ }
519
+ }
520
+
521
+ // Si no hay token, verificar si estamos en el proceso de callback
522
+ const parsedUrl = require('url').parse(req.url, true);
523
+ const { query } = parsedUrl;
524
+
525
+ if (query.code) {
526
+ // Este es el callback con el código de autorización
527
+ try {
528
+ // Intercambiar el código por tokens
529
+ const tokenResponse = await this.exchangeCodeForToken(
530
+ query.code,
531
+ clientId,
532
+ clientSecret,
533
+ callbackURL,
534
+ `${issuer}/token`
535
+ );
536
+
537
+ // Verificar y decodificar el ID token
538
+ const idToken = tokenResponse.id_token;
539
+
540
+ if (idToken) {
541
+ // Almacenar el token en la sesión si existe
542
+ if (req.session) {
543
+ req.session.idToken = idToken;
544
+ req.session.accessToken = tokenResponse.access_token;
545
+ }
546
+
547
+ // Agregar información del usuario a la solicitud
548
+ const jwt = require('jsonwebtoken');
549
+ req.oidcUser = jwt.decode(idToken);
550
+ }
551
+
552
+ return Promise.resolve(true);
553
+ } catch (error) {
554
+ console.error('Error en el proceso de OIDC:', error);
555
+ return Promise.resolve(false);
556
+ }
557
+ }
558
+
559
+ // Si no hay token ni código de autorización, la autenticación falla
560
+ return Promise.resolve(false);
561
+ };
562
+ }
563
+ }
564
+
565
+ module.exports = Authenticator;