Implementing SMS-enabled Two-Factor Authentication using NestJS, Twilio and Prisma
Tags: tutorial, webdev, typescript, programming
Published: 2023-05-08 13:45:40

## Introduction

When building robust and secure applications, implementing an additional layer of security is strongly recommended. The username and password are a basic first step in the security architecture of digital applications. This first layer of security is prone to attacks and can lead to account breaches when it is the only barrier sitting in front of restricted resources.

Two-factor authentication acts as a second barrier to accessing protected resources. It requires that a user provides additional information after the username-password identity declaration step has been fulfilled. Two-factor authentication can be achieved by requiring a user to enter a one-time passcode or fingerprint.

In this article, we will be going over how to implement an SMS-based two-factor authentication in a NestJS backend project. We will be using Twilio as the SMS provider for sending out a time-based one-time passcode to the user's phone number. If you are coming from the Express framework but want to get started with NestJS, this is a good guide to get a feel of how things are done in NestJS.

## Prerequisites

To follow along with this guide, you should have Node.js and Postgres installed on your computer. The project source code is written in Typescript. However, a knowledge of Javascript will suffice to understand what's going on.

## Project architecture

We will be building a couple of REST endpoints in this project. Below is a high-level overview of the project structure.

The user signs up on the application and proceeds to log in. A JSON web token is used to authenticate the user upon successful login. 

The user can enable two-factor authentication on their account. To enable 2FA, they need to first verify their phone number since they will be receiving their one-time password on that number. 

Once 2FA is enabled on their account, the user will be required to enter a time-based OTP once they have successfully fulfilled the username-password component of the login process.

## Setting up the Nest project

Compared to Express, NestJS shines when it comes to project organization and setup, thanks to its robust CLI. In your terminal, run the command below to install the NestJS CLI globally.

```bash
npm install -g @nestjs/cli
```


Once installed, we can use the CLI to generate a new nest project.

```bash
nest new project-name
```

Our newly scaffolded project will have a structure like this:

```
project-name
|-- node_modules
|-- src
|   |-- app.controller.spec.ts
|   |-- app.controller.ts
|   |-- app.module.ts
|   |-- app.service.ts
|   |-- main.ts
|-- test
|-- .eslintrc.js
|-- .gitignore
|-- .prettierrc
|-- nest-cli.json
|-- package-lock.json
|-- package.json
|-- README.md
|-- tsconfig.build.json
|-- tsconfig.json
```

NestJS has a module-oriented approach to project development. This means that each feature is developed as a module that comprises a controller and a service. This approach makes projects clean, organized, and easy to maintain.

Let's start our project on a clean slate by deleting files we won't be needing. Please delete the files: `app.controller.spec.ts`, `app.controller.ts`, and `app.service.ts` and remove their references in `app.module.ts`.

### Setting up utilities

In this section, we will set up some utilities such as the project base URL, logging middleware, and custom exception filter which will be used throughout the application.

#### Setting global prefix in NestJS

In the `src/main.ts`, we can set a global prefix for all our endpoints by adding our preferred prefix inside the application bootstrap function.

```ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
	const app = await NestFactory.create(AppModule);
	app.setGlobalPrefix('api/v1');
	await app.listen(3000);
}

bootstrap();
```

#### Logging middleware

NestJs has an inbuilt logging package that we can customize to log requests to our endpoints. We will create a global middleware that will log incoming requests, request methods, status codes, request content length, and user agent.

In your `src` folder, create the logger middleware in this path: `src/common/utils/logger.ts`.

```ts
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()

export class LoggerMiddleware implements NestMiddleware {
	private logger = new Logger('HTTP');
	
	use(request: Request, response: Response, next: NextFunction): void {
		const { ip, method, originalUrl } = request;
		const userAgent = request.get('user-agent') || '';
		
		response.on('finish', () => {
		const { statusCode } = response;
		const contentLength = response.get('content-length');
		this.logger.log(`${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`);
		});
		
		next();

	}

}
```

We will then register our logger middleware in our app module. In `src/app.module.ts`, import the logger middleware like so:

```ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/utils/logger';

@Module({
	imports: [],
	controllers: [],
	providers: [],
})

export class AppModule implements NestModule {
	configure(consumer: MiddlewareConsumer) {
	consumer.apply(LoggerMiddleware).forRoutes('*');
	
	}
}
```

#### Custom exception filter

NestJs has a built-in exception filter that automatically sends a response to the client for all unhandled errors. This global filter sends a generic response in this format:

```json
{
  "statusCode": 500,
  "message": "Internal server error"
}
```

This response doesn't let the client know what went wrong and that is why you should create a custom filter that propagates the actual error to the client. 

First, we'll create an interface for our custom error filter in this path: `src/common/interfaces/error.interface.ts`. I don't like slapping a 500 status code if I can help it, so this interface will extend the `Error` object and we can give an appropriate status code for errors.

```ts
export interface ResponseError extends Error {
	statusCode?: number;
}
```

Create the custom filter in this path: `src/common/filters/custom-exception.filter.ts`.

```ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { ResponseError } from '../interfaces/error.interface';

@Catch()
export class CustomExceptionFilter implements ExceptionFilter {
  catch(error: ResponseError, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse < Response > ();
    const request = ctx.getRequest < Request > ();

    if (error instanceof HttpException) {
      response.status(error.getStatus()).json({
        statusCode: error.getStatus(),
        message: error.message,
        error: error.name,
      });
    } else {
      const status = error?.statusCode || 500;
      response.status(status).json({
        statusCode: status,
        message: error.message,
        error: 'Internal Server Error',
      });
    }
  }
}

```

## Setting up Postgres database

Now we'll create our database using psql, a command line utility for interacting with Postgres. Start up psql in your terminal by running the command:

```bash
psql -U postgres
```

The command we ran connects us to Postgres with the default user. We can then create a database by running the following command:

```sql
CREATE DATABASE two-fa-demo;
```

## Setting up Prisma

In this section, we will be connecting to our Postgres database using the Prisma ORM. Prisma simplifies our interaction with the database and generates migrations for every model update we make.

🔥 Tip: If you're using Visual Studio Code, I recommend installing the Prisma extension to enable syntax highlighting and linting for `.prisma` files.

Install the Prisma CLI as a development dependency.

```bash
npm install -D prisma
```

Initialize Prisma in your project by running the command below in your terminal.

```bash
npx prisma init
```

The command above creates a Prisma folder in our project's root directory. The Prisma folder contains the `schema.prisma` file which we'll use to define the models for our database tables. When we migrate our model to our database, Prisma will generate a migration file that will map our model to our database. 

You should also notice that Prisma created a `.env` file in our project root directory. Update the database URL with our Postgres database connection string.

```bash
DATABASE_URL="postgres://postgres@localhost:5432/two-fa-demo"
```

In the `prisma/schema.prisma` file, make sure that the data source provider is set to `postgresql`. 

```ts
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
```

### Defining models

We will be creating 2 tables in this project: A `User` table and an `Otp` table. Update the `prisma/schema.prisma` file with the models:

```ts
model User {
  id        String   @id @default(uuid())
  email     String   @unique
  phone     String   @unique
  password  String
  firstName String
  lastName  String
  twoFA     Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  Otp       Otp[]
  isPhoneVerified Boolean @default(false)
}

model Otp {
  id        String   @id @default(uuid())
  owner     User     @relation(fields: [userId], references: [id])
  userId    String
  code      String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  expiresAt DateTime @db.Timestamp(5)
  useCase   UseCase
}

enum UseCase {
  LOGIN
  D2FA
  PHV
}
```

We declare a one-to-many relationship between the User table and the Otp table. A user can have many OTPs. When a user is created in our application, 2FA is disabled by default and their phone number is set as unverified. 

The OTPs generated in our application will be valid for 5 minutes and will have 3 use cases: 
- LOGIN: This is for OTPs that are sent to users who have 2FA-enabled accounts
- D2FA: When a user decides to disable two-factor authentication on their account, we will send an OTP to their phone number. If they verify this, we will disable 2FA on their account. This use case addresses that.
- PHV: This is for OTPs that are sent to the user's phone number when they want to verify their phone number on the application.

Now that we have defined our models, we can run the following command to generate and run a migration against our database.

```bash
npx prisma migrate dev --name init
```

With the above command, Prisma does the following:
- Generates an SQL migration file in the `prisma/migrations/<migration-folder>`. 
- Runs the migration against the database to create the tables according to the defined models.
- Generates the Prisma client, a type-safe query builder adapted to our models. You'll notice that our `package.json` has been updated with a new dependency: `@prisma/client`.

> Whenever you update the prisma/schema.prisma file, it is required that you regenerate the Prisma client with the command `*npx prisma generate*`. This way the client is adapted to the new changes in your schema file.


### Creating Prisma service

We will now encapsulate the Prisma client in its service. The idea of creating a service for the Prisma client is to isolate it from the rest of our application code, making our code more organized and maintainable. The Prisma service will be used to create an instance of the Prisma client and query our database.

In your terminal, run the following command:

```bash
nest generate module prisma
nest generate service prisma
```

When the above command is run, the Nest CLI does the following:
- Creates the files `src/prisma/prisma.module.ts` and `src/prisma/prisma.service.ts`. 
- Updates the `app.module.ts` file by importing the Prisma module and adding it to the imports array.

In the Prisma service file, we'll create a PrismaService class that extends the Prisma Client. Update the Prisma service file with the code:

```ts
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }

  async enableShutdownHooks(app: INestApplication) {
    this.$on('beforeExit', async () => {
      await app.close();
    });
  }
}

```

The `onModuleInit` method ensures that a connection to the database is established when the Prisma module is initialized. The `enableShutdownHooks` method ensures that our application gracefully shutdowns whenever the `beforeExit` event is triggered.

Next, import the PrismaService in the Prisma Module file and add the PrismaService to the providers and exports array. The Prisma module file should look like this:

```ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}
```

## Implementing sign up

In this section, we'll be creating everything regarding signing up to the application. In your terminal run these commands to create a module, controller, and service for account creation on the application:

```bash
nest generate module account
nest generate controller account
nest generate service account
```

Notice that Nest CLI has:
- Created the files `src/account/account.controller.ts`, `src/account/account.service.ts`, `src/account/account.module.ts`.
- Imported the account controller and service into the account module and updated the account module controller and provider arrays.

### Create validation pipe

Next, we'll create a pipe for validating incoming requests. We'll be using Joi for request body validation. Install the `joi` npm package like so:

```bash
npm i joi
```

Pipes are classes that sit in front of a controller route handler. NestJS uses pipes to validate or transform the data on incoming requests. We'll create a custom pipe that will use Joi to validate incoming requests.

Create the custom pipe in this path: `src/common/pipes/joi.ts`.

```ts
import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
} from '@nestjs/common';
import { ObjectSchema } from 'joi';

@Injectable()
export class JoiValidationPipe implements PipeTransform {
  constructor(private schema: ObjectSchema) {}

  transform(value: any, metadata: ArgumentMetadata) {
    const { error } = this.schema.validate(value);
    if (error) {
      const errorMessage = error.details[0].message.replace(/"/g, '');
      throw new BadRequestException(errorMessage);
    }
    return value;
  }
}

```

Our custom pipe takes a joi schema as an argument. The joi schema is a representation of data that is acceptable for a route. Whenever an incoming request fails the joi validation, we throw a 400 error and propagate the error message to the client.

### Create joi sign-up schema

In the path `src/common/joi-schema/signup.ts`, create the schema for sign-up.

```ts
import * as Joi from 'joi';

export const signupSchema = Joi.object({
  email: Joi.string().email().required(),
  phone: Joi.string().required(),
  password: Joi.string().required(),
  firstName: Joi.string().required(),
  lastName: Joi.string().required(),
});

```

Our sign-up route will be expecting a post request with a body that meets the rules outlined in the joi schema.

Also, we'll create a dto class to represent the expected data for our sign-up route. This dto class will be used to infer the type of the incoming request's body. In `src/account/dto/create-user.dto.ts` add this code:

```ts
export class CreateUserDto {
  email: string;
  phone: string;
  password: string;
  firstName: string;
  lastName: string;
}
```

### Create sign-up service

In `src/account/account.service.ts`, we'll create the service method for signing up.

```ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { hashPassword } from '../common/utils/passwordHasher';
import _ from 'underscore';

@Injectable()
export class AccountService {
  constructor(private prisma: PrismaService) {}

  async signUp(createUserDto: CreateUserDto) {
    createUserDto.password = await hashPassword(createUserDto.password);
    const user = await this.prisma.user.create({ data: createUserDto });
    return _.omit(user, 'password');
  }
}
```

We import the Prisma service, inject it into the account service class and use it to create a new user record in the database. I created a utility to hash the user's password before the user account is created. Once the user account is created, I use the underscore package to omit the password from the user record before sending it back to our route controller. 

Install some dependencies we'll be needing.

```bash
npm i bcrypt
npm i underscore
```

Create the password-hasher utility in `src/common/utils/passwordHasher.ts`.

```ts
import * as bcrypt from 'bcrypt';

export const hashPassword = async (password: string): Promise<string> => {
  const hash = await bcrypt.hash(password, 10);
  return hash;
};
```

### Create sign-up route handler

Update the `src/account/account.controller.ts` file to look like this:

 ```ts
 import {
  Controller,
  Post,
  Body,
  UsePipes,
} from '@nestjs/common';
import { AccountService } from './account.service';
import { CreateUserDto } from './dto/create-user.dto';
import { signupSchema } from '../common/joi-schema/signup';
import { JoiValidationPipe } from '../common/pipes/joi';

@Controller('account')
export class AccountController {
  constructor(private readonly accountService: AccountService) {}

  @Post('signup')
  @UsePipes(new JoiValidationPipe(signupSchema))
  create(@Body() createUserDto: CreateUserDto) {
    return this.accountService.signUp(createUserDto);
  }
}

```

The `create` method in the account controller accepts a post request whose body will be validated by our custom pipe. Notice how we pass the signup schema to the pipe using the `@UsePipes` decorator. We extract the body from the request using the `@Body` decorator and finally pass it on to the service method. NestJS will automatically send a 201 status code when the request resolves successfully.

Our sign-up route will be accessed at the path `api/v1/account/signup`. Notice that the `@Controller` decorator is passed a parameter: `account`. Now every route created in this controller class will be prefixed with `/account`.

## Implementing login

Our login will take this flow:
- User will enter email and password
- User details will be validated
- If correct, we generate a JSON web token based on the user credentials and return the token to the client.

First, we'll create an auth module to handle login and authorization-related functionality. Run the below commands in your terminal to create the auth module, controller, and service files.

```bash
nest generate module auth
nest generate controller auth
nest generate service auth
```

### Register the JWT module

In your `.env`, add a secret for your JSON web token. This will be used in creating and validating our JWTs.

```bash
JWT_SECRET=twofademo
```

Create a constants file to easily access the JWT secret. In `src/common/utils/constants.ts`, add this code:

```ts
export const constants = {
  jwtSecret: process.env.JWT_SECRET
};
```

NestJS has a built-in module that makes it easy to handle creating JWTs. Register the NestJS JWT module in the auth module:

```ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { PrismaModule } from '../prisma/prisma.module';
import { constants } from '../common/utils/constants';

@Module({
  controllers: [AuthController],
  providers: [AuthService],
  imports: [
    PrismaModule,
    JwtModule.register({
      global: true,
      secret: constants.jwtSecret,
      signOptions: { expiresIn: '2 days' },
    }),
  ],
})
export class AuthModule {}
```

### Validation for login route

Create the login dto in this path: `src/auth/dto/login.dto.ts` and  add this code:

```ts
export class LoginDto {
  email: string;
  password: string;
}
```

Create the login joi schema in this path: `src/common/joi-schema/login.ts` :

```ts
import * as Joi from 'joi';

export const loginSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().required(),
});
```

### Set up login service handler

In `src/auth/auth.service.ts`, add the code:

```ts
import { HttpStatus, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { HttpException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { Request } from 'express';
import { PrismaService } from '../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';
import { verifyPassword } from '../common/utils/passwordHasher';

@Injectable()
export class AuthService {
  constructor(private prisma: PrismaService, private jwtService: JwtService) {}

  async login(loginDto: LoginDto) {
    const user = await this.prisma.user.findUnique({
      where: { email: loginDto.email },
    });
    if (!user) {
      throw new HttpException(
        'Invalid email or password',
        HttpStatus.BAD_REQUEST,
      );
    }
    const validPassword = await verifyPassword(
      loginDto.password,
      user.password,
    );

    if (!validPassword) {
      throw new HttpException(
        'Invalid email or password',
        HttpStatus.BAD_REQUEST,
      );
    }

    if (!user.twoFA) {
      const payload = {
        email: user.email,
        first_name: user.firstName,
        last_name: user.lastName,
        sub: user.id,
      };
      return {
        success: true,
        access_token: await this.jwtService.signAsync(payload),
      };
    }
  }
}
```

In the login method of the `AuthService` class, we check if the user exists in our database. If true, we verify their password. If they do not have 2FA enabled on their account, we immediately generate a JWT using their credentials. If the user doesn't exist in the database, we throw an error message with a 400 status code.

Create `verifyPassword` function in the passwordHasher utility file we previously created.

```ts
export const verifyPassword = async (password, hash): Promise<boolean> => {
  const isMatch = await bcrypt.compare(password, hash);
  return isMatch;
};
```

### Set up login route handler

Set up the route handler for login by adding the code below to the `AuthController` file.

```ts
import {
  Body,
  Controller,
  HttpCode,
  Post,
  UsePipes,
} from '@nestjs/common';
import { Request } from 'express';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { loginSchema } from '../common/joi-schema/login';
import { JoiValidationPipe } from '../common/pipes/joi';
import { tokenSchema } from '../common/joi-schema/token';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @UsePipes(new JoiValidationPipe(loginSchema))
  @HttpCode(200)
  @Post('login')
  login(@Body() loginDto: LoginDto) {
    return this.authService.login(loginDto);
  }
  
}
```

The login route can be accessed at the path: `api/v1/auth/login`. Successful requests resolve with a status code of 200. In the login method, we extract the body of the request and pass it on to our service handler method.

## Phone verification 

Before users can enable two-factor authentication on their account, their phone number must be verified on the application. In this section, we will be using Twilio to send a verification OTP to their registered phone number.

### Set up Twilio

Sign up for an account on the [Twilio website](https://www.twilio.com/en-us). Once signed up on Twilio, you'll get a phone number that you'll use to send out SMS. Also, grab your Twilio Account SID and Twilio Auth Token from your Twilio console. Update your `.env` with your new credentials like so:

```bash
TWILIO_AUTH_TOKEN=
TWILIO_ACCOUNT_SID=
TWILIO_PHONE_NUMBER=
```

Install the Twilio npm package which we'll use to access the Twilio API.

```bash
npm i twilio
```

Update the constants utility file previously created to look like this:

```ts
export const constants = {
  jwtSecret: process.env.JWT_SECRET,
  twilioAuthToken: process.env.TWILIO_AUTH_TOKEN,
  twilioAccountSID: process.env.TWILIO_ACCOUNT_SID,
  twilioPhoneNumber: process.env.TWILIO_PHONE_NUMBER,
};
```

Next, we'll create a function to handle sending out SMS. Create the function in this file path: `src/common/utils/twilio.ts`.

```ts
import { Twilio } from 'twilio';
import { constants } from './constants';

const { twilioAccountSID, twilioAuthToken, twilioPhoneNumber } = constants;
const client = new Twilio(twilioAccountSID, twilioAuthToken);

export const sendSMS = async (phoneNumber: string, message: string) => {
  try {
    const smsResponse = await client.messages.create({
      from: twilioPhoneNumber,
      to: phoneNumber,
      body: message,
    });
    console.log(smsResponse.sid);
  } catch (error) {
    error.statusCode = 400;
    throw error;
  }
};

```

### Create Auth Guard

In NestJS, guards are classes that are used to determine if a request has the necessary authorization to access a resource. Our phone verification route will be a protected route. Hence, requests to this route must carry a bearer token from a signed-in user.

Create the auth guard in this path: `src/common/guards/auth.guard.ts`.

```ts
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { constants } from '../utils/constants';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new UnauthorizedException();
    }
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: constants.jwtSecret,
      });
      // 💡 We're assigning the payload to the request object here
      // so that we can access it in our route handlers
      request['user'] = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

```


In the auth guard, we have a private method that extracts the bearer token from the authorization header of the incoming request and a `canActivate`  method that validates the bearer token. If the token is not valid, we throw a 401 error else we append a user property to the request and pass the request on to its route handler.

### Send phone verification OTP

The phone verification request will be sent to a post endpoint. Create a joi validation schema for the request in this path: `src/common/joi/verify-phone.ts`.

```ts
import * as Joi from 'joi';

export const verifyPhoneSchema = Joi.object({
  verify: Joi.boolean().valid(true).required(),
});

```

The schema shows that the endpoint expects a request with a `verify` key in the request body. 

In the `AccountController` file, add the route handler method for phone verification.

```ts
 import {
  Controller,
  Post,
  Body,
  UsePipes,
  UseGuards,
  HttpCode,
  Req,
} from '@nestjs/common';
import { Request } from 'express';
import { AccountService } from './account.service';
import { CreateUserDto } from './dto/create-user.dto';
import { signupSchema } from '../common/joi-schema/signup';
import { JoiValidationPipe } from '../common/pipes/joi';
import { AuthGuard } from '../common/guards/auth.guard';
import { verifyPhoneSchema } from '../common/joi-schema/verify-phone';

@Controller('account')
export class AccountController {
  constructor(private readonly accountService: AccountService) {}
  
  ...
  
  @HttpCode(200)
  @Post('phone/verify')
  @UsePipes(new JoiValidationPipe(verifyPhoneSchema))
  @UseGuards(AuthGuard)
  verifyPhone(@Body() _body, @Req() request: Request) {
    return this.accountService.verifyPhone(request);
  }
}
```

The request will be sent to the path: `api/v1/account/phone/verify`. We use the `@UseGuards` decorator to ensure that only authorized requests can access this route. Once the request successfully passes the guard validation, we pass the entire request to the account service method. The reason for this is that we need to access the user property on the request. This way, we know the user who is requesting a phone verification.

Create the service method for phone verification and update your imports.

```ts
...
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { generateOTP } from '../common/utils/codeGenerator';
import { Prisma } from '@prisma/client';
import { getExpiry, isTokenExpired } from '../common/utils/dateTimeUtility';
import { sendSMS } from '../common/utils/twilio';

@Injectable()
export class AccountService {
  constructor(private prisma: PrismaService) {}

    ...
  async verifyPhone(req: Request) {
      const userDetails = req['user'];
      const user = await this.prisma.user.findUnique({
        where: { id: userDetails.sub },
      });
      if (!user) {
        throw new HttpException('User not found', HttpStatus.NOT_FOUND);
      }
      if (user.isPhoneVerified) {
        return { success: true };
      }
      const otp = generateOTP(6);
      const otpPayload: Prisma.OtpUncheckedCreateInput = {
        userId: user.id,
        code: otp,
        useCase: 'PHV',
        expiresAt: getExpiry(),
      };
  
      await this.prisma.otp.create({
        data: otpPayload,
      });
      await sendSMS(
        user.phone,
        `Use this code ${otp} to verify the phone number registered on your account`,
      );
      return { success: true };
    }
  }
}
```

In the `verifyPhone` method, we extract the user property on the request and check if the user indeed exists on the database. If the user is unverified, we generate a 6-digit code, create a record in our Otp table with the use case of `PHV` and send the user an sms containing an Otp that will expire in 5 minutes.

Create the utility functions `generateOTP` and `getExpiry`.

```ts
//src/common/utils/codeGenerator.ts
export const generateOTP = (n: number): string => {
  const digits = '0123456789';
  let otp = '';

  for (let i = 0; i < n; i++) {
    otp += digits[Math.floor(Math.random() * digits.length)];
  }

  return otp;
};

```

Import the moment npm package by running `npm install moment`.

```ts
//src/common/utils/dateTimeUtility.ts
import * as moment from 'moment';
export const getExpiry = () => {
  const createdAt = new Date();
  const expiresAt = moment(createdAt).add(5, 'minutes').toDate();
  return expiresAt;
};

export function isTokenExpired(expiry: Date): boolean {
  const expirationDate = new Date(expiry);
  const currentDate = new Date();
  return expirationDate.getTime() <= currentDate.getTime();
}
```

### Verify PHV OTP

The client will make a request to verify the OTP the user received for phone verification. Create the joi schema for this endpoint in this path: `src.common/joi/token.ts`.

```ts
import * as Joi from 'joi';

export const tokenSchema = Joi.object({
  token: Joi.string().required(),
});
```

Create a route handler method for this verification in the `AccountController`.

```ts
@Controller('account')
export class AccountController {
  constructor(private readonly accountService: AccountService) {}
  
  ...
  
  @UsePipes(new JoiValidationPipe(tokenSchema))
  @UseGuards(AuthGuard)
  @HttpCode(200)
  @Post('phone/verify/token')
  validatePhoneVerification(@Body() _body, @Req() request: Request) {
    return this.accountService.validatePhoneVerification(request);
  }
}
```

Create the service method to handle this verification in the `AccountService` class.

```ts
@Injectable()
export class AccountService {
  constructor(private prisma: PrismaService) {}

    ...
  async validatePhoneVerification(req: Request) {
    const {
      body: { token },
    } = req;
    const userDetails = req['user'];
    // find otp record
    const otpRecord = await this.prisma.otp.findFirst({
      where: { code: token, useCase: 'PHV', userId: userDetails.sub },
    });
    if (!otpRecord) {
      throw new HttpException('Invalid OTP', HttpStatus.NOT_FOUND);
    }
    // check if otp is expired
    const isExpired = isTokenExpired(otpRecord.expiresAt);
    if (isExpired) {
      throw new HttpException('Expired token', HttpStatus.NOT_FOUND);
    }
    // update user isPhoneVerified to true
    await this.prisma.user.update({
      where: { id: userDetails.sub },
      data: { isPhoneVerified: true },
    });

    // delete the otp record
    await this.prisma.otp.delete({ where: { id: otpRecord.id } });
    return { success: true };
  }

}
```

The service method does the following:
- Extracts the token from the request body and checks if it exists in the database.
- Checks the token expiry if it exists
- Update the `isPhoneVerified` field in the user record to true if the token is valid
- Deletes the OTP record from the database 

## Setting 2FA

In this section, we'll create an endpoint to enable or disable two-factor authentication on the user account.

Create the joi schema for this endpoint in `src/common/joi-schema/set2fa.ts`.

```ts
import * as Joi from 'joi';

export const set2faSchema = Joi.object({
  set_2fa: Joi.boolean().required(),
});
```

Create the route handler method in `AccountController`.

```ts
@Controller('account')
export class AccountController {
  constructor(private readonly accountService: AccountService) {}

 ...

  @UsePipes(new JoiValidationPipe(set2faSchema))
  @UseGuards(AuthGuard)
  @HttpCode(200)
  @Post('set/twofa')
  enableTwoFA(@Body() _body, @Req() request: Request) {
    return this.accountService.setTwoFA(request);
  }
}
```

The request will be sent to the path: `api/v1/account/set/twofa`. Create the service method for this route in the `AccountService` class.

```ts
@Injectable()
export class AccountService {
  constructor(private prisma: PrismaService) {}

  ...

  async setTwoFA(req: Request) {
    const {
      body: { set_2fa },
    } = req;
    const userDetails = req['user'];

    const user = await this.prisma.user.findUnique({
      where: { id: userDetails.sub },
    });

    if (!user) {
      throw new HttpException('User not found', HttpStatus.NOT_FOUND);
    }

    if (user.twoFA === set_2fa) {
      return { success: true };
    }

    if (user.twoFA && set_2fa == false) {
      // generate otp to disable MFA, create otp record, send sms and return
      const otp = generateOTP(6);
      const otpPayload: Prisma.OtpUncheckedCreateInput = {
        userId: user.id,
        code: otp,
        useCase: 'D2FA',
        expiresAt: getExpiry(),
      };

      await this.prisma.otp.create({
        data: otpPayload,
      });
      await sendSMS(
        user.phone,
        `Use this code ${otp} to disable multifactor authentication on your account`,
      );
      return { success: true };
    }

    await this.prisma.user.update({
      where: { id: user.id },
      data: { twoFA: set_2fa },
    });

    return { success: true };
  }
}
```

This service method does the following:
- If the `set_2fa` parameter of the request body is `true` and the user does not have 2FA enabled, two-factor authentication is enabled on their account.
 - If the `set_2fa` parameter of the request body is `false` and the user already has 2FA enabled, an OTP with the use case `D2FA` is sent to their phone number. The OTP will be validated on a separate endpoint before 2FA is disabled on the user account.

### Disabling 2FA

To disable two-factor authentication on their account, the user will receive an OTP. This OTP will be validated before 2FA is finally disabled on their account. We'll create the route handler and service method for this.

```ts
@Controller('account')
export class AccountController {
  constructor(private readonly accountService: AccountService) {}

 ...

  @UsePipes(new JoiValidationPipe(tokenSchema))
  @UseGuards(AuthGuard)
  @HttpCode(200)
  @Post('disable-twofa/verify')
  disable2FAVerification(@Body() _body, @Req() request: Request) {
    return this.accountService.disable2FAVerification(request);
  }
}
```

```ts
@Injectable()
export class AccountService {
  constructor(private prisma: PrismaService) {}

  ...
  async disable2FAVerification(req: Request) {
	  const {
	    body: { token },
	  } = req;
	  const userDetails = req['user'];
	  const otpRecord = await this.prisma.otp.findFirst({
	    where: { code: token, useCase: 'D2FA', userId: userDetails.sub },
	  });
	  if (!otpRecord) {
	    throw new HttpException('Invalid OTP', HttpStatus.NOT_FOUND);
	  }
	  const isExpired = isTokenExpired(otpRecord.expiresAt);
	  if (isExpired) {
	    throw new HttpException('Expired token', HttpStatus.NOT_FOUND);
	  }
	  await this.prisma.user.update({
	    where: { id: userDetails.sub },
	    data: { twoFA: false },
	  });
	
	  await this.prisma.otp.delete({ where: { id: otpRecord.id } });
	  return { success: true };
	}
}

```


To verify the OTP sent for disabling 2FA, a post request will be sent to`api/v1/account/disable-twofa/verify`. In the `disable2FAVerification` method, we check if the OTP exists in the database and if it does, we check if it has expired. If the OTP is still valid, we update the user record in the database by setting the `twoFA` field to false. Finally, we delete the OTP record from the database.

## Login flow for 2FA enabled account 

Recall that in the login method of the `AuthService` class, if the user does not have two-factor authentication enabled, we immediately generate a JWT and send it back to the client. We'll now update the login method to cater to users who have enabled 2FA. Update the login method of the `AuthService` class:

```ts
@Injectable()
export class AuthService {
  constructor(private prisma: PrismaService, private jwtService: JwtService) {}

  async login(loginDto: LoginDto) {
    const user = await this.prisma.user.findUnique({
      where: { email: loginDto.email },
    });
    if (!user) {
      throw new HttpException(
        'Invalid email or password',
        HttpStatus.BAD_REQUEST,
      );
    }
    const validPassword = await verifyPassword(
      loginDto.password,
      user.password,
    );

    if (!validPassword) {
      throw new HttpException(
        'Invalid email or password',
        HttpStatus.BAD_REQUEST,
      );
    }

    if (!user.twoFA) {
      const payload = {
        email: user.email,
        first_name: user.firstName,
        last_name: user.lastName,
        sub: user.id,
      };
      return {
        success: true,
        access_token: await this.jwtService.signAsync(payload),
      };
    }

    const otp = generateOTP(6);
    const otpPayload: Prisma.OtpUncheckedCreateInput = {
      userId: user.id,
      code: otp,
      useCase: 'LOGIN',
      expiresAt: getExpiry(),
    };
    await this.prisma.otp.create({
      data: otpPayload,
    });
    await sendSMS(
      user.phone,
      `Use this code ${otp} to finalize login on your account`,
    );
    return { success: true };
  }
}
```

Now for accounts with 2FA enabled, a 6-digit OTP  with a use case of `LOGIN` will be sent to their phone number. The OTP will be validated on a separate endpoint and if it is valid, a JWT will be sent back to the client.

### Verify LOGIN OTP

Update the `AuthController` to include a route handler method which will be used to validate the login OTP.

```ts
@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  ...

  @UsePipes(new JoiValidationPipe(tokenSchema))
  @HttpCode(200)
  @Post('login/verify/token')
  verifyLogin(@Body() _body, @Req() request: Request) {
    return this.authService.verifyLogin(request);
  }
}
```

The login OTP is sent as a post request to `api/v1/auth/login/verify/token`. Create a service method for this route handler in the `AuthService` class.

```ts
@Injectable()
export class AuthService {
  constructor(private prisma: PrismaService, private jwtService: JwtService) {}
  async verifyLogin(req: Request) {
    const {
      body: { token },
    } = req;
    const otpRecord = await this.prisma.otp.findFirst({
      where: { code: token, useCase: 'LOGIN' },
    });
    if (!otpRecord) {
      throw new HttpException('Invalid OTP', HttpStatus.NOT_FOUND);
    }
    const isExpired = isTokenExpired(otpRecord.expiresAt);
    if (isExpired) {
      throw new HttpException('Expired token', HttpStatus.NOT_FOUND);
    }
    const user = await this.prisma.user.findUnique({
      where: { id: otpRecord.userId },
    });

    if (!user) {
      throw new HttpException('Invalid OTP', HttpStatus.NOT_FOUND);
    }
    const payload = {
      email: user.email,
      first_name: user.firstName,
      last_name: user.lastName,
      sub: user.id,
    };
    return {
      success: true,
      access_token: await this.jwtService.signAsync(payload),
    };
  }
}
```

The `verifyLogin` service method does the following:
- Checks if the OTP exists in the database.
- If OTP exists, it checks if it is expired.
- If OTP is valid, a JWT is generated based on the user credentials and returned to the client.

## Conclusion

In conclusion, implementing two-factor authentication (2FA) can significantly enhance the security of an application. By requiring users to provide an additional verification factor, such as a time-based code from their mobile device, you can reduce the risk of unauthorized access and protect sensitive data.

In this article, we have covered various aspects of implementing 2FA in a NestJS project, including sign-up, login, authentication, request validation, and route protection. We have also discussed how to enable and disable 2FA for users.

By using NestJS's built-in features such as pipes and guards, we can ensure that our implementation is robust and secure. It is important to note that while 2FA adds an extra layer of security, it is not a replacement for strong passwords and other security best practices.

🔥 [Project GitHub repository](https://github.com/AlgoDame/nest-2fa-article-project)
🔥 [Postman documentation for endpoints](https://documenter.getpostman.com/view/15503893/2s93eYVsFF)

Thank you for reading. Feel free to share your thoughts in the comments.