¿Qué es JWT y Por Qué se Usa?
JWT (JSON Web Token) es un estándar abierto que define una forma compacta y segura de transmitir información entre partes como un objeto JSON. Este token se ha convertido en una de las soluciones más adoptadas para la autenticación y autorización en aplicaciones web modernas, especialmente en arquitecturas de APIs REST y aplicaciones de una sola página (SPA).
Un JWT es esencialmente un token autocontenido que lleva consigo toda la información necesaria para verificar la identidad del usuario y sus permisos. A diferencia de los sistemas tradicionales de sesiones que requieren almacenar información en el servidor, los JWT permiten que toda la información relevante viaje con el propio token.
Características principales de JWT
Los JWT presentan varias características que los hacen especialmente útiles en el desarrollo de aplicaciones modernas:
- Autocontenidos: Toda la información necesaria está incluida en el propio token
- Compactos: Su formato permite una transmisión eficiente a través de URLs, headers HTTP o dentro del cuerpo de peticiones POST
- Seguros: Pueden ser firmados digitalmente para garantizar su integridad
- Independientes del lenguaje: Al estar basados en JSON, pueden ser procesados por cualquier lenguaje de programación
Por qué usar JWT en lugar de sesiones tradicionales
Las sesiones tradicionales requieren que el servidor mantenga un registro de cada usuario conectado, típicamente en memoria o en una base de datos. Esto presenta varios inconvenientes en aplicaciones modernas:
Problemas de las sesiones tradicionales:
- Escalabilidad limitada: Cada servidor debe mantener el estado de las sesiones
- Complejidad en arquitecturas distribuidas: Requiere compartir el estado entre múltiples servidores
- Dependencia del servidor: El cliente no puede funcionar de forma independiente
Ventajas de JWT:
- Stateless: El servidor no necesita almacenar información de sesión
- Escalabilidad horizontal: Cualquier servidor puede validar un token sin consultar una base de datos central
- Flexibilidad: Permite trabajar con múltiples dominios y servicios
- Rendimiento: Reduce la carga en la base de datos al evitar consultas constantes de validación
Casos de uso ideales para JWT
JWT resulta especialmente útil en los siguientes escenarios:
Autenticación en APIs REST:
// Ejemplo de uso típico en un controlador NestJS
@Controller('protected')
export class ProtectedController {
@Get('profile')
@UseGuards(JwtAuthGuard)
getProfile(@Request() req) {
// El token JWT ya ha sido validado por el guard
return req.user; // Información extraída del token
}
}
Aplicaciones de una sola página (SPA):
Los JWT permiten que las aplicaciones frontend mantengan la autenticación sin depender de cookies o sesiones del servidor. El token se almacena en el cliente y se envía con cada petición.
Arquitecturas de microservicios:
En sistemas distribuidos, JWT facilita la comunicación entre servicios sin necesidad de consultar un servicio de autenticación centralizado en cada petición:
// Servicio A puede validar un token generado por Servicio B
@Injectable()
export class OrderService {
async createOrder(token: string, orderData: any) {
// Validación local del token sin consultar otro servicio
const payload = this.jwtService.verify(token);
if (payload.permissions.includes('create_orders')) {
return this.processOrder(orderData);
}
}
}
Cuándo NO usar JWT
Aunque JWT ofrece muchas ventajas, no es la solución ideal para todos los casos:
- Aplicaciones con sesiones de larga duración: Los JWT no pueden ser revocados fácilmente una vez emitidos
- Información sensible: Nunca debe incluirse información confidencial en el payload, ya que es fácilmente decodificable
- Aplicaciones simples: Para aplicaciones pequeñas con pocos usuarios, las sesiones tradicionales pueden ser más sencillas de implementar
JWT en el contexto de NestJS
NestJS proporciona un ecosistema robusto para trabajar con JWT a través del paquete @nestjs/jwt
y las estrategias de Passport. Esta integración permite implementar autenticación JWT de forma declarativa y mantenible:
// Configuración básica del módulo JWT en NestJS
@Module({
imports: [
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1h' },
}),
],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
La filosofía de NestJS se alinea perfectamente con JWT al promover aplicaciones escalables y modulares. Los decoradores, guards y estrategias de NestJS simplifican significativamente la implementación de autenticación basada en tokens, permitiendo a los desarrolladores centrarse en la lógica de negocio en lugar de los detalles de implementación de seguridad.
Anatomía de un JWT: Header, Payload y Signature
Un JWT está compuesto por tres partes distintas separadas por puntos (.), cada una con un propósito específico en la estructura del token. Esta composición tripartita permite que el token sea tanto informativo como seguro, manteniendo un equilibrio entre funcionalidad y protección.
La estructura básica de un JWT sigue el patrón: header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header: La cabecera del token
El header contiene metadatos sobre el token, especificando el tipo de token y el algoritmo de firma utilizado. Esta información es crucial para que cualquier sistema pueda procesar correctamente el JWT.
La estructura típica del header incluye dos campos principales:
{
"alg": "HS256",
"typ": "JWT"
}
Campos del header:
alg
(Algorithm): Especifica el algoritmo criptográfico utilizado para firmar el tokentyp
(Type): Indica el tipo de token, generalmente "JWT"
Los algoritmos más comunes incluyen:
- HS256: HMAC con SHA-256 (simétrico)
- RS256: RSA con SHA-256 (asimétrico)
- ES256: ECDSA con SHA-256 (asimétrico)
En NestJS, puedes configurar el algoritmo durante la inicialización del módulo JWT:
@Module({
imports: [
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: {
expiresIn: '1h',
algorithm: 'HS256' // Especifica el algoritmo
},
}),
],
})
export class AuthModule {}
Payload: El contenido del token
El payload contiene las declaraciones (claims) sobre la entidad, típicamente el usuario, y metadatos adicionales. Esta sección transporta la información real que necesita la aplicación para tomar decisiones de autorización.
Existen tres tipos de claims:
Claims registrados (estándar):
{
"iss": "https://mi-app.com",
"sub": "user123",
"aud": "mi-aplicacion",
"exp": 1735689600,
"iat": 1735603200,
"nbf": 1735603200
}
iss
(Issuer): Identifica quién emitió el tokensub
(Subject): Identifica el sujeto del token (generalmente el ID del usuario)aud
(Audience): Identifica los destinatarios del tokenexp
(Expiration Time): Tiempo de expiración del tokeniat
(Issued At): Momento en que se emitió el tokennbf
(Not Before): Momento antes del cual el token no debe ser aceptado
Claims públicos y privados:
Además de los claims estándar, puedes incluir información personalizada:
{
"sub": "user123",
"email": "usuario@ejemplo.com",
"roles": ["admin", "editor"],
"permissions": ["read", "write", "delete"],
"exp": 1735689600
}
Implementación en NestJS:
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
async login(user: any) {
const payload = {
sub: user.id,
email: user.email,
roles: user.roles,
// Claims personalizados según las necesidades de la aplicación
};
return {
access_token: this.jwtService.sign(payload),
};
}
}
Signature: La firma de seguridad
La signature es la parte que garantiza la integridad del token y verifica que no ha sido alterado durante la transmisión. Se genera combinando el header codificado, el payload codificado, un secreto y el algoritmo especificado en el header.
Proceso de creación de la firma:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
La firma permite:
- Verificar la integridad: Detectar si el token ha sido modificado
- Autenticar el emisor: Confirmar que el token fue creado por quien posee el secreto
- Prevenir ataques: Evitar la manipulación maliciosa del contenido
Ejemplo de verificación en NestJS:
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET, // Mismo secreto usado para firmar
});
}
async validate(payload: any) {
// Si llegamos aquí, la firma ya fue verificada automáticamente
return {
userId: payload.sub,
email: payload.email,
roles: payload.roles
};
}
}
Codificación Base64URL
Cada parte del JWT se codifica usando Base64URL antes de ser concatenada. Esta codificación es similar a Base64 pero utiliza caracteres seguros para URLs:
- Reemplaza
+
con-
- Reemplaza
/
con_
- Elimina el padding
=
Ejemplo de decodificación manual:
// Función para decodificar una parte del JWT (solo para fines educativos)
function decodeJWTPart(encodedPart: string): any {
const decoded = Buffer.from(encodedPart, 'base64url').toString('utf8');
return JSON.parse(decoded);
}
// Uso
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const [header, payload, signature] = token.split('.');
console.log('Header:', decodeJWTPart(header));
console.log('Payload:', decodeJWTPart(payload));
// La signature no se decodifica, es un hash binario
Consideraciones importantes sobre el contenido
Es fundamental entender que el header y payload de un JWT son fácilmente decodificables por cualquier persona que tenga acceso al token. La seguridad no reside en ocultar esta información, sino en la imposibilidad de modificarla sin conocer el secreto de firma.
Buenas prácticas para el payload:
- Nunca incluir contraseñas o información extremadamente sensible
- Minimizar el tamaño del payload para optimizar el rendimiento
- Incluir solo información necesaria para las decisiones de autorización
- Usar claims estándar cuando sea posible para mejorar la interoperabilidad
// ❌ Incorrecto: información sensible en el payload
const badPayload = {
sub: user.id,
password: user.password, // ¡Nunca hacer esto!
creditCard: user.creditCard // ¡Información demasiado sensible!
};
// ✅ Correcto: solo información necesaria y no sensible
const goodPayload = {
sub: user.id,
email: user.email,
roles: user.roles,
exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1 hora
};
La anatomía tripartita del JWT proporciona un equilibrio elegante entre funcionalidad, seguridad y eficiencia, permitiendo que las aplicaciones NestJS implementen autenticación robusta sin comprometer el rendimiento o la escalabilidad.
El Flujo de Autenticación con JWT
Guarda tu progreso
Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.
Más de 25.000 desarrolladores ya confían en CertiDevs
El flujo de autenticación con JWT sigue un patrón específico que permite a las aplicaciones verificar la identidad de los usuarios de forma eficiente y segura. Este proceso involucra varios pasos coordinados entre el cliente, el servidor de autenticación y los recursos protegidos.
Flujo básico de autenticación
El proceso de autenticación JWT se desarrolla en cuatro etapas principales que establecen y mantienen la sesión del usuario:
1. Solicitud de autenticación inicial
El usuario envía sus credenciales (email/contraseña) al endpoint de login. En NestJS, esto se maneja típicamente a través de un controlador dedicado:
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
async login(@Body() loginDto: LoginDto) {
// Validar credenciales del usuario
const user = await this.authService.validateUser(
loginDto.email,
loginDto.password
);
if (!user) {
throw new UnauthorizedException('Credenciales inválidas');
}
// Generar y retornar el JWT
return this.authService.login(user);
}
}
2. Validación de credenciales y generación del token
El servidor verifica las credenciales contra la base de datos y, si son correctas, genera un JWT con la información relevante del usuario:
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async validateUser(email: string, password: string): Promise<any> {
const user = await this.usersService.findByEmail(email);
if (user && await bcrypt.compare(password, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = {
sub: user.id,
email: user.email,
roles: user.roles
};
return {
access_token: this.jwtService.sign(payload),
user: {
id: user.id,
email: user.email,
name: user.name
}
};
}
}
3. Almacenamiento del token en el cliente
Una vez recibido el token, el cliente debe almacenarlo de forma segura para incluirlo en futuras peticiones. Las opciones más comunes incluyen:
- LocalStorage: Persistente pero vulnerable a XSS
- SessionStorage: Se elimina al cerrar la pestaña
- Memoria: Más seguro pero se pierde al recargar la página
- HttpOnly Cookies: Más seguro contra XSS pero requiere configuración CSRF
4. Inclusión del token en peticiones posteriores
Para acceder a recursos protegidos, el cliente debe incluir el JWT en el header Authorization
:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Implementación del flujo en NestJS
NestJS facilita la implementación de este flujo mediante guards y estrategias que automatizan la validación de tokens:
Configuración de la estrategia JWT:
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: any) {
// Este método se ejecuta automáticamente si el token es válido
return {
userId: payload.sub,
email: payload.email,
roles: payload.roles
};
}
}
Protección de rutas con guards:
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Get('profile')
@UseGuards(JwtAuthGuard)
getProfile(@Request() req) {
// req.user contiene la información extraída del token
return this.usersService.findById(req.user.userId);
}
@Put('profile')
@UseGuards(JwtAuthGuard)
updateProfile(@Request() req, @Body() updateData: UpdateUserDto) {
return this.usersService.update(req.user.userId, updateData);
}
}
Manejo de la expiración de tokens
Los JWT tienen un tiempo de vida limitado definido durante su creación. Cuando un token expira, el servidor rechaza automáticamente las peticiones:
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user, info) {
if (err || !user) {
if (info?.name === 'TokenExpiredError') {
throw new UnauthorizedException('Token expirado');
}
throw new UnauthorizedException('Token inválido');
}
return user;
}
}
Estrategias para manejar la expiración:
- Refresh tokens: Implementar un sistema de tokens de actualización de larga duración
- Renovación automática: Generar nuevos tokens antes de que expiren
- Logout forzado: Redirigir al usuario al login cuando el token expire
Flujo con refresh tokens
Para mejorar la experiencia del usuario y la seguridad, es común implementar un sistema de refresh tokens:
@Injectable()
export class AuthService {
async login(user: any) {
const payload = { sub: user.id, email: user.email };
return {
access_token: this.jwtService.sign(payload, { expiresIn: '15m' }),
refresh_token: this.jwtService.sign(payload, { expiresIn: '7d' }),
};
}
async refreshToken(refreshToken: string) {
try {
const payload = this.jwtService.verify(refreshToken);
const newPayload = { sub: payload.sub, email: payload.email };
return {
access_token: this.jwtService.sign(newPayload, { expiresIn: '15m' }),
};
} catch (error) {
throw new UnauthorizedException('Refresh token inválido');
}
}
}
Flujo de logout
Aunque los JWT son stateless por naturaleza, es importante proporcionar un mecanismo de logout efectivo:
@Controller('auth')
export class AuthController {
@Post('logout')
@UseGuards(JwtAuthGuard)
async logout(@Request() req) {
// En aplicaciones simples, el logout es responsabilidad del cliente
// que debe eliminar el token del almacenamiento local
// Para mayor seguridad, se puede implementar una blacklist de tokens
await this.authService.blacklistToken(req.user.jti); // JWT ID
return { message: 'Logout exitoso' };
}
}
Consideraciones de seguridad en el flujo
El flujo de autenticación debe implementar varias medidas de seguridad para proteger contra ataques comunes:
Validación de origen:
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
async validate(payload: any) {
// Verificar que el usuario aún existe y está activo
const user = await this.usersService.findById(payload.sub);
if (!user || !user.isActive) {
throw new UnauthorizedException('Usuario no válido');
}
return user;
}
}
Rate limiting en endpoints de autenticación:
@Controller('auth')
@UseGuards(ThrottlerGuard)
@Throttle(5, 60) // 5 intentos por minuto
export class AuthController {
@Post('login')
async login(@Body() loginDto: LoginDto) {
// Lógica de login con protección contra fuerza bruta
}
}
Integración con frontend
El cliente frontend debe manejar el flujo de autenticación de forma coordinada con el backend:
// Ejemplo de servicio de autenticación en el frontend (conceptual)
class AuthService {
private token: string | null = null;
async login(email: string, password: string) {
const response = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (response.ok) {
const data = await response.json();
this.token = data.access_token;
localStorage.setItem('token', this.token);
return data;
}
throw new Error('Login fallido');
}
async makeAuthenticatedRequest(url: string, options: RequestInit = {}) {
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.token}`
}
});
}
}
Este flujo de autenticación proporciona una base sólida para implementar sistemas de autenticación escalables y seguros en aplicaciones NestJS, manteniendo la simplicidad del desarrollo mientras se asegura la protección adecuada de los recursos de la aplicación.
Consideraciones Clave y Seguridad
La implementación de JWT en aplicaciones NestJS requiere una atención especial a diversos aspectos de seguridad que pueden comprometer la integridad del sistema si no se manejan correctamente. Aunque JWT ofrece ventajas significativas, también introduce vectores de ataque específicos que deben ser mitigados mediante buenas prácticas y configuraciones adecuadas.
Gestión segura de secretos
El secreto JWT es el elemento más crítico en la seguridad del sistema de autenticación. Su compromiso permite a un atacante generar tokens válidos para cualquier usuario:
// ❌ Incorrecto: secreto hardcodeado
@Module({
imports: [
JwtModule.register({
secret: 'mi-secreto-super-secreto', // ¡Nunca hacer esto!
signOptions: { expiresIn: '1h' },
}),
],
})
export class AuthModule {}
// ✅ Correcto: secreto desde variables de entorno
@Module({
imports: [
ConfigModule.forRoot(),
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
inject: [ConfigService],
}),
],
})
export class AuthModule {}
Características de un secreto seguro:
- Longitud mínima: Al menos 256 bits (32 caracteres) para HS256
- Aleatoriedad: Generado mediante funciones criptográficamente seguras
- Rotación periódica: Cambiar el secreto regularmente sin interrumpir el servicio
- Almacenamiento seguro: Usar servicios de gestión de secretos en producción
// Generación de secreto seguro
import { randomBytes } from 'crypto';
const generateSecureSecret = (): string => {
return randomBytes(32).toString('hex');
};
Configuración de expiración apropiada
Los tiempos de expiración deben equilibrar seguridad y experiencia de usuario. Tokens de larga duración aumentan el riesgo de compromiso, mientras que tokens muy cortos pueden degradar la usabilidad:
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
async generateTokens(user: any) {
const payload = { sub: user.id, email: user.email };
return {
// Token de acceso: corta duración para operaciones frecuentes
access_token: this.jwtService.sign(payload, {
expiresIn: '15m'
}),
// Refresh token: mayor duración para renovación
refresh_token: this.jwtService.sign(
{ ...payload, type: 'refresh' },
{ expiresIn: '7d' }
),
};
}
async validateRefreshToken(token: string) {
try {
const payload = this.jwtService.verify(token);
if (payload.type !== 'refresh') {
throw new UnauthorizedException('Token tipo inválido');
}
// Verificar que el usuario sigue siendo válido
const user = await this.usersService.findById(payload.sub);
if (!user || !user.isActive) {
throw new UnauthorizedException('Usuario inválido');
}
return user;
} catch (error) {
throw new UnauthorizedException('Refresh token inválido');
}
}
}
Validación robusta de tokens
La validación de tokens debe ir más allá de la simple verificación de firma. Es necesario implementar controles adicionales que verifiquen la validez contextual del token:
@Injectable()
export class EnhancedJwtStrategy extends PassportStrategy(Strategy) {
constructor(
private usersService: UsersService,
private configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
passReqToCallback: true, // Permite acceso al request
});
}
async validate(request: Request, payload: any) {
// Validar estructura del payload
if (!payload.sub || !payload.email) {
throw new UnauthorizedException('Payload de token inválido');
}
// Verificar que el usuario existe y está activo
const user = await this.usersService.findById(payload.sub);
if (!user || !user.isActive) {
throw new UnauthorizedException('Usuario no encontrado o inactivo');
}
// Validar que el email coincide (previene ataques de sustitución)
if (user.email !== payload.email) {
throw new UnauthorizedException('Token no coincide con usuario');
}
// Verificar timestamp de último cambio de contraseña
if (payload.iat < user.passwordChangedAt) {
throw new UnauthorizedException('Token invalidado por cambio de contraseña');
}
return {
userId: user.id,
email: user.email,
roles: user.roles,
permissions: user.permissions,
};
}
}
Prevención de ataques comunes
Cross-Site Scripting (XSS):
Los tokens almacenados en localStorage son vulnerables a XSS. Implementa medidas de protección:
@Injectable()
export class SecurityService {
// Sanitizar datos de entrada
sanitizeInput(input: string): string {
return input
.replace(/[<>]/g, '') // Eliminar caracteres peligrosos
.trim()
.substring(0, 1000); // Limitar longitud
}
// Validar origen de peticiones
validateOrigin(request: Request): boolean {
const origin = request.headers.origin;
const allowedOrigins = this.configService.get<string[]>('ALLOWED_ORIGINS');
return allowedOrigins.includes(origin);
}
}
Cross-Site Request Forgery (CSRF):
Aunque JWT en headers Authorization es menos vulnerable a CSRF, implementa protecciones adicionales:
@Controller('auth')
export class AuthController {
@Post('login')
@UseGuards(CsrfGuard) // Guard personalizado para CSRF
async login(@Body() loginDto: LoginDto, @Req() request: Request) {
// Validar token CSRF si se usan cookies
const csrfToken = request.headers['x-csrf-token'];
if (!this.securityService.validateCsrfToken(csrfToken)) {
throw new ForbiddenException('Token CSRF inválido');
}
return this.authService.login(loginDto);
}
}
Implementación de blacklist de tokens
Para casos donde es necesario revocar tokens antes de su expiración natural:
@Injectable()
export class TokenBlacklistService {
constructor(
@InjectRedis() private readonly redis: Redis,
) {}
async blacklistToken(jti: string, exp: number): Promise<void> {
const ttl = exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await this.redis.setex(`blacklist:${jti}`, ttl, 'revoked');
}
}
async isTokenBlacklisted(jti: string): Promise<boolean> {
const result = await this.redis.get(`blacklist:${jti}`);
return result === 'revoked';
}
}
@Injectable()
export class BlacklistJwtStrategy extends PassportStrategy(Strategy) {
constructor(
private tokenBlacklistService: TokenBlacklistService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: any) {
// Verificar si el token está en la blacklist
if (payload.jti && await this.tokenBlacklistService.isTokenBlacklisted(payload.jti)) {
throw new UnauthorizedException('Token revocado');
}
return { userId: payload.sub, email: payload.email };
}
}
Logging y monitoreo de seguridad
Implementa un sistema de auditoría que registre eventos de seguridad relevantes:
@Injectable()
export class SecurityAuditService {
private readonly logger = new Logger(SecurityAuditService.name);
logAuthenticationAttempt(email: string, success: boolean, ip: string) {
const event = {
type: 'AUTHENTICATION_ATTEMPT',
email,
success,
ip,
timestamp: new Date().toISOString(),
};
if (success) {
this.logger.log(`Successful login: ${email} from ${ip}`);
} else {
this.logger.warn(`Failed login attempt: ${email} from ${ip}`);
}
// Enviar a sistema de monitoreo externo
this.sendToMonitoringSystem(event);
}
logSuspiciousActivity(userId: string, activity: string, details: any) {
const event = {
type: 'SUSPICIOUS_ACTIVITY',
userId,
activity,
details,
timestamp: new Date().toISOString(),
};
this.logger.warn(`Suspicious activity detected: ${activity}`, details);
this.sendToMonitoringSystem(event);
}
private sendToMonitoringSystem(event: any) {
// Integración con sistemas como Datadog, New Relic, etc.
}
}
Rate limiting y protección contra fuerza bruta
Protege los endpoints de autenticación contra ataques de fuerza bruta:
@Injectable()
export class AuthRateLimitGuard implements CanActivate {
constructor(
@InjectRedis() private readonly redis: Redis,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const ip = request.ip;
const email = request.body?.email;
// Rate limiting por IP
const ipKey = `rate_limit:ip:${ip}`;
const ipAttempts = await this.redis.incr(ipKey);
if (ipAttempts === 1) {
await this.redis.expire(ipKey, 300); // 5 minutos
}
if (ipAttempts > 10) {
throw new TooManyRequestsException('Demasiados intentos desde esta IP');
}
// Rate limiting por email si se proporciona
if (email) {
const emailKey = `rate_limit:email:${email}`;
const emailAttempts = await this.redis.incr(emailKey);
if (emailAttempts === 1) {
await this.redis.expire(emailKey, 900); // 15 minutos
}
if (emailAttempts > 5) {
throw new TooManyRequestsException('Demasiados intentos para este email');
}
}
return true;
}
}
Configuración de headers de seguridad
Establece headers HTTP que mejoren la seguridad general de la aplicación:
@Injectable()
export class SecurityHeadersMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// Prevenir clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Prevenir MIME type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Habilitar protección XSS del navegador
res.setHeader('X-XSS-Protection', '1; mode=block');
// Política de seguridad de contenido
res.setHeader('Content-Security-Policy', "default-src 'self'");
// Forzar HTTPS en producción
if (process.env.NODE_ENV === 'production') {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
next();
}
}
Validación de algoritmos criptográficos
Asegúrate de que solo se acepten algoritmos seguros y prevé ataques de confusión de algoritmos:
@Injectable()
export class SecureJwtService {
private readonly allowedAlgorithms = ['HS256', 'RS256', 'ES256'];
constructor(private jwtService: JwtService) {}
verifyToken(token: string): any {
try {
// Decodificar header sin verificar para obtener el algoritmo
const header = JSON.parse(
Buffer.from(token.split('.')[0], 'base64url').toString()
);
// Validar que el algoritmo está en la lista permitida
if (!this.allowedAlgorithms.includes(header.alg)) {
throw new UnauthorizedException('Algoritmo no permitido');
}
// Verificar con el algoritmo específico
return this.jwtService.verify(token, {
algorithms: [header.alg], // Forzar algoritmo específico
});
} catch (error) {
throw new UnauthorizedException('Token inválido');
}
}
}
La implementación de estas consideraciones de seguridad es fundamental para mantener la integridad del sistema de autenticación. Cada medida contribuye a crear múltiples capas de protección que, en conjunto, proporcionan una defensa robusta contra los vectores de ataque más comunes en aplicaciones web modernas.
Aprendizajes de esta lección
- Comprender qué es un JWT y por qué se utiliza en autenticación y autorización.
- Identificar la estructura y componentes de un JWT: header, payload y signature.
- Conocer el flujo completo de autenticación con JWT en aplicaciones NestJS.
- Aprender a implementar y proteger la autenticación JWT en NestJS, incluyendo manejo de expiración y refresh tokens.
- Reconocer las principales consideraciones y buenas prácticas de seguridad para el uso de JWT.
Completa Nest y certifícate
Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.
Asistente IA
Resuelve dudas al instante
Ejercicios
Practica con proyectos reales
Certificados
Valida tus conocimientos
Más de 25.000 desarrolladores ya se han certificado con CertiDevs