Frequently Asked Questions
This guide details setting up 2FA using NestJS, Twilio, Prisma, and PostgreSQL. It covers user sign-up, login, phone verification, and 2FA-protected login. Key technologies include JWT for session management, bcrypt for password hashing, and Joi for validation.
NestJS is the core backend framework, providing structure, features, and TypeScript support for building the API. It's chosen for its modularity and efficiency in handling requests and coordinating with services like Twilio and Prisma.
Twilio is a reliable cloud communication platform. It's used to send OTPs via SMS for phone verification and two-factor authentication, adding an extra security layer to user logins.
Logging middleware is crucial for debugging and monitoring, providing context for each request. It's recommended to set this up early in the project for visibility into HTTP requests and responses, helping track issues in development and production.
Create a custom exception filter that implements NestJS's `ExceptionFilter` interface. This allows you to catch and handle errors gracefully, providing consistent JSON error responses to clients, and logging errors with stack traces on the server-side, which is important for debugging and maintenance.
Install the Prisma CLI globally using `npm install -D prisma` and the client library with `npm install @prisma/client`. Initialize Prisma with `npx prisma init --datasource-provider postgresql`. This sets up the necessary files for defining your data models.
Prisma is a modern database toolkit that simplifies database access with type safety and migrations in Node.js and TypeScript. It's used as the Object-Relational Mapper (ORM) to interact with the PostgreSQL database, managing connections and queries.
Generate a module and service using the NestJS CLI: `nest generate module prisma` and `nest generate service prisma`. In the service, extend the `PrismaClient` and implement `onModuleInit` to establish a database connection when the module initializes.
Creating a Prisma service follows SOLID principles and improves code organization and testability. It centralizes database logic, making it easier to manage database interactions within your NestJS application.
The project uses PostgreSQL, chosen for its reliability and robustness. The Prisma schema defines User and OTP models, which are migrated to the database.
Joi is a data validation library. You create a schema and use it with a custom pipe in your NestJS controller. This validates incoming requests, ensuring they adhere to the schema, and provides specific error messages for improved DX.
Hashing protects sensitive data even if the database is compromised. This guide uses bcrypt with a recommended salt round of 10. Never store passwords in plain text.
Generate an account module, controller, and service. Implement the signup logic in the service and define the POST route handler in the controller. Use a validation pipe (e.g., Joi) for data integrity.
Before creating a new user, query the database to check if a user with the given email or phone number already exists. If a duplicate is found, throw a `ConflictException` to inform the client.
JWT (JSON Web Token) is used for stateless session management. After successful password verification, the server generates and returns a JWT, which the client uses for subsequent authenticated requests.
This guide provides a step-by-step walkthrough for implementing robust SMS-based Two-Factor Authentication (2FA) and phone number verification using One-Time Passwords (OTP) in a NestJS application. We'll leverage Twilio for SMS delivery and Prisma for database interactions with a PostgreSQL database.
Last Updated: October 26, 2023
Project Overview and Goals
We will build a secure NestJS backend application featuring:
Problem Solved: This implementation adds a critical layer of security beyond simple passwords, mitigating risks associated with compromised credentials by requiring access to the user's registered phone.
Technologies Used:
System Architecture:
The system involves the following components interacting:
Prerequisites:
Final Outcome: A functional NestJS API with secure user authentication, phone verification, and optional SMS-based 2FA.
1. Setting up the Project
Let's scaffold our NestJS project and configure the basic utilities.
1.1 Install NestJS CLI: If you haven't already, install the NestJS CLI globally.
1.2 Create New Project: Generate a new NestJS project. Replace
nestjs-twilio-2fa
with your desired project name.This creates a standard NestJS project structure.
1.3 Initial Cleanup (Optional): The default project includes sample controller and service files. Let's remove them for a clean start.
src/app.controller.ts
,src/app.service.ts
, andsrc/app.controller.spec.ts
.src/app.module.ts
and removeAppController
from thecontrollers
array andAppService
from theproviders
array.src/app.module.ts
(after cleanup):1.4 Set Global API Prefix: It's good practice to prefix your API routes (e.g.,
/api/v1
).Edit
src/main.ts
:1.5 Configure Logging Middleware: NestJS has built-in logging, but a custom middleware provides more request context.
Create
src/common/middleware/logger.middleware.ts
:Register the middleware globally in
src/app.module.ts
:1.6 Custom Exception Filter: Handle errors gracefully and provide consistent error responses.
Create the custom filter (
src/common/filters/custom-exception.filter.ts
):Register the filter globally in
src/main.ts
:Why these choices?
HttpException
s and standard JavaScriptError
s.2. Database Setup with Prisma
We'll set up PostgreSQL and use Prisma to interact with it.
2.1 Set up PostgreSQL Database: Using
psql
or a GUI tool, create your database.2.2 Install Prisma: Install the Prisma CLI as a development dependency and the Prisma Client.
2.3 Initialize Prisma: This command creates a
prisma
directory with aschema.prisma
file and a.env
file at the project root.2.4 Configure Database Connection: Prisma automatically creates a
.env
file (or adds to an existing one). Update theDATABASE_URL
with your PostgreSQL connection string..env
:YOUR_DB_USER
,YOUR_DB_PASSWORD
, etc.) with your actual database credentials.schema=public
is present if you're using the default schema.""
).2.5 Define Data Models: Open
prisma/schema.prisma
and define theUser
andOtp
models.prisma/schema.prisma
:Explanation:
User
: Stores user details, including flags fortwoFA
andisPhoneVerified
.Otp
: Stores OTP codes, linked to a user (userId
), with a specificuseCase
and anexpiresAt
timestamp.UseCase
Enum: Defines the distinct purposes for which an OTP can be generated, improving clarity and logic.onDelete: Cascade
: If a User is deleted, their associated OTPs are also deleted.@@index
: Improves query performance when finding OTPs for a specific user and use case.2.6 Run Database Migration: Apply the schema changes to your database. Prisma generates SQL migration files and runs them.
This command will:
prisma/migrations
folder.User
andOtp
tables.@prisma/client
) based on your schema.Important: Whenever you change
prisma/schema.prisma
, runnpx prisma migrate dev --name <descriptive_migration_name>
to update your database andnpx prisma generate
to update the Prisma Client typings.2.7 Create Prisma Service: Encapsulate Prisma Client logic within a dedicated NestJS service for better organization and testability.
Generate the module and service:
Implement the service (
src/prisma/prisma.service.ts
):Configure the module (
src/prisma/prisma.module.ts
):Import
PrismaModule
into the rootAppModule
(src/app.module.ts
):Enable shutdown hooks in
src/main.ts
:Why these choices?
PrismaService
easily injectable across the application.3. Implementing User Sign-up
Create the endpoints and logic for user registration.
3.1 Install Dependencies:
joi
: For request validation schemas used with our custom pipe.bcrypt
: For hashing passwords securely.class-validator
,class-transformer
: Standard NestJS validation libraries. While this guide uses a Joi pipe for validation consistency, these are often used for DTO validation.3.2 Create Account Module, Controller, Service:
3.3 Create Validation Pipe: We need a pipe to validate incoming request bodies using Joi schemas.
Create
src/common/pipes/joi-validation.pipe.ts
:3.4 Create Sign-up DTO and Schema:
Define the expected request body structure.
Create
src/account/dto/create-user.dto.ts
:Create the Joi validation schema
src/account/validation/signup.schema.ts
:3.5 Create Password Hashing Utility:
Create
src/common/utils/password.util.ts
:3.6 Implement Sign-up Service Logic:
Inject
PrismaService
and implement the sign-up logic.Edit
src/account/account.service.ts
:3.7 Implement Sign-up Controller Endpoint:
Define the route handler in the controller.
Edit
src/account/account.controller.ts
:Make sure the
AccountService
andAccountController
are correctly listed insrc/account/account.module.ts
.src/account/account.module.ts
:Import
AccountModule
into the rootAppModule
(src/app.module.ts
):Why these choices?
4. Implementing User Login (Password Phase)
Set up the initial login mechanism using email and password, returning a JWT if credentials are valid but before handling the 2FA OTP step.
4.1 Install JWT Dependency:
4.2 Configure JWT Module:
Define a JWT secret in your
.env
file. Use a strong, randomly generated secret in production..env
:Create a configuration file for easy access to constants (
src/common/config/config.ts
):