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
Learn how to implement SMS-based One-Time Password (OTP) authentication in NestJS using Plivo's messaging API. This comprehensive guide walks you through building a production-ready two-factor authentication (2FA) system with TypeScript, complete with security best practices, NIST SP 800-63B compliance, rate limiting, and error handling. You'll create secure API endpoints for generating and verifying OTP codes via SMS, protecting user accounts with an additional authentication layer beyond passwords.
This NestJS OTP authentication implementation significantly enhances application security by adding a second verification factor. Even if passwords are compromised, attackers cannot access accounts without the SMS-delivered one-time code.
⚠️ Security Notice: This guide uses
speakeasyfor OTP generation. As of 2024, the speakeasy library is marked as "NOT MAINTAINED" on npm and GitHub. For production applications, consider using actively maintained alternatives likeotplib(https://www.npmjs.com/package/otplib) which provides similar TOTP/HOTP functionality with continued security updates. Source: Speakeasy GitHub Repository (2024)NIST Compliance: SMS-based OTP is acceptable for NIST Authentication Assurance Levels 1 and 2 (AAL1/AAL2) but is not considered phishing-resistant. NIST SP 800-63B permits SMS OTPs when delivered over separate communication channels, though stronger alternatives (FIDO2, app-based authenticators) are recommended for higher security requirements. Source: NIST SP 800-63B Digital Identity Guidelines (2024)
What You'll Learn
This tutorial covers:
Technologies Used
plivonpm package, actively maintained) to send OTP messages reliably.otplibinstead for production.How SMS OTP Authentication Works
The basic flow for OTP verification involves these steps:
What You'll Build
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
curlor Postman).SMS OTP Security Best Practices
@nestjs/cache-managerwith TTL-based expiration. Database storage requires manual cleanup jobs.crypto.randomBytes(32).toString('base32')). Source: NIST SP 800-63B (2024)1. Setting Up Your NestJS 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 library for generating OTP codes. ⚠️ Note: Marked as NOT MAINTAINED as of 2024. This guide uses it for educational purposes, but considerotplibfor production. Source: Speakeasy GitHub (2024)@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
srcdirectory, creating modules, controllers, and services.Environment Variables (
.env): Create a.envfile 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 byspeakeasyfor 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 theConfigModulein your main application module (src/app.module.ts) to load the.envfile. Make it global so you don't need to import it into every module.ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }): Loads variables from.envand makesConfigServiceinjectable anywhere.ThrottlerModule.forRoot([...]): Configures the rate limiter using environment variables. We convert TTL to milliseconds as required by the module.APP_GUARD: Applies theThrottlerGuardto all routes in the application.Enable Validation Pipe: Globally enable the
ValidationPipeinsrc/main.tsto automatically validate incoming DTOs based onclass-validatordecorators.2. Implementing OTP Generation and Verification
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.tsto declare the controller and provider, and importsAuthModuleintoapp.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-manageror 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 simpleMapto storeOtpDatakeyed by phone number.sendOtp:speakeasy.hotpwith a custom approach (timestamp as counter, phone in secret).otpStore.this.plivoClient.messages.createto send the SMS. Note thesrc,dst, andtextparameters.setTimeoutto clean up the OTP from the store after it expires (a simple cleanup strategy).verifyOtp:otpCodewith theexpectedCode.HttpExceptionsubclasses (NotFoundException,UnauthorizedException) for different failure scenarios, which NestJS automatically maps to HTTP status codes (404, 401).3. Building API Endpoints with 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-otpand/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.AuthServicemethods and return responses. Errors thrown by the service (likeUnauthorizedException) are handled automatically by NestJS.Testing Your NestJS OTP API (
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. Configuring Plivo for SMS Delivery
We've already initialized the client, but let's refine the integration details.
Obtaining Credentials:
.envfile.Getting a Sender ID (Plivo Number):
+14155552671) and set it asPLIVO_SENDER_IDin your.envfile.Plivo Client Initialization (Recap): The client is initialized in the
AuthServiceconstructor using credentials fromConfigService:Sending the SMS (Recap): The core Plivo call happens in
sendOtp:Fallback Mechanisms (Considerations):
client.calls.create).AuthServicefor transient network errors when calling the Plivo API itself, using libraries likeasync-retry.5. Error Handling and Production Logging
Robust error handling and logging are crucial for production systems.
Error Handling Strategy:
HttpExceptionsubclasses (BadRequestException,UnauthorizedException,NotFoundException,InternalServerErrorException) from theAuthServicefor predictable client errors.InternalServerErrorExceptionto avoid leaking sensitive details.plivoNode.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:
Loggerservice.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:
Plivo API Calls: As mentioned, use
async-retryor similar if you frequently encounter transient network errors connecting to Plivo. Wrap thethis.plivoClient.messages.createcall.Related Resources