@hemia/jwt-manager 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -1,20 +1,23 @@
1
1
  # @hemia/jwt-manager
2
2
 
3
- Una clase sencilla y reutilizable para la generación, validación y decodificación de tokens JWT. Ideal para gestionar autenticación basada en tokens dentro de aplicaciones Node.js.
3
+ Una clase completa y robusta para la generación, validación y decodificación de tokens JWT con soporte completo para estándares **OIDC (OpenID Connect)** y **OAuth 2.0**. Ideal para gestionar autenticación y autorización basada en tokens dentro de aplicaciones Node.js modernas.
4
4
 
5
5
  ---
6
6
 
7
- ## Características principales
7
+ ## Características principales
8
8
 
9
- * Firma y verificación de tokens JWT
10
- * Soporte para claves secretas personalizadas o por entorno
11
- * Control del tiempo de expiración
12
- * Soporte para distintos tipos de token (como "clean credentials")
13
- * Decodificación sin validación
9
+ * **Estándares OIDC/OAuth 2.0**: Claims estándar y tokens compatibles con especificaciones oficiales
10
+ * 🔒 **Seguridad mejorada**: Algoritmos explícitos, validación de issuer/audience, clock skew tolerance
11
+ * 🎯 **Tres tipos de tokens**: ID Token, Access Token y Refresh Token
12
+ * 🔑 **Gestión de permisos**: Sistema de scopes para autorización granular
13
+ * 📝 **Claims estándar**: Soporte completo para profile, email, phone, address claims
14
+ * 🔄 **Retrocompatible**: Los métodos existentes siguen funcionando
15
+ * 🛡️ **Manejo de errores detallado**: Identificación específica de tipos de error
16
+ * 🚀 **TypeScript nativo**: Tipado completo y autocomplete
14
17
 
15
18
  ---
16
19
 
17
- ## Instalación
20
+ ## 📦 Instalación
18
21
 
19
22
  ```bash
20
23
  npm install @hemia/jwt-manager
@@ -22,69 +25,532 @@ npm install @hemia/jwt-manager
22
25
 
23
26
  ---
24
27
 
25
- ## Uso básico
28
+ ## 🚀 Inicio rápido
26
29
 
27
- ```ts
30
+ ```typescript
28
31
  import { JwtManager } from '@hemia/jwt-manager';
29
32
 
30
- const jwt = new JwtManager();
33
+ const jwtManager = new JwtManager();
34
+
35
+ // Crear un ID Token (OIDC)
36
+ const idToken = jwtManager.createIdToken({
37
+ sub: 'user_12345',
38
+ name: 'Cristian Mendez',
39
+ email: 'cristian@hemia.com',
40
+ email_verified: true
41
+ }, '1h');
42
+
43
+ // Crear un Access Token con permisos
44
+ const accessToken = jwtManager.createAccessToken(
45
+ 'user_12345',
46
+ ['read:products', 'write:orders'],
47
+ '15m'
48
+ );
49
+
50
+ // Validar token
51
+ const payload = jwtManager.verify(accessToken);
52
+ if (payload) {
53
+ console.log('✅ Token válido:', payload);
54
+ }
55
+
56
+ // Verificar permisos
57
+ const hasPermission = jwtManager.hasScope(accessToken, 'read:products');
58
+ console.log('Tiene permiso:', hasPermission);
59
+ ```
60
+
61
+ ---
62
+
63
+ ## 📖 API Reference
64
+
65
+ ### 🎫 Métodos de Creación de Tokens
66
+
67
+ #### `createIdToken(claims, expiresIn?)`
68
+
69
+ Crea un **ID Token** según estándar OIDC con información de identidad del usuario.
70
+
71
+ ```typescript
72
+ const idToken = jwtManager.createIdToken({
73
+ sub: 'user_12345', // Requerido
74
+ name: 'Cristian Mendez',
75
+ email: 'cristian@hemia.com',
76
+ email_verified: true,
77
+ picture: 'https://example.com/avatar.jpg',
78
+ preferred_username: 'cristianm',
79
+ locale: 'es-MX'
80
+ }, '1h');
81
+
82
+ // Token contiene:
83
+ // {
84
+ // sub, name, email, email_verified, picture, preferred_username, locale,
85
+ // iss: 'hemia-app',
86
+ // aud: 'hemia-api',
87
+ // iat: 1703779200,
88
+ // exp: 1703782800
89
+ // }
90
+ ```
91
+
92
+ #### `createAccessToken(sub, scopes, expiresIn?)`
93
+
94
+ Crea un **Access Token** OAuth 2.0 con scopes para autorización.
95
+
96
+ ```typescript
97
+ const accessToken = jwtManager.createAccessToken(
98
+ 'user_12345',
99
+ ['read:products', 'write:orders', 'admin:users'],
100
+ '15m' // Access tokens suelen ser de corta duración
101
+ );
102
+
103
+ // Token contiene:
104
+ // {
105
+ // sub: 'user_12345',
106
+ // scope: 'read:products write:orders admin:users',
107
+ // iss: 'hemia-app',
108
+ // aud: 'hemia-api',
109
+ // iat: 1703779200,
110
+ // exp: 1703780100
111
+ // }
112
+ ```
113
+
114
+ #### `createRefreshToken(sub, expiresIn?)`
115
+
116
+ Crea un **Refresh Token** para renovar access tokens expirados.
117
+
118
+ ```typescript
119
+ const refreshToken = jwtManager.createRefreshToken('user_12345', '30d');
120
+
121
+ // Token contiene:
122
+ // {
123
+ // sub: 'user_12345',
124
+ // type: 'refresh',
125
+ // jti: '1703779200-xyz123abc', // JWT ID único para revocación
126
+ // iss: 'hemia-app',
127
+ // aud: 'hemia-api',
128
+ // iat: 1703779200,
129
+ // exp: 1706371200
130
+ // }
131
+ ```
132
+
133
+ #### `createToken(payload, expiresIn?, options?)`
134
+
135
+ Crea un JWT genérico con payload personalizado (usa clave por defecto).
136
+
137
+ ```typescript
138
+ const token = jwtManager.createToken(
139
+ { userId: 'abc123', role: 'admin' },
140
+ '2h',
141
+ { issuer: 'my-app' } // Opcional
142
+ );
143
+ ```
144
+
145
+ #### `createTokenWithSecret(payload, secretKey, expiresIn?, options?)`
146
+
147
+ Crea un JWT genérico con una clave secreta específica.
148
+
149
+ ```typescript
150
+ const token = jwtManager.createTokenWithSecret(
151
+ { data: 'sensitive' },
152
+ 'custom-secret-key',
153
+ '1h'
154
+ );
155
+ ```
156
+
157
+ #### `createCleanCredentialsToken(operative?, expiresIn?)`
158
+
159
+ Crea un token especial para operaciones sin credenciales completas.
31
160
 
32
- const token = jwt.getTokenWithoutKey({ userId: 'abc123' });
33
- const decoded = jwt.validateToken(token);
161
+ ```typescript
162
+ const token = jwtManager.createCleanCredentialsToken(Operatives.CATALOG, '2h');
34
163
  ```
35
164
 
36
165
  ---
37
166
 
38
- ## API
167
+ ### ✅ Métodos de Validación
39
168
 
40
- ### `getTokenWithoutKey(payload, expiresIn?, options?)`
169
+ #### `verify(token, secretKey?, options?)`
41
170
 
42
- Genera un token firmado con la clave definida por entorno (`JWT_SECRET`).
171
+ Valida un JWT y retorna el payload si es válido.
43
172
 
44
- ### `getTokenWithKey(payload, secretKey, expiresIn?, options?)`
173
+ ```typescript
174
+ const payload = jwtManager.verify(token);
45
175
 
46
- Genera un token firmado con una clave personalizada.
176
+ if (payload) {
177
+ console.log('✅ Token válido');
178
+ console.log('Usuario:', payload.sub);
179
+ console.log('Email:', payload.email);
180
+ } else {
181
+ console.log('❌ Token inválido o expirado');
182
+ }
47
183
 
48
- ### `getTokenCleanCredentials(operative, expiresIn?)`
184
+ // Con secret key personalizada
185
+ const payload2 = jwtManager.verify(token, 'custom-secret');
186
+ ```
187
+
188
+ #### `verifyDetailed(token, secretKey?, options?)`
189
+
190
+ Valida un JWT y retorna resultado detallado con información de error.
191
+
192
+ ```typescript
193
+ const result = jwtManager.verifyDetailed(token);
194
+
195
+ console.log('Válido:', result.valid);
196
+
197
+ if (result.valid) {
198
+ console.log('Payload:', result.payload);
199
+ } else {
200
+ console.log('Error:', result.error);
201
+ console.log('Tipo de error:', result.errorType);
202
+ // errorType: 'expired' | 'invalid' | 'not_before' | 'malformed' | 'signature_invalid'
203
+ }
204
+ ```
205
+
206
+ ---
207
+
208
+ ### 🔍 Métodos de Decodificación y Consulta
209
+
210
+ #### `decode(token, complete?)`
211
+
212
+ Decodifica un JWT **sin validarlo** (no verifica firma ni expiración).
49
213
 
50
- Genera un token especial con un `accessType` para operaciones específicas (como `catalog`).
214
+ ```typescript
215
+ // Solo payload
216
+ const payload = jwtManager.decode(token);
51
217
 
52
- ### `validateToken(token, secretKey?)`
218
+ // Con header incluido
219
+ const full = jwtManager.decode(token, true);
220
+ console.log('Header:', full.header); // { alg: 'HS256', typ: 'JWT' }
221
+ console.log('Payload:', full.payload);
222
+ ```
223
+
224
+ #### `getClaims(token)`
53
225
 
54
- Valida un token JWT. Si no se pasa `secretKey`, usa la interna.
226
+ Extrae solo los **claims estándar OIDC** de un token.
55
227
 
56
- ### `decode(token)`
228
+ ```typescript
229
+ const claims = jwtManager.getClaims(token);
230
+ // Retorna solo: sub, name, email, email_verified, picture, etc.
231
+ // NO incluye: iss, aud, iat, exp (que son JWT claims)
232
+ ```
57
233
 
58
- Decodifica un token sin verificar su firma. Útil para leer datos sin validar.
234
+ #### `hasScope(token, requiredScope)`
59
235
 
60
- ### `getJwtKey()`
236
+ Verifica si un token tiene un scope/permiso específico.
61
237
 
62
- Devuelve la clave interna usada para firmar los tokens.
238
+ ```typescript
239
+ const canRead = jwtManager.hasScope(token, 'read:products');
240
+ const canDelete = jwtManager.hasScope(token, 'delete:products');
241
+
242
+ if (canRead) {
243
+ console.log('✅ Usuario puede leer productos');
244
+ }
245
+ ```
246
+
247
+ #### `getJwtKey()`
248
+
249
+ Obtiene la clave secreta configurada.
250
+
251
+ ```typescript
252
+ const secretKey = jwtManager.getJwtKey();
253
+ ```
63
254
 
64
255
  ---
65
256
 
66
- ## Variables de entorno
257
+ ### 🔄 Métodos Legacy (Deprecated)
258
+
259
+ Estos métodos siguen funcionando para mantener compatibilidad hacia atrás:
260
+
261
+ ```typescript
262
+ // ⚠️ Deprecated - Usa createToken() en su lugar
263
+ jwtManager.getTokenWithoutKey(payload, expiresIn, options);
264
+
265
+ // ⚠️ Deprecated - Usa createTokenWithSecret() en su lugar
266
+ jwtManager.getTokenWithKey(payload, secretKey, expiresIn, options);
267
+
268
+ // ⚠️ Deprecated - Usa createCleanCredentialsToken() en su lugar
269
+ jwtManager.getTokenCleanCredentials(operative, expiresIn);
270
+
271
+ // ⚠️ Deprecated - Usa verify() en su lugar
272
+ jwtManager.validateToken(token, secretKey, options);
273
+
274
+ // ⚠️ Deprecated - Usa verifyDetailed() en su lugar
275
+ jwtManager.validateTokenDetailed(token, secretKey, options);
276
+
277
+ // ⚠️ Deprecated - Usa getClaims() en su lugar
278
+ jwtManager.getStandardClaims(token);
279
+ ```
280
+
281
+ ---
282
+
283
+ ## 🔧 Configuración
284
+
285
+ ### Variables de entorno
67
286
 
68
287
  ```env
69
- JWT_SECRET=mi_clave_super_secreta
288
+ # Clave secreta para firmar tokens (REQUERIDO en producción)
289
+ JWT_SECRET=tu_clave_super_secreta_aqui
290
+
291
+ # Emisor de los tokens (quien genera el token)
292
+ JWT_ISSUER=hemia-app
293
+
294
+ # Audiencia de los tokens (para quién es el token)
295
+ JWT_AUDIENCE=hemia-api
70
296
  ```
71
297
 
72
- > ⚠️ **Importante:** No uses una clave por defecto en producción. Define `JWT_SECRET` siempre en tus entornos seguros.
298
+ > ⚠️ **Importante:** Nunca uses una clave por defecto en producción. Define `JWT_SECRET` siempre en entornos seguros.
73
299
 
74
300
  ---
75
301
 
76
- ## Ejemplo con expiración y opciones
302
+ ## 💡 Ejemplos de Uso
303
+
304
+ ### Ejemplo 1: Autenticación completa
305
+
306
+ ```typescript
307
+ import { JwtManager } from '@hemia/jwt-manager';
308
+
309
+ const jwtManager = new JwtManager();
77
310
 
78
- ```ts
79
- const token = jwt.getTokenWithoutKey(
80
- { userId: 'abc123' },
81
- '1h', // expira en 1 hora
82
- { issuer: 'hemia' }
311
+ // 1. Usuario inicia sesión
312
+ const idToken = jwtManager.createIdToken({
313
+ sub: 'user_12345',
314
+ name: 'Cristian Mendez',
315
+ email: 'cristian@hemia.com',
316
+ email_verified: true
317
+ }, '1h');
318
+
319
+ const accessToken = jwtManager.createAccessToken(
320
+ 'user_12345',
321
+ ['read:profile', 'read:products', 'write:orders'],
322
+ '15m'
83
323
  );
324
+
325
+ const refreshToken = jwtManager.createRefreshToken('user_12345', '30d');
326
+
327
+ // 2. Enviar tokens al cliente
328
+ res.json({ idToken, accessToken, refreshToken });
329
+
330
+ // 3. Cliente usa accessToken en requests
331
+ // Authorization: Bearer <accessToken>
332
+
333
+ // 4. Validar token en cada request
334
+ const payload = jwtManager.verify(accessToken);
335
+ if (!payload) {
336
+ return res.status(401).json({ error: 'Token inválido' });
337
+ }
338
+
339
+ // 5. Verificar permisos específicos
340
+ if (!jwtManager.hasScope(accessToken, 'write:orders')) {
341
+ return res.status(403).json({ error: 'Sin permisos' });
342
+ }
84
343
  ```
85
344
 
345
+ ### Ejemplo 2: Middleware de autenticación (Express)
346
+
347
+ ```typescript
348
+ import { Request, Response, NextFunction } from 'express';
349
+ import { JwtManager } from '@hemia/jwt-manager';
350
+
351
+ const jwtManager = new JwtManager();
352
+
353
+ // Middleware de autenticación
354
+ export const authenticateToken = (req: Request, res: Response, next: NextFunction) => {
355
+ const authHeader = req.headers['authorization'];
356
+ const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
357
+
358
+ if (!token) {
359
+ return res.status(401).json({ error: 'Token no proporcionado' });
360
+ }
361
+
362
+ const payload = jwtManager.verify(token);
363
+
364
+ if (!payload) {
365
+ return res.status(403).json({ error: 'Token inválido o expirado' });
366
+ }
367
+
368
+ req.user = payload;
369
+ next();
370
+ };
371
+
372
+ // Middleware de autorización por scope
373
+ export const requireScope = (requiredScope: string) => {
374
+ return (req: Request, res: Response, next: NextFunction) => {
375
+ const authHeader = req.headers['authorization'];
376
+ const token = authHeader && authHeader.split(' ')[1];
377
+
378
+ if (!token || !jwtManager.hasScope(token, requiredScope)) {
379
+ return res.status(403).json({
380
+ error: `Permiso denegado. Se requiere: ${requiredScope}`
381
+ });
382
+ }
383
+
384
+ next();
385
+ };
386
+ };
387
+
388
+ // Uso en rutas
389
+ app.get('/profile', authenticateToken, (req, res) => {
390
+ res.json({ user: req.user });
391
+ });
392
+
393
+ app.get('/products', authenticateToken, requireScope('read:products'), (req, res) => {
394
+ res.json({ products: [...] });
395
+ });
396
+
397
+ app.post('/orders', authenticateToken, requireScope('write:orders'), (req, res) => {
398
+ res.json({ success: true });
399
+ });
400
+ ```
401
+
402
+ ### Ejemplo 3: Manejo de errores detallado
403
+
404
+ ```typescript
405
+ const result = jwtManager.verifyDetailed(token);
406
+
407
+ if (!result.valid) {
408
+ switch (result.errorType) {
409
+ case 'expired':
410
+ // Token expirado - solicitar refresh
411
+ return res.status(401).json({
412
+ error: 'Token expirado',
413
+ code: 'TOKEN_EXPIRED',
414
+ message: 'Por favor renueva tu sesión'
415
+ });
416
+
417
+ case 'signature_invalid':
418
+ // Firma inválida - posible manipulación
419
+ console.error('⚠️ Token con firma inválida detectado');
420
+ return res.status(403).json({
421
+ error: 'Token inválido',
422
+ code: 'INVALID_SIGNATURE'
423
+ });
424
+
425
+ case 'not_before':
426
+ // Token aún no válido
427
+ return res.status(403).json({
428
+ error: 'Token no válido aún',
429
+ code: 'TOKEN_NOT_YET_VALID'
430
+ });
431
+
432
+ default:
433
+ return res.status(403).json({
434
+ error: 'Token inválido',
435
+ code: 'INVALID_TOKEN'
436
+ });
437
+ }
438
+ }
439
+
440
+ // Token válido
441
+ console.log('Usuario autenticado:', result.payload.sub);
442
+ ```
443
+
444
+ ### Ejemplo 4: Renovar tokens con Refresh Token
445
+
446
+ ```typescript
447
+ app.post('/auth/refresh', async (req, res) => {
448
+ const { refreshToken } = req.body;
449
+
450
+ // Validar refresh token
451
+ const payload = jwtManager.verify(refreshToken);
452
+
453
+ if (!payload || payload.type !== 'refresh') {
454
+ return res.status(401).json({ error: 'Refresh token inválido' });
455
+ }
456
+
457
+ // Opcional: Verificar que el jti no esté en lista negra
458
+ // const isRevoked = await checkIfRevoked(payload.jti);
459
+ // if (isRevoked) return res.status(401).json({ error: 'Token revocado' });
460
+
461
+ // Generar nuevos tokens
462
+ const newAccessToken = jwtManager.createAccessToken(
463
+ payload.sub,
464
+ ['read:profile', 'read:products'],
465
+ '15m'
466
+ );
467
+
468
+ const newRefreshToken = jwtManager.createRefreshToken(payload.sub, '30d');
469
+
470
+ res.json({
471
+ accessToken: newAccessToken,
472
+ refreshToken: newRefreshToken
473
+ });
474
+ });
475
+ ```
476
+
477
+ ---
478
+
479
+ ## 📚 TypeScript Types
480
+
481
+ ```typescript
482
+ import {
483
+ JwtManager,
484
+ StandardClaims,
485
+ TokenPayload,
486
+ TokenValidationResult,
487
+ TokenType
488
+ } from '@hemia/jwt-manager';
489
+
490
+ // StandardClaims (OIDC)
491
+ interface StandardClaims {
492
+ sub: string; // Subject (required)
493
+ name?: string;
494
+ given_name?: string;
495
+ family_name?: string;
496
+ email?: string;
497
+ email_verified?: boolean;
498
+ picture?: string;
499
+ // ... más claims
500
+ }
501
+
502
+ // TokenValidationResult
503
+ interface TokenValidationResult {
504
+ valid: boolean;
505
+ payload?: any;
506
+ error?: string;
507
+ errorType?: 'expired' | 'invalid' | 'not_before' | 'malformed' | 'signature_invalid';
508
+ }
509
+
510
+ // TokenType
511
+ enum TokenType {
512
+ ID_TOKEN = 'id_token',
513
+ ACCESS_TOKEN = 'access_token',
514
+ REFRESH_TOKEN = 'refresh_token'
515
+ }
516
+ ```
517
+
518
+ ---
519
+
520
+ ## 🔒 Mejoras de Seguridad Implementadas
521
+
522
+ ### ✅ Algoritmo Explícito
523
+ Previene ataques de confusión de algoritmo especificando siempre `HS256` o `RS256`.
524
+
525
+ ### ✅ Validación de Issuer y Audience
526
+ Verifica que el token fue emitido por quien dice serlo y para la audiencia correcta.
527
+
528
+ ### ✅ Clock Skew Tolerance
529
+ 30 segundos de tolerancia para diferencias de reloj entre servidores.
530
+
531
+ ### ✅ JWT ID (jti) para Refresh Tokens
532
+ Permite implementar revocación de tokens mediante lista negra.
533
+
534
+ ### ✅ Lista Blanca de Algoritmos
535
+ Solo acepta `HS256` y `RS256`, rechazando algoritmos inseguros como `none`.
536
+
537
+ ---
538
+
539
+ ## 📖 Documentación Adicional
540
+
541
+ Para más ejemplos y documentación detallada, consulta [USAGE_EXAMPLES.md](./USAGE_EXAMPLES.md).
542
+
543
+ ---
544
+
545
+ ## 🌐 Recursos
546
+
547
+ - [OpenID Connect Standard Claims](https://www.cerberauth.com/blog/openid-connect-standard-claims/)
548
+ - [RFC 7519 - JWT](https://tools.ietf.org/html/rfc7519)
549
+ - [OAuth 2.0 Scopes](https://oauth.net/2/scope/)
550
+ - [OIDC Specification](https://openid.net/specs/openid-connect-core-1_0.html)
551
+
86
552
  ---
87
553
 
88
- ## Licencia
554
+ ## 📄 Licencia
89
555
 
90
556
  MIT — Desarrollado por [Hemia Technologies](https://hemia.mx)