@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
Markdown
# 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