JWT token validation package with offline JWKS validation and Redis-based token revocation support
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.
- ✅ 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)
``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.
`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);
}
`
`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);
`
`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
});
`
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')
);
}
`
`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
});
`
`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");
`
El paquete es totalmente compatible con los patrones de clave de auth-service:
- Permisos de usuario: user:permissions:{userId}app:{appId}
- Aplicaciones: org:{appId}:{organizationId}
- Organizaciones: app:roles:{appId}:{organizationId}
- Roles de aplicación: app-schemas
- Esquemas de aplicación: (clave global)permissions:cache:{userId}:{appId}:{orgId}
- Permisos efectivos:
`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
}
`
`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});`
- 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
#### Métodos principales
`typescript
// Validación principal (automática según ambiente)
await validator.validateToken(token: string, forceSecure?: boolean): Promise
// Validación con API Key opcional
await validator.validateTokenWithApiKey(token: string, apiKey?: string, forceSecure?: boolean): Promise
// NUEVO: Validación con enriquecimiento de datos de usuario
await validator.validateTokenEnriched(token: string, apiKey?: string, forceSecure?: boolean): Promise
// Validación específica para Access Tokens
await validator.validateAccessToken(token: string): Promise
// Validación específica para ID Tokens
await validator.validateIdToken(token: string): Promise
// Validación segura (siempre usa JWKS)
await validator.validateTokenSecure(token: string): Promise
// Validación básica (sin verificación JWKS)
await validator.validateTokenBasic(token: string): Promise
// Validación múltiple en lote
await validator.validateMultipleTokens(tokens: string[]): Promise
`
#### 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
// 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
// Obtener organizaciones del usuario
await validator.getUserOrganizations(userId: string): Promise
// Obtener aplicaciones accesibles por el usuario
await validator.getUserApplications(userId: string): Promise
// 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
`
#### 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
`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
});
`
`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
});
`
`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
// Revocar todos los tokens de un usuario
await validator.revokeUserTokens(userId: string, tokens: string[]): Promise
`
#### 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);
`
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));
},
},
});
`
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.
`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 });
});
`
`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
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;
}
}
`
`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 });
});
`
`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");
}
};
`
`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
});
});
`
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
`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"]
}
`
`bashConfiguració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 Development
$3
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-proyectoOpció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...
`$3
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.)$3
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/"
}
]
}
`$3
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
$3
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.
$3
- 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
$3
`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);
`$3
`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"
);
`$3
- 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
$3
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=trueLogs 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
`$3
`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
`$3
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)"
`$3
`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:$3
`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" }
}
`$3
#### 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"
}
`$3
`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);
}
}
`$3
| 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 |$3
- 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:$3
`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;
}
`$3
`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"
}
}
}
`$3
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; // Ejemplo: { createdFor: "Team", environment: "production" }
}
`$3
| 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 |$3
#### 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());
}
`$3
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
}
]
}
`$3
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
$3
- 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
$3
- No requiere llamadas al servicio de autenticación para validar tokens
- Validación local usando claves públicas cacheadas
- Ideal para microservicios y alta concurrencia
$3
- Usa Redis para almacenamiento distribuido de tokens revocados
- TTL automático basado en la expiración del token
- Optimización de memoria usando hashes de tokens
$3
- Mensajes de error amigables y descriptivos
- Logging limpio sin información técnica excesiva
- Traducción inteligente de errores JWT a mensajes claros
- Códigos de error estructurados para manejo programático
Seguridad
$3
- Verificación de firma usando claves públicas
- Validación de claims estándar (exp, iss, aud)
- Verificación específica de tokens Cognito
$3
- Validación básica sin verificación de firma para desarrollo
- Advertencias claras cuando no se usa validación segura
- Automáticamente usa validación segura en producción
Licencia
MIT
Contribuciones
Las contribuciones son bienvenidas. Por favor:
1. Fork el repositorio
2. Crea una rama para tu feature (
git checkout -b feature/nueva-funcionalidad)
3. Commit tus cambios (git commit -am 'Agregar nueva funcionalidad')
4. Push a la rama (git push origin feature/nueva-funcionalidad`)Para reportar bugs o solicitar features, por favor crea un issue en el repositorio.