🚨 ¡Nueva review! ✨ Mi ratón favorito para programar: el Logitech MX Master 3S . ¡Échale un ojo! 👀

Authentication en NestJS: Passport, JWT y Refresh Tokens

Serie NestJS #10 — Login, registro, JWT access + refresh tokens, Passport strategies y AuthGuard

Escrito por domin el 4 de abril de 2026

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.

Candado digital sobre una API NestJS representando autenticación con JWT y Passport.

1. Autenticación vs Autorización

Antes de nada, vamos a aclararnos con dos conceptos que se pueden confundir facilmente:

🪪 Autenticación (AuthN)

¿QUIÉN eres? Verificar la identidad del usuario. "Demuéstrame que eres domin@domin.es". Se resuelve con email+password, OAuth, tokens, biometría...

🛡️ Autorización (AuthZ)

¿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 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 , 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:

TokenDuraciónPropósitoDónde se guarda
Access Token15 minutosAutenticar cada petición. Corta duración para limitar el daño si se robaMemoria del cliente (variable JS, no localStorage)
Refresh Token7 díasObtener un nuevo access token cuando expira. Larga duración pero solo se usa para renovarhttpOnly 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

Instalación 0 / 2
$
Pulsa para ejecutar el siguiente comando
PaqueteQué es
@nestjs/passportIntegración oficial de Passport con NestJS. AuthGuard, PassportStrategy
passportFramework de autenticación para Node.js con 500+ estrategias
@nestjs/jwtMódulo para generar y verificar JWT. Envuelve jsonwebtoken
passport-localEstrategia de autenticación por email/username + password
passport-jwtEstrategia 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

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;
    }
}

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:


10. Guards para proteger los endpoints

Los 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);
}
Úsalo cuando...
  • 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
Evítalo cuando...
  • 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

Flujo de autenticación completo 0 / 6
$
Pulsa para ejecutar el siguiente comando

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 . Si alguien roba un refresh token y lo usa, el token del usuario legítimo deja de funcionar.


15. Recapitulando

🔑 Passport Strategies

LocalStrategy para login con email+password. JwtStrategy para verificar access tokens. JwtRefreshStrategy para refresh tokens.

🔄 Access + Refresh

Access token corto (15min) para cada request. Refresh token largo (7d) hasheado en la DB para renovar.

🛡️ Guard global

JwtAuthGuard como APP_GUARD. Todo protegido por defecto. @Public() para excepciones explícitas.

👤 @GetUser()

Decorador custom para acceder al payload JWT tipado sin casteos ni req.user as any.

🔐 Token Rotation

Cada refresh genera nuevos tokens e invalida el anterior. Detecta uso de tokens robados.

⚠️ Errores genéricos

"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?