Frequently Asked Questions
Use the /otp/send
endpoint with a POST request containing the recipient's phone number in E.164 format. The NestJS application will interact with the Infobip 2FA API to generate and send the OTP via SMS, returning a unique pinId
for verification. Ensure your request body includes a phoneNumber
field formatted correctly as a string, for example, '+14155552671'.
First, obtain an Infobip account, Base URL, and API Key. Then, create a 2FA application and message template in the Infobip portal. Store the applicationId
, messageId
, Base URL, and API Key securely in your .env
file, loaded using NestJS's ConfigService
.
The pinId
is a unique identifier generated by the Infobip API after sending an OTP. It's crucial for verifying the OTP code submitted by the user. Your NestJS application should temporarily store the pinId
linked to the user's pending action (e.g. login, registration).
These libraries facilitate request body validation using decorators in your DTOs (Data Transfer Objects). class-validator
provides decorators like @IsNotEmpty
, @IsPhoneNumber
, etc., while class-transformer
handles transformations between plain objects and class instances.
A global exception filter is highly recommended for production applications to handle errors consistently. It provides centralized error logging and standardized error responses, improving maintainability and the user experience.
Inject the HttpService
from @nestjs/axios
into your NestJS service. Use it to make POST requests to Infobip's 2FA API endpoints, setting appropriate headers, including the Authorization
header with your API key. Manage all API credentials using environment variables and the ConfigService
.
Send a POST request to the /otp/verify
endpoint, including the pinId
obtained during the send request and the user-entered otpCode
in the request body. The NestJS application verifies the code against Infobip, returning a boolean verified
status in the response.
The @nestjs/throttler
module provides rate limiting to prevent API abuse. This mitigates brute-force attacks on the /otp/send
and /otp/verify
endpoints and safeguards your application.
Yes, use the @Throttle
decorator on individual controller methods to override global rate limiting settings from ThrottlerModule.forRoot
. This allows for fine-grained control, such as permitting more verify attempts than send requests.
TypeScript enhances JavaScript with static typing, improving code quality, maintainability, and developer experience. It makes large projects like this NestJS application easier to manage, debug, and scale.
An LTS (Long Term Support) version like Node.js v18 or v20 is recommended for stability and maintenance. These versions receive security updates and performance improvements for an extended period.
Implement a dedicated error handling method in your service to catch AxiosError
instances. This provides an opportunity to log details like error messages, response data, and status codes, along with appropriate context.
The client application initiates OTP requests to the NestJS API, which acts as an intermediary for interacting with the Infobip 2FA API. This setup decoupled direct client interaction with Infobip, providing greater flexibility and control over the authentication flow.
Build a secure One-Time Password (OTP) system for Two-Factor Authentication (2FA) in your Node.js application using NestJS and the Infobip 2FA API. This guide covers everything from initial setup to production deployment.
You'll create a robust, scalable OTP service that integrates seamlessly into user authentication flows – registration, login, or sensitive action confirmation. By the end, you'll have a functional NestJS API that sends OTPs via SMS and verifies user-submitted codes, backed by Infobip's global communication infrastructure.
Project Overview and Goals
What You'll Build:
A NestJS-based microservice or module that:
Problem Solved:
Enhance your application security by adding a second authentication factor. Prevent unauthorized access even if passwords are compromised by verifying possession of a trusted device (the user's phone).
Technologies Used:
@nestjs/axios
wrapper).@nestjs/config
wrapper).System Architecture:
The interaction flow is as follows:
POST
request to the NestJS OTP API endpoint (e.g.,/otp/send
) containing the user's phone number.pinId
. The NestJS application might temporarily store thispinId
linked to the user's session or action.POST
request to the NestJS OTP API endpoint (e.g.,/otp/verify
) containing thepinId
received earlier and the OTP code entered by the user.pinId
and the submitted OTP code.verified: true/false
).Prerequisites:
curl
).Setting Up Your Project
Bootstrap a new NestJS project and install the necessary dependencies.
Step 1: Create a New NestJS Project
Open your terminal and run the NestJS CLI command:
Choose your preferred package manager (npm or yarn) when prompted.
Step 2: Install Dependencies
We need modules for configuration, making HTTP requests, validation, and rate limiting.
Step 3: Configure Environment Variables
Create a
.env
file in your project root. This file stores sensitive information like API keys and configuration IDs. Never commit this file to version control.Retrieve Your Infobip Credentials:
INFOBIP_BASE_URL
&INFOBIP_API_KEY
:.env
file. Treat the API Key like a password.INFOBIP_APP_ID
&INFOBIP_MESSAGE_ID
:POST
request to{INFOBIP_BASE_URL}/2fa/1/applications
.applicationId
. Copy this value intoINFOBIP_APP_ID
in your.env
file.curl
or Postman to send aPOST
request to{INFOBIP_BASE_URL}/2fa/1/applications/{INFOBIP_APP_ID}/messages
. Replace{INFOBIP_APP_ID}
with the ID you just received.senderId
s depending on the country and regulations.InfoSMS
is often a default shared sender. Check Infobip documentation for details.messageId
. Copy this value intoINFOBIP_MESSAGE_ID
in your.env
file.Step 4: Load Environment Variables Using ConfigModule
Update
src/app.module.ts
to load and validate your environment variables.Step 5: Add
.env
to.gitignore
Ensure your
.gitignore
file includes.env
:Create a
.env.example
file listing required variables (without values) and commit it to your repository. This helps collaborators set up their environment.Implementing Core Functionality
Create a dedicated module (
OtpModule
) containing a service (OtpService
) to handle Infobip API interactions.Step 1: Generate the OTP Module and Service
Use the NestJS CLI:
Step 2: Configure HttpModule for Axios
Make the
HttpModule
available within yourOtpModule
to injectHttpService
.Step 3: Implement the OtpService
This service will contain the methods for sending and verifying OTPs.
Explanation:
HttpService
(for making requests) andConfigService
(for environment variables).sendOtp
: Constructs the URL and payload for Infobip's/2fa/2/pin
endpoint, sets theAuthorization
header using the API key, makes the POST request, and returns thepinId
from the response.verifyOtp
: Constructs the URL (/2fa/2/pin/{pinId}/verify
) and payload, makes the POST request, and checks theverified
field in the response. Returnstrue
orfalse
.handleInfobipError
: A private helper to log errors consistently, distinguishing between Axios HTTP errors and other unexpected errors. It re-throws an error to be caught higher up (e.g., in the controller or an exception filter).Building Your API Layer
Expose the OTP functionality through a controller with specific endpoints.
Step 1: Create Data Transfer Objects (DTOs) for Validation
Create files for request body validation.
Create
SendOtpDto
insrc/otp/dto/send-otp.dto.ts
:Create
VerifyOtpDto
insrc/otp/dto/verify-otp.dto.ts
:SendOtpDto
: Requires aphoneNumber
field in E.164 format (e.g.,+14155552671
).VerifyOtpDto
: RequirespinId
(received from the send request) andotpCode
(entered by user), validating length based on your message template.Step 2: Generate the OTP Controller
Step 3: Implement the OtpController
Step 4: Register the Controller
Uncomment the controller in
src/otp/otp.module.ts
:Explanation:
POST
endpoints:/otp/send
and/otp/verify
.@UsePipes(new ValidationPipe(...))
automatically validates incoming request bodies against the DTOs (SendOtpDto
,VerifyOtpDto
).whitelist: true
strips any properties not defined in the DTO.@UseGuards(ThrottlerGuard)
applies the global rate limiting configured inAppModule
.@Throttle(...)
allows overriding the global limits for specific endpoints. We allow fewersend
requests thanverify
requests per time window.OtpService
.pinId
on send,verified
boolean on verify).@HttpCode(HttpStatus.OK)
ensures a 200 status code is returned even if verification fails (as the API call itself succeeded). Theverified
flag in the response body indicates the outcome.Testing with
curl
:Start the application:
npm run start:dev
oryarn start:dev
Send OTP:
Verify OTP: Use the
pinId
from the previous response and the code from the SMS.Integrating with Infobip
This section summarizes the Infobip-specific setup. Refer to the Setting Up Your Project section, Step 3 for detailed
curl
examples if needed.Configuration Steps:
.env
file asINFOBIP_BASE_URL
andINFOBIP_API_KEY
.POST /2fa/1/applications
) to create a 2FA application.pinAttempts
,pinTimeToLive
, etc.applicationId
in.env
asINFOBIP_APP_ID
.POST /2fa/1/applications/{APP_ID}/messages
) to create a message template linked to your application.messageText
(including{{pin}}
),pinType
,pinLength
, and optionallysenderId
.messageId
in.env
asINFOBIP_MESSAGE_ID
.Secure Handling of Credentials:
.env
file..gitignore
: The.env
file is explicitly excluded from Git commits. Ensure.env.example
is committed instead.ConfigService
is used to load these variables securely at runtime..env
files. Use the deployment environment's mechanism for managing secrets (e.g., AWS Secrets Manager, Kubernetes Secrets, Platform Environment Variables).Fallback Mechanisms:
Direct fallback for OTP delivery failure is challenging. If Infobip experiences an outage:
OtpService
catches errors from Infobip. The API response will indicate failure.Error Handling, Logging, and Retry Mechanisms
Robust error handling and logging are crucial for production systems.
Error Handling Strategy:
OtpService
catchesAxiosError
s, logs details, and throws standardized errors.Create the filter in
src/common/filters/http-exception.filter.ts
:main.ts
:Logging:
Logger
(@nestjs/common
).pino
withnestjs-pino
for JSON logs suitable for aggregation tools (Datadog, Splunk, ELK).Retry Mechanisms:
pinAttempts
). Our rate limiter prevents API abuse.Database Schema and Data Layer
For this specific guide focusing solely on Infobip interaction, a dedicated database for managing the OTP state itself is not required, as Infobip handles the
pinId
lifecycle (expiration, attempts).However, in a real-world application, you would integrate this OTP service with a user management system, likely requiring a database for:
pinId
to Context: This is crucial. When an OTP is sent (e.g., during login), the application often needs to temporarily store the receivedpinId
associated with the specific user or session attempting the action. This ensures that when the user submits the OTP for verification, the application verifies it against the correct context (e.g., the pending login attempt for that user). This temporary storage might happen in user sessions, a Redis cache, or a database table with short TTLs.@nestjs/throttler
provides IP-based limiting, storing per-user OTP request counts in a database enables more granular control.Example Database Integration Pattern:
When integrating with a user database (using TypeORM, Prisma, or similar):
id
,email
,phoneNumber
,isPhoneVerified
.pinId
temporarily (e.g., in Redis with 10-minute expiration) keyed by user ID or session ID.pinId
via Infobip → StorepinId
in cache linked to user sessionpinId
from cache → Verify with Infobip → Update user status if successfulThis guide intentionally omits database setup to focus on the Infobip integration. Refer to the NestJS documentation for TypeORM or Prisma integration guides.