UNPKG

@theoptimalpartner/jwt-auth-validator

Version:

JWT token validation package with offline JWKS validation and Redis-based token revocation support

495 lines (381 loc) 13.4 kB
# JWT Auth Validator Un package de Node.js para validación offline de tokens JWT con soporte para JWKS y lista negra de tokens en Redis. Diseñado especialmente para tokens de AWS Cognito. ## Características - ✅ **Validación offline de JWT** con verificación de firma usando JWKS - ✅ **Cache inteligente** de claves públicas para mejor rendimiento - ✅ **Lista negra de tokens** usando Redis para revocación inmediata - ✅ **Soporte completo para AWS Cognito** incluyendo client secret - ✅ **Cognito Client Secret** para configuraciones seguras - ✅ **TypeScript** con tipado completo - ✅ **Validación flexible** (modo desarrollo vs producción) - ✅ **Compatible con Node.js 18+** (última versión LTS) ## Instalación ```bash npm install @theoptimalpartner/jwt-auth-validator ioredis ``` > **Nota:** `ioredis` es requerido ya que el paquete siempre verifica la lista negra de tokens revocados para máxima seguridad. ## Uso Rápido ### Configuración básica para AWS Cognito ```typescript import { createCognitoValidator } from "@theoptimalpartner/jwt-auth-validator"; // Configuración usando variables de entorno (recomendado) const validator = createCognitoValidator( "us-east-1", // AWS region "us-east-1_XXXXXXXXX", // User Pool ID "your-client-id", // Client ID (opcional) "your-client-secret" // Client Secret (opcional, para mayor seguridad) // Redis se configura automáticamente con variables de entorno ); // O configuración manual con Redis const validator = createCognitoValidator( "us-east-1", "us-east-1_XXXXXXXXX", "your-client-id", "your-client-secret", // Client Secret opcional { host: "your-redis-host.com", port: 6379, password: "your-password", tls: true, caCertPath: "/path/to/certs", caCertName: "redis-ca.crt" } ); // Validar un token const authHeader = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."; const token = validator.extractTokenFromHeader(authHeader); if (token) { const result = await validator.validateToken(token); if (result.valid) { console.log("Token válido:", result.decoded); // result.decoded contiene el payload del token } else { console.log("Token inválido:", result.error); } } ``` ### Configuración avanzada con Redis ```typescript import { createCognitoValidator } from "@theoptimalpartner/jwt-auth-validator"; // Configuración con Redis para lista negra de tokens const validator = createCognitoValidator( "us-east-1", "us-east-1_XXXXXXXXX", "your-client-id", { host: "your-redis-host.com", port: 6380, password: "your-redis-password", tls: true, // Para conexiones seguras } ); // El validator ahora verificará automáticamente la lista negra const result = await validator.validateToken(token); ``` ## API Completa ### JWTValidator #### Métodos principales ```typescript // Validación principal (automática según ambiente) await validator.validateToken(token: string, forceSecure?: boolean): Promise<ValidationResult> // Validación específica para Access Tokens await validator.validateAccessToken(token: string): Promise<ValidationResult> // Validación específica para ID Tokens await validator.validateIdToken(token: string): Promise<ValidationResult> // Validación múltiple en lote await validator.validateMultipleTokens(tokens: string[]): Promise<ValidationResult[]> ``` #### Utilidades ```typescript // Extraer token del header Authorization validator.extractTokenFromHeader(authHeader: string): string | null // Verificar si un token está expirado validator.isTokenExpired(token: string): boolean // Obtener tiempo restante hasta expiración (en segundos) validator.getTimeToExpiry(token: string): number // Decodificar token sin validar validator.decodeToken(token: string): DecodedToken | null // Obtener información completa del token validator.getTokenInfo(token: string): object | null ``` #### Gestión de lista negra (requiere Redis) ```typescript // Revocar un token específico await validator.revokeToken(token: string): Promise<void> // Revocar todos los tokens de un usuario await validator.revokeUserTokens(userId: string, tokens: string[]): Promise<void> ``` #### Estadísticas y monitoreo ```typescript // Estadísticas del cache JWKS validator.getCacheStats(); // Estadísticas de la lista negra await validator.getBlacklistStats(); // Cerrar conexiones await validator.disconnect(); ``` ## Configuración Manual Para casos de uso avanzados, puedes configurar manualmente: ```typescript import { JWTValidator } from "@theoptimalpartner/jwt-auth-validator"; const validator = new JWTValidator({ jwks: { jwksUri: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX/.well-known/jwks.json", issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX", audience: "your-client-id", clientSecret: "your-client-secret", // Opcional, para mayor seguridad cacheTimeout: 3600, // Cache de claves por 1 hora }, enableRedisBlacklist: true, forceSecureValidation: true, // Siempre usar validación JWKS redis: { host: "your-redis-host.com", port: 6380, password: "your-password", tls: { rejectUnauthorized: true, checkServerIdentity: () => undefined, servername: "your-redis-host.com", minVersion: "TLSv1.2", maxVersion: "TLSv1.3", }, family: 4, connectTimeout: 60000, commandTimeout: 30000, maxRetriesPerRequest: 3, reconnectOnError: (err) => { const reconnectErrors = ["READONLY", "ECONNRESET", "EPIPE"]; return reconnectErrors.some((target) => err.message.includes(target)); }, }, }); ``` ## Ejemplos de Uso ### Express.js Middleware ```typescript import express from "express"; import { createCognitoValidator } from "@theoptimalpartner/jwt-auth-validator"; const app = express(); const validator = createCognitoValidator("us-east-1", "us-east-1_XXXXXXXXX"); // Middleware de autenticación const authMiddleware = async (req: any, res: any, next: any) => { try { const authHeader = req.headers.authorization; const token = validator.extractTokenFromHeader(authHeader); if (!token) { return res.status(401).json({ error: "Token missing" }); } const result = await validator.validateAccessToken(token); if (!result.valid) { return res.status(401).json({ error: result.error }); } req.user = result.decoded; next(); } catch (error) { res.status(500).json({ error: "Authentication error" }); } }; app.use("/api/protected", authMiddleware); app.get("/api/protected/profile", (req: any, res: any) => { res.json({ user: req.user }); }); ``` ### Nest.js Guard ```typescript import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common"; import { createCognitoValidator } from "@theoptimalpartner/jwt-auth-validator"; @Injectable() export class JwtAuthGuard implements CanActivate { private validator = createCognitoValidator( process.env.AWS_REGION!, process.env.COGNITO_USER_POOL_ID!, process.env.COGNITO_CLIENT_ID ); async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const authHeader = request.headers.authorization; const token = this.validator.extractTokenFromHeader(authHeader); if (!token) return false; const result = await this.validator.validateToken(token); if (result.valid) { request.user = result.decoded; return true; } return false; } } ``` ### Lambda Authorizer ```typescript import { createCognitoValidator } from "@theoptimalpartner/jwt-auth-validator"; const validator = createCognitoValidator( process.env.AWS_REGION!, process.env.COGNITO_USER_POOL_ID! ); export const handler = async (event: any) => { try { const token = validator.extractTokenFromHeader(event.authorizationToken); if (!token) { throw new Error("Unauthorized"); } const result = await validator.validateToken(token); if (!result.valid) { throw new Error("Unauthorized"); } return { principalId: result.decoded!.sub, policyDocument: { Version: "2012-10-17", Statement: [ { Action: "execute-api:Invoke", Effect: "Allow", Resource: event.methodArn, }, ], }, context: { userId: result.decoded!.sub, email: result.decoded!.email, }, }; } catch (error) { throw new Error("Unauthorized"); } }; ``` ## Variables de Entorno ```bash # Configuración básica AWS_REGION=us-east-1 COGNITO_USER_POOL_ID=us-east-1_XXXXXXXXX COGNITO_CLIENT_ID=your-client-id COGNITO_CLIENT_SECRET=your-client-secret # Opcional, para configuraciones seguras # Configuración Redis (requerido) REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD='' REDIS_TLS=false REDIS_CA_CERT_PATH=/path/to/certs REDIS_CA_CERT_NAME=redis-ca.crt # Forzar validación segura NODE_ENV=production # Automáticamente usa validación JWKS en producción ``` ## AWS Cognito Client Secret ### ¿Qué es el Client Secret? El Client Secret es una configuración adicional de seguridad en AWS Cognito que requiere que todas las operaciones incluyan un hash calculado usando HMAC-SHA256. Esto añade una capa extra de seguridad a tu aplicación. ### Cuándo usar Client Secret - **Aplicaciones del lado del servidor**: Donde puedes mantener el secret seguro - **Microservicios**: Para validación entre servicios - **Entornos altamente seguros**: Donde se requiere autenticación adicional ### Configuración con Client Secret ```typescript import { createCognitoValidator } from "@theoptimalpartner/jwt-auth-validator"; // Método 1: Parámetro directo const validator = createCognitoValidator( "us-east-1", "us-east-1_XXXXXXXXX", "your-client-id", "your-client-secret" ); // Método 2: Variable de entorno (recomendado) process.env.COGNITO_CLIENT_SECRET = "your-client-secret"; const validator = createCognitoValidator( "us-east-1", "us-east-1_XXXXXXXXX", "your-client-id" ); // Verificar si el client secret está configurado console.log("Client secret configurado:", validator.hasClientSecret()); // Calcular secret hash para operaciones de Cognito const secretHash = validator.calculateSecretHash("user@example.com"); console.log("Secret hash:", secretHash); ``` ### Funciones utilitarias para Client Secret ```typescript import { calculateSecretHash, hasClientSecret, safeCalculateSecretHash } from "@theoptimalpartner/jwt-auth-validator"; // Calcular hash manualmente const hash = calculateSecretHash({ identifier: "user@example.com", clientId: "your-client-id", clientSecret: "your-client-secret" }); // Verificar si un secret está disponible const hasSecret = hasClientSecret("your-client-secret"); // Calcular hash de forma segura (maneja errores) const safeHash = safeCalculateSecretHash( "user@example.com", "your-client-id", "your-client-secret" ); ``` ### Notas importantes sobre Client Secret - **Seguridad**: Nunca expongas el client secret en el frontend - **Compatibilidad**: Solo usar cuando tu configuración de Cognito lo requiera - **Opcional**: La librería funciona perfectamente sin client secret - **Consistencia**: Los hashes generados son compatibles con AWS SDK ## Tipos TypeScript ```typescript interface DecodedToken { sub: string; email?: string; email_verified?: boolean; phone_number?: string; phone_number_verified?: boolean; aud: string; iss: string; exp: number; iat: number; token_use: "access" | "id"; scope?: string; auth_time?: number; jti?: string; username?: string; "cognito:username"?: string; "cognito:groups"?: string[]; [key: string]: unknown; } interface ValidationResult { valid: boolean; decoded?: DecodedToken; error?: string; } ``` ## Rendimiento ### Cache JWKS - Las claves públicas se cachean por 1 hora por defecto - Reduce significativamente las consultas a los endpoints JWKS - Cache configurable por necesidades específicas ### Validación offline - No requiere llamadas al servicio de autenticación para validar tokens - Validación local usando claves públicas cacheadas - Ideal para microservicios y alta concurrencia ### Lista negra eficiente - Usa Redis para almacenamiento distribuido de tokens revocados - TTL automático basado en la expiración del token - Optimización de memoria usando hashes de tokens ## Seguridad ### Validación robusta - Verificación de firma usando claves públicas - Validación de claims estándar (exp, iss, aud) - Verificación específica de tokens Cognito ### Modo de desarrollo - Validación básica sin verificación de firma para desarrollo - Advertencias claras cuando no se usa validación segura - Automáticamente usa validación segura en producción ## Licencia MIT ## Contribuciones Las contribuciones son bienvenidas. Por favor: 1. Fork el repositorio 2. Crea una rama para tu feature (`git checkout -b feature/nueva-funcionalidad`) 3. Commit tus cambios (`git commit -am 'Agregar nueva funcionalidad'`) 4. Push a la rama (`git push origin feature/nueva-funcionalidad`) 5. Crea un Pull Request ## Soporte Para reportar bugs o solicitar features, por favor crea un issue en el repositorio.