Frequently Asked Questions
Use the Plivo Node.js SDK within a NestJS service to send SMS messages containing one-time passwords (OTPs). The `@plivo/rest-client` library provides methods for sending SMS messages given a Plivo phone number, recipient's number, and the OTP.
Plivo is a cloud communications platform used to send the actual SMS messages containing the OTPs. The provided code example uses the Plivo Node.js helper library to interact with the Plivo API for sending SMS messages reliably.
NestJS provides a structured, scalable framework for building server-side applications in Node.js. Its features like dependency injection and modules make it well-suited for complex tasks like two-factor authentication (2FA) implementation using OTP, as shown in the tutorial.
Rate limiting is crucial in production to prevent abuse and protect your application from excessive requests. This article recommends using the `@nestjs/throttler` module in your NestJS application to implement rate limiting, especially for endpoints like OTP sending, as shown in the example code with the `Throttle` decorator.
Yes, the article suggests using a cache like Redis or Memcached for OTP storage in production, though the example utilizes an in-memory store (`Map`) for simplicity. Integrate Redis by installing `@nestjs/cache-manager` and including it within the modules using Redis for OTP storage and retrieval.
Sign up for a Plivo account, obtain API keys (Auth ID and Auth Token), and rent a Plivo phone number capable of sending SMS. Configure these credentials within your NestJS project, making sure to keep your Auth Token secret (e.g., within a `.env` file).
The `speakeasy` library is used for generating time-based one-time passwords (TOTP) and HMAC-based one-time passwords (HOTP). The example code utilizes it within a NestJS service to generate a unique OTP code to send to a user for 2FA.
Store your Plivo API keys (Auth ID and Auth Token) in a `.env` file within your project, as shown in the guide. Ensure this file is added to your `.gitignore` to prevent it from being committed to version control and exposed publicly.
OTPs have an expiry time to limit their validity, enhancing security. In the example, the `OTP_EXPIRY_SECONDS` environment variable (with a default of 300 seconds/5 minutes) sets how long each OTP is valid before it must be regenerated by the user.
The Plivo Node.js library will throw errors for issues like invalid credentials or network problems. Use a `try-catch` block within your NestJS service to handle these exceptions, log the errors for debugging, and return appropriate responses to the client, typically a generic server error message to avoid exposing sensitive details.
Create a NestJS service method (and corresponding controller endpoint) to receive the user's phone number and OTP. The example includes a dedicated `AuthService` for verification with the `verifyOtp` method, comparing the received OTP against the stored, unexpired OTP to verify user authentication.
The tutorial uses `speakeasy.hotp` in combination with a secret key, the user's phone number, and the current timestamp to generate a one-time password (OTP). This OTP is then sent to the user via SMS using the Plivo API.
Two-factor authentication (2FA), such as using SMS OTP, adds an extra layer of security, protecting user accounts even if passwords are compromised. If an attacker gains access to a user's password, they still need the temporary OTP to access the account.
You'll need Node.js and npm/yarn installed, a Plivo account with API keys and a rented phone number, and a basic understanding of NestJS, TypeScript, and a suitable code editor. The guide also suggests familiarity with making API requests using tools like `curl` or Postman.
Implement Production-Ready SMS OTP/2FA in NestJS with Plivo
This guide provides a complete walkthrough for implementing a robust SMS-based One-Time Password (OTP) — also known as Two-Factor Authentication (2FA) — system within a NestJS application using the Plivo messaging platform. We will build secure, production-grade API endpoints for sending OTPs via SMS and verifying them.
Implementing OTP/2FA significantly enhances application security by adding a second layer of verification beyond traditional passwords. This protects user accounts from unauthorized access even if passwords are compromised.
Technologies Used:
System Architecture:
The basic flow for OTP verification involves these steps:
Final Outcome & Prerequisites:
By the end of this guide, you will have:
AuthModule
) responsible for OTP logic.POST /auth/send-otp
: Accepts a phone number, generates an OTP, stores it temporarily, and sends it via Plivo SMS.POST /auth/verify-otp
: Accepts a phone number and the user-provided OTP, validating it against the stored value and expiry.Prerequisites:
curl
or Postman).1. Setting Up the Project
Let's initialize our NestJS project and install the necessary dependencies.
Install NestJS CLI: If you don't have it, install it globally.
Create New NestJS Project:
Choose your preferred package manager (npm or yarn).
Navigate to Project Directory:
Install Dependencies:
plivo
: The official Plivo Node.js SDK.@nestjs/config
: For managing environment variables.class-validator
,class-transformer
: For validating incoming request bodies (DTOs).speakeasy
: A reliable library for generating OTP codes.@types/speakeasy
: TypeScript definitions forspeakeasy
.@nestjs/throttler
: For implementing rate limiting (optional but recommended).bcrypt
,@types/bcrypt
: For hashing secrets (used conceptually in DB section).libphonenumber-js
: For robust phone number handling (optional).async-retry
,@types/async-retry
: For API call retry logic (optional).Project Structure: NestJS CLI creates a standard structure. We'll primarily work within the
src
directory, creating modules, controllers, and services.Environment Variables (
.env
): Create a.env
file in the project root. Never commit this file to version control. Add your Plivo credentials and configuration secrets:PLIVO_AUTH_ID
/PLIVO_AUTH_TOKEN
: Your unique Plivo API credentials. Find these in your Plivo Console under ""API Keys & Credentials"".PLIVO_SENDER_ID
: A Plivo phone number you have rented that is enabled for sending SMS messages. Ensure it's in E.164 format (e.g.,+14155552671
). You can rent numbers in the Plivo console under ""Phone Numbers"".OTP_SECRET
: A strong, unique secret string used byspeakeasy
for HMAC-based OTP generation (HOTP). Generate a secure random string for this.OTP_EXPIRY_SECONDS
: How long an OTP remains valid after generation.OTP_DIGITS
: The length of the OTP code sent to the user.PORT
: The port your NestJS application will run on.THROTTLE_TTL
/THROTTLE_LIMIT
: Configuration for the rate limiter.Configure
ConfigModule
: Import and configure theConfigModule
in your main application module (src/app.module.ts
) to load the.env
file. Make it global so you don't need to import it into every module.ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' })
: Loads variables from.env
and makesConfigService
injectable anywhere.ThrottlerModule.forRoot([...])
: Configures the rate limiter using environment variables. We convert TTL to milliseconds as required by the module.APP_GUARD
: Applies theThrottlerGuard
to all routes in the application.Enable Validation Pipe: Globally enable the
ValidationPipe
insrc/main.ts
to automatically validate incoming DTOs based onclass-validator
decorators.2. Implementing Core Functionality (Auth Module)
We'll encapsulate the OTP logic within an
AuthModule
.Generate Auth Module, Controller, and Service: Use the NestJS CLI:
This creates
src/auth/auth.module.ts
,src/auth/auth.controller.ts
, andsrc/auth/auth.service.ts
. The CLI automatically updatesauth.module.ts
to declare the controller and provider, and importsAuthModule
intoapp.module.ts
(we already did this manually, but it's good practice).OTP Storage (In-Memory): For simplicity, we'll store OTPs in memory within the
AuthService
. This is suitable for demonstrations or low-traffic scenarios with short expiry times. For production, consider using a cache like Redis or Memcached via@nestjs/cache-manager
or storing them in a database.Auth Service (
AuthService
): This service handles OTP generation, storage, validation, and interacts with Plivo.ConfigService
, retrieves necessary config values, validates them, and initializes the Plivo client.otpStore
: A simpleMap
to storeOtpData
keyed by phone number.sendOtp
:speakeasy.hotp
with a custom approach (timestamp as counter, phone in secret).otpStore
.this.plivoClient.messages.create
to send the SMS. Note thesrc
,dst
, andtext
parameters.setTimeout
to clean up the OTP from the store after it expires (a simple cleanup strategy).verifyOtp
:otpCode
with theexpectedCode
.HttpException
subclasses (NotFoundException
,UnauthorizedException
) for different failure scenarios, which NestJS automatically maps to HTTP status codes (404, 401).3. Building the API Layer (Controller and DTOs)
Now, let's define the API endpoints and the Data Transfer Objects (DTOs) for request validation.
Create DTOs: Create files for request body definitions.
class-validator
(@IsNotEmpty
,@IsString
,@IsPhoneNumber
,@Length
) to define validation rules.@IsPhoneNumber(null, ...)
attempts to validate against E.164 format when no region code is provided. For robust validation, considerlibphonenumber-js
.ValidationPipe
(enabled globally inmain.ts
) will automatically use these rules.Implement Controller (
AuthController
): Define the routes and link them to the service methods.@Controller('auth')
: Sets the base route for all methods in this controller to/auth
.@Post('send-otp')
/@Post('verify-otp')
: Defines POST endpoints at/auth/send-otp
and/auth/verify-otp
.@Body()
: Injects the validated request body (automatically transformed into the DTO instance byValidationPipe
).@HttpCode(HttpStatus.OK)
: Sets the default success status code to 200.@Throttle(...)
: Applies specific rate limits to these endpoints, overriding the global default set inapp.module.ts
. Adjust limits as needed.AuthService
methods and return responses. Errors thrown by the service (likeUnauthorizedException
) are handled automatically by NestJS.API Testing Examples (
curl
):Send OTP:
Expected JSON Response (Success - 200 OK):
Expected JSON Response (Validation Error - 400 Bad Request):
Verify OTP: (Assuming you received an OTP, e.g.,
123456
)Expected JSON Response (Success - 200 OK):
Expected JSON Response (Incorrect/Expired OTP - 401 Unauthorized):
4. Integrating with Plivo (Deep Dive)
We've already initialized the client, but let's refine the integration details.
Obtaining Credentials:
.env
file.Getting a Sender ID (Plivo Number):
+14155552671
) and set it asPLIVO_SENDER_ID
in your.env
file.Plivo Client Initialization (Recap): The client is initialized in the
AuthService
constructor using credentials fromConfigService
:Sending the SMS (Recap): The core Plivo call happens in
sendOtp
:Fallback Mechanisms (Considerations):
client.calls.create
).AuthService
for transient network errors when calling the Plivo API itself, using libraries likeasync-retry
.5. Error Handling, Logging, and Retries
Robust error handling and logging are crucial for production systems.
Error Handling Strategy:
HttpException
subclasses (BadRequestException
,UnauthorizedException
,NotFoundException
,InternalServerErrorException
) from theAuthService
for predictable client errors.InternalServerErrorException
to avoid leaking sensitive details.plivo
Node.js library throws errors for API issues (e.g., invalid credentials, insufficient funds). Catch these in theAuthService
, log details, and return a generic server error to the client. Consult Plivo API Error Codes for specific meanings.Logging:
Logger
service.log
: General information (e.g., ""OTP Sent to X"", ""Verification successful for X"").debug
: Detailed internal state (e.g., ""Stored OTP for X: code, expiresAt""). Enable only in development or for troubleshooting.warn
: Potential issues or expected errors (e.g., ""Invalid OTP provided"", ""Max attempts reached"").error
: Critical failures (e.g., ""Failed to send via Plivo"", ""Config missing""). Include error messages and stack traces.AuthService
). Consider JSON logging libraries (likepino
) for easier parsing by log aggregation systems (e.g., Datadog, Splunk, ELK stack).Failed to send OTP via Plivo to [phoneNumber]
or check Plivo's response logs for specific error messages ormessageUuid
. If users report invalid OTPs, check logs forInvalid OTP provided for [phoneNumber]
orOTP expired for [phoneNumber]
.Retry Mechanisms:
async-retry
or similar if you frequently encounter transient network errors connecting to Plivo. Wrap thethis.plivoClient.messages.create
call.