Antes de empezar
Esta es la semana 4 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.
Configurar passport
Primero vamos importar el modulo de passport y jwt en nuestro auth module.
En esta serie vamos a dejar el secret dentro del código pero ten en cuenta que esto es una mala practica de seguridad. El secreto tiene que venir de una variable de entorno pero esto no cae en el contexto de esta serie.
// src/auth/users.repository.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { EncoderService } from './encoder.service';
import { JwtStrategy } from './jwt.strategy';
import { UsersRepository } from './users.repository';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: 'super-secret',
signOptions: {
expiresIn: 3600,
},
}),
TypeOrmModule.forFeature([UsersRepository]),
],
controllers: [AuthController],
providers: [AuthService, EncoderService, JwtStrategy],
exports: [JwtStrategy, PassportModule],
})
export class AuthModule {}
Lo siguiente que vamos a hacer es crear una interfaz para el payload de nuestro jwt. Además creamos el jwt strategy para passport. El strategy nos permite de verificar el payload del jwt (la verificación de signature se hace por la librería passport-jwt).
// src/auth/jwt-payload.interface.ts
export interface JwtPayload {
id: string;
email: string;
active: boolean;
}
// src/auth/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtPayload } from './jwt-payload.interface';
import { User } from './user.entity';
import { UsersRepository } from './users.repository';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@InjectRepository(UsersRepository)
private usersRepository: UsersRepository,
) {
super({
secretOrKey: 'super-secret',
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
});
}
async validate(payload: JwtPayload): Promise {
const { email } = payload;
const user = this.usersRepository.findOneByEmail(email);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Como puedes ver estamos pasando la información necesaria a passport-jwt sobre cómo sacar el jwt (de header) y cuál es el secreto. En el método validate nos encrgamos de comprobar si el usuario existe en la base de datos.
Y ahora exportamos este strategy y el modulo passport y además pasamos nuestro strategy como un provider.
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { EncoderService } from './encoder.service';
import { JwtStrategy } from './jwt.strategy';
import { UsersRepository } from './users.repository';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: 'super-secret',
signOptions: {
expiresIn: 3600,
},
}),
TypeOrmModule.forFeature([UsersRepository]),
],
controllers: [AuthController],
providers: [AuthService, EncoderService, JwtStrategy],
exports: [JwtStrategy, PassportModule],
})
export class AuthModule {}
Y por lo ultimo vamos a actualizar nuestro servicio para que genere el jwt a la hora de login.
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { LoginDto } from './dto/login.dto';
import { RegisterUserDto } from './dto/register-user.dto';
import { EncoderService } from './encoder.service';
import { JwtPayload } from './jwt-payload.interface';
import { UsersRepository } from './users.repository';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(UsersRepository)
private usersRepository: UsersRepository,
private encoderService: EncoderService,
private jwtService: JwtService,
) {}
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<{ accessToken: string }> {
const { email, password } = loginDto;
const user = await this.usersRepository.findOneByEmail(email);
if (
user &&
(await this.encoderService.checkPassword(password, user.password))
) {
const payload: JwtPayload = { id: user.id, email, active: user.active };
const accessToken = await this.jwtService.sign(payload);
return { accessToken };
}
throw new UnauthorizedException('Please check your credentials');
}
}
Ahora si probamos con postman vemos que a la hora de hacer login la aplicación nos devuelve el jwt en la respuesta.
Activación de cuenta
Para implementar el proceso de activación de cuenta vamos a necesitar un token que lo mandamos al email del usuario y cuando el usuario haga click en el link en nuestro frontend llamamos a una ruta pasando ese token y así activamos la cuenta.
Primero creamos un atributo para el token en el usuario. Adicionalmente corregimos el nombre de la columna de createOn porque por defecto typeorm crea la columna con el mismo nombre del atributo y en mysql es myql es más común utilizar snake case (separar las palabras con la barra baja).
// src/auth/user.entity.ts
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 20 })
name: string;
@Column({ length: 100, unique: true })
email: string;
@Column({ length: 100 })
password: string;
@Column({ type: 'boolean', default: false })
active: boolean;
@Column({ type: 'uuid', unique: true, name: 'activation_token' })
activationToken: string;
@CreateDateColumn({ name: 'created_on' })
createdOn: Date;
}
Utilizar uuid no es muy seguro para generar un token por lo tanto te recomiendo que utilices otro método como crypto (ejemplo aquí).
Y después creamos nuestro de dto de la ruta de activación.
// src/auth/dto/activate-user.dto.ts
import { IsNotEmpty, IsUUID } from 'class-validator';
export class ActivateUserDto {
@IsNotEmpty()
@IsUUID('4')
id: string;
@IsNotEmpty()
@IsUUID('4')
code: string;
}
Y para poder generar el uuid instalamos la librería uuid.
yarn add uuid
Lo siguiente vamos a actualizar user repository para que reciba el token de activación.
// src/auth/users.repository.dto.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,
activationToken: string,
): Promise {
const user = this.create({ name, email, password, activationToken });
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 });
}
}
Y ahora desde el user service tenemos que pasar un token al repositorio.
// src/auth/auth.service.ts
import {
Injectable,
UnauthorizedException,
UnprocessableEntityException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { LoginDto } from './dto/login.dto';
import { RegisterUserDto } from './dto/register-user.dto';
import { EncoderService } from './encoder.service';
import { JwtPayload } from './jwt-payload.interface';
import { UsersRepository } from './users.repository';
import { v4 } from 'uuid';
import { ActivateUserDto } from './dto/activate-user.dto';
import { User } from './user.entity';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(UsersRepository)
private usersRepository: UsersRepository,
private encoderService: EncoderService,
private jwtService: JwtService,
) {}
async registerUser(registerUserDto: RegisterUserDto): Promise {
const { name, email, password } = registerUserDto;
const hashedPassword = await this.encoderService.encodePassword(password);
return this.usersRepository.createUser(name, email, hashedPassword, v4());
}
async login(loginDto: LoginDto): Promise<{ accessToken: string }> {
const { email, password } = loginDto;
const user = await this.usersRepository.findOneByEmail(email);
if (
user &&
(await this.encoderService.checkPassword(password, user.password))
) {
const payload: JwtPayload = { id: user.id, email, active: user.active };
const accessToken = await this.jwtService.sign(payload);
return { accessToken };
}
throw new UnauthorizedException('Please check your credentials');
}
}
Ahora que tenemos la parte de generación del token ahora tenemos que implemntar la parte de activación.
Primero creamos un método en el repositorio para encontrar un usuario inactivo utilizando el id del usuario y el token de la activación.
// 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,
activationToken: string,
): Promise {
const user = this.create({ name, email, password, activationToken });
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 });
}
async activateUser(user: User): Promise {
user.active = true;
this.save(user);
}
async findOneInactiveByIdAndActivationToken(
id: string,
code: string,
): Promise {
return this.findOne({ id: id, activationToken: code, active: false });
}
}
En el servicio creamos el método que va a buscar el usuario con el id y el token y si encuentra el usario lo activa.
// src/auth/auth.service.ts
import {
Injectable,
UnauthorizedException,
UnprocessableEntityException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { LoginDto } from './dto/login.dto';
import { RegisterUserDto } from './dto/register-user.dto';
import { EncoderService } from './encoder.service';
import { JwtPayload } from './jwt-payload.interface';
import { UsersRepository } from './users.repository';
import { v4 } from 'uuid';
import { ActivateUserDto } from './dto/activate-user.dto';
import { User } from './user.entity';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(UsersRepository)
private usersRepository: UsersRepository,
private encoderService: EncoderService,
private jwtService: JwtService,
) {}
async registerUser(registerUserDto: RegisterUserDto): Promise {
const { name, email, password } = registerUserDto;
const hashedPassword = await this.encoderService.encodePassword(password);
return this.usersRepository.createUser(name, email, hashedPassword, v4());
}
async login(loginDto: LoginDto): Promise<{ accessToken: string }> {
const { email, password } = loginDto;
const user = await this.usersRepository.findOneByEmail(email);
if (
user &&
(await this.encoderService.checkPassword(password, user.password))
) {
const payload: JwtPayload = { id: user.id, email, active: user.active };
const accessToken = await this.jwtService.sign(payload);
return { accessToken };
}
throw new UnauthorizedException('Please check your credentials');
}
async activateUser(activateUserDto: ActivateUserDto): Promise {
const { id, code } = activateUserDto;
const user: User =
await this.usersRepository.findOneInactiveByIdAndActivationToken(
id,
code,
);
if (!user) {
throw new UnprocessableEntityException('This action can not be done');
}
this.usersRepository.activateUser(user);
}
}
Y por lo ultimo creamos la ruta dentro de nuestro controlador.
// src/auth/auth.controller.ts
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { AuthService } from './auth.service';
import { ActivateUserDto } from './dto/activate-user.dto';
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<{ accessToken: string }> {
return this.authService.login(loginDto);
}
@Get('/activate-account')
activateAccount(@Query() activateUserDto: ActivateUserDto): Promise {
return this.authService.activateUser(activateUserDto);
}
}
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.