Antes de empezar
Esta es la semana 3 de la serie Sistema de autenticación con NestJS. Puedes encontrar todas las semanas publicadas aquí.
Puedes encontrar todo el código de esta serie en el repositorio.
Y recuerda que siguiendo la serie de youtube vas a tener explicaciones mucho más detalladas.
Y si quieres estar al día con nuestros contenidos suscríbete a nuestro canal de youtube y síguenos en twitter y suscríbete a nuestro newsletter.
Encriptar la contraseña
Hasta ahora hemos conseguido guardar el usuario pero estamos guardando la contraseña en texto pleno.
Vamos a utilizar la librería de bcrypt que instalamos en la primera semana. Primero generamos un salt y despues generamos un hash utilizando la contraseña y el salt. Es importante utilizar await ya que las operaciones de bcrypt so asincronas.
// src/auth/users.repository.ts
import {
ConflictException,
InternalServerErrorException,
} from '@nestjs/common';
import { EntityRepository, Repository } from 'typeorm';
import { RegisterUserDto } from './dto/register-user.dto';
import { User } from './user.entity';
import * as bcrypt from 'bcrypt';
@EntityRepository(User)
export class UsersRepository extends Repository {
async createUser(registerUserDto: RegisterUserDto): Promise {
const { name, email, password } = registerUserDto;
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(password, salt);
const user = this.create({ name, email, password: hashedPassword });
try {
await this.save(user);
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') {
throw new ConflictException('This email is already registered');
}
throw new InternalServerErrorException();
}
}
}
Probamos con postman y vemos si se guarda la contraseña encriptada.
Ahora que vemos que funciona vamos a mover la parte de hashing a un servicio dedicado y así solo pasamos la contraseña encriptada al repositorio.
Primer creamos el servicio encoder.
// src/auth/encoder.service.ts
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
@Injectable()
export class EncoderService {
async encodePassword(password: string): Promise {
const salt = await bcrypt.genSalt();
return await bcrypt.hash(password, salt);
}
}
Después actualizamos el repository para que reciba la contraseña directamente. También vamos a pasar directamente los parametros necesarios al método de createUser y así desacomplamos nuestro repositorio del dto.
// src/auth/users.repository.ts
import {
ConflictException,
InternalServerErrorException,
} from '@nestjs/common';
import { EntityRepository, Repository } from 'typeorm';
import { User } from './user.entity';
@EntityRepository(User)
export class UsersRepository extends Repository {
async createUser(
name: string,
email: string,
password: string,
): Promise {
const user = this.create({ name, email, password });
try {
await this.save(user);
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') {
throw new ConflictException('This email is already registered');
}
throw new InternalServerErrorException();
}
}
}
Y delegamos la responsabilidad de llamar al encoder service a auth service.
// src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { RegisterUserDto } from './dto/register-user.dto';
import { EncoderService } from './encoder.service';
import { UsersRepository } from './users.repository';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(UsersRepository)
private usersRepository: UsersRepository,
private encoderService: EncoderService,
) {}
async registerUser(registerUserDto: RegisterUserDto): Promise {
const { name, email, password } = registerUserDto;
const hashedPassword = await this.encoderService.encodePassword(password);
return this.usersRepository.createUser(name, email, hashedPassword);
}
}
Login
Igual que register vamos a crear un dto para login.
// src/auth/dto/login.dto.ts
import { IsNotEmpty, IsEmail, Length } from 'class-validator';
export class LoginDto {
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
@Length(6, 20)
password: string;
}
Y creamos un método en user repository para poder buscar el usuario en nuestra base de datos con el email.
// src/auth/users.repository.ts
import {
ConflictException,
InternalServerErrorException,
} from '@nestjs/common';
import { EntityRepository, Repository } from 'typeorm';
import { User } from './user.entity';
@EntityRepository(User)
export class UsersRepository extends Repository {
async createUser(
name: string,
email: string,
password: string,
): Promise {
const user = this.create({ name, email, password });
try {
await this.save(user);
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') {
throw new ConflictException('This email is already registered');
}
throw new InternalServerErrorException();
}
}
async findOneByEmail(email: string): Promise {
return await this.findOne({ email });
}
}
En el controlador creamos la ruta login que va a llamar al método login de auth service.
// src/auth/auth.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RegisterUserDto } from './dto/register-user.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('/register')
register(@Body() registerUserDto: RegisterUserDto): Promise {
return this.authService.registerUser(registerUserDto);
}
@Post('/login')
login(@Body() loginDto: LoginDto): Promise {
return this.authService.login(loginDto);
}
}
Y ahora vamos crear un método en el encoder service para poder comparar una contraseña en texto pleno con el texto encriptado que tenemos en la base de datos.
// src/auth/encoder.service.ts
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
@Injectable()
export class EncoderService {
async encodePassword(password: string): Promise {
const salt = await bcrypt.genSalt();
return await bcrypt.hash(password, salt);
}
async checkPassword(
password: string,
userPassword: string,
): Promise {
return await bcrypt.compare(password, userPassword);
}
}
Y por lo ultimo implementamos la lógica del servicio. Para ello primero consultamos el usuario del repositorio y después llamamos al encoder service para validar la contraseña. En el caso de que no exista el usuario o la contraseña este incorrecta lanzamos una excepción y en el caso contrario devolvemos el jwt (por ahora devolvemos el string jwt
y luego implementamos la lógica de creación de jwt).
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { LoginDto } from './dto/login.dto';
import { RegisterUserDto } from './dto/register-user.dto';
import { EncoderService } from './encoder.service';
import { UsersRepository } from './users.repository';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(UsersRepository)
private usersRepository: UsersRepository,
private encoderService: EncoderService,
) {}
async registerUser(registerUserDto: RegisterUserDto): Promise {
const { name, email, password } = registerUserDto;
const hashedPassword = await this.encoderService.encodePassword(password);
return this.usersRepository.createUser(name, email, hashedPassword);
}
async login(loginDto: LoginDto): Promise {
const { email, password } = loginDto;
const user = await this.usersRepository.findOneByEmail(email);
if (
user &&
(await this.encoderService.checkPassword(password, user.password))
) {
return 'jwt';
}
throw new UnauthorizedException('Please check your credentials');
}
}
Esto es todo para esta semana.
Recuerda que tenemos esta misma serie en nuestro canal de youtube donde Juan explica todo con mucho más detalles.