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