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/axioswrapper).@nestjs/configwrapper).System Architecture:
The interaction flow is as follows:
POSTrequest to the NestJS OTP API endpoint (e.g.,/otp/send) containing the user's phone number.pinId. The NestJS application might temporarily store thispinIdlinked to the user's session or action.POSTrequest to the NestJS OTP API endpoint (e.g.,/otp/verify) containing thepinIdreceived earlier and the OTP code entered by the user.pinIdand 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
.envfile 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:.envfile. Treat the API Key like a password.INFOBIP_APP_ID&INFOBIP_MESSAGE_ID:POSTrequest to{INFOBIP_BASE_URL}/2fa/1/applications.applicationId. Copy this value intoINFOBIP_APP_IDin your.envfile.curlor Postman to send aPOSTrequest to{INFOBIP_BASE_URL}/2fa/1/applications/{INFOBIP_APP_ID}/messages. Replace{INFOBIP_APP_ID}with the ID you just received.senderIds depending on the country and regulations.InfoSMSis often a default shared sender. Check Infobip documentation for details.messageId. Copy this value intoINFOBIP_MESSAGE_IDin your.envfile.Step 4: Load Environment Variables Using ConfigModule
Update
src/app.module.tsto load and validate your environment variables.Step 5: Add
.envto.gitignoreEnsure your
.gitignorefile includes.env:Create a
.env.examplefile 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
HttpModuleavailable within yourOtpModuleto 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/pinendpoint, sets theAuthorizationheader using the API key, makes the POST request, and returns thepinIdfrom the response.verifyOtp: Constructs the URL (/2fa/2/pin/{pinId}/verify) and payload, makes the POST request, and checks theverifiedfield in the response. Returnstrueorfalse.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
SendOtpDtoinsrc/otp/dto/send-otp.dto.ts:Create
VerifyOtpDtoinsrc/otp/dto/verify-otp.dto.ts:SendOtpDto: Requires aphoneNumberfield 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:
POSTendpoints:/otp/sendand/otp/verify.@UsePipes(new ValidationPipe(...))automatically validates incoming request bodies against the DTOs (SendOtpDto,VerifyOtpDto).whitelist: truestrips 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 fewersendrequests thanverifyrequests per time window.OtpService.pinIdon send,verifiedboolean on verify).@HttpCode(HttpStatus.OK)ensures a 200 status code is returned even if verification fails (as the API call itself succeeded). Theverifiedflag in the response body indicates the outcome.Testing with
curl:Start the application:
npm run start:devoryarn start:devSend OTP:
Verify OTP: Use the
pinIdfrom 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
curlexamples if needed.Configuration Steps:
.envfile asINFOBIP_BASE_URLandINFOBIP_API_KEY.POST /2fa/1/applications) to create a 2FA application.pinAttempts,pinTimeToLive, etc.applicationIdin.envasINFOBIP_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.messageIdin.envasINFOBIP_MESSAGE_ID.Secure Handling of Credentials:
.envfile..gitignore: The.envfile is explicitly excluded from Git commits. Ensure.env.exampleis committed instead.ConfigServiceis used to load these variables securely at runtime..envfiles. 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:
OtpServicecatches 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:
OtpServicecatchesAxiosErrors, logs details, and throws standardized errors.Create the filter in
src/common/filters/http-exception.filter.ts:main.ts:Logging:
Logger(@nestjs/common).pinowithnestjs-pinofor 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
pinIdlifecycle (expiration, attempts).However, in a real-world application, you would integrate this OTP service with a user management system, likely requiring a database for:
pinIdto Context: This is crucial. When an OTP is sent (e.g., during login), the application often needs to temporarily store the receivedpinIdassociated 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/throttlerprovides 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.pinIdtemporarily (e.g., in Redis with 10-minute expiration) keyed by user ID or session ID.pinIdvia Infobip → StorepinIdin cache linked to user sessionpinIdfrom 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.