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.
This guide provides a complete walkthrough for implementing a robust One-Time Password (OTP) system, often used for Two-Factor Authentication (2FA), within a Node.js application using the NestJS framework and the Infobip 2FA API. We'll cover everything from initial project setup to deployment considerations, ensuring you have a production-ready solution.
We aim to build a secure, reliable, and scalable OTP service that can be easily integrated into various user authentication flows like registration, login, or sensitive action confirmation. By the end, you'll have a functional NestJS API capable of sending OTPs via SMS and verifying user-submitted codes, backed by Infobip's global communication infrastructure.
Project Overview and Goals
What We're Building:
A NestJS-based microservice or module responsible for:
Problem Solved:
This implementation addresses the need for enhanced application security by adding a second factor of authentication. It helps prevent unauthorized access even if user passwords are compromised, 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
for testing the API endpoints.1. Setting up the project
Let's 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 the project root directory. This file will store sensitive information like API keys and configuration IDs. Never commit this file to version control.How to get Infobip Credentials:
INFOBIP_BASE_URL
&INFOBIP_API_KEY
:.env
file. Treat the API Key like a password.INFOBIP_APP_ID
&INFOBIP_MESSAGE_ID
:curl
or Postman to send aPOST
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
Modify
src/app.module.ts
to load and validate the environment variables.Step 5: Add
.env
to.gitignore
Ensure your
.gitignore
file includes.env
:It's also a best practice to create a
.env.example
file listing the required variables (without their values) and commit it to your repository. This helps collaborators set up their environment.2. Implementing core functionality
We'll create a dedicated module (
OtpModule
) containing a service (OtpService
) to handle the interaction logic with the Infobip API.Step 1: Generate the OTP Module and Service
Use the NestJS CLI:
Step 2: Configure HttpModule for Axios
We need to make the
HttpModule
available within ourOtpModule
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. A note reminds the developer to check Infobip's documentation for current API paths.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).3. Building a complete API layer
Now, let's 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 the
SendOtpDto
insrc/otp/dto/send-otp.dto.ts
:Create the
VerifyOtpDto
insrc/otp/dto/verify-otp.dto.ts
:SendOtpDto
: Requires aphoneNumber
field, validates it's a non-empty string and attempts basic phone number validation (ensure E.164 format like+14155552671
as Infobip usually requires this).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.4. Integrating with necessary third-party services (Infobip)
This section summarizes the Infobip-specific setup. Refer to Section 1, 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.5. Implementing proper 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.6. Creating a 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 u