Vamos con el décimo post de la serie NestJS y primero del bloque de Seguridad. Hasta ahora nuestra API es un buffet libre porque cualquiera puede crear usuarios, leer datos, borrar registros… y sin identificarse, y eso no puede ser así que se acabará hoy.
Ya tenemos una base bastante completa con Docker (post 1), controllers (post 2), DI (post 3), módulos (post 4), middleware y pipeline (post 5), validación (post 6), PostgreSQL con TypeORM (posts 7-8) y relaciones y migraciones (post 9), y hoy le echamos el candao.
Vamos a montar un sistema de autenticación completo con registro de usuarios, login con email y password, JWT access tokens de corta duración, refresh tokens para renovarlos, y Guards que protegen los endpoints.
EA, vamos a blindar esta API.

1. Autenticación vs Autorización
Antes de nada, vamos a aclararnos con dos conceptos que se pueden confundir facilmente:
¿QUIÉN eres? Verificar la identidad del usuario. "Demuéstrame que eres domin@domin.es". Se resuelve con email+password, OAuth, tokens, biometría...
¿QUÉ puedes hacer? Una vez identificado, ¿tienes permiso para esta acción? "Eres Domin, pero ¿puedes borrar usuarios?" Se resuelve con roles, permisos, RBAC...
Este post solo cubre autenticación así que la autorización (roles, permisos, RBAC) la vemos en el post 11, que si no salen 30 min de lectura en el post.
2. ¿Cómo funciona JWT?
Ya hemos leído JWT, y ¿qué es JWT? Pues 💡 JWT JSON Web Token. Un string codificado en Base64 que contiene tres partes separadas por puntos: Header (algoritmo), Payload (datos del usuario) y Signature (firma digital). El servidor lo genera al hacer login y el cliente lo envía en cada petición para demostrar su identidad. Más info → es un token autofirmado que el servidor genera al hacer login. El cliente lo guarda y lo envía en cada petición posterior y el servidor lo puede verificar sin tener que consultar la base de datos.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. ← Header
eyJzdWIiOiJ1dWlkLTEyMyIsImVtYWlsIjoiZG9taW5AZG9taW4uZXMiLCJyb2xlIjoiYWRtaW4ifQ. ← Payload
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature
El flujo:
1. Cliente envía email + password → POST /auth/login
2. Servidor verifica las credenciales
3. Servidor genera un JWT firmado con un secreto
4. Cliente recibe el JWT
5. Cliente incluye el JWT en cada petición: Authorization: Bearer <token>
6. Servidor verifica la firma del JWT sin consultar la DB
¿Por qué no usar sesiones? Porque los JWT son stateless stateless Sin estado en el servidor. El token contiene toda la información necesaria. El servidor no guarda nada. Esto escala mejor que las sesiones porque cualquier instancia del servidor puede verificar el token. , esto quiere decir que el servidor no guarda estados y así se escala horizontalmente sin problemas.
3. Access Token + Refresh Token: el patrón profesional
Un solo token no es suficiente para producción, porque si alguien roba el token, tiene acceso indefinido. La solución estándar es usar dos tokens:
| Token | Duración | Propósito | Dónde se guarda |
|---|---|---|---|
| Access Token | 15 minutos | Autenticar cada petición. Corta duración para limitar el daño si se roba | Memoria del cliente (variable JS, no localStorage) |
| Refresh Token | 7 días | Obtener un nuevo access token cuando expira. Larga duración pero solo se usa para renovar | httpOnly cookie o almacenamiento seguro. Se guarda hasheado en la DB |
1. Login → Recibe access token (15min) + refresh token (7d)
2. Access token en cada request → Authorization: Bearer <access>
3. Access token expira → 401 Unauthorized
4. Cliente envía refresh token → POST /auth/refresh
5. Servidor verifica refresh token en la DB
6. Servidor genera NUEVO access token + NUEVO refresh token
7. Vuelta al paso 2
Si alguien roba el access token, tiene 15 minutos. Si roba el refresh token, lo invalidas en la DB y el atacante pierde acceso.
4. Instalación de dependencias
| Paquete | Qué es |
|---|---|
@nestjs/passport | Integración oficial de Passport con NestJS. AuthGuard, PassportStrategy |
passport | Framework de autenticación para Node.js con 500+ estrategias |
@nestjs/jwt | Módulo para generar y verificar JWT. Envuelve jsonwebtoken |
passport-local | Estrategia de autenticación por email/username + password |
passport-jwt | Estrategia que verifica JWT del header Authorization |
5. Variables de entorno para JWT
Añadimos los secretos al .env:
# .env
JWT_ACCESS_SECRET=tu-secreto-super-largo-y-aleatorio-para-access-tokens
JWT_REFRESH_SECRET=otro-secreto-diferente-para-refresh-tokens
JWT_ACCESS_EXPIRATION=15m
JWT_REFRESH_EXPIRATION=7d
Y actualizamos la validación de Joi del post 7:
// src/config/env.validation.ts
export const envValidationSchema = Joi.object({
// ... variables de DB existentes ...
JWT_ACCESS_SECRET: Joi.string().required(),
JWT_REFRESH_SECRET: Joi.string().required(),
JWT_ACCESS_EXPIRATION: Joi.string().default('15m'),
JWT_REFRESH_EXPIRATION: Joi.string().default('7d'),
});
6. El módulo Auth
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy';
import { UsersModule } from '../users/users.module';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({}), // Config dinámica en cada sign()
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy, JwtRefreshStrategy],
exports: [AuthService],
})
export class AuthModule {}
Fíjate que JwtModule.register({}) va vacío. No fijamos secreto ni expiración aquí porque usamos dos secretos diferentes (uno para access, otro para refresh) y los configuramos en el momento de firmar cada token.
7. AuthService: la lógica central
// src/auth/auth.service.ts
import {
Injectable,
UnauthorizedException,
ConflictException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { User } from '../users/entities/user.entity';
import { type RegisterDto } from './dto/register.dto';
import { type AuthTokens } from './interfaces/auth-tokens.interface';
import { type JwtPayload } from './interfaces/jwt-payload.interface';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
async register(registerDto: RegisterDto): Promise<AuthTokens> {
const emailTaken = await this.usersRepository.exists({
where: { email: registerDto.email },
});
if (emailTaken) {
throw new ConflictException('El email ya está registrado');
}
const hashedPassword = await bcrypt.hash(registerDto.password, 10);
const user = this.usersRepository.create({
...registerDto,
password: hashedPassword,
});
const savedUser = await this.usersRepository.save(user);
const tokens = await this.generateTokens(savedUser);
await this.updateRefreshToken(savedUser.id, tokens.refreshToken);
return tokens;
}
async validateUser(email: string, password: string): Promise<User> {
const user = await this.usersRepository.findOne({
where: { email },
select: ['id', 'email', 'name', 'password', 'role', 'active'],
});
if (!user) {
throw new UnauthorizedException('Credenciales inválidas');
}
if (!user.active) {
throw new UnauthorizedException('Usuario desactivado');
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Credenciales inválidas');
}
return user;
}
async login(user: User): Promise<AuthTokens> {
const tokens = await this.generateTokens(user);
await this.updateRefreshToken(user.id, tokens.refreshToken);
return tokens;
}
async refreshTokens(userId: string, refreshToken: string): Promise<AuthTokens> {
const user = await this.usersRepository.findOne({
where: { id: userId },
select: ['id', 'email', 'name', 'role', 'refreshTokenHash'],
});
if (!user || !user.refreshTokenHash) {
throw new UnauthorizedException('Acceso denegado');
}
const isRefreshValid = await bcrypt.compare(
refreshToken,
user.refreshTokenHash,
);
if (!isRefreshValid) {
throw new UnauthorizedException('Refresh token inválido');
}
const tokens = await this.generateTokens(user);
await this.updateRefreshToken(user.id, tokens.refreshToken);
return tokens;
}
async logout(userId: string): Promise<void> {
await this.usersRepository.update(userId, {
refreshTokenHash: null,
});
}
private async generateTokens(user: User): Promise<AuthTokens> {
const payload: JwtPayload = {
sub: user.id,
email: user.email,
role: user.role,
};
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, {
secret: this.configService.getOrThrow<string>('JWT_ACCESS_SECRET'),
expiresIn: this.configService.getOrThrow<string>('JWT_ACCESS_EXPIRATION'),
}),
this.jwtService.signAsync(payload, {
secret: this.configService.getOrThrow<string>('JWT_REFRESH_SECRET'),
expiresIn: this.configService.getOrThrow<string>('JWT_REFRESH_EXPIRATION'),
}),
]);
return { accessToken, refreshToken };
}
private async updateRefreshToken(
userId: string,
refreshToken: string,
): Promise<void> {
const hash = await bcrypt.hash(refreshToken, 10);
await this.usersRepository.update(userId, {
refreshTokenHash: hash,
});
}
}
Varios puntos clave:
El refresh token se guarda HASHEADO
private async updateRefreshToken(userId: string, refreshToken: string): Promise<void> {
const hash = await bcrypt.hash(refreshToken, 10);
await this.usersRepository.update(userId, { refreshTokenHash: hash });
}
Si alguien accede a tu base de datos, no puede usar los refresh tokens directamente. Están hasheados con bcrypt, igual que los passwords.
Necesitas añadir esta columna a la entidad User:
// Añadir a src/users/entities/user.entity.ts
@Column({ type: 'varchar', length: 255, nullable: true, select: false })
refreshTokenHash: string | null;
Mensajes de error genéricos
Tanto si el email no existe como si el password es incorrecto, el mensaje es “Credenciales inválidas”. Nunca deberías decir cuál de los dos falló, porque un atacante no debería saber si un email está registrado o no.
Promise.all para generar ambos tokens
Los dos tokens se generan en paralelo porque son operaciones independientes.
8. Interfaces y DTOs
// src/auth/interfaces/jwt-payload.interface.ts
import { type UserRole } from '../../users/enums/user-role.enum';
export interface JwtPayload {
sub: string; // User ID (convención JWT: "subject")
email: string;
role: UserRole;
}
// src/auth/interfaces/auth-tokens.interface.ts
export interface AuthTokens {
accessToken: string;
refreshToken: string;
}
// src/auth/dto/register.dto.ts
import { IsString, IsEmail, IsNotEmpty, MinLength, MaxLength, Matches } from 'class-validator';
export class RegisterDto {
@IsString()
@IsNotEmpty({ message: 'El nombre es obligatorio' })
@MinLength(2)
@MaxLength(100)
readonly name: string;
@IsEmail({}, { message: 'El email no tiene un formato válido' })
@IsNotEmpty()
readonly email: string;
@IsString()
@MinLength(8, { message: 'La contraseña debe tener al menos 8 caracteres' })
@MaxLength(128)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: 'La contraseña debe contener al menos una mayúscula, una minúscula y un número',
})
readonly password: string;
}
// src/auth/dto/login.dto.ts
import { IsEmail, IsString, IsNotEmpty } from 'class-validator';
export class LoginDto {
@IsEmail()
@IsNotEmpty()
readonly email: string;
@IsString()
@IsNotEmpty()
readonly password: string;
}
9. Passport Strategies: LocalStrategy y JwtStrategy
💡 Passport Strategy Clase que define CÓMO se autentica al usuario. Cada estrategia implementa un mecanismo diferente: local (email+password), jwt (verificar token), google (OAuth), etc. NestJS las integra con PassportStrategy() que combina el mixin de Passport con @Injectable(). Más info → es donde definimos cómo se verifica la identidad del usuario.
9.1. LocalStrategy: email + password
Se usa solo en el endpoint de login para verificar credenciales:
// src/auth/strategies/local.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
import { type User } from '../../users/entities/user.entity';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
constructor(private readonly authService: AuthService) {
super({
usernameField: 'email', // Passport usa "username" por defecto, nosotros usamos "email"
});
}
async validate(email: string, password: string): Promise<User> {
return this.authService.validateUser(email, password);
}
}
El método validate() es lo que Passport ejecuta cuando se activa esta estrategia. Si lanza una excepción, la autenticación falla y si devuelve un valor, ese valor se inyecta en req.user.
9.2. JwtStrategy para verificar el access token
Se usa en todos los endpoints protegidos para verificar el JWT del header Authorization:
// src/auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { type JwtPayload } from '../interfaces/jwt-payload.interface';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.getOrThrow<string>('JWT_ACCESS_SECRET'),
});
}
validate(payload: JwtPayload): JwtPayload {
// El payload ya está verificado y decodificado por Passport
// Lo que devolvamos aquí se inyecta en req.user
return payload;
}
}
ExtractJwt.fromAuthHeaderAsBearerToken(): Extrae el token del headerAuthorization: Bearer <token>.ignoreExpiration: false: Si el token expiró, rechazar. Nunca lo pongas atrue.secretOrKey: El mismo secreto que usamos para firmar. Si alguien cambia el payload, la firma no coincide y se rechaza.
El validate() de JwtStrategy recibe el payload ya verificado. Passport ya ha comprobado la firma y la expiración. Solo devolvemos el payload para que esté disponible en req.user.
9.3. JwtRefreshStrategy para verificar el refresh token
// src/auth/strategies/jwt-refresh.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { type Request } from 'express';
import { type JwtPayload } from '../interfaces/jwt-payload.interface';
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.getOrThrow<string>('JWT_REFRESH_SECRET'),
passReqToCallback: true, // Pasa el req al validate()
});
}
validate(req: Request, payload: JwtPayload): JwtPayload & { refreshToken: string } {
const authHeader = req.headers.authorization;
const refreshToken = authHeader?.replace('Bearer ', '').trim() ?? '';
return {
...payload,
refreshToken,
};
}
}
La diferencia con JwtStrategy:
- Usa
JWT_REFRESH_SECRETen vez deJWT_ACCESS_SECRET. Cada token tiene su propio secreto. passReqToCallback: truenos da acceso alreqenvalidate()para extraer el refresh token original y poder compararlo con el hash de la DB.
10. Guards para proteger los endpoints
Los 💡 Guards Clases que implementan CanActivate y deciden si una request puede proceder. Se ejecutan DESPUÉS del middleware y ANTES de los interceptors y pipes en el pipeline de NestJS. AuthGuard de Passport activa la estrategia correspondiente automáticamente. Más info → son la pieza del pipeline que decide si la request pasa o no. ¿Recuerdas el post 5? Los Guards van justo después del middleware.
10.1. Guards basados en Passport
// src/auth/guards/local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
// src/auth/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// src/auth/guards/jwt-refresh.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtRefreshGuard extends AuthGuard('jwt-refresh') {}
El string que pasas a AuthGuard() debe coincidir con el segundo argumento de PassportStrategy(Strategy, 'jwt'). Así NestJS sabe qué estrategia ejecutar.
10.2. Decorador @GetUser(): acceso tipado al usuario
En vez de acceder a req.user con @Req(), creamos un decorador custom:
// src/auth/decorators/get-user.decorator.ts
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
import { type JwtPayload } from '../interfaces/jwt-payload.interface';
export const GetUser = createParamDecorator(
(data: keyof JwtPayload | undefined, ctx: ExecutionContext): JwtPayload | string => {
const request = ctx.switchToHttp().getRequest();
const user = request.user as JwtPayload;
return data ? user[data] : user;
},
);
Ahora en cualquier controller:
@Get('me')
@UseGuards(JwtAuthGuard)
getMe(@GetUser() user: JwtPayload): JwtPayload {
return user;
}
// O extraer solo una propiedad
@Get('my-id')
@UseGuards(JwtAuthGuard)
getMyId(@GetUser('sub') userId: string): string {
return userId;
}
Tipado, limpio y sin casteos. Mucho mejor que @Req() req: Request y luego req.user as any.
11. AuthController: los endpoints
// src/auth/auth.controller.ts
import {
Controller,
Post,
Body,
UseGuards,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { JwtRefreshGuard } from './guards/jwt-refresh.guard';
import { GetUser } from './decorators/get-user.decorator';
import { type User } from '../users/entities/user.entity';
import { type AuthTokens } from './interfaces/auth-tokens.interface';
import { type JwtPayload } from './interfaces/jwt-payload.interface';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
register(@Body() registerDto: RegisterDto): Promise<AuthTokens> {
return this.authService.register(registerDto);
}
@Post('login')
@HttpCode(HttpStatus.OK)
@UseGuards(LocalAuthGuard)
login(@GetUser() user: User): Promise<AuthTokens> {
// LocalAuthGuard ya validó email+password
// user viene de LocalStrategy.validate()
return this.authService.login(user);
}
@Post('refresh')
@HttpCode(HttpStatus.OK)
@UseGuards(JwtRefreshGuard)
refresh(
@GetUser('sub') userId: string,
@GetUser() payload: JwtPayload & { refreshToken: string },
): Promise<AuthTokens> {
return this.authService.refreshTokens(userId, payload.refreshToken);
}
@Post('logout')
@HttpCode(HttpStatus.OK)
@UseGuards(JwtAuthGuard)
logout(@GetUser('sub') userId: string): Promise<void> {
return this.authService.logout(userId);
}
}
Vamos a ver cómo funciona cada endpoint:
POST /auth/register
No necesita Guard y cualquiera puede registrarse. El body se valida con RegisterDto (class-validator del post 6).
POST /auth/login
@UseGuards(LocalAuthGuard) activa la LocalStrategy. Passport extrae email y password del body, los pasa a validate() que llama a authService.validateUser(). Si las credenciales son válidas, el usuario queda en req.user y @GetUser() lo extrae.
El LoginDto no hace falta pasarlo a @Body() porque Passport lee el body directamente. Pero lo definimos como DTO para documentar la API.
POST /auth/refresh
@UseGuards(JwtRefreshGuard) activa la JwtRefreshStrategy. Verifica el refresh token del header Authorization con el secreto de refresh. El refreshTokens() del service además compara con el hash de la DB.
POST /auth/logout
@UseGuards(JwtAuthGuard) verifica que el usuario está autenticado. Luego borra el refresh token hash de la DB, invalidando futuros refreshes.
12. Guard global para proteger toda la API por defecto
En vez de poner @UseGuards(JwtAuthGuard) en cada controller, lo registramos globalmente y marcamos las rutas públicas con un decorador:
// src/auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// src/auth/guards/jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private readonly reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext): boolean | Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true; // Deja pasar sin verificar JWT
}
return super.canActivate(context) as boolean | Promise<boolean>;
}
}
Registro global en AppModule:
// src/app.module.ts
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
@Module({
// ...
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}
Ahora todos los endpoints están protegidos por defecto. Para hacer público un endpoint:
@Public()
@Post('register')
register(@Body() registerDto: RegisterDto): Promise<AuthTokens> {
return this.authService.register(registerDto);
}
@Public()
@Post('login')
@HttpCode(HttpStatus.OK)
@UseGuards(LocalAuthGuard)
login(@GetUser() user: User): Promise<AuthTokens> {
return this.authService.login(user);
}
- Guard global + @Public() para rutas que no requieren auth. Seguridad por defecto, excepciones explícitas
- @Public() en register, login, health checks, documentación y endpoints públicos
- Poner @UseGuards(JwtAuthGuard) manualmente en cada controller. Fácil olvidarlo en un endpoint
- Dejar endpoints sin protección por accidente. Con guard global, lo que no es @Public() está protegido
13. Probando el flujo completo
14. Seguridad: lo que NO debes hacer
No guardes tokens en localStorage
localStorage es accesible desde cualquier script JavaScript en la misma página. Un ataque XSS roba todos los tokens. El access token va en memoria (variable JS), el refresh token en una cookie httpOnly.
No pongas datos sensibles en el payload del JWT
El payload del JWT se puede decodificar (Base64) sin la firma. No metas passwords, datos personales completos o secretos. Solo lo mínimo: sub (user ID), email, role.
No ignores la expiración del refresh token
Si un refresh token se filtra y no expira nunca, el atacante tiene acceso para siempre. 7 días es un buen balance. Algunos sistemas revocan todos los refresh tokens del usuario si detectan uso sospechoso.
Rotación de refresh tokens
Nuestro refreshTokens() genera un nuevo refresh token y actualiza el hash en la DB. Esto es token rotation token rotation Cada vez que se usa un refresh token para obtener nuevos tokens, el refresh token antiguo se invalida y se emite uno nuevo. Si un atacante usa un refresh token robado, el siguiente refresh del usuario legítimo fallará, alertando del compromiso. . Si alguien roba un refresh token y lo usa, el token del usuario legítimo deja de funcionar.
15. Recapitulando
LocalStrategy para login con email+password. JwtStrategy para verificar access tokens. JwtRefreshStrategy para refresh tokens.
Access token corto (15min) para cada request. Refresh token largo (7d) hasheado en la DB para renovar.
JwtAuthGuard como APP_GUARD. Todo protegido por defecto. @Public() para excepciones explícitas.
Decorador custom para acceder al payload JWT tipado sin casteos ni req.user as any.
Cada refresh genera nuevos tokens e invalida el anterior. Detecta uso de tokens robados.
"Credenciales inválidas" sin revelar si el email existe o no. No des pistas a los atacantes.
En el próximo post montamos la autorización: Guards custom, decorador @Roles(), RBAC, @SetMetadata + Reflector y permisos granulares con CASL. La autenticación te dice quién eres y la autorización decide qué puedes hacer.
EA, nos vemos en los bares!! 🍺
Pon a prueba lo aprendido
1. ¿Cuál es la diferencia entre autenticación y autorización?
2. ¿Por qué el refresh token se guarda hasheado en la base de datos?
3. ¿Qué hace el decorador @Public() en nuestra implementación?
4. ¿Por qué usamos dos secretos diferentes para access token y refresh token?
5. ¿Qué ventaja tiene registrar JwtAuthGuard como APP_GUARD global?