The previous article demonstrated the initial steps in developing a full-stack JavaScript system using an NX monorepository. This included generating a NestJS app for the backend, creating an Angular app, and configuring a PostgreSQL database in a Docker container. Additionally, it covered setting up the first TypeORM migrations to facilitate further development.
The next step according to the plan is to develop user cookie authentication on the backend. This includes developing an API with the following endpoints:
- api/auth/register: Register a new user using an email address and password, sign an Access Token and Refresh Token, place them in a cookie, and grant access.
- api/auth/login: Log in a registered user using an email address and password, sign an Access Token and Refresh Token, place them in a cookie, and grant access.
- api/auth/profile: Get user profile data.
- api/auth/refresh: Refresh the Access Token
- api/auth/logout: Log out and remove Refresh Token from cookies
These API endpoints will be developed in the AuthController, which is part of the Auth Module. But before we start developing this module, we need to do two important things:
- Develop the RolesModule with a Role entity, join the Role entity with the User entity, and seed basic roles (Admin and User) into the ‘roles’ table of the database
- Implement some additional functionality in the UsersModule that will be used in the AuthModule
RolesModule development
1 Create Role interface at In libs/shared/domain library
import { IBase } from "./base.interface";
import { IUser } from "./user.interface";
export interface IRole extends IBase{
name:string;
users: IUser[]
}
2 Update User interface to join it with Role interface
import { IBase } from "./base.interface";
import { IRole } from "./role.interface";
export interface IUser extends IBase{
email: string;
password: string | undefined; //undefined
phoneNumber: string;
firstname: string;
lastname: string;
role?: IRole;
roleId?: string;
}
3 Generate Role resource at libs/backend/features library
nx g @nx/nest:resource roles \ --directory=libs/backend/features/src/lib/roles \ --crud=true \ --type=rest
4 Update the generated Role entity at libs/backend/resources/src/lib/roles/entities/role.entity.ts:
import { IRole } from "@mtfs/shared/domain";
import { Column, Entity, JoinColumn, OneToMany } from "typeorm";
import { Base } from "../../base-entities/base.entity";
import { User } from "../../users/entities/user.entity";
import { ApiProperty, ApiTags } from "@nestjs/swagger";
@ApiTags('roles')
@Entity('roles')
export class Role extends Base implements IRole {
@ApiProperty({
type: String,
description: 'The name of the role.',
example: 'admin',
})
@Column({type: 'varchar', length: 120, nullable: false})
name!: string;
@ApiProperty({
type: [User],
description: 'The list of users associated with this role.',
})
@OneToMany(()=> User, (user) => user.role)
@JoinColumn()
users!: User[];
}
5 Update the User entity to link it to the Role entity and, by the way, add ApiProperties for the User entity documentation:
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
import {IUser} from '@mtfs/shared/domain';
import { Base } from "../../base-entities/base.entity";
import { ApiProperty } from "@nestjs/swagger";
import { Role } from "../../roles/entities/role.entity";
@Entity('users')
export class User extends Base implements IUser {
@ApiProperty({
type: String,
description: 'The email address of the user.',
example: 'user@example.com',
})
@Column({ type: 'varchar', length: 255, nullable: false, unique: true })
email!: string;
@ApiProperty({
type: String,
description: 'The phone number of the user.',
example: '+1234567890',
})
@Column({ type: 'varchar', length: 14, nullable: false })
phoneNumber!: string;
@ApiProperty({
type: String,
description: 'The password of the user.',
example: 'Password12!',
})
@Column({ type: 'varchar', select: false, nullable: false })
password!: string;
@ApiProperty({
type: String,
description: 'The first name of the user.',
example: 'John',
})
@Column({ type: 'varchar', length: 120, nullable: false })
firstname!: string;
@ApiProperty({
type: String,
description: 'The last name of the user.',
example: 'Doe',
})
@Column({ type: 'varchar', length: 120, nullable: false })
lastname!: string;
@ApiProperty({
type: String,
description: 'The ID of the role associated with the user.',
example: 'DCA76BCC-F6CD-4211-A9F5-CD4E24381EC8',
})
@Column()
roleId!: string;
@ApiProperty({
type: Role,
description: 'The role associated with the user.',
})
@ManyToOne(()=> Role, (role)=> role.users, {
eager: true
})
@JoinColumn({ name: "roleId" })
role!: Role
}
6 Update RolesService by adding functions findOneById, findOneByName and findAll
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Role } from './entities/role.entity';
@Injectable()
export class RolesService {
constructor(
@InjectRepository(Role) private readonly repo: Repository<Role>
){}
async findOneById(id: string) {
return await this.repo.findOne({
where: {
id: id
}
});
}
async findOneByName(roleName: string) {
return await this.repo.findOne({
where: {
name: roleName
}
});
}
async findAll(){
return await this.repo.find()
}
}
7 Update the RolesModule to set up TypeOrm
@Module({
imports: [TypeOrmModule.forFeature([Role])],
controllers: [RolesController],
providers: [RolesService],
exports: [
RolesService,
TypeOrmModule.forFeature([Role]),
],
})
export class RolesModule {}
8 Everything is ready for the migration to update the database
npm run db:migration:generate
and
npm run db:migration:run
9 Create a migration to seed basic roles (Admin and User) into the ‘roles’ table
import { MigrationInterface, QueryRunner } from "typeorm";
import { Role } from "../../roles/entities/role.entity";
export class SeedRoles1715871858000 implements MigrationInterface {
name = `SeedRoles1715871858000`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public async up(queryRunner: QueryRunner): Promise<any> {
const rolesRepo = queryRunner.manager.getRepository(Role);
const userRole = rolesRepo.create({
name: 'User'
});
const adminRole = rolesRepo.create({
name: 'Admin'
});
await rolesRepo.save([userRole, adminRole]);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
public async down(queryRunner: QueryRunner): Promise<any> { }
}
10 Run migration again
npm run db:migration:run
And now we have a ‘roles’ table in our database with Admin and User records as basic roles!
These roles are enough for a basic full-stack boilerplate. However, for our system in the next stage, we will update these roles by removing the ‘User’ role and adding ‘Customer’, ‘Manager’, ‘Driver’, and ‘Director’ roles.
We also create the libs/shared/enums library and the first enum, ‘Roles’. This enum will help us address various tasks in features that utilize roles
npx nx generate @nx/js:library enums \
--directory=libs/shared/enums \
--importPath=@mtfs/shared/enums \
--skipBabelrc --tags=scope:shared,type:enums
create the RoleEnum at libs/shared/enums/src/lib/user.enum.ts
export enum RoleEnum {
User = 'User',
Admin = 'Admin'
}
Update UsersModule to prepare for development of AuthModule
1 Install bcrypt for hashing user password and refresh token
npm i bcrypt npm i -D @types/bcrypt
2 Update the User entity to include password hashing:
@BeforeInsert()
@BeforeUpdate()
async hash(): Promise<void>{
const salt = await bcrypt.genSalt();
if(this.password){
this.password = bcrypt.hashSync(this.password, salt);
}
}
3 Update the libs/shared/domain/src/lib/user.interface.ts file to add the ICreateUser interface, which inherits from the IUser interface
export type ICreateUser = Pick<IUser, 'email' | 'password' | 'phoneNumber' | 'firstname' | 'lastname'>;
4 install helper classes
npm i --save class-validator class-transformer class-sanitizer
5 update CreateUserDto at `libs/backend/features/src/lib/users/dto/create-user.dto.ts` file
import {
IsEmail,
IsNotEmpty,
IsObject,
IsOptional,
IsPhoneNumber,
IsString,
IsStrongPassword,
IsUUID,
Length,
MaxLength } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
import { Role } from "../../roles/entities/role.entity";
import { ICreateUser } from "@mtfs/shared/domain";
export class CreateUserDto implements ICreateUser {
//email
@ApiProperty({
type: String,
required: true,
example: `email@internet.com`,
maxLength: 255
})
@IsEmail()
@IsNotEmpty()
@MaxLength(255)
email!: string;
//password
@ApiProperty({
type: String,
required: true,
example: '!123Qwerty',
minLength: 8
})
@IsStrongPassword({
minLength: 8,
minNumbers: 1,
minUppercase: 1,
minSymbols: 1
},
{
message: `Password is not strong enough. Must contain: 8 characters, 1 number, 1 uppercase letter, 1 symbol`,
})
@IsNotEmpty()
@MaxLength(25)
password!: string;
//phoneNumber
@ApiProperty({
type: String,
required: true,
example: '+2347063644568',
minLength: 10,
maxLength: 16
})
@IsNotEmpty()
@IsPhoneNumber(undefined, {message: 'Invalid phone number format'})
@Length(10, 16)
phoneNumber!: string;
//firstname
@ApiProperty({
type: String,
required: true,
example: 'Jonh'
})
@IsNotEmpty()
@IsString()
@MaxLength(120)
firstname!: string;
//lastname
@ApiProperty({
type: String,
required: true,
example: 'Doe',
maxLength: 120
})
@IsNotEmpty()
@IsString()
@MaxLength(120)
lastname!: string;
//roleId
@ApiProperty({
type: String,
readOnly: true,
example: 'DCA76BCC-F6CD-4211-A9F5-CD4E24381EC8',
})
@IsOptional()
@IsUUID()
roleId?: string;
//role
@ApiProperty({
type: Role,
readOnly: true,
example: '{"id": "DCA76BCC-F6CD-4211-A9F5-CD4E24381EC8", "name": "user"}'
})
@IsOptional()
@IsObject()
role?: Role;
}
6 Update UsersService methods to create user and get user by email
6.1 First, add a constructor with variables repo and rolesService to the UsersService class:
constructor(
@InjectRepository(User) private readonly repo: Repository<User>,
private readonly rolesService: RolesService
){}
6.2. Update the generated create method to check certain conditions and create a new User. Before creating a new user, we need to check the following conditions:
- If the entered email address of the user being created is unique
- If entered user data include of a role object or roleId, otherwise a default role User have to set
async create(createUserDto: CreateUserDto) {
//check user by email to avoid dublicate email
const checkUserByEmail = await this.repo.findOne({
where: {
email: createUserDto.email
}
});
if (checkUserByEmail){
throw new ConflictException('User with this email already exist');
} else {
const user = new User();
if(createUserDto.roleId){
//check role by id
const role = await this.rolesService.getRoleById(createUserDto.roleId )
if (!role) {
throw new NotFoundException("Entered user role not found");
}
user.role=role;
} else if (createUserDto.role){
//check role by role.id
const role = await this.rolesService.getRoleById(createUserDto.role.id )
if (!role) {
throw new NotFoundException("Entered user role not found");
}
} else {
// role not enter - set default role
//get default role 'user'
const defaultRole = await this.rolesService.getRoleByName(RoleEnum.User);
if (!defaultRole) {
throw new NotFoundException("Default user role not found");
}
user.role = defaultRole;
}
Object.assign(user, createUserDto);
try {
this.repo.create(user);
const createdUser= await this.repo.save(user);
delete createdUser['password'];
delete createdUser.refreshToken;
return createdUser;
} catch (error){
if (error instanceof Error){
throw new BadRequestException(error.message)
} else {
throw new BadRequestException("Something went wrong! Try again later.")
}
}
}
}
6.2 Create getUserByEmail method
async getOneByEmail(email: string) {
this.logger.debug(`login.getOneByEmail`);
const user = await this.repo
.createQueryBuilder('user')
.addSelect('user.password')
.leftJoinAndSelect('user.role', 'role')
.where('user.email = :email', {email: email})
.withDeleted()
.getOne()
if(!user){
throw new HttpException(
`User with email ${email} not found`,
HttpStatus.NOT_FOUND
)
} else {
this.logger.debug(`login.getOneByEmail.success`);
return user;
}
}
And now we are ready to start develop AuthModule. Some UsersService features related to the refresh token will be developed later.
AuthModule
AuthModule will have next structure (except spec files):
auth.module.ts– core module fileauth.service.ts– the file that contains the set of module servicesauth.controller.ts– the file that contains the module’s API endpoint code.interceptors– the folder that contains set of interceptors:access-token.interceptor.ts– generate and add AccessToken to cookierefresh-token.interceptor.ts– generate and add RefreshToken to cookieclear-cookies.interceptor.ts– delete tokens
strategies– the folder that contains a set of strategies that are part of the PassportJS ecosystem and support our authentication and authorization mechanisms.local.strategy.ts– strategy used to login userjwt.strategy.ts– a strategy used to check authenticated users.jwt-refrest-token.strategy.ts– a strategy used to check user’s refresh token.
guards– the folder that contains a set of guardslocal-auth.guard.ts– a guard for login API endpointjwt-auth.guard.ts– a guard to protect API routes of our systemjwt-refresh-access-token.guard.ts– a guard to protect refresh API endpoint
Install and setup cookie-parser
As we use cookie based authentication we have install and configurate cookie-parser
npm i cookie-parser npm i -D @types/cookie-parser
and update apps/backend/src/main.ts file by adding the following line of code
app.use(cookieParser());
Generate AuthModule
nx g @nx/nest:resource auth \ --directory=libs/backend/features/src/lib/auth \ --crud=true \ --type=rest
Develop /auth/register API endpoint
1 Add the UsersService variable to the AuthService constructor
constructor(
private readonly usersService: UsersService
){}
2 Update createAuthDto
export class CreateAuthDto extends CreateUserDto {}
3 Create register user method at AuthService
async register(createAuthDto: CreateAuthDto) {
return await this.usersService.create(createAuthDto);
}
When registering or logging into the system, it is necessary for us to create an access token and a refresh token for the user. One way to do this is to use interceptors: AccessTokenInterceptor will be used to generate the access token, and RefreshTokenInterceptor will be used to generate the refresh token. We also create a ClearCookieInterceptor to remove these tokens when the user ends the session. This all interceptors will be use as decorators in AuthController for our API endpoints
But before to create this interceptors we have to add functionality that will be use this interceptors
4 Install nest/jwt dependency
npm install --save @nestjs/jwt
5 Register JwtModule in AuthModule (libs/backend/resources/src/lib/auth/auth.module.ts)
@Module({
imports: [
UsersModule,
JwtModule.register({}),
TypeOrmModule.forFeature([User])
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy, JwtRefreshAccessTokenStrategy],
exports: [AuthService]
})
export class AuthModule {}
6 Add signToken method to AuthService
signToken(user: User, options: any) {
const payload = { sub: user.id, email: user.email };
return this.jwtService.sign(payload, options);
}
7 Install parse-duration
npm i parse-duration
8 Generate AccessTokenInterceptor
nx g @nx/nest:interceptor access-token \ --directory=libs/backend/features/src/lib/auth/interceptors \ --nameAndDirectoryFormat=as-provided
9 Update generated AccesTokenInterceptor
import {
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Response } from 'express';
import parse from 'parse-duration';
import { AuthService } from '../auth.service';
import { ConfigService } from '@nestjs/config';
import { User } from '../../users/entities/user.entity';
@Injectable()
export class AccessTokenInterceptor implements NestInterceptor {
private readonly logger = new Logger(AccessTokenInterceptor.name)
constructor(
private readonly authService: AuthService,
private readonly configService: ConfigService,
){}
intercept(
context: ExecutionContext,
next: CallHandler<User>): Observable<User> {
return next.handle().pipe(
map(user => {
const response = context.switchToHttp().getResponse<Response>();
const accessTokenExpiresIn = parse(this.configService.get('JWT_ACCESS_TOKEN_EXPIRES_IN') as string);
const accessTokenSecret = `${parse(this.configService.get('JWT_ACCESS_TOKEN_SECRET')as string)}`;
const accessTokenOptions = {expiresIn: `${accessTokenExpiresIn}`, secret: accessTokenSecret};
const accessToken = this.authService.signToken(user, accessTokenOptions);
this.logger.debug(`intercept -> map`)
response.cookie("Authentication", accessToken, {
httpOnly: true,
maxAge: accessTokenExpiresIn
});
return user;
})
);
}
}
10 In order to develop the RefreshTokenInterceptor, we need to prepare some additional things because the refresh token must be stored in the ‘users’ table of our database.
10.1 Add refreshToken property to User Entity (libs/backend/resources/src/lib/users/entities/user.entity.ts)
@ApiProperty({
type: String,
description: 'The refresh token of the user.',
example: 'refreshTokenExample',
})
@Column({type: 'varchar', select: false, nullable: true })
refreshToken: string | null | undefined;
10.2 Hash the refresh token before saving it to the database, so we need to update the hash method in the User Entity
@BeforeInsert()
@BeforeUpdate()
async hash(): Promise<void>{
const salt = await bcrypt.genSalt();
if(this.password){
this.password = bcrypt.hashSync(this.password, salt);
}
if(this.refreshToken){
this.refreshToken = await bcrypt.hashSync(this.refreshToken, salt)
}
}
10.3 add setCurrentRefreshToken method in UsersService (libs/backend/resources/src/lib/users/users.service.ts):
async setCurrentRefreshToken(id: string, refreshToken: string) {
const currentUser = await this.repo
.createQueryBuilder('user')
.addSelect('user.refreshToken')
.where('user.id =:id', { id: id })
.getOne();
if (currentUser) {
currentUser.refreshToken = refreshToken;
await this.repo.save(currentUser);
} else {
throw new BadRequestException("User not found");
}
}
10.4 create setCurrentRefreshToken method in AuthService
async setCurrentRefreshToken(id: string, refreshToken: string) {
await this.usersService.setCurrentRefreshToken(id, refreshToken);
}
11 Generate RefreshTokenInterceptor
nx g @nx/nest:interceptor refresh-token \ --directory=libs/backend/features/src/lib/auth/interceptors \ --nameAndDirectoryFormat=as-provided
12 Update RefreshTokenInterceptor
import {
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Response } from 'express';
import parse from 'parse-duration';
import { AuthService } from '../auth.service';
import { ConfigService } from '@nestjs/config';
import { User } from '../../users/entities/user.entity';
@Injectable()
export class RefreshTokenInterceptor implements NestInterceptor {
private readonly logger = new Logger(RefreshTokenInterceptor.name)
constructor(
private readonly authService: AuthService,
private readonly configService: ConfigService,
){}
intercept(
context: ExecutionContext,
next: CallHandler<User>): Observable<User> {
return next.handle().pipe(
map(user => {
const response = context.switchToHttp().getResponse<Response>();
const refreshTokenExpiresIn = parse(this.configService.get('JWT_REFRESH_TOKEN_EXPIRES_IN') as string);
const refreshTokenSecret = `${parse(this.configService.get('JWT_REFRESH_TOKEN_SECRET') as string)}`;
const refreshTokenOptions = { expiresIn: `${refreshTokenExpiresIn}`, secret: refreshTokenSecret };
const refreshToken = this.authService.signToken(user, refreshTokenOptions);
response.cookie("Refresh", refreshToken, {
httpOnly: true,
maxAge: refreshTokenExpiresIn
});
this.authService.setCurrentRefreshToken(user.id, refreshToken);
return user;
})
)
}
}
13 Update AuthController (libs/backend/resources/src/lib/auth/auth.controller.ts) to create /auth/register API endpoint
@Post('register')
@ApiOperation({ summary: 'Register a new user' })
@ApiResponse({ status: 201, type: User, description: 'User successfully registered' })
@UseInterceptors(AccessTokenInterceptor, RefreshTokenInterceptor)
create(@Body() createAuthDto: CreateAuthDto) {
//this.logger.debug(`register`);
return this.authService.register(createAuthDto);
}
auth/register API feature is ready!
Develop auth/login API endpoint
1 Install Passport dependencies
npm install --save @nestjs/passport passport passport-local npm install --save-dev @types/passport-local
2 Add verifyPassword method to User entity (libs/backend/features/src/lib/users/entities/user.entity.ts)
async veryfyPassword(enterdPassword: string): Promise<boolean | void> {
return bcrypt.compare(enterdPassword, this.password as string) ;
}
3 Add login method to AuthService
async login(email: string, password: string) {
//this.logger.debug(`login`);
const user = await this.usersService.findOneByEmail(email);
if (await user.veryfyPassword(password)) {
delete user.password;
delete user.refreshToken;
//this.logger.debug(`login.success`);
return user;
} else {
throw new UnauthorizedException('Password missmatched')
}
}
4 Create LocalStrategy to validate user credentials
import { Strategy } from "passport-local";
import { Injectable, Logger } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { AuthService } from "../auth.service";
import { User } from '../../users/entities/user.entity';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
private readonly logger = new Logger(LocalStrategy.name)
constructor(
private readonly authService: AuthService
) {
super({
usernameField: 'email',
passReqToCallback: false,
});
}
validate(email: string, password: string): Promise<User> {
this.logger.debug(`validate`)
return this.authService.login(email, password);
}
}
5 Generate LocalAuthGuard
nx g @nx/nest:guard local-auth \
--directory=libs/backend/features/src/lib/auth/guards \
--nameAndDirectoryFormat=as-provided
6 Update AuthGuard
import { Injectable, Logger } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local'){}
7 Create currentUserDecorator to get data about currentUser
7.1 Generate nest library “utils”
npx nx generate @nx/nest:library utils \ --directory=libs/backend/utils \ --importPath=@mtfs/backend/utils \ --strict --tags=type:util,scope:backend \ --projectNameAndRootFormat=as-provided \ --buildable=true \ --unitTestRunner=jest
7.2 Create folder decorators and add CurrentUser decorator, that return data of authenticated user
import { ExecutionContext, Logger, createParamDecorator } from "@nestjs/common"
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) =>{
const request= ctx.switchToHttp().getRequest();
return request.user|| null;
},
);
8 Add login endpoint to AuthController
@Post('login')
@ApiOperation({ summary: 'Log in and obtain access and refresh tokens' })
@ApiResponse({ status: 200, type: User, description: 'Login successful' })
@UseGuards(LocalAuthGuard)
@HttpCode(HttpStatus.OK)
@UseInterceptors(AccessTokenInterceptor, RefreshTokenInterceptor)
async login(
@CurrentUser() user: User
) {
return user;
}
auth/login API feature is ready!
Develop auth/profile API endpoint
1 Install jwt and passport-jwt
npm install --save @nestjs/jwt passport-jwt npm install --save-dev @types/passport-jwt
2 Add verify method to AuthService
async verify(email: string): Promise<User> {
if (!email) {
throw new UnauthorizedException();
}
const user = await this.usersService.findOneByEmail(email);
if (!user) {
throw new UnauthorizedException();
}
delete user.password;
delete user.refreshToken;
return user;
}
3 Create JwtPayload interface at shared/domain library
export interface IJwtPayload {
sub: string;
email: string
}
4 Create JwtStrategy
import { PassportStrategy } from "@nestjs/passport";
import { Injectable, Logger } from "@nestjs/common";
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request } from 'express';
import { AuthService } from "../auth.service";
import { ConfigService } from "@nestjs/config";
import { IJwtPayload } from "@mtfs/shared/domain";
import parse = require("parse-duration");
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
private readonly logger = new Logger(JwtStrategy.name)
constructor(
private readonly authServce: AuthService,
private readonly configService: ConfigService
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
this.logger.debug(`cookie Authentication`);
this.logger.debug(request?.cookies?.Authentication);
return request?.cookies?.Authentication;
}]),
secretOrKey: `${parse(configService.get('JWT_ACCESS_TOKEN_SECRET') as string)}`,
ignoreExpiration: false,
passReqToCallback: false,
});
}
async validate(payload: IJwtPayload) {
this.logger.debug(`validate`)
return await this.authService.verify(payload.email);
}
}
5 Generate JwtAuthGuard
nx g @nx/nest:guard jwt-auth \
--directory=libs/backend/features/src/lib/auth/guards \
--nameAndDirectoryFormat=as-provided
6 Update JwtAuthGuard
import { ExecutionContext, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { AuthGuard, IAuthGuard } from '@nestjs/passport';
import { User } from '../../users/entities/user.entity';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') implements IAuthGuard {
private readonly logger = new Logger(JwtAuthGuard.name)
override canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
override handleRequest(err: unknown, user: User): any {
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
7 Add profile endpoint to AuthController
@Get('profile')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Get user profile' })
@ApiResponse({ status: 200, type: User, description: 'User profile retrieved successfully' })
getProfile(@CurrentUser() user: User) {
return user;
}
auth/profile API feature is ready!
Develop auth/refresh API endpoint
1 Update getOneByEmail method of UsersService (libs/backend/resources/src/lib/users/users.service.ts) to add refreshToken property to query:
async getOneByEmail(email: string) {
this.logger.debug(`login.getOneByEmail`);
const user = await this.repo
.createQueryBuilder('user')
.addSelect('user.password')
.addSelect('user.refreshToken')
.leftJoinAndSelect('user.role', 'role')
.where('user.email = :email', {email: email})
.withDeleted()
.getOne()
if(!user){
throw new HttpException(
`User with email ${email} not found`,
HttpStatus.NOT_FOUND
)
} else {
this.logger.debug(`login.getOneByEmail.success`);
return user;
}
}
2 Create JwtRefreshStrategy
import { PassportStrategy } from "@nestjs/passport";
import { Injectable, Logger } from "@nestjs/common";
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request } from 'express';
import parse from 'parse-duration';
import { AuthService } from "../auth.service";
import { ConfigService } from "@nestjs/config";
import { IJwtPayload } from "@mtfs/shared/domain";
@Injectable()
export class JwtRefreshAccessTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh-access-token') {
private readonly logger = new Logger(JwtRefreshAccessTokenStrategy.name)
constructor(
private readonly authService: AuthService,
private readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
this.logger.debug(`cookie Refresh`);
this.logger.debug(request?.cookies?.Refresh);
return request?.cookies?.Refresh;
}]),
secretOrKey: `${parse(configService.get('JWT_REFRESH_TOKEN_SECRET') as string)}`,
ignoreExpiration: false,
passReqToCallback: true,
});
}
async validate(request: Request, payload: IJwtPayload) {
this.logger.debug(`validate`)
const refreshToken = request.cookies?.Refresh;
return await this.authService.verifyRefreshToken(payload.email, refreshToken);
}
}
3 Generate JwtRefreshAccessTokenGuard
nx g @nx/nest:guard jwt-refresh-access-token \
--directory=libs/backend/features/src/lib/auth/guards \
--nameAndDirectoryFormat=as-provided
4 Update JwtRefreshAccessTokenGuard
import { ExecutionContext, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { AuthGuard, IAuthGuard } from '@nestjs/passport';
import { User } from '../../users/entities/user.entity';
@Injectable()
export class JwtRefreshAccessTokenGuard extends AuthGuard('jwt-refresh-access-token') implements IAuthGuard {
private readonly logger = new Logger(JwtRefreshAccessTokenGuard.name)
override canActivate(
context: ExecutionContext
){
this.logger.debug(`canActivate`);
return super.canActivate(context);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
override handleRequest(err: unknown, user: User): any {
this.logger.debug(`handleRequest`);
console.log(user);
if (err || !user) {
console.log(err);
console.log(user);
throw err || new UnauthorizedException();
}
this.logger.debug(`handleRequest.user authentincated`);
return user;
}
}
5 Add refresh endpoint to AuthController
@Post('refresh')
@ApiOperation({ summary: 'Refresh access token' })
@ApiResponse({ status: 200, type: User, description: 'Access token refreshed successfully' })
@UseGuards(JwtRefreshAccessTokenGuard)
@HttpCode(HttpStatus.OK)
@UseInterceptors(AccessTokenInterceptor)
async refreshAccessToken(@CurrentUser() user: User) {
return user;
}
auth/refresh API feature is ready!
Develop auth/logout API endpoint
1 Generate ClearCookies Interceptor
nx g @nx/nest:interceptor clear-cookies \ --directory=libs/backend/features/src/lib/auth/interceptors \ --nameAndDirectoryFormat=as-provided
2 Update ClearCookiesInterceptor
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Response } from 'express';
import { User } from '../../users/entities/user.entity';
@Injectable()
export class ClearCookiesInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler<User>): Observable<User> {
return next.handle().pipe(
map(result => {
const response = context.switchToHttp().getResponse<Response>();
response.clearCookie("Authentication");
response.clearCookie("Refresh");
return result;
})
)
}
}
3 Add removeCurrentRefreshToken method to UsersService (libs/backend/resources/src/lib/users/users.service.ts)
async removeCurrentRefreshToken(id: string) {
return await this.repo.update(id, {
refreshToken: null
})
}
4 add removeCurrentRefreshToken to AuthService
async removeCurrentRefreshToken(id: string) {
await this.usersService.removeCurrentRefreshToken(id);
}
5 Add logout endpoint to AuthController
@Get('logout')
@ApiOperation({ summary: 'Log out and remove refresh token' })
@ApiResponse({ status: 200, description: 'Logout successful' })
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@UseInterceptors(ClearCookiesInterceptor)
async logout(@CurrentUser() user: User) {
if (user) {
this.authService.removeCurrentRefreshToken(user.id);
}
return { succes: true };
}
auth/logout API feature is ready!
Summary and conclusions
This article describes process of implementing task “Create backend authentication” using cookie, JWT and PassportJS. As a result, an authentication module was created that has five API endpoints, allowing a new user to register, log in subsequently, get their profile data, refresh the access token, and log out. These are standard API endpoints for a typical fullstack boilerplate, and they will be used to develop the frontend part of our fullstack boilerplate. And this server authentication will be further updated during the development of the next stages of the Awesome Trucks Company system, in accordance with the business case and requirements.
Discover more from More Than Fullstack
Subscribe to get the latest posts sent to your email.
