Frequently Asked Questions
Implement 2FA by first setting up a NestJS project, integrating Sinch for SMS, and then building the authentication flow with phone verification and OTP delivery. The Sinch SMS API is used for sending OTPs, while Prisma handles database interactions for secure storage of user data and OTPs. This guide provides step-by-step instructions for setting up the entire process.
Prisma is used as an ORM (Object-Relational Mapper) to simplify database operations. It provides a type-safe interface for interacting with the PostgreSQL database, managing database migrations, and ensuring data integrity. This makes it easier to manage user data, OTP records, and other information related to authentication securely.
NestJS provides a robust and modular architecture well-suited for building scalable server-side applications. Its TypeScript support enhances code quality and maintainability. The modular structure helps organize code into controllers, services, and modules, improving code reusability and maintainability.
Enable 2FA after a user has successfully registered and verified their phone number. This adds an extra layer of security to the login process, protecting against unauthorized access even if the user's password is compromised. It's usually presented as an optional feature for users to activate.
While this guide focuses on Sinch, you can adapt the principles to integrate other SMS API providers. You'll need to adjust the SinchService to interact with your chosen provider's API and ensure it's compatible with the rest of the architecture. Key adjustments would involve modifying the service to make requests to the new provider's API endpoints and handle their specific response format.
PostgreSQL serves as the persistent data store for user information, including hashed passwords, phone numbers, 2FA status, and OTP records. It is a robust and reliable database solution chosen for its stability and open-source nature.
Create a `.env` file in the project root and add your Sinch Service Plan ID, API token, and optional Sinch phone number. The values for `SINCH_SERVICE_PLAN_ID` and `SINCH_API_TOKEN` are obtained from your Sinch account dashboard. The `SINCH_PHONE_NUMBER` is the virtual number or shortcode you use to send SMS.
You need Node.js, npm or yarn, a running PostgreSQL server, a Sinch account with SMS API credentials, and a basic understanding of TypeScript, REST APIs, and async/await. The NestJS CLI should also be installed globally to manage the project effectively.
bcrypt is used to securely hash user passwords before they are stored in the database. This ensures that even if the database is compromised, the raw passwords are not exposed, making it significantly harder for attackers to gain access to user accounts.
Class-validator and class-transformer are used for validating and transforming request data using Data Transfer Objects (DTOs). This helps ensure that data conforms to expected formats and types, improving the security and robustness of the API endpoints.
JSON Web Tokens (JWTs) are used for managing stateless authentication sessions after a user successfully logs in. Once authenticated, a JWT is issued to the client, which can be used to access protected routes without needing to send credentials each time.
The architecture consists of a Client, NestJS API, Prisma Client, PostgreSQL Database and the Sinch SMS API. The client interacts with the NestJS API, which handles the logic and communication with other services, like the Sinch API for SMS and Prisma for database interactions.
Use the NestJS CLI to create a new project: `nest new sinch-otp-nestjs-app` and navigate into the directory `cd sinch-otp-nestjs-app`. This command sets up the basic structure of your NestJS project.
Two-factor authentication (2FA) adds a critical layer of security beyond traditional username and password logins. By requiring a second verification step – often a time-based one-time password (TOTP) sent via SMS – you significantly reduce the risk of unauthorized account access.
This guide provides a step-by-step walkthrough for implementing a robust SMS-based OTP and 2FA system in your NestJS application using the Sinch SMS API for message delivery and Prisma for database interactions. We will build a secure authentication flow including user registration, login, phone number verification, and enabling/disabling 2FA.
Project Overview and Goals
What We'll Build:
Problem Solved:
This implementation addresses the need for enhanced application security by adding a second authentication factor, mitigating risks associated with compromised passwords. It provides a reliable way to verify user phone numbers before enabling sensitive features like 2FA.
Technologies Used:
System Architecture:
Prerequisites:
npm install -g @nestjs/cli
Expected Outcome:
By the end of this guide, you will have a fully functional NestJS backend with secure user authentication, phone verification via Sinch SMS OTPs, and optional 2FA for login.
1. Setting up the NestJS Project
Let's scaffold a new NestJS project and configure the essential components.
1.1 Create NestJS Project
Open your terminal and run:
This creates a new project with a standard structure.
1.2 Initial Cleanup
For a cleaner start, delete the default
app.controller.ts
,app.controller.spec.ts
, andapp.service.ts
from thesrc
directory. Remove their references fromsrc/app.module.ts
.src/app.module.ts
(Initial Cleanup):1.3 Set Global API Prefix
Define a global prefix (e.g.,
api/v1
) for all your API routes.src/main.ts
:1.4 Environment Variables Setup
We need a secure way to manage configuration like database URLs and API keys.
Install the necessary package:
Configure it in
AppModule
.src/app.module.ts
:Create a
.env
file in the project root:.env
:DATABASE_URL
: Replace placeholders with your actual PostgreSQL username, password, host (if not localhost), and desired database name (sinch_otp_demo
).JWT_SECRET
: Generate a strong, random secret for signing JWTs.JWT_EXPIRATION_TIME
: Define how long JWTs should be valid.SINCH_PHONE_NUMBER
: Optional virtual number or sender ID. Crucially, check the official Sinch documentation for sender ID requirements in your target countries, as rules (including whether this is optional or allowed) vary significantly by country and plan type.1.5 Database Setup with Prisma
Install Prisma CLI and Client:
Initialize Prisma:
This command:
prisma
directory with aschema.prisma
file..env
file (if it doesn't exist) and addsDATABASE_URL
..gitignore
.Ensure your
prisma/schema.prisma
file reflects the.env
configuration:prisma/schema.prisma
:Define the
User
andOtp
models:prisma/schema.prisma
:User
: Stores standard user info, plus flags for phone verification (isPhoneVerified
) and 2FA status (twoFactorEnabled
). The phone is initially optional.Otp
: Stores the OTP code, its expiry time, the use case (why it was generated), and links back to theUser
.onDelete: Cascade
ensures OTPs are deleted if the user is deleted.OtpUseCase
: An enum to distinguish between OTPs for initial phone verification and those for 2FA login.Generate and apply the initial migration:
This command:
prisma/migrations
.User
andOtp
tables.@prisma/client
).Create a Prisma service module for database interactions:
Implement the
PrismaService
:src/prisma/prisma.service.ts
:Make the service available within its module and globally:
src/prisma/prisma.module.ts
:Import
PrismaModule
intoAppModule
:src/app.module.ts
:1.6 Basic Utilities (Optional but Recommended)
Logger
from@nestjs/common
). We will use this throughout the services.HttpException
types.2. Implementing Core User Authentication
Let's build the signup and basic login functionality (without 2FA initially).
2.1 Install Dependencies
@nestjs/jwt
,passport
,passport-jwt
: For handling JWT authentication.bcrypt
: For hashing passwords.class-validator
,class-transformer
: For validating request DTOs (Data Transfer Objects).moment
: For easy date/time manipulation (OTP expiry).2.2 Create Auth Module
2.3 Configure JWT Module
Register the
JwtModule
within theAuthModule
. We'll useConfigService
to read the secret and expiration time from.env
.src/auth/auth.module.ts
:2.4 Create Users Module/Service
It's good practice to separate user data access logic.
src/users/users.service.ts
:src/users/users.module.ts
:Remember to import
UsersModule
intoAppModule
as well.src/app.module.ts
(Updated):2.5 Implement JWT Strategy
This strategy validates incoming JWTs in protected routes.
Create
src/auth/strategies/jwt.strategy.ts
:Create the
JwtPayload
interface:src/auth/interfaces/jwt-payload.interface.ts
:2.6 Implement Signup
Create DTO for signup request validation:
src/auth/dto/signup.dto.ts
:Update
AuthService
for signup logic:src/auth/auth.service.ts
:Update
AuthController
for the signup endpoint:src/auth/auth.controller.ts
:2.7 Implement Basic Login
Create DTO for login request validation:
src/auth/dto/login.dto.ts
:Add login logic to
AuthService
:src/auth/auth.service.ts
(Additions):Add the login endpoint to
AuthController
:src/auth/auth.controller.ts
(Additions):At this point, you have basic user registration and login without 2FA fully implemented.
3. Integrating Sinch for SMS OTP
Now, let's integrate Sinch to send SMS messages.
3.1 Install Sinch SDK
Choose the appropriate Sinch SDK package. Check Sinch's official documentation for the latest recommended packages (e.g.,
@sinch/sdk-core
,@sinch/sms
).3.2 Create Sinch Module and Service
Encapsulate Sinch logic within its own module.
3.3 Implement Sinch Service
This service will initialize the Sinch client and provide a method to send SMS.
src/sinch/sinch.service.ts
:Make the
SinchService
available and importConfigModule
:src/sinch/sinch.module.ts
:Import
SinchModule
intoAppModule
:src/app.module.ts
(Updated):Now you have a dedicated service for sending SMS via Sinch, configured using environment variables. The next steps would involve creating OTP generation logic, endpoints for requesting/verifying OTPs, and integrating this with the user profile and login flow.