Estrategia de autenticación JWT

Avanzado
Nest
Nest
Actualizado: 15/06/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

¿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 token
  • typ (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 token
  • sub (Subject): Identifica el sujeto del token (generalmente el ID del usuario)
  • aud (Audience): Identifica los destinatarios del token
  • exp (Expiration Time): Tiempo de expiración del token
  • iat (Issued At): Momento en que se emitió el token
  • nbf (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.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

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

⭐⭐⭐⭐⭐
4.9/5 valoración