En esta página
Autenticación con JWT y Passport
Autenticación en APIs REST
La autenticación es el proceso de verificar la identidad de quien hace una petición. En APIs REST stateless, el mecanismo más común es JWT (JSON Web Token): el servidor emite un token firmado al hacer login, y el cliente lo incluye en cada petición posterior en el header Authorization: Bearer <token>.
Instalando las dependencias
npm install @nestjs/passport @nestjs/jwt passport passport-local passport-jwt bcrypt
npm install --save-dev @types/passport-local @types/passport-jwt @types/bcryptLa entidad Usuario con manejo seguro de contraseñas
// usuarios/entities/usuario.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
@Entity('usuarios')
export class Usuario {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 100 })
nombre: string;
@Column({ unique: true, length: 255 })
email: string;
@Column({ name: 'password_hash', select: false }) // nunca se incluye por defecto
passwordHash: string;
@Column({ type: 'text', array: true, default: ['usuario'] })
roles: string[];
@Column({ default: true })
activo: boolean;
@CreateDateColumn()
creadoEn: Date;
}DTOs de autenticación
// auth/dto/registro.dto.ts
import { IsEmail, IsString, MinLength, MaxLength, Matches } from 'class-validator';
import { Transform } from 'class-transformer';
export class RegistroDto {
@IsString()
@MinLength(2)
@MaxLength(100)
nombre: string;
@IsEmail()
@Transform(({ value }: { value: string }) => value.toLowerCase().trim())
email: string;
@IsString()
@MinLength(8)
@Matches(/^(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])/, {
message: 'La contraseña debe tener una mayúscula, un número y un símbolo especial',
})
password: string;
}
// auth/dto/login.dto.ts
export class LoginDto {
@IsEmail()
@Transform(({ value }: { value: string }) => value.toLowerCase())
email: string;
@IsString()
@IsNotEmpty()
password: string;
}El controlador de autenticación
// auth/auth.controller.ts
import {
Controller, Post, Body, UseGuards, Get, HttpCode, HttpStatus
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { RegistroDto } from './dto/registro.dto';
import { Public } from './decorators/public.decorator';
import { UsuarioActual } from './decorators/usuario-actual.decorator';
import { JwtPayload } from './strategies/jwt.strategy';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('registro')
@Public()
registro(@Body() registroDto: RegistroDto) {
return this.authService.registro(registroDto);
}
@Post('login')
@Public()
@HttpCode(HttpStatus.OK)
@UseGuards(AuthGuard('local')) // ejecuta LocalStrategy.validate()
login(@UsuarioActual() usuario: UsuarioPayload) {
return this.authService.login(usuario);
}
@Get('perfil')
// El JwtAuthGuard global protege esta ruta automáticamente
getPerfil(@UsuarioActual() usuario: JwtPayload) {
return usuario;
}
@Post('logout')
@HttpCode(HttpStatus.NO_CONTENT)
logout() {
// En JWT stateless no hay logout real en el servidor
// El cliente debe eliminar el token del almacenamiento local
// Si tienes refresh tokens, invalida el refresh token aquí
return;
}
}Implementando refresh tokens
Para un sistema de autenticación más robusto con refresh tokens:
// En la entidad Usuario, añade:
@Column({ name: 'refresh_token_hash', nullable: true, select: false })
refreshTokenHash: string | null;
// En AuthService:
async generarTokens(usuario: Usuario): Promise<{ accessToken: string; refreshToken: string }> {
const payload: JwtPayload = {
sub: usuario.id,
email: usuario.email,
roles: usuario.roles,
};
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, { expiresIn: '15m' }),
this.jwtService.signAsync(
{ sub: usuario.id },
{ secret: process.env['JWT_REFRESH_SECRET'], expiresIn: '7d' }
),
]);
// Guardar hash del refresh token en la BD
const refreshTokenHash = await bcrypt.hash(refreshToken, 10);
await this.usuarioRepo.update(usuario.id, { refreshTokenHash });
return { accessToken, refreshToken };
}
async refrescarToken(usuarioId: string, refreshToken: string): Promise<{ accessToken: string }> {
const usuario = await this.usuarioRepo.findOne({
where: { id: usuarioId },
select: ['id', 'email', 'roles', 'refreshTokenHash', 'activo'],
});
if (!usuario || !usuario.activo || !usuario.refreshTokenHash) {
throw new UnauthorizedException('Sesión inválida');
}
const tokenValido = await bcrypt.compare(refreshToken, usuario.refreshTokenHash);
if (!tokenValido) {
// Posible robo de token — invalida todos los tokens del usuario
await this.usuarioRepo.update(usuarioId, { refreshTokenHash: null });
throw new UnauthorizedException('Refresh token inválido');
}
const accessToken = this.generarToken(usuario);
return { accessToken };
}Protección de rutas con el patrón global
La forma más limpia de proteger toda la aplicación es registrar el JwtAuthGuard globalmente y usar el decorador @Public() para las excepciones:
// 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 {}Variables de entorno necesarias
Crea un archivo .env con:
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASS=secreto
DB_NAME=mi_api
JWT_SECRET=tu_secret_muy_largo_y_aleatorio_aqui
JWT_EXPIRES_IN=15m
JWT_REFRESH_SECRET=otro_secret_diferente_para_refresh
NODE_ENV=developmentCon el sistema de autenticación JWT implementado, tu API tiene una capa de seguridad sólida. En la próxima lección aprenderemos sobre interceptores y middleware —herramientas para transformar respuestas, agregar logging y gestionar el ciclo completo de la petición.
Inicia sesión para guardar tu progreso