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 implementing OTP verification is sending a one-time password (OTP) via SMS to verify phone numbers during user authentication.
This comprehensive guide shows you how to implement SMS OTP verification in Node.js using the Express framework, Redis for temporary OTP storage, and the Plivo SMS API for reliable message delivery. You'll learn everything from project setup and core logic to security best practices, error handling, rate limiting, and production deployment considerations for building a secure phone number verification system.
What You'll Build: Production-Ready OTP Authentication System
Project Goal: Build a secure backend API service that handles OTP generation, SMS delivery via Plivo, and verification for user phone numbers within a Node.js Express application.
Problem This Solves: 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, login, or critical actions like password resets and payment confirmations.
When to Use SMS OTP vs Other 2FA Methods
SMS-based OTP verification 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.
Technology Stack for SMS OTP Verification:
.envfile intoprocess.env, keeping sensitive credentials out of the codebase.System Architecture:
Prerequisites for Building OTP Verification:
curl, Postman, or Insomnia) for testing your OTP endpoints.Final Outcome: A secure, robust, and testable Node.js Express API service with endpoints to request and verify SMS OTPs using Plivo for phone number authentication.
1. Set Up Your Node.js OTP Verification Project
Create the project directory, initialize Node.js, and install the necessary dependencies for building SMS OTP authentication.
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.jsonfile 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
nodemonto 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_modulesand.env).Configure
.gitignore: Add the following lines to your.gitignorefile:Configure Environment Variables (
.env): Open the.envfile 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 thescriptssection in yourpackage.json. Note that the defaulttestscript 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/awaitprovides a cleaner way to handle promises compared to.then()chains.try...catchblocks are used. Errors are logged, and custom error messages are thrown to be handled by the API layer. Redis connection readiness is checked.crypto.randomIntfor generating a cryptographically stronger pseudo-random numeric OTP, which is preferred for production overMath.random.sendOtpSmsfunction usesplivoClient.messages.createwith the sender number from config, the recipient number, and the message text including the OTP.verifyOtpfetches 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 for phone number verification.
Define Routes (
src/routes.js): We need two main endpoints: one to request an OTP and one to verify it. We'll also useexpress-validatorfor input validation.express-validatorchecks ifphoneNumberlooks like a valid mobile number (usingisMobilePhone) and if theotphas the correct length and is numeric. This prevents invalid data from reaching the service layer. ThehandleValidationErrorsmiddleware centralizes error reporting for validation failures.otp.service.js. It wraps calls intry...catchto 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.customSanitizerhelps clean up input before validation and processing./requestendpoint 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.bodyavailable.express-rate-limitis crucial to prevent brute-force attacks or abuse of the OTP system./api/otp.app.listenreturn value is assigned toserver. TheredisClientis imported fromotp.service.jsandredisClient.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 for Phone Number Verification
You've already initialized the Plivo client using credentials from
.env. Here's a recap of the crucial configuration steps for integrating Plivo SMS API:.envfile. Never commit.envto 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.createto 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.This setup is beyond the scope of this API guide but is documented on Plivo's developer portal.
5. Implement Error Handling, Logging, and Retry Logic
Robust error handling and logging are essential for production OTP 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.winstonorpinofor structured logging (JSON), log levels, and multiple transports (file, external services).Retry Mechanisms:
Plivo SMS: Plivo handles some retries internally. For transient network errors between your server and Plivo, implement retries in
otp.service.js.Simple Retry Example:
Note: This is a basic example. Production retries often involve exponential backoff and jitter.
Redis: Redis operations are generally fast and reliable. Implement connection retry logic if needed, but monitor Redis health closely.
6. Security Best Practices for OTP Verification
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 OTP 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
verifyOtpfunction: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=120Request 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 in Node.js
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
plivonpm 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.