UNPKG

@theoptimalpartner/jwt-auth-validator

Version:

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

1,658 lines (1,298 loc) 51.8 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 - ✅ **Enriquecimiento de datos de usuario** con información contextual desde Redis - ✅ **Integración completa con auth-service** (permisos, organizaciones, aplicaciones) - ✅ **Logging inteligente** - mensajes limpios en producción, debug detallado en desarrollo - ✅ **Manejo de errores mejorado** - mensajes user-friendly en lugar de stack traces técnicos - ✅ **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. ## 🚀 Nueva API Simplificada (v2.0+) ### ✨ Un Solo Método Principal - `validate()` ```typescript import { createCognitoValidator } from "@theoptimalpartner/jwt-auth-validator"; // Configuración con conexión Redis para blacklist y datos de usuario const validator = createCognitoValidator( "us-east-1", "us-east-1_XXXXXXXXX", "your-client-id", "your-client-secret", { // Configuración Redis para blacklist de tokens y enriquecimiento de datos host: process.env.REDIS_HOST || "your-redis-host.com", port: parseInt(process.env.REDIS_PORT || "6379"), password: process.env.REDIS_PASSWORD, tls: process.env.REDIS_TLS === 'true', // Opción SSM - Certificado desde AWS Parameter Store (compatible con auth-service) caCertPath: process.env.REDIS_CA_CERT_PATH, // ej: "redis" caCertName: process.env.REDIS_CA_CERT_NAME // ej: "ca-cert" }, true, // enableApiKeyValidation (opcional) true // enableUserDataRetrieval (opcional) ); // Inicializar conexión Redis await validator.initialize(); // 🌟 NUEVO: Un método principal que hace todo const result = await validator.validate(token, { apiKey: 'your-api-key', // Opcional - validación de API key forceSecure: true, // Opcional - forzar JWKS en desarrollo enrichUserData: true, // Opcional - incluir datos del usuario requireAppAccess: false // Opcional - requiere acceso a aplicación }); if (result.valid) { console.log("✅ Token válido:"); console.log("Usuario:", result.decoded?.sub); console.log("Permisos:", result.userPermissions); console.log("Organizaciones:", result.userOrganizations); console.log("Aplicaciones:", result.applications); } else { console.log("❌ Token inválido:", result.error); } ``` ### 🎯 Métodos Especializados Simples ```typescript // Solo validar JWT token (incluye blacklist check) const basic = await validator.validateToken(token); // Con API key (validación automática de appId) const withApi = await validator.validateWithApiKey(token, apiKey); // Datos completos del usuario const enriched = await validator.validateEnriched(token, apiKey); // Acceso estricto a aplicación (DEBE tener acceso) const strict = await validator.validateWithAppAccess(token, apiKey); ``` ### ⚡ Uso Rápido - Casos Comunes ```typescript // 1. Validación básica de JWT const result = await validator.validateToken(token); // 2. Con API key y datos del usuario const result = await validator.validateWithApiKey(token, apiKey); // 3. Todo: JWT + API key + datos completos + verificación de appId const result = await validator.validate(token, { apiKey: 'your-api-key', enrichUserData: true, requireAppAccess: true }); ``` ## 📊 Validación con Datos Enriquecidos ### Datos Completos del Usuario Automáticamente Con la nueva API, obtener datos del usuario es súper fácil: ```typescript // 🌟 Automático: JWT + API key + datos completos del usuario const result = await validator.validateEnriched(token, apiKey); if (result.valid) { console.log("✅ Usuario autenticado:", result.decoded?.sub); // 📊 Datos enriquecidos incluidos automáticamente: console.log("🔑 Permisos:", result.userPermissions); console.log("🏢 Organizaciones:", result.userOrganizations); console.log("📱 Aplicaciones:", result.applications); // 🛡️ Autorización basada en datos del usuario const isAdmin = result.userOrganizations?.some(org => org.appId === 'my-app' && org.roles.includes('admin') ); const hasPermission = result.userPermissions?.permissions?.some(p => p.includes('users:manage') ); } ``` ### Control Granular de Datos ```typescript // Solo token (sin datos extras) const basic = await validator.validateToken(token); // Con datos del usuario incluidos const enriched = await validator.validate(token, { apiKey: 'your-api-key', enrichUserData: true // 👈 Controla si incluir datos del usuario }); // Sin datos del usuario (más rápido) const fast = await validator.validate(token, { apiKey: 'your-api-key', enrichUserData: false // 👈 Solo validación básica }); ``` ### Acceso Directo a Datos de Usuario ```typescript // Obtener permisos de un usuario específico const userPermissions = await validator.getUserPermissions("user-123"); // Obtener organizaciones del usuario const userOrganizations = await validator.getUserOrganizations("user-123"); // Obtener aplicaciones a las que tiene acceso const userApplications = await validator.getUserApplications("user-123"); // Obtener datos completos del usuario const comprehensiveData = await validator.getComprehensiveUserData("user-123"); ``` ### Patrones de Clave Redis Compatibles El paquete es totalmente compatible con los patrones de clave de auth-service: - **Permisos de usuario**: `user:permissions:{userId}` - **Aplicaciones**: `app:{appId}` - **Organizaciones**: `org:{appId}:{organizationId}` - **Roles de aplicación**: `app:roles:{appId}:{organizationId}` - **Esquemas de aplicación**: `app-schemas` (clave global) - **Permisos efectivos**: `permissions:cache:{userId}:{appId}:{orgId}` ### Interfaces TypeScript para Datos de Usuario ```typescript interface EnrichedValidationResult extends ValidationResult { userPermissions?: UserPermissions | null; userOrganizations?: UserOrganization[]; applications?: Application[]; } interface UserPermissions { userId: string; permissions: { [appId: string]: { [organizationId: string]: OrganizationPermissions; }; }; cacheVersion?: number; } interface UserOrganization { appId: string; organizationId: string; roles: string[]; status: 'active' | 'suspended' | 'revoked'; effectivePermissions?: string[]; } interface Application { appId: string; name: string; description?: string; isActive: boolean; allowedDomains?: string[]; redirectUrls?: string[]; schema: AppSchema; createdAt: number; updatedAt: number; metadata?: Record<string, unknown>; } ``` ### Gestión de Cache ```typescript // Limpiar cache específico del usuario validator.clearUserCache("user-123"); // Limpiar todo el cache validator.clearAllCache(); // Obtener estadísticas del cache const stats = validator.getUserDataStats(); console.log(`Cache hits: ${stats.cacheHits}, misses: ${stats.cacheMisses}`); ``` ### Rendimiento y Fallback - **Cache local** con TTL configurable para reducir consultas Redis - **Fallback graceful**: Si Redis no está disponible, devuelve validación básica - **Lazy loading**: Solo carga datos cuando se solicita enriquecimiento - **Optimización de memoria**: Cache inteligente con limpieza automática ## 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 con API Key opcional await validator.validateTokenWithApiKey(token: string, apiKey?: string, forceSecure?: boolean): Promise<ValidationResult> // NUEVO: Validación con enriquecimiento de datos de usuario await validator.validateTokenEnriched(token: string, apiKey?: string, forceSecure?: boolean): Promise<EnrichedValidationResult> // 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 segura (siempre usa JWKS) await validator.validateTokenSecure(token: string): Promise<ValidationResult> // Validación básica (sin verificación JWKS) await validator.validateTokenBasic(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 // Extraer API key del header X-API-Key validator.extractApiKeyFromHeader(apiKeyHeader: string): string | null // Extraer API key de headers múltiples (X-API-Key, X-Api-Key, API-Key) validator.extractApiKeyFromHeaders(headers: Record<string, 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 ``` #### NUEVO: Métodos de datos de usuario ```typescript // Obtener permisos de usuario desde Redis await validator.getUserPermissions(userId: string): Promise<UserPermissions | null> // Obtener organizaciones del usuario await validator.getUserOrganizations(userId: string): Promise<UserOrganization[]> // Obtener aplicaciones accesibles por el usuario await validator.getUserApplications(userId: string): Promise<Application[]> // Obtener datos completos del usuario await validator.getComprehensiveUserData(userId: string): Promise<{ permissions: UserPermissions | null; organizations: UserOrganization[]; applications: Application[]; }> // Gestión de cache de datos de usuario validator.clearUserCache(userId: string): void validator.clearAllCache(): void validator.getUserDataStats(): UserDataStats // Verificar si el enriquecimiento de datos está habilitado validator.isUserDataEnabled(): boolean ``` ### Funciones Helper #### createCognitoValidator Función de conveniencia para crear un validator configurado para AWS Cognito: ```typescript createCognitoValidator( region: string, // AWS region (ej: "us-east-1") userPoolId: string, // Cognito User Pool ID clientId?: string, // Cognito App Client ID (opcional) clientSecret?: string, // Cognito App Client Secret (opcional) redisConfig?: { // Configuración Redis (opcional) host?: string; port?: number; password?: string; tls?: boolean; // Nota: Solo boolean - la función configura TLS internamente caCertPath?: string; caCertName?: string; }, enableApiKeyValidation?: boolean, // Habilitar validación de API keys (default: false) enableUserDataRetrieval?: boolean // Habilitar enriquecimiento de datos (default: false) ): JWTValidator ``` **Ejemplos de uso:** ```typescript // Básico - solo validación JWT const validator = createCognitoValidator("us-east-1", "us-east-1_XXXXXXXXX"); // Con Redis y todas las funciones habilitadas const validator = createCognitoValidator( "us-east-1", "us-east-1_XXXXXXXXX", "your-client-id", "your-client-secret", { host: "redis-host.com", password: "redis-password", tls: true }, true, // enableApiKeyValidation true // enableUserDataRetrieval ); // Solo con validación de API keys const validator = createCognitoValidator( "us-east-1", "us-east-1_XXXXXXXXX", "your-client-id", undefined, // sin client secret { host: "redis-host.com" }, true, // enableApiKeyValidation false // sin enriquecimiento de datos ); ``` #### createCognitoValidatorAsync Versión asíncrona con soporte para AWS Parameter Store: ```typescript await createCognitoValidatorAsync( region: string, userPoolId: string, clientId?: string, clientSecret?: string, redisConfig?: { /* ... */ }, enableApiKeyValidation?: boolean, enableUserDataRetrieval?: boolean ): Promise<JWTValidator> ``` ## 🔄 Migración desde v1.x ### De Múltiples Métodos → Un Método Principal ```typescript // ❌ ANTES (v1.x): Múltiples métodos confusos await validator.validateToken(token); await validator.validateTokenWithApiKey(token, apiKey); await validator.validateTokenEnriched(token, apiKey); await validator.validateTokenWithAppId(token, apiKey); // ✅ AHORA (v2.x): Un método principal inteligente await validator.validate(token, { apiKey, enrichUserData: true }); ``` ### Migración Paso a Paso ```typescript // Paso 1: Reemplazar validateTokenWithApiKey // ANTES: const result = await validator.validateTokenWithApiKey(token, apiKey); // AHORA: const result = await validator.validateWithApiKey(token, apiKey); // Paso 2: Reemplazar validateTokenEnriched // ANTES: const result = await validator.validateTokenEnriched(token, apiKey); // AHORA: const result = await validator.validateEnriched(token, apiKey); // Paso 3: Casos complejos // ANTES: Múltiples llamadas separadas const tokenResult = await validator.validateTokenWithApiKey(token, apiKey); const enrichedResult = await validator.validateTokenEnriched(token, apiKey); // AHORA: Una sola llamada const result = await validator.validate(token, { apiKey, enrichUserData: true, requireAppAccess: true }); ``` ### Compatibilidad hacia Atrás ```typescript // ✅ Los métodos antiguos siguen funcionando (con warnings) const result = await validator.validateTokenWithApiKey(token, apiKey); // WARNING: validateTokenWithApiKey is deprecated. Use validateWithApiKey() instead. // Pero es mejor migrar a la nueva API: const result = await validator.validateWithApiKey(token, apiKey); ``` #### Métodos de Client Secret (Cognito) ```typescript // Verificar si hay client secret configurado validator.hasClientSecret(): boolean // Obtener client secret (si está configurado) validator.getClientSecret(): string | undefined // Calcular hash secreto para operaciones Cognito validator.calculateSecretHash(identifier: string): string ``` #### 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(); // NUEVO: Funciones de diagnóstico para debugging const diagnosis = validator.diagnoseToken(token); console.log("Token diagnosis:", diagnosis); ``` ## Configuración Manual Para casos de uso avanzados, puedes configurar manualmente con control completo sobre TLS: ```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, // Lista negra de tokens revocados enableApiKeyValidation: true, // Validación de API keys enableUserDataRetrieval: true, // Enriquecimiento de datos de usuario 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)); }, }, }); ``` ### Opciones de Configuración Boolean El `ValidatorConfig` soporta las siguientes opciones boolean para habilitar funcionalidades específicas: - **`enableRedisBlacklist?: boolean`** - Habilita la verificación de lista negra de tokens usando Redis (default: false) - **`enableApiKeyValidation?: boolean`** - Habilita la validación de API keys para control de acceso a nivel de sistema y aplicación (default: false) - **`enableUserDataRetrieval?: boolean`** - Habilita el enriquecimiento de datos de usuario con permisos, organizaciones y aplicaciones (default: false) - **`forceSecureValidation?: boolean`** - Fuerza la validación JWKS segura incluso en entornos de desarrollo (default: false) Estas banderas boolean proveen control granular sobre qué características están activas, permitiendo optimizar el rendimiento habilitando solo la funcionalidad necesaria. ## 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; } } ``` ### NUEVO: Express.js Middleware con Enriquecimiento de Datos ```typescript import express from "express"; import { createCognitoValidator } from "@theoptimalpartner/jwt-auth-validator"; const app = express(); // Validator con configuración de datos de usuario const validator = createCognitoValidator( process.env.AWS_REGION!, process.env.COGNITO_USER_POOL_ID!, process.env.COGNITO_CLIENT_ID, process.env.COGNITO_CLIENT_SECRET, { host: process.env.REDIS_HOST!, password: process.env.REDIS_PASSWORD, tls: process.env.REDIS_TLS === 'true', }, { enableUserDataRetrieval: true, includeApplications: true, includeOrganizations: true, includeRoles: true, cacheTimeout: 300, } ); // Middleware de autenticación con datos de usuario const enrichedAuthMiddleware = 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" }); } // Usa validateTokenEnriched para obtener datos de usuario const result = await validator.validateTokenEnriched(token); if (!result.valid) { return res.status(401).json({ error: result.error }); } // Contexto enriquecido disponible req.user = result.decoded; req.userPermissions = result.userPermissions; req.userOrganizations = result.userOrganizations; req.userApplications = result.applications; next(); } catch (error) { res.status(500).json({ error: "Authentication error" }); } }; // Middleware de autorización por rol const requireRole = (appId: string, role: string) => { return (req: any, res: any, next: any) => { const hasRole = req.userOrganizations?.some((org: any) => org.appId === appId && org.roles.includes(role) ); if (!hasRole) { return res.status(403).json({ error: "Insufficient permissions" }); } next(); }; }; // Uso del middleware app.use("/api/protected", enrichedAuthMiddleware); app.use("/api/admin", requireRole("my-app", "admin")); app.get("/api/protected/profile", (req: any, res: any) => { res.json({ user: req.user, organizations: req.userOrganizations, applications: req.userApplications, }); }); app.get("/api/admin/dashboard", (req: any, res: any) => { res.json({ message: "Welcome admin!", user: req.user }); }); ``` ### 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"); } }; ``` ### Validación con API Keys ```typescript import express from "express"; import { JWTValidator } from "@theoptimalpartner/jwt-auth-validator"; // Validator con validación de API Keys habilitada 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", }, redis: { host: process.env.REDIS_HOST!, password: process.env.REDIS_PASSWORD, tls: process.env.REDIS_TLS === 'true', }, enableApiKeyValidation: true, // Habilitar validación de API Keys enableRedisBlacklist: true, }); const app = express(); // Middleware que valida JWT con API Key opcional const authWithApiKeyMiddleware = 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" }); } // Extraer API key de headers const apiKey = validator.extractApiKeyFromHeaders(req.headers); // Validar token con API key opcional const result = await validator.validateTokenWithApiKey(token, apiKey); if (!result.valid) { return res.status(401).json({ error: result.error }); } req.user = result.decoded; req.apiKey = result.apiKey; // Información del API key si se usó next(); } catch (error) { res.status(500).json({ error: "Authentication error" }); } }; app.use("/api/protected", authWithApiKeyMiddleware); app.get("/api/protected/data", (req: any, res: any) => { res.json({ user: req.user, apiKeyUsed: !!req.apiKey, apiKeyInfo: req.apiKey ? { name: req.apiKey.name, scope: req.apiKey.scope, permissions: req.apiKey.permissions } : null }); }); ``` ## Solución de Problemas (Troubleshooting) ### Problema: "aud is undefined" o errores de audience Si estás viendo errores relacionados con audience undefined, usa la función de diagnóstico: ```typescript import { createCognitoValidator } from "@theoptimalpartner/jwt-auth-validator"; const validator = createCognitoValidator( "us-east-1", "us-east-1_XXXXXXXXX", "tu-client-id", // ⚠️ Asegúrate de pasar el client ID "tu-client-secret" // Opcional ); // Diagnosticar problemas de configuración const diagnosis = validator.diagnoseToken(yourToken); console.log("Diagnóstico:", diagnosis); // Esto te mostrará: // - Configuración actual (issuer, audience, client secret) // - Payload del token (aud, client_id, sub, iss, token_use) // - Lista de problemas detectados ``` **Causas comunes del error de audience:** 1. **Client ID no pasado**: Asegúrate de pasar el `clientId` al crear el validator 2. **Token sin aud ni client_id**: Algunos flujos de Cognito no incluyen estos campos 3. **Configuración incorrecta**: Verifica que el client ID coincida con tu configuración de Cognito ### Problema: Tokens que no validan correctamente ```typescript // Revisar la configuración y payload del token const diagnosis = validator.diagnoseToken(token); if (diagnosis.issues.length > 0) { console.error("Problemas detectados:", diagnosis.issues); // Ejemplo: ["No audience configured in JWKS config", "Token missing 'sub' claim"] } ``` ## 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 AWS para Parameter Store (SSM) # Nota: Si no se configuran, usa la cadena de credenciales estándar de AWS (aws configure, IAM roles, etc.) AWS_ACCESS_KEY_ID=your-access-key # Opcional, usa aws configure si no se proporciona AWS_SECRET_ACCESS_KEY=your-secret-key # Opcional, usa aws configure si no se proporciona AWS_SESSION_TOKEN=your-session-token # Opcional, para credenciales temporales AWS_SSM_ENDPOINT=https://ssm.us-east-1.amazonaws.com # Opcional, para VPC endpoints # 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 # Debugging y Logging (opcional) JWT_DEBUG=true # Habilita logging detallado para debugging # NODE_ENV=development # También habilita debug logs # NODE_ENV=production # Logs limpios, sin información técnica detallada ``` ## Configuración AWS para Development ### Desarrollo Local Para desarrollo local, el paquete usa la **cadena de credenciales estándar de AWS**: ```bash # Opción 1: Configurar perfil por defecto (recomendado para desarrollo) aws configure # Configura: access key, secret key, región, formato # Opción 2: Usar perfil específico aws configure --profile mi-proyecto export AWS_PROFILE=mi-proyecto # Opción 3: Variables de entorno específicas del proyecto export AWS_REGION=us-east-1 export AWS_ACCESS_KEY_ID=AKIA... export AWS_SECRET_ACCESS_KEY=xyz123... ``` ### Orden de Prioridad de Credenciales 1. **Variables de entorno** (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) 2. **Archivo de credenciales** (`~/.aws/credentials`) 3. **Perfil AWS** (`AWS_PROFILE` o `[default]`) 4. **IAM roles** (en EC2, ECS, Lambda, etc.) ### Permisos Necesarios para SSM Tu usuario/rol AWS necesita permisos para acceder a Parameter Store: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ssm:GetParameter", "ssm:GetParameters" ], "Resource": "arn:aws:ssm:us-east-1:*:parameter/redis/*" } ] } ``` ### Debugging de Configuración AWS El paquete incluye logging detallado para diagnosis: ``` 📡 Getting certificate from Parameter Store: /redis/ca-cert 🌍 AWS Region: us-east-1 🔑 Credentials configured: No (using IAM role/profile) 👈 Indica uso de aws configure ✅ Certificate obtained from SSM and cached ``` ## 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 ## Sistema de Logging y Manejo de Errores ### Logging Inteligente El paquete incluye un sistema de logging que se adapta automáticamente al entorno: #### Producción (Logs Limpios) ```bash # En producción, verás mensajes concisos y claros: JWKS token validation: Token has expired (TOKEN_EXPIRED) JWT Validator initialization: Configuration error (INITIALIZATION_FAILED) Token validation: Invalid audience (AUDIENCE_MISMATCH) ``` #### Desarrollo (Debug Detallado) ```bash # En desarrollo o con JWT_DEBUG=true, verás información detallada: NODE_ENV=development # O JWT_DEBUG=true # Logs incluyen: ✅ JWKS Service initialized with remote JWKS set using jose library 🔍 JWKS Configuration: { issuer: "...", audience: "...", hasClientSecret: true } 🔍 Token payload aud/client_id: { aud: "...", client_id: "...", token_use: "access" } 🔍 Verify options: { issuer: "...", audience: "...", clockTolerance: "60s" } ✅ Token verified successfully with remote JWKS using jose ``` ### Configuración de Logging ```typescript // Control de logging por entorno process.env.NODE_ENV = 'production'; // Logs limpios process.env.NODE_ENV = 'development'; // Logs detallados // O control específico de JWT process.env.JWT_DEBUG = 'true'; // Fuerza debug logs independiente del NODE_ENV ``` ### Manejo de Errores User-Friendly El sistema convierte errores técnicos en mensajes comprensibles: ```typescript // Antes (verboso): // Error: JWTExpired: jwt expired // at verify (/node_modules/jsonwebtoken/verify.js:147:19) // at JWKSService.validateTokenWithJWKS (/lib/jwks-service.js:195:23) // ... [stack trace completo] // Ahora (limpio): "Token has expired" // Con context para debugging: "JWKS token validation: Token has expired (TOKEN_EXPIRED)" ``` ### Funciones de Error Handling ```typescript import { extractErrorDetails, getUserFriendlyErrorMessage, logError, JWT_ERROR_MESSAGES } from "@theoptimalpartner/jwt-auth-validator"; // Obtener detalles estructurados del error const errorDetails = extractErrorDetails(error, 'Token validation'); console.log(errorDetails); // { message: "Token has expired", code: "TOKEN_EXPIRED", context: "Token validation" } // Obtener mensaje user-friendly const message = getUserFriendlyErrorMessage(error); console.log(message); // "Token has expired" // Log con formato consistente logError(error, 'Custom operation'); // Output: "Custom operation: Token has expired (TOKEN_EXPIRED)" ``` ## 📋 Estructura del Token Decodificado (decodedToken) Después de una validación exitosa, el objeto `result.decoded` contiene los claims del JWT con la siguiente estructura: ### Claims Principales ```typescript interface DecodedToken { // ============ Claims Obligatorios JWT Standard (RFC 7519) ============ /** Subject - Identificador único del usuario (UUID) */ sub: string; // Ejemplo: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" /** Audience - Cliente/aplicación para la cual el token fue emitido */ aud: string; // Ejemplo: "1234567890abcdefghijklmnop" /** Issuer - URL del User Pool de Cognito que emitió el token */ iss: string; // Ejemplo: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX" /** Expiration Time - Timestamp Unix (segundos) de expiración */ exp: number; // Ejemplo: 1735689600 (equivale a 2025-01-01 00:00:00 UTC) /** Issued At - Timestamp Unix (segundos) de emisión */ iat: number; // Ejemplo: 1735603200 (equivale a 2024-12-31 00:00:00 UTC) /** Token Use - Tipo de token Cognito */ token_use: 'access' | 'id'; // 'access' para Access Tokens, 'id' para ID Tokens // ============ Claims Opcionales de Usuario ============ /** Email del usuario (opcional) */ email?: string; // Ejemplo: "usuario@ejemplo.com" /** Verificación de email (opcional) */ email_verified?: boolean; // true si el email ha sido verificado /** Número de teléfono (opcional) */ phone_number?: string; // Ejemplo: "+12025551234" /** Verificación de teléfono (opcional) */ phone_number_verified?: boolean; // true si el teléfono ha sido verificado /** Nombre de usuario (opcional) */ username?: string; // Ejemplo: "john.doe" // ============ Claims Específicos de AWS Cognito ============ /** Username en formato Cognito (opcional) */ 'cognito:username'?: string; // Ejemplo: "john.doe" o "Google_123456789" /** Grupos de Cognito a los que pertenece el usuario (opcional) */ 'cognito:groups'?: string[]; // Ejemplo: ["admin", "users"] /** Scope OAuth2 - Permisos concedidos al token (solo Access Tokens) */ scope?: string; // Ejemplo: "openid email profile" /** Authentication Time - Timestamp Unix de cuando el usuario se autenticó (opcional) */ auth_time?: number; // Ejemplo: 1735603200 /** JWT ID - Identificador único del token (opcional) */ jti?: string; // Ejemplo: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" // ============ Custom Attributes ============ /** Cualquier atributo custom definido en Cognito */ [key: string]: unknown; // Ejemplo: { "custom:tenant_id": "company-123" } } ``` ### Ejemplos Reales de Tokens Decodificados #### Access Token (token_use: "access") ```typescript { sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX", client_id: "1234567890abcdefghijklmnop", aud: "1234567890abcdefghijklmnop", token_use: "access", scope: "openid email profile", auth_time: 1735603200, exp: 1735689600, iat: 1735603200, jti: "xyz-789-def-456", username: "john.doe", "cognito:username": "john.doe", "cognito:groups": ["admin", "users"] } ``` #### ID Token (token_use: "id") ```typescript { sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX", aud: "1234567890abcdefghijklmnop", token_use: "id", auth_time: 1735603200, exp: 1735689600, iat: 1735603200, email: "john.doe@example.com", email_verified: true, phone_number: "+12025551234", phone_number_verified: true, "cognito:username": "john.doe", "cognito:groups": ["admin"], "custom:department": "engineering", "custom:employee_id": "EMP-12345" } ``` ### Uso del Token Decodificado en tu Aplicación ```typescript const result = await validator.validateToken(token); if (result.valid && result.decoded) { const decoded = result.decoded; // ✅ Identificación del usuario console.log('User ID:', decoded.sub); console.log('Username:', decoded['cognito:username'] || decoded.username); // ✅ Información de contacto if (decoded.email) { console.log('Email:', decoded.email); console.log('Email verificado:', decoded.email_verified); } // ✅ Autorización basada en grupos if (decoded['cognito:groups']?.includes('admin')) { console.log('Usuario es administrador'); } // ✅ Validación de tiempo const expiresAt = new Date(decoded.exp * 1000); console.log('Token expira:', expiresAt.toLocaleString()); const issuedAt = new Date(decoded.iat * 1000); console.log('Token emitido:', issuedAt.toLocaleString()); // ✅ Atributos custom if (decoded['custom:tenant_id']) { console.log('Tenant ID:', decoded['custom:tenant_id']); } // ✅ Scope OAuth2 (solo Access Tokens) if (decoded.token_use === 'access' && decoded.scope) { const scopes = decoded.scope.split(' '); console.log('Permisos OAuth2:', scopes); } } ``` ### Diferencias entre Access Token e ID Token | Campo | Access Token | ID Token | |-------|-------------|----------| | **`token_use`** | `"access"` | `"id"` | | **`scope`** | ✅ Incluido | ❌ No incluido | | **`client_id`** | ✅ Incluido | ❌ No incluido | | **`email`** | ❌ No incluido | ✅ Incluido | | **`email_verified`** | ❌ No incluido | ✅ Incluido | | **`phone_number`** | ❌ No incluido | ✅ Incluido | | **Custom Attributes** | ❌ No incluido | ✅ Incluido | | **Uso Principal** | Autorización en APIs | Información del usuario | ### Notas Importantes - **Claims opcionales**: La disponibilidad de campos como `email`, `phone_number`, y `custom:*` depende de tu configuración de Cognito - **Token Use**: Usa `decoded.token_use` para determinar el tipo de token y qué campos esperar - **Timestamps**: Los campos `exp`, `iat`, y `auth_time` están en formato Unix timestamp (segundos desde 1970-01-01) - **Custom Attributes**: Los atributos custom de Cognito tienen el prefijo `custom:` en sus nombres - **Grupos Cognito**: Los grupos se almacenan en el array `cognito:groups` cuando están configurados ## 📦 Estructura de la Respuesta de Validación (ValidationResult) Cuando validas un token, el paquete devuelve un objeto `ValidationResult` con la siguiente estructura completa: ### Estructura Completa de ValidationResult ```typescript interface ValidationResult { /** Indica si el token es válido */ valid: boolean; /** Token JWT decodificado (solo si valid === true) */ decoded?: DecodedToken; /** Información del API Key usado (solo si se validó con API Key) */ apiKey?: ApiKeyData; /** Mensaje de error (solo si valid === false) */ error?: string; } ``` ### Ejemplo Real de Respuesta Exitosa (con API Key) ```typescript const result = await validator.validateWithApiKey(token, apiKey); // Resultado completo: { valid: true, decoded: { sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX", aud: "1234567890abcdefghijklmnop", token_use: "access", exp: 1735689600, iat: 1735603200, username: "john.doe", "cognito:username": "john.doe", "cognito:groups": ["users"] }, apiKey: { name: "production-api-key", permissions: ["auth:access", "users:read"], appId: "my-application", scope: "client", createdAt: 1735603200000, lastUsed: 1735689600000, isActive: true, metadata: { createdFor: "Integration Team", description: "API Key for production integration", environment: "production" } } } ``` ### Estructura de ApiKeyData Cuando se valida con un API Key, el campo `apiKey` contiene información detallada sobre la clave: ```typescript interface ApiKeyData { /** Nombre identificador del API Key */ name: string; // Ejemplo: "production-api-key" /** Lista de permisos asignados al API Key */ permissions: string[]; // Ejemplo: ["auth:access", "users:read"] /** ID de la aplicación asociada (opcional para scope 'system') */ appId?: string; // Ejemplo: "my-application" /** Alcance del API Key */ scope: 'app' | 'system' | 'client'; // 'app': app específica, 'system': transversal, 'client': cliente /** Timestamp Unix de creación del API Key */ createdAt: number; // Ejemplo: 1735603200000 (milisegundos) /** Timestamp Unix del último uso (null si nunca se ha usado) */ lastUsed: number | null; // Ejemplo: 1735689600000 (milisegundos) /** Estado del API Key */ isActive: boolean; // true: activo, false: desactivado /** Metadatos adicionales personalizados */ metadata?: Record<string, unknown>; // Ejemplo: { createdFor: "Team", environment: "production" } } ``` ### Diferencias entre Scopes de API Keys | Scope | Descripción | Restricciones | Uso Típico | |-------|-------------|---------------|------------| | **`system`** | Acceso transversal a todas las aplicaciones | Ninguna - acceso completo | Administración, integraciones de sistema | | **`app`** | Acceso limitado a una aplicación específica | Solo puede acceder al `appId` asociado | Integraciones de aplicaciones específicas | | **`client`** | Acceso de cliente/frontend | Restricciones según permisos asignados | Aplicaciones frontend, móviles | ### Ejemplos de Uso con API Key #### 1. Validación Básica con API Key ```typescript const result = await validator.validateWithApiKey(token, apiKey); if (result.valid) { console.log('✅ Token válido'); console.log('Usuario:', result.decoded?.username); // Información del API Key if (result.apiKey) { console.log('API Key:', result.apiKey.name); console.log('Scope:', result.apiKey.scope); console.log('Permisos:', result.apiKey.permissions); console.log('App ID:', result.apiKey.appId); } } else { console.log('❌ Token inválido:', result.error); } ``` #### 2. Autorización basada en API Key Scope ```typescript const result = await validator.validateWithApiKey(token, apiKey); if (result.valid && result.apiKey) { const { scope, appId, permissions } = result.apiKey; // Verificar si tiene acceso system (transversal) if (scope === 'system') { console.log('✅ Acceso system - puede acceder a todas las apps'); } // Verificar si tiene acceso a app específica if (scope === 'app' && appId === 'my-target-app') { console.log('✅ Acceso autorizado a my-target-app'); } // Verificar permisos específicos if (permissions.includes('users:write')) { console.log('✅ Puede modificar usuarios'); } } ``` #### 3. Usar Metadata del API Key ```typescript const result = await validator.validateWithApiKey(token, apiKey); if (result.valid && result.apiKey?.metadata) { const metadata = result.apiKey.metadata; // Acceder a información personalizada console.log('Creado para:', metadata.createdFor); console.log('Descripción:', metadata.description); console.log('Ambiente:', metadata.environment); // Control de acceso basado en ambiente if (metadata.environment === 'production') { console.log('⚠️ API Key de producción - logging extra habilitado'); } } ``` #### 4. Tracking de Último Uso ```typescript const result = await validator.validateWithApiKey(token, apiKey); if (result.valid && result.apiKey) { const { lastUsed, createdAt } = result.apiKey; // Verificar última vez usado if (lastUsed) { const lastUsedDate = new Date(lastUsed); console.log('Última vez usado:', lastUsedDate.toLocaleString()); // Detectar API Keys inactivos (más de 30 días sin uso) const daysSinceLastUse = (Date.now() - lastUsed) / (1000 * 60 * 60 * 24); if (daysSinceLastUse > 30) { console.log('⚠️ API Key inactivo por más de 30 días'); } } else { console.log('ℹ️ API Key nunca usado antes'); } // Antigüedad del API Key const createdDate = new Date(createdAt); console.log('Creado el:', createdDate.toLocaleString()); } ``` ### Respuesta con Datos Enriquecidos (EnrichedValidationResult) Cuando usas `validateEnriched()`, obtienes campos adicionales: ```typescript interface EnrichedValidationResult extends ValidationResult { /** Permisos del usuario desde Redis (opcional) */ userPermissions?: UserPermissions | null; /** Organizaciones del usuario (opcional) */ userOrganizations?: UserOrganization[]; /** Aplicaciones accesibles por el usuario (opcional) */ applications?: Application[]; } ``` #### Ejemplo Real de Respuesta Enriquecida ```typescript const result = await validator.validateEnriched(token, apiKey); // Resultado completo con datos enriquecidos: { valid: true, decoded: { sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", username: "john.doe", email: "john.doe@example.com", // ... otros campos del token }, apiKey: { name: "production-api-key", permissions: ["auth:access", "users:read"], appId: "my-application", scope: "client", createdAt: 1735603200000, lastUsed: 1735689600000, isActive: true, metadata: { environment: "production" } }, userPermissions: { userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", permissions: { "my-app": { "org-123": { roles: ["admin", "user"], effectivePermissions: ["users:read", "users:write"], status: "active" } } } }, userOrganizations: [ { appId: "my-app", organizationId: "org-123", roles: ["admin"], status: "active", effectivePermissions: ["users:read", "users:write"] } ], applications: [ { appId: "my-app", name: "My Application", isActive: true, schema: { /* AppSchema */ }, createdAt: 1735603200000, updatedAt: 1735689600000 } ] } ``` ### Manejo de Errores Cuando la validación falla, obtienes un mensaje de error claro: ```typescript const result = await validator.validateToken(token); if (!result.valid) { console.log('❌ Error:', result.error); // Ejemplos de errores comunes: // - "Token has expired" // - "Invalid token signature" // - "Invalid audience" // - "Token is blacklisted" // - "API key is inactive" // - "No access to application" } ``` ## 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; } // NUEVO: Utilidades de manejo de errores interface ErrorDetails { message: string; code?: string; context?: string; } // Funciones de utilidad exportadas function extractErrorDetails(error: unknown, context?: string): ErrorDetails function getUserFriendlyErrorMessage(error: unknown): string function logError(error: unknown, context?: string): void // Constantes de mensajes de error const JWT_ERROR_MESSAGES = { TOKEN_EXPIRED: 'Token has expired', INVALID_TOKEN: 'Invalid token format', TOKEN_NOT_ACTIVE: 'Token not active yet', INVALID_SIGNATURE: 'Invalid token signature', // ... más constantes disponibles } as const ``` ## 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 ca