Frequently Asked Questions
Set up a Node.js project with Express, install the Plivo Node.js SDK, configure Redis for OTP storage, and implement API endpoints for requesting and verifying OTPs. Use environment variables to securely manage Plivo credentials and other sensitive information. Follow the detailed step-by-step instructions provided in the guide for a complete implementation.
Redis acts as a fast, in-memory data store for temporarily storing OTPs. Its ability to set expiration times (TTL) on keys is crucial for automatically deleting expired OTPs. The system uses the phone number as a key and the generated OTP as the value in Redis.
Plivo is a cloud communication platform offering a reliable and easy-to-use SMS API. The official Plivo Node.js SDK simplifies integration with your Node.js application and enables global SMS delivery for OTP verification.
Rate limiting is essential for all production OTP systems to prevent abuse and brute-force attacks. Implement rate limiting middleware, like express-rate-limit, to restrict the number of OTP requests and verifications from a single IP address within a specific time window (e.g., 10 requests per 5 minutes).
Yes, you can customize both the length and expiry time. These are set using the OTP_LENGTH and OTP_EXPIRY_SECONDS environment variables. The code provides default values of 6 digits for length and 300 seconds (5 minutes) for expiry.
The provided code uses `crypto.randomInt()` for generating cryptographically secure random numeric OTPs. This is the preferred method for production environments due to its higher security compared to using `Math.random()`. A fallback mechanism using Math.random is also included in case of errors.
You need Node.js v14 or later, npm or yarn, a Plivo account with an SMS-enabled number and sufficient credits, a running Redis instance (local or cloud), basic understanding of Node.js and Express, and a tool for making API requests (Postman, Insomnia, etc.).
Storing sensitive credentials directly in your code is a security risk. Using a .env file and the dotenv library keeps these values separate from your source code. Never commit your .env file to version control.
The submitted OTP is compared to the value stored in Redis associated with the user's phone number. If the codes match and haven't expired, the verification is successful, and the OTP in Redis is immediately deleted to prevent reuse.
The client requests an OTP, the server generates and stores it in Redis, and Plivo sends it via SMS. The client submits the OTP and the server verifies it against the Redis value. Node.js/Express, Redis, and Plivo are core system components.
Create directories for source code (src), initialize npm, and create files for application logic (app.js, config.js, otp.service.js, routes.js), environment variables (.env), and .gitignore. This structure helps organize the project effectively.
Implement comprehensive error handling in both service and API layers, log errors with context, and utilize appropriate HTTP status codes (4xx/5xx for client and server errors respectively). Use specific error messages without revealing sensitive data.
Use a dedicated logging library like Winston or Pino for structured logging (JSON format), different log levels (info, warn, error), and support for multiple transports (e.g., logging to files, external services).
Use a retry mechanism within your sendOtpSms function in otp.service.js to handle transient network errors between your server and Plivo. Include a delay, potentially with exponential backoff, between retry attempts.
Express-validator sanitizes and validates user inputs in the API layer. This prevents invalid data from causing issues in the service layer and enhances security by mitigating common vulnerabilities like injection attacks.
Implement OTP Verification with Node.js, Express, and Plivo
Two-factor authentication (2FA) adds a critical layer of security to applications by requiring users to provide a second form of verification beyond just a password. One of the most common and user-friendly methods for 2FA is sending a one-time password (OTP) via SMS.
This guide provides a complete walkthrough for building a production-ready OTP verification system using Node.js, the Express framework, Redis for temporary OTP storage, and the Plivo communications platform for sending SMS messages. We will cover everything from project setup and core logic to security best practices, error handling, deployment, and testing.
Project Overview and Goals
Goal: To build a secure backend API service that handles OTP generation, delivery via Plivo SMS, and verification for user phone numbers within a Node.js Express application.
Problem Solved: This implementation addresses the need for enhanced application security by adding an SMS-based 2FA layer, protecting user accounts even if passwords are compromised. It provides a reliable way to verify user phone numbers during registration or critical actions.
Technologies:
.env
file intoprocess.env
, keeping sensitive credentials out of the codebase.System Architecture:
Prerequisites:
curl
, Postman, or Insomnia).Final Outcome: A secure, robust, and testable Node.js Express API service with endpoints to request and verify SMS OTPs using Plivo.
1. Setting up the Project
Let's start by creating the project directory, initializing Node.js, and installing the necessary dependencies.
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
Initialize Node.js Project: This creates a
package.json
file to manage project dependencies and scripts.Install Dependencies: Install Express, the Plivo SDK, the Redis client, dotenv for environment variables, and middleware for rate limiting and validation.
Install Development Dependencies (Optional but Recommended):
nodemon
automatically restarts the server during development when file changes are detected.Create Project Structure: Set up a basic structure for clarity.
src/app.js
: Main application file (Express server setup).src/config.js
: Application configuration (loaded from environment variables).src/otp.service.js
: Business logic for OTP generation, storage, and Plivo interaction.src/routes.js
: Defines the API endpoints..env
: Stores sensitive credentials (Plivo Auth ID, Token, Number, Redis URL). Never commit this file..gitignore
: Specifies intentionally untracked files that Git should ignore (likenode_modules
and.env
).Configure
.gitignore
: Add the following lines to your.gitignore
file:Configure Environment Variables (
.env
): Open the.env
file and add your Plivo credentials, Plivo phone number, and Redis connection details. Replace the placeholders with your actual values. For production Redis instances requiring authentication, use the formatredis://:yourpassword@your_redis_host:6379
.PLIVO_AUTH_ID
/PLIVO_AUTH_TOKEN
: Your API credentials from the Plivo dashboard. Essential for authenticating API requests.PLIVO_PHONE_NUMBER
: The SMS-enabled number purchased from Plivo, used as the sender ID for OTP messages. Must be in E.164 format (e.g.,+12125551234
).REDIS_URL
: Connection string for your Redis instance. Used by the Redis client to connect. Ensure it includes credentials if needed for production.PORT
: The port your Express application will listen on.OTP_LENGTH
: The desired number of digits for the OTP.OTP_EXPIRY_SECONDS
: How long (in seconds) the OTP remains valid in Redis.Load Environment Variables (
src/config.js
): Configure the application to load these variables usingdotenv
.dotenv
? It keeps sensitive information like API keys out of your source code, which is crucial for security, especially when using version control like Git.Add Run Scripts to
package.json
: Modify thescripts
section in yourpackage.json
. Note that the defaulttest
script is a placeholder; for production, you should implement actual unit and integration tests.npm start
: Runs the application using Node.npm run dev
: Runs the application usingnodemon
, which watches for file changes and restarts the server automatically – ideal for development.npm test
: Placeholder script. Real tests using frameworks like Jest or Mocha are recommended.Now the basic project structure, dependencies, and configuration loading are in place.
2. Implementing Core Functionality
This section focuses on the heart of the OTP system: generating, storing, sending, and verifying OTPs. We'll implement this logic within
src/otp.service.js
.Initialize Plivo and Redis Clients: We need instances of the Plivo client to send SMS and the Redis client to store/retrieve OTPs.
EX
), automatically cleaning up old OTPs.async/await
? Plivo and Redis operations are asynchronous (network I/O).async/await
provides a cleaner way to handle promises compared to.then()
chains.try...catch
blocks are used. Errors are logged, and custom error messages are thrown to be handled by the API layer. Redis connection readiness is checked.crypto.randomInt
for generating a cryptographically stronger pseudo-random numeric OTP, which is preferred for production overMath.random
.sendOtpSms
function usesplivoClient.messages.create
with the sender number from config, the recipient number, and the message text including the OTP.verifyOtp
fetches the OTP from Redis using the phone number key. It checks if the OTP exists (not expired) and if it matches the submitted code. It deletes the key on successful verification (best practice).3. Building the API Layer
Now, let's create the Express routes that expose our OTP functionality as API endpoints.
Define Routes (
src/routes.js
): We need two main endpoints: one to request an OTP and one to verify it. We'll also useexpress-validator
for input validation.express-validator
checks ifphoneNumber
looks like a valid mobile number (usingisMobilePhone
) and if theotp
has the correct length and is numeric. This prevents invalid data from reaching the service layer. ThehandleValidationErrors
middleware centralizes error reporting for validation failures.otp.service.js
. It wraps calls intry...catch
to handle errors gracefully and return appropriate HTTP status codes (e.g., 200 for success, 400 for bad input/invalid OTP, 500/502 for server/provider errors)..trim()
and.customSanitizer
helps clean up input before validation and processing./request
endpoint has been removed.Integrate Routes into Express App (
src/app.js
): Set up the Express application, include necessary middleware (JSON body parsing, rate limiting), and mount the OTP routes.express.json()
: Middleware to parse incoming JSON request bodies, makingreq.body
available.express-rate-limit
is crucial to prevent brute-force attacks or abuse of the OTP system./api/otp
.app.listen
return value is assigned toserver
. TheredisClient
is imported fromotp.service.js
andredisClient.quit()
is called within the shutdown handler, ensuring resources are released properly. Checks are added to ensure the client exists and is ready before attempting to quit.4. Integrating with Plivo (Configuration Recap)
We've already initialized the Plivo client using credentials from
.env
. Let's recap the crucial configuration steps:.env
file. Never commit.env
to version control.Fallback Mechanisms: While this guide focuses on SMS OTP, Plivo supports Voice calls for OTP delivery. Implementing a fallback (e.g., if SMS fails or user requests it) would typically involve:
plivoClient.calls.create
to initiate a text-to-speech call reading the OTP.<Speak>
element) to read the code. Alternatively, Plivo's visual workflow builder, PHLO, can orchestrate this.5. Error Handling, Logging, and Retry Mechanisms
Robust error handling and logging are essential for production systems.
Consistent Error Strategy:
otp.service.js
): Catches specific errors, logs them, and throws standardized errors.routes.js
): Catches service errors, logs them, sends appropriate HTTP status codes (4xx/5xx) and user-friendly JSON messages.app.js
): Includes a final middleware for unhandled exceptions.Logging:
console.log/warn/error
.winston
orpino
for structured logging (JSON), log levels, and multiple transports (file, external services).Retry Mechanisms:
otp.service.js
.