Securing Email Synchronization in NestJS: Finalizing authentication and authorization

Abdullah Irfan
5 min readMar 12, 2024

--

This is the sixteenth story of series Building a Robust Backend: A Comprehensive Guide Using NestJS, TypeORM, and Microservices. Our purpose is to build an email sync system for Gmail, since we have built the main functionality, now let’s proceed with finalizing authentication and authorization. This story is highly dependent on previous stories for implementing guards and strategies so for complete understanding go through those stories first.

We will start by creating validateUser method that will first identify if someone is using email or username, and get records accordingly, check user status and validate if password is correct. This method won’t be directly used for login, rather it will be used by local-auth guard, making it usable for other places (though not required in our current scope). The guard will pass the user object in request, and in controller, for login method, we will get the request object (need to define custom interface from Request), get user object from it and will pass it to login method for creating token and response object.

In auth service we will also define forgot password and reset password (one for by user and other for by admin), the forgot password and reset password by user are straight forward, we will check if email of specific user exists, if it exists, send reset password link on email. User will go to the email, press on link and enter new password and confirm password to reset the account password. Now for reset password by admin, only the admin role will have access to this route, the admin will provide id of respective user and reset password email will be sent to the respective user.

The purpose of using different approach by admin i.e. using id instead of email is just to demonstrate different use case, in actual application I would recommend to use email rather than id and utilize same service method. Now with all the theoretical explanation with major parts done, the major codes are below, for auth controller:

import {
Controller,
Get,
Post,
Body,
Param,
UseGuards,
UseInterceptors,
ClassSerializerInterceptor,
ValidationPipe,
UsePipes,
UseFilters,
Req,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from '../shared/guards/local-auth.guard';
import { Role } from '../shared/enums/roles.enum';
import { Roles } from '../shared/decorators/roles.decorator';
import { JwtAuthGuard } from '../shared/guards/jwt-auth.guard';
import { RolesGuard } from '../shared/guards/roles.guard';
import { ResetPasswordDto } from './dto/reset-password.dto';
import { HttpExceptionFilter } from '../shared/filters/http-exception.filter';
import { ForgotPasswordDto } from './dto/forgot-password.dto';
import { RequestWithUser } from 'src/shared/utils/types';

@UseInterceptors(ClassSerializerInterceptor)
@UsePipes(ValidationPipe)
@UseFilters(HttpExceptionFilter)
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@UseGuards(LocalAuthGuard)
@Post()
async create(@Req() req: RequestWithUser) {
return this.authService.login(req.user);
}

@Post('reset-password/:id/:token')
resetPassord(
@Param('id') id: string,
@Param('token') token: string,
@Body() resetPasswordDto: ResetPasswordDto,
) {
return this.authService.resetPassord(id, token, resetPasswordDto);
}

@Roles(Role.Admin)
@UseGuards(JwtAuthGuard, RolesGuard)
@Get('reset-password/:id')
resetPassordEmail(@Param('id') id: string) {
return this.authService.resetPasswordEmail(id);
}

@Post('forgot-password')
forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
return this.authService.forgotPassword(forgotPasswordDto);
}
}

As for the service, the code will be:

import {
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
HttpStatus,
ServiceUnavailableException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Users } from '../users/entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { comparePasswords, encodePassword } from '../shared/utils/bcrypt';
import { ResetPasswordDto } from './dto/reset-password.dto';
import customMessage from '../shared/responses/customMessage.response';
import { ForgotPasswordDto } from './dto/forgot-password.dto';
import { EmailService } from '../services/email-service';
import { EmailTemplateParams } from '../shared/utils/types';

@Injectable()
export class AuthService {
constructor(
@InjectRepository(Users) private readonly userRepository: Repository<Users>,
private readonly jwtService: JwtService,
private readonly emailService: EmailService,
) {}

async validateUser(identifier: string, pass: string): Promise<any> {
const user = identifier.includes('@')
? await this.userRepository.findOneBy({ email: identifier })
: await this.userRepository.findOneBy({ username: identifier });

if (!user) return null;
if (!user.active)
throw new ForbiddenException(
customMessage(HttpStatus.FORBIDDEN, 'User is not authorized'),
);
if (!comparePasswords(pass, user.password)) return null;
const { password, ...result } = user;
return result;
}

async login(user: Users): Promise<object> {
const payload = { id: user.id, email: user.email, roles: user.role };
return customMessage(HttpStatus.CREATED, 'token created successfully', {
id: user.id,
full_name: user.full_name,
email: user.email,
username: user.username,
role: user.role,
active: user.active,
token: this.jwtService.sign(payload),
});
}

async resetPasswordEmail(id: string) {
const user = await this.userRepository.findOneBy({ id });
if (!user)
throw new NotFoundException(
customMessage(HttpStatus.NOT_FOUND, "user doesn't exist"),
);
const payload = { id: user.id, email: user.email };
const token = this.createToken(payload, user);
const link = `${process.env.BASE_URL}/resetPassword/${user.id}/${token}`;
const mailData: EmailTemplateParams = {
to_name: user.full_name,
to_email: user.email,
link: link,
};
if (await this.emailService.sendForgotPasswordEmail(mailData)) {
return customMessage(
HttpStatus.OK,
'you will shortly receive reset email link',
);
}
throw new ServiceUnavailableException(
customMessage(HttpStatus.SERVICE_UNAVAILABLE, 'Service is unavailable'),
);
}

async forgotPassword(forgotPasswordDto: ForgotPasswordDto) {
const email = forgotPasswordDto.email;
const user = await this.userRepository.findOneBy({ email });
if (!user)
throw new NotFoundException(
customMessage(HttpStatus.NOT_FOUND, "user doesn't exist"),
);
const payload = { id: user.id, email: email };
const token = this.createToken(payload, user);
const link = `${process.env.BASE_URL}resetPassword/${user.id}/${token}`;
const mailData: EmailTemplateParams = {
to_name: user.full_name,
to_email: email,
link: link,
};
if (await this.emailService.sendForgotPasswordEmail(mailData)) {
return customMessage(
HttpStatus.OK,
'if you are registered, you will shortly receive reset email link',
);
}
throw new ServiceUnavailableException(
customMessage(HttpStatus.SERVICE_UNAVAILABLE, 'Service is unavailable'),
);
}

async resetPassord(
id: string,
token: string,
resetPasswordDto: ResetPasswordDto,
) {
const user = await this.userRepository.findOneBy({ id });

try {
const secret = JSON.stringify({
secret: process.env.SECRET_NON_AUTH,
updatedAt: user.updatedAt,
});
this.jwtService.verify(token, {
secret,
});
} catch (err) {
throw new ForbiddenException(
customMessage(HttpStatus.FORBIDDEN, 'expired or invalid token'),
);
}
if (!user)
throw new NotFoundException(
customMessage(HttpStatus.NOT_FOUND, "user doesn't exist"),
);
if (resetPasswordDto.password !== resetPasswordDto.confirm_password)
throw new ConflictException(
customMessage(HttpStatus.CONFLICT, "passwords don't match"),
);
const password = encodePassword(resetPasswordDto.password);
const newUser = this.userRepository.create({
password,
});
if (!(await this.userRepository.update(id, newUser)))
throw new ServiceUnavailableException(
customMessage(
HttpStatus.SERVICE_UNAVAILABLE,
'something went wrong, please try again later',
),
);
return customMessage(HttpStatus.OK, 'password reset successful');
}

createToken(payload: object, user: Users) {
return this.jwtService.sign(payload, {
secret: JSON.stringify({
secret: process.env.SECRET_NON_AUTH,
updatedAt: user.updatedAt,
}),
});
}
}

With this our auth module is complete. This story code (the story doesn’t cover all aspects of code, review branch code from GitHub) is available on GitHub in feature/add-auth branch. If you appreciate this work, please show your support by clapping for the story and starring the repository.

--

--