Frequently Asked Questions
Install necessary dependencies like the Vonage Server SDK, NestJS ConfigModule, and class-validator. Set up environment variables for your Vonage API key and secret. Create a Vonage service to interact with the Verify API, a controller to expose API endpoints, and DTOs for request validation. Implement API endpoints for sending and verifying OTPs. Thoroughly test endpoint functionality with tools like curl and consider implementing a persistence layer to track verification attempts if required by your specific use case.
The Vonage Verify API is a service that simplifies the process of sending one-time passwords (OTPs) across various channels like SMS and voice. It manages the verification lifecycle, from sending the initial code to checking its validity, allowing developers to easily integrate 2FA into their applications. The API handles complexities such as delivery retries and various workflows for robust delivery.
NestJS provides a structured and efficient framework for building server-side applications with features like dependency injection and validation pipes. This simplifies the development process, improves code maintainability, and makes it easier to integrate external services like the Vonage Verify API. NestJS also offers a robust module system for organizing the OTP logic within the application.
Use the Vonage Verify API's `verify.start()` method, providing the user's phone number and your brand name. This API call triggers an SMS message containing a verification code sent to the specified phone number. The API returns a `request_id` that must be stored and used for the subsequent verification step.
Call the Vonage Verify API's `verify.check()` method with the `request_id` and the user-submitted OTP code. The Vonage service checks the code against the request details. The response will contain a status to indicate verification success, failure, or errors, such as an invalid code or an expired request.
The Vonage brand name, configurable in your Vonage dashboard or `.env` file, is the name displayed to users when they receive an OTP via SMS. It helps users identify the legitimate source of the message and enhances trust, preventing confusion with potential phishing attempts. The brand name should clearly represent your application (e.g. 'MyApp').
Implement `try...catch` blocks in your Vonage service and controller. Log detailed error messages, including Vonage response status and error text. Throw specific NestJS HTTP exceptions like `BadRequestException` and `InternalServerErrorException`. Consider a custom global exception filter to standardize error responses and use the built-in `Logger` for detailed tracing.
A database isn't always essential for basic OTP with Vonage. You would need one if you're linking successful verification to users, tracking verification status, implementing complex rate limiting, or maintaining audit trails. Vonage handles the OTP lifecycle state; use a database for features requiring persistent data or relations to your app's users or data.
While OTP enhances security, be mindful of potential vulnerabilities. These could include insecure handling of API keys and weak rate-limiting, which may lead to brute-force attacks. Ensure your application's integration with Vonage and any related data handling adheres to security best practices and secure configuration guidelines.
Yes, you can optionally specify workflow settings, particularly within the Vonage dashboard configuration. Workflows determine how the OTP is delivered and how retry attempts are managed if the initial delivery fails. Refer to the Vonage documentation for available parameters and settings to fine-tune the verification process.
Use application-level rate limiting with libraries like `nestjs-rate-limiter` to restrict requests per IP or user. Configure suitable rate limits for your `/otp/send` and `/otp/verify` endpoints. Vonage also implements its own rate limits, providing additional protection against attempts to guess codes or flood the service with requests.
Log the initiation of send and verify requests, successful outcomes (request IDs, statuses), warnings for non-critical issues (failed verification attempts, error details), and errors with stack traces. Contextualize logs with the class name for easier analysis. Consider using a logging library that outputs JSON for production use.
This guide provides a step-by-step walkthrough for integrating Vonage's Verify API into a NestJS application to implement One-Time Password (OTP) or Two-Factor Authentication (2FA). We will build a secure and robust API endpoint capable of sending verification codes via SMS and verifying user-submitted codes.
Adding OTP/2FA significantly enhances application security by requiring users to provide something they have (access to their phone) in addition to something they know (like a password), mitigating risks associated with compromised credentials.
Project Overview and Goals
What We'll Build:
Problem Solved: Implementing a reliable and secure second factor of authentication for user actions like login, password reset, or sensitive operations.
Technologies Used:
@vonage/server-sdk
.System Architecture:
Prerequisites:
npm install -g @nestjs/cli
).1. Setting up the NestJS Project
Let's start by creating a new NestJS project and installing the necessary dependencies.
Create a new NestJS Project: Open your terminal and run:
Choose your preferred package manager (npm or yarn) when prompted.
Install Dependencies: We need the Vonage Server SDK, NestJS config module for environment variables, and class-validator/transformer for input validation.
Set up Environment Variables: Create a
.env
file in the root of your project. This file will store your sensitive Vonage credentials. Never commit this file to version control. Add a.gitignore
entry for.env
if it's not already there..env
Replace
YOUR_VONAGE_API_KEY
andYOUR_VONAGE_API_SECRET
with your actual credentials from the Vonage Dashboard.Configure NestJS ConfigModule: Import and configure the
ConfigModule
in your main application module (src/app.module.ts
) to load environment variables from the.env
file.src/app.module.ts
Using
isGlobal: true
makes theConfigService
available throughout your application without needing to importConfigModule
into every feature module.Enable Validation Pipe Globally: In
src/main.ts
, enable the built-inValidationPipe
to automatically validate incoming request payloads based on DTOs (Data Transfer Objects).src/main.ts
The options
whitelist
andforbidNonWhitelisted
enhance security by ensuring only expected data fields are processed.transform
automatically converts incoming JSON into typed DTO instances.2. Implementing Core Functionality (Vonage Service)
We'll encapsulate the Vonage SDK interactions within a dedicated NestJS service.
Generate the OTP Module and Service: Use the NestJS CLI to generate a module and service for OTP functionality.
The
--flat
flag prevents the CLI from creating an extra sub-directory for the service.Implement the
VonageService
: This service will handle initializing the Vonage client and calling the Verify API methods.src/otp/vonage.service.ts
Injecting
ConfigService
allows secure access to API keys from environment variables rather than hardcoding them. Using theLogger
is essential for debugging and monitoring API interactions. Checking the API key/secret in the constructor ensures a fast failure if essential configuration is missing. Therequest_id
is returned because the client needs this ID for the subsequent verification request.Update the
OtpModule
: Make sureVonageService
is provided and exported by theOtpModule
.src/otp/otp.module.ts
3. Building the API Layer (Controller and DTOs)
Now, let's create the controller that exposes the OTP functionality via REST endpoints and the DTOs for request validation.
Generate the OTP Controller:
Create Data Transfer Objects (DTOs): Create DTO files to define the expected shape and validation rules for incoming request bodies.
src/otp/dto/send-otp.dto.ts
Using
IsPhoneNumber
ensures the input is a valid phone number format (E.164 is recommended for Vonage).src/otp/dto/verify-otp.dto.ts
Using
Length
ensures the code matches the expected format. Vonage codes are typically 4 or 6 digits; match this to your configuration or the default (4).Implement the
OtpController
: Inject theVonageService
and define the API endpoints using decorators.src/otp/otp.controller.ts
The
@Body()
decorator extracts and automatically validates the request body using the provided DTO and the globalValidationPipe
. UsingHttpCode(HttpStatus.OK)
is often more appropriate than the default 201 Created for POST requests that don't create a new resource.Testing Endpoints with
curl
:Start the Application:
Send OTP Request: Replace
+14155551234
with a real phone number you have access to (in E.164 format).Expected JSON Response:
You should receive an SMS with a code on the provided number. Note down the
requestId
.Verify OTP Code: Replace
SOME_REQUEST_ID_FROM_VONAGE
with the actual ID received and1234
with the code from the SMS.Expected JSON Response (Success):
Expected JSON Response (Failure - e.g., wrong code):
4. Integrating with Vonage (Configuration Details)
Let's ensure the Vonage integration is correctly configured.
Obtaining API Credentials:
Environment Variables: As set up in Section 1, Step 3:
.env
Secure Handling:
.env
File: Keep API keys in the.env
file..gitignore
: Ensure.env
is listed in your.gitignore
file to prevent accidental commits.Fallback Mechanisms: The Vonage Verify API itself has built-in fallbacks (e.g., SMS followed by Voice call if configured or default). For application-level resilience:
VonageService
includestry...catch
blocks. If a call tosendOtp
orverifyOtp
fails due to network issues or Vonage downtime, theInternalServerErrorException
will be thrown.5. Error Handling, Logging, and Retry Mechanisms
We've already incorporated basic logging and error handling. Let's refine it.
Consistent Error Handling:
VonageService
catches errors during SDK calls. It logs detailed errors and throws specific NestJS HTTP exceptions (BadRequestException
,InternalServerErrorException
) based on the Vonage response status or SDK errors.Logging:
Logger
. It provides levels (log
,error
,warn
,debug
,verbose
).Logger
instance is created with the class name (VonageService.name
,OtpController.name
) providing context in logs.pino
withnestjs-pino
.Retry Mechanisms (Vonage Internal):
requestId
). If verification fails, the user usually needs to request a new code (triggering/otp/send
again).Testing Error Scenarios:
/otp/send
with an incorrectly formatted number (e.g.,12345
). Expect a 400 Bad Request due to DTO validation.VONAGE_API_KEY
orVONAGE_API_SECRET
in.env
and restart. Calls to Vonage should fail, likely resulting in a 500 Internal Server Error from the service./otp/verify
with a validrequestId
but an incorrectcode
. Expect a 400 Bad Request with a message like ""The code provided does not match..."".Log Analysis:
requestId
.ERROR
orWARN
level to quickly identify problems.6. Database Schema and Data Layer (Optional)
For this basic OTP implementation using Vonage Verify, a database is often not strictly required on the server-side to manage the OTP state itself. Vonage manages the request lifecycle using the
requestId
.When You Would Need a Database:
If a Database is Needed (Example using Prisma):
Install Prisma:
Define Schema: Update
prisma/schema.prisma
.Set
DATABASE_URL
: Add your database connection string to the.env
file.Run Migrations:
Integrate PrismaClient: Create a
PrismaService
and use it in yourOtpService
or a dedicatedUserService
to update user records upon successful verification.vonageService.sendOtp
, you might create anOtpVerificationAttempt
record withstatus: 'PENDING'
.vonageService.verifyOtp
, update the correspondingOtpVerificationAttempt
tostatus: 'VERIFIED'
and potentially update the linkedUser
record (phoneVerified: true
).State Management without Database: The crucial piece of state is the
requestId
. The client application receives this from the/otp/send
response and must send it back with the code in the/otp/verify
request. The state is temporarily held by the client and validated by Vonage.7. Adding Security Features
Security is paramount for authentication mechanisms.
Input Validation and Sanitization:
class-validator
: Already implemented (Section 3, Step 2). This prevents invalid data types, unexpected fields (whitelist
,forbidNonWhitelisted
), and enforces formats (likeIsPhoneNumber
,Length
). This is the primary defense against injection-like attacks on the input data structure.class-validator
doesn't automatically sanitize against XSS if you were reflecting input back, but for this API (which primarily deals with phone numbers, IDs, and codes passed to another service), the validation is the key.Common Vulnerabilities:
requestId
is generated by Vonage and acts as a temporary, single-use capability token. EnsurerequestId
isn't easily guessable (Vonage handles this)./otp/verify
) is robustly linked to the action it protects (e.g., login, password reset). Don't allow the protected action to proceed if verification fails.Rate Limiting and Brute Force Protection:
/otp/send
,/otp/verify
) from excessive requests.npm install --save nestjs-rate-limiter
app.module.ts
:points
andduration
based on expected usage and security posture. You might apply stricter limits specifically to the/otp/verify
endpoint using method decorators if needed.requestId
.Testing for Security Vulnerabilities:
npm audit
or tools like Snyk to check for known vulnerabilities in dependencies.Security Implications of Configuration:
8. Handling Special Cases
+14155552671
). TheIsPhoneNumber
validator helps enforce this on input. Ensure numbers stored or processed internally consistently use this format.