Los procesos de registro (sign-up) y login (sign-in) son la puerta de entrada a tu aplicación, esenciales para gestionar el acceso de usuarios y proteger tus recursos. En esta lección, integraremos completamente el registro y el login utilizando JSON Web Tokens (JWT), construyendo sobre la base que ya hemos sentado con las lecciones anteriores.
Configuración inicial
Antes de sumergirse en la lógica de autenticación, se necesita una nueva aplicación NestJS e instalar las siguientes dependencias:
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt class-validator class-transformer @nestjs/mapped-types
# o
yarn add @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt class-validator class-transformer @nestjs/mapped-types
Definición del Role
Enum y la Entidad User
Como ya establecimos en lecciones anteriores, tenemos nuestro Role
enum y la entidad User
que representa la tabla de usuarios en nuestra base de datos. Nos aseguraremos de que esta entidad sea la base para nuestro AuthModule
.
src/user/role.enum.ts7
export enum Role {
USER = 'user',
ADMIN = 'admin',
}
src/user/user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Role } from './role.enum'; // Asegúrate de que la ruta sea correcta
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
password?: string;
@Column({ nullable: true })
phone: string;
@Column({ nullable: true })
addressStreet: string;
@Column({ nullable: true })
photoUrl: string;
@Column({
type: 'enum',
enum: Role,
default: Role.USER,
})
role: Role;
}
DTOs para Registro y Login
Para la validación de la entrada, utilizaremos DTOs. Asumimos que RegisterDto
ya existe, y crearemos o confirmaremos LoginDto
.
src/auth/dto/register.dto.ts
import {
IsEmail,
IsString,
MinLength,
IsNotEmpty,
IsOptional,
} from 'class-validator';
export class RegisterDto {
@IsEmail({}, { message: 'El email debe ser una dirección de correo válida.' })
@IsNotEmpty({ message: 'El email es un campo requerido.' })
email: string;
@IsString({ message: 'La contraseña debe ser una cadena de texto.' })
@MinLength(6, { message: 'La contraseña debe tener al menos 6 caracteres.' })
@IsNotEmpty({ message: 'La contraseña es un campo requerido.' })
password: string;
// Puedes añadir más campos opcionales si son parte de tu formulario de registro inicial
@IsOptional()
@IsString()
phone?: string;
@IsOptional()
@IsString()
addressStreet?: string;
@IsOptional()
@IsString()
photoUrl?: string;
}
src/auth/dto/login.dto.ts
import { IsEmail, IsString, IsNotEmpty } from 'class-validator';
export class LoginDto {
@IsEmail({}, { message: 'El email debe ser una dirección de correo válida.' })
@IsNotEmpty({ message: 'El email es un campo requerido.' })
email: string;
@IsString({ message: 'La contraseña debe ser una cadena de texto.' })
@IsNotEmpty({ message: 'La contraseña es un campo requerido.' })
password: string;
}
Definición de la Interfaz para el Payload JWT
Para una mejor tipificación, definiremos la estructura del payload que se codifica y decodifica en el JWT.
src/auth/interfaces/jwt-payload.interface.ts
import { Role } from '../../user/role.enum'; // Asegúrate de que la ruta sea correcta
export interface JwtPayload {
sub: number; // El ID del usuario (subject)
email: string;
role: Role;
}
Configuración del AuthModule
Nuestro AuthModule
ya existente centraliza toda la lógica de autenticación. Nos aseguraremos de que esté correctamente configurado para trabajar con TypeORM, Passport y JWT, incluyendo la lectura segura del secreto JWT desde variables de entorno.
src/auth/auth.module.ts
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
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { User } from '../user/user.entity';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtStrategy } from './strategies/jwt.strategy';
@Module({
imports: [
TypeOrmModule.forFeature([User]),
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '7d' },
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [
AuthService,
JwtStrategy,
],
exports: [AuthService, JwtStrategy, PassportModule],
})
export class AuthModule { }
Servicio de Autenticación (AuthService
)
Este servicio, ya existente, contendrá la lógica principal para el registro y el login. Implementaremos el método login
y ajustaremos el register
para la exclusión segura de la contraseña al devolver el usuario.
src/auth/auth.service.ts
import {
Injectable,
ConflictException,
InternalServerErrorException,
UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../user/user.entity';
import * as bcrypt from 'bcrypt';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
import { JwtService } from '@nestjs/jwt';
import { Role } from '../user/role.enum';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
private jwtService: JwtService,
) {}
/**
* Registra un nuevo usuario en el sistema.
* Cifra la contraseña de forma segura y verifica que el email no esté ya en uso.
*/
async register(registerDto: RegisterDto): Promise<Omit<User, 'password'>> { // Tipo de retorno sin 'password'
const { email, password, ...userData } = registerDto;
const existingUser = await this.userRepository.findOne({ where: { email } });
if (existingUser) {
throw new ConflictException('El email ya está registrado.');
}
try {
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = this.userRepository.create({
email,
password: hashedPassword,
role: Role.USER,
...userData,
});
await this.userRepository.save(newUser);
// Creamos una copia del objeto usuario sin la contraseña por seguridad
const { password: _, ...result } = newUser; // Desestructura para omitir 'password'
return result; // Devuelve el nuevo objeto sin la contraseña
} catch (error) {
console.error('Error al registrar usuario:', error);
throw new InternalServerErrorException('Error al registrar el usuario. Por favor, inténtelo de nuevo.');
}
}
/**
* Autentica a un usuario y genera un token JWT si las credenciales son correctas.
* Utiliza bcrypt.compare para verificar la contraseña de forma segura.
*/
async login(loginDto: LoginDto): Promise<{ accessToken: string }> {
const { email, password } = loginDto;
const user = await this.userRepository.findOne({ where: { email } });
// Si el usuario no existe O la contraseña no coincide, lanza UnauthorizedException.
// Esto evita la enumeración de usuarios por razones de seguridad.
if (!user || !(await bcrypt.compare(password, user.password ?? ''))) {
throw new UnauthorizedException('Credenciales incorrectas (email o contraseña).');
}
// Preparamos el payload para el token JWT con información esencial del usuario
const payload = {
sub: user.id,
email: user.email,
role: user.role,
};
const accessToken = await this.jwtService.signAsync(payload);
return { accessToken }; // Devuelve el token de acceso
}
}
Controlador de Autenticación (AuthController
)
El AuthController
ya existente, expone los endpoints HTTP para el registro y el login, delegando la lógica al AuthService
.
src/auth/auth.controller.ts
import {
Controller,
Post,
Body,
HttpCode,
HttpStatus,
// Ya no necesitamos UsePipes o ValidationPipe aquí si es global en main.ts
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
import { User } from '../user/user.entity';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
/**
* Endpoint para registrar nuevos usuarios.
* Utiliza RegisterDto para la validación de entrada.
*/
@Post('register')
@HttpCode(HttpStatus.CREATED)
async register(@Body() registerDto: RegisterDto): Promise<Omit<User, 'password'>> {
return this.authService.register(registerDto);
}
/**
* Endpoint para el login de usuarios.
* Recibe las credenciales en LoginDto y, si son válidas, devuelve un JWT.
*/
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Body() loginDto: LoginDto): Promise<{ accessToken: string }> {
return this.authService.login(loginDto);
}
}
Estrategia JWT de Passport (JwtStrategy
)
La JwtStrategy
(ya existente) se encarga de validar los tokens JWT entrantes. La hemos ajustado para leer el secreto de forma segura y para devolver el usuario sin la contraseña.
src/auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { Repository } from 'typeorm';
import { User } from '../../user/user.entity';
import { JwtPayload } from '../interfaces/jwt-payload.interface';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
private configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'), // Lee la clave secreta de .env
});
}
/**
* Método `validate` de Passport.
* Se llama después de que el token JWT ha sido decodificado y verificado.
* `payload` contiene los datos que firmaste en el token (id, email, role).
* Carga el usuario de la DB y lo adjunta a `request.user`.
*/
async validate(payload: JwtPayload): Promise<Omit<User, 'password'>> { // Tipo de retorno sin 'password'
const { sub: id, email } = payload;
const user = await this.userRepository.findOne({ where: { id, email } });
if (!user) {
throw new UnauthorizedException('Token inválido o usuario no encontrado.');
}
// Creamos una copia del objeto usuario sin la contraseña por seguridad.
// Este objeto es el que estará disponible en `request.user` en tus controladores protegidos.
const { password: _, ...result } = user; // Desestructura para omitir 'password'
return result; // Devuelve el objeto usuario sin la contraseña
}
}
Configuración del AppModule
Tu AppModule
se mantiene limpio, ya que AuthModule
centraliza la configuración de TypeORM (para User
), Passport y JWT.
src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthModule } from './auth/auth.module'; // Importa tu AuthModule
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: configService.get<'mysql'>('DB_TYPE'),
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USERNAME'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_DATABASE'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
inject: [ConfigService],
}),
AuthModule, // Tu módulo de autenticación
// Aquí importarías otros módulos de tu aplicación (ej. BookModule, ProductsModule)
],
})
export class AppModule {}
Probando el Flujo Completo
Ahora tienes un sistema de autenticación completo y seguro.
- Asegúrate de que
JWT_SECRET
esté en tu.env
. - Inicia tu aplicación NestJS. (
npm run start
) - Realiza un Registro:
- Método:
POST
- URL:
http://localhost:3000/auth/register
- Body (raw JSON):
- Método:
{
"email": "user2@gmail.com",
"password": "admin1",
"phone": "987654321"
}
- Realiza un Login:
- Método:
POST
- URL:
http://localhost:3000/auth/login
- Body (raw JSON):
- Método:
{
"email": "user2@gmail.com",
"password": "admin1"
}
- Si las credenciales son válidas, obtendrás un
200 OK
con unaccessToken
. Este es el token que el frontend usará para acceder a rutas protegidas.
Aprendizajes de esta lección
- Comprender los fundamentos de la autenticación.
- Aprender a implementar el registro y el login en NestJS.
- Conocer las estrategias de autenticación más comunes.
- Implementar el registro de usuarios y el login con 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