NestJS

Nest

Tutorial Nest: Login y registro

Nest login registro usuarios: implementación. Aprende a implementar sistemas de login y registro de usuarios en Nest con ejemplos prácticos.

Aprende Nest y certifícate

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


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.

  1. Asegúrate de que JWT_SECRET esté en tu .env.
  2. Inicia tu aplicación NestJS. (npm run start)
  3. Realiza un Registro:
    • Método: POST
    • URL: http://localhost:3000/auth/register
    • Body (raw JSON):
{
  "email": "user2@gmail.com",
  "password": "admin1",
  "phone": "987654321"
}

  1. Realiza un Login:
    • Método: POST
    • URL: http://localhost:3000/auth/login
    • Body (raw JSON):
{
  "email": "user2@gmail.com",
  "password": "admin1"
}
  • Si las credenciales son válidas, obtendrás un 200 OK con un accessToken. Este es el token que el frontend usará para acceder a rutas protegidas.

Aprende Nest online

Otras lecciones de Nest

Accede a todas las lecciones de Nest y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Nest y certifícate

Ejercicios de programación de Nest

Evalúa tus conocimientos de esta lección Login y registro con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje 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.