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.
Node.js OTP Verification: Build SMS 2FA with Express, Plivo & Redis
Two-factor authentication (2FA) adds a critical security layer 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. You'll implement everything from project setup and core logic to security best practices, error handling, deployment, and testing.
Project Overview and Goals
Goal: 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. You'll have a reliable way to verify user phone numbers during registration or critical actions.
Security Note: SMS-based OTP is widely used and provides meaningful security improvements over password-only authentication. However, NIST SP 800-63B guidelines (2024) discourage SMS as an out-of-band authenticator for high-security applications due to vulnerabilities in the SS7 telecom network and risks like SIM swapping. For applications requiring higher security assurance levels (banking, healthcare, government), consider implementing stronger alternatives such as:
SMS OTP remains appropriate for moderate-security scenarios like e-commerce, social media, and general account verification where ease of use and universal accessibility are priorities. This guide demonstrates production-ready implementation patterns applicable to any OTP delivery method.
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. Set Up the Project
Create the project directory, initialize Node.js, and install 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: Create 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): Install
nodemon
to automatically restart 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. Implement OTP Generation and Storage with Redis
This section focuses on the heart of the OTP system: generating, storing, sending, and verifying OTPs. You'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. Build Express API Endpoints for OTP Request and Verification
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. Configure Plivo SMS API Integration
You've already initialized the Plivo client using credentials from
.env
. Here's a recap of 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. Implement Error Handling, Logging, and Retry Logic
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
.6. Security Best Practices
Rate Limiting and Brute Force Prevention
The current implementation uses IP-based rate limiting via
express-rate-limit
, which is essential but insufficient on its own. Production systems should implement multiple layers of protection:Critical Security Measures:
Account Lockout: Implement temporary account lockout after excessive failed verification attempts (e.g., 5–10 attempts within 30 minutes). This is more effective than rate limiting alone.
CAPTCHA After Failed Attempts: Add CAPTCHA verification after 2–3 failed OTP verification attempts to prevent automated brute force attacks.
OTP Attempt Limit: Invalidate the OTP after a fixed number of verification attempts (e.g., 3–5 attempts) regardless of expiry time. Update
verifyOtp
function:Shorter OTP Expiry: Consider reducing OTP validity from 5 minutes to 2–3 minutes (120–180 seconds) to reduce the brute force window. Update
.env
:OTP_EXPIRY_SECONDS=120
Request Throttling: Limit OTP generation requests per phone number (e.g., maximum 3 OTP requests per hour per phone number) to prevent SMS flooding attacks and cost abuse.
Additional Security Recommendations
Constant-Time Comparison: While string comparison for numeric OTPs is generally safe, for extra security, use constant-time comparison to prevent timing attacks:
OTP Length: Use 6-digit OTPs minimum. Longer OTPs (8 digits) provide stronger security but may impact user experience.
Secure Redis Connection: Always use TLS/SSL for Redis connections in production (
rediss://
protocol). Never expose Redis to the public internet without authentication.Monitor Anomalies: Log and alert on suspicious patterns like: repeated OTP requests from the same IP, high verification failure rates, or requests for non-existent phone numbers.
Phone Number Verification: Validate phone numbers are properly formatted (E.164) and optionally verify they exist using phone validation APIs before sending OTPs to prevent SMS cost abuse.
Redis Client v5 Specific Considerations
Critical: Node Redis v5 requires explicit error handling. Always attach error listeners to prevent process crashes:
Connection Management: Unlike v3, Redis client v5 requires calling
.connect()
explicitly. The connection is not automatic. Ensure proper connection lifecycle management in production.Frequently Asked Questions About OTP Verification
How do I implement OTP verification in Node.js?
Implement OTP verification in Node.js by: (1) generating a random numeric code using
crypto.randomInt
, (2) storing it temporarily in Redis with expiration, (3) sending it via SMS using a provider like Plivo, and (4) verifying the user-submitted code against the stored value. Use Express.js to create API endpoints for requesting and verifying OTPs.What is the recommended OTP expiry time?
The recommended OTP expiry time is 2–3 minutes (120–180 seconds) for security. While 5 minutes is common, shorter expiry windows reduce the brute force attack surface. Balance security with user experience – users need enough time to receive and enter the code.
How secure is SMS-based two-factor authentication?
SMS-based 2FA is moderately secure and significantly better than password-only authentication. However, NIST SP 800-63B (2024) discourages SMS for high-security applications due to SS7 network vulnerabilities and SIM swapping risks. For banking, healthcare, or government applications, use TOTP authenticator apps or FIDO2 hardware keys instead.
How do I prevent OTP brute force attacks?
Prevent OTP brute force attacks by implementing: (1) per-user rate limiting (not just IP-based), (2) account lockout after 5–10 failed attempts, (3) OTP invalidation after 3–5 verification attempts, (4) CAPTCHA after failed attempts, (5) shorter expiry times, and (6) request throttling per phone number. Use Redis to track attempts across sessions.
What is the best length for OTP codes?
The best length for OTP codes is 6 digits minimum. Six-digit OTPs provide 1 million possible combinations and balance security with user convenience. Eight-digit OTPs (100 million combinations) offer stronger security but may frustrate users. Avoid 4-digit OTPs – they're too easily brute-forced (10,000 combinations).
Why use Redis for OTP storage instead of a database?
Use Redis for OTP storage because: (1) in-memory operations are extremely fast (microseconds vs milliseconds), (2) built-in TTL (time-to-live) automatically expires old OTPs without cleanup jobs, (3) atomic operations prevent race conditions, and (4) it's designed for temporary data. Traditional databases add unnecessary overhead for short-lived data.
How do I integrate Plivo SMS API with Node.js?
Integrate Plivo SMS API by: (1) installing the official
plivo
npm package, (2) initializing the client with your Auth ID and Auth Token, (3) callingplivoClient.messages.create()
with sender number (E.164 format), recipient, and message text. Store credentials in environment variables usingdotenv
. Plivo SDK version 4.74.0 (December 2024) works with Node.js v18+.What Node.js version should I use for production OTP systems?
Use Node.js v22.x LTS for production OTP systems (Active LTS until October 2025, maintained until April 2027). Minimum requirement is Node.js v18.x. LTS (Long Term Support) versions receive security updates and are stable for production. Avoid odd-numbered versions (v19, v21, v23) – they're for testing only.
How do I handle SMS delivery failures?
Handle SMS delivery failures by: (1) implementing retry logic with exponential backoff (2–3 attempts), (2) logging failures with phone number prefixes for debugging, (3) providing fallback options like voice calls, (4) validating phone numbers before sending, and (5) monitoring Plivo's delivery receipts (DLRs). Return user-friendly error messages without exposing system details.
What's the difference between OTP and TOTP?
OTP (One-Time Password) is a single-use code sent via SMS or email that's valid for a fixed time. TOTP (Time-based One-Time Password) is generated by an algorithm using a shared secret and current time, displayed in authenticator apps like Google Authenticator. TOTP is more secure (no SMS interception risk) but requires users to install an app. Use TOTP for high-security applications.