4.2. Develop Full Stack Boilerplate: Create backend authentication

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:

  1. 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
  2. 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 file
  • auth.service.ts – the file that contains the set of module services
  • auth.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 cookie 
    • refresh-token.interceptor.ts – generate and add RefreshToken to cookie 
    • clear-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 user
    • jwt.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 guards
    • local-auth.guard.ts  –  a guard for login API endpoint
    • jwt-auth.guard.ts  – a guard to protect API routes of our system
    • jwt-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.

Leave a Comment