Frequently Asked Questions
Use the Vonage Verify API with Fastify and Node.js. Create a Fastify API endpoint that takes the user's phone number and makes a request to the Vonage Verify API to initiate the OTP process. This sends a unique OTP to the user's phone number via SMS. The response includes a request ID for verification.
The Vonage Verify API handles generating, sending, and verifying one-time passwords (OTPs) for two-factor authentication (2FA). It simplifies the process by providing pre-built logic and infrastructure for OTP delivery via SMS or voice calls. This offloads the complexity of building and maintaining an OTP system yourself.
Fastify is a high-performance Node.js web framework known for its speed and extensibility. Its built-in features like schema validation, logging, and a plugin-friendly architecture make it a strong choice for building robust and maintainable APIs. The low overhead also adds to improved efficiency.
Implement SMS OTP/2FA for actions requiring enhanced security, such as user registration, login, password resets, and sensitive transactions. This provides a strong second layer of authentication. The Vonage Verify API makes implementing 2FA easier.
While the default is 4 digits, you can configure Vonage to send 6-digit OTPs or define custom workflows. Uncomment and adjust the `code_length` or `workflow_id` options in the `vonage.verify.start()` call in the Node.js code for customizations.
You'll need Node.js v16 or later with npm/yarn, a Vonage API account with an API key and secret, and a basic understanding of APIs and asynchronous JavaScript. Installing the Vonage CLI is optional but helpful. These steps allow you to get up and running quickly.
Your Fastify API should have a separate endpoint that receives the `requestId` (from the OTP request) and the `code` entered by the user. Call `vonage.verify.check()` to verify the OTP against Vonage's records and return a verification success or failure status to the client app based on the response.
The `dotenv` module loads environment variables from a `.env` file into `process.env`. This is crucial for keeping sensitive data like your Vonage API credentials secure. The file is put in the .gitignore file and not committed to the repository.
Fastify uses JSON Schema to validate request bodies. Define the expected request structure in route options. Fastify automatically checks incoming requests and returns a 400 Bad Request error if they don't match, enhancing security and providing clear error messaging to clients.
A status of '0' in the Vonage Verify API response means the request was successfully initiated. This typically appears after calling `vonage.verify.start()` indicating Vonage has accepted the request to send the OTP. It does not guarantee delivery.
Adding prefixes like `/api/v1` to your API routes is a best practice for versioning. This allows you to introduce future versions (e.g., `/api/v2`) with breaking changes without impacting existing integrations that rely on the older version.
Check the `status` code in the Vonage Verify API response. Non-zero status codes indicate an error and should be handled appropriately. The `error_text` provides further details. Use a centralized error handler in Fastify and custom messages for specific error codes.
Use the `@fastify/rate-limit` plugin to protect your OTP endpoints from abuse. Configure global limits and/or per-route limits to control how often clients can request or verify OTPs. Use the `keyGenerator` option to customize which factor (IP, phone number, requestId) the rate limiting should apply to.
Key security measures include: always using HTTPS, rate limiting OTP requests, validating and sanitizing input, storing API keys securely (environment variables, secrets management), using Helmet for security headers, and writing thorough automated tests to ensure the reliability of your OTP system.
Two-factor authentication (2FA), often using One-Time Passwords (OTP) sent via SMS, adds a critical layer of security to user verification processes. This guide provides a complete walkthrough for building a robust SMS OTP verification system using Node.js, the Fastify framework, and the Vonage Verify API.
We will build a backend API with two primary endpoints: one to request an OTP sent to a user's phone number and another to verify the OTP entered by the user. This guide covers everything from project setup to deployment considerations, focusing on production readiness.
Project Overview and Goals
What We're Building:
A backend API service built with Node.js and Fastify that:
Problem Solved:
This implementation addresses the need for a secure, out-of-band verification method commonly used during registration, login, or sensitive actions within an application. It mitigates risks associated with compromised passwords by requiring possession of the user's registered phone.
Technologies Used:
dotenv
: A module to load environment variables from a.env
file intoprocess.env
.@vonage/server-sdk
: The official Vonage Node.js SDK for interacting with Vonage APIs.System Architecture:
The system involves a user interacting with a client application, which communicates with our Fastify API. The Fastify API, in turn, uses the Vonage Verify API to handle SMS OTP sending and verification. The typical flow is: User provides phone number -> Client App sends number to Fastify API -> Fastify API asks Vonage to send OTP -> Vonage sends SMS to User -> User enters OTP in Client App -> Client App sends OTP and request ID to Fastify API -> Fastify API asks Vonage to verify -> Vonage confirms/denies -> Fastify API informs Client App -> Client App shows result to User.
Prerequisites:
npm install -g @vonage/cli
.Expected Outcome:
A functional Fastify API service capable of sending and verifying SMS OTPs via Vonage, ready for integration into a larger application. The service will include basic security, error handling, and logging.
1. Setting Up the Project
Let's initialize our Node.js project and install Fastify along with necessary dependencies.
1. Create Project Directory:
Open your terminal and create a new directory for the project, then navigate into it.
2. Initialize npm Project:
This creates a
package.json
file with default settings.3. Install Dependencies:
We need Fastify, the Vonage SDK,
dotenv
for environment variables, andpino-pretty
for development logging.fastify
: The core web framework.@vonage/server-sdk
: The official Vonage Node.js library.dotenv
: Loads environment variables from a.env
file.pino-pretty
: Makes Fastify's default JSON logs human-readable during development.4. Configure
package.json
for Development:Add a
dev
script topackage.json
to run the server with readable logs usingpino-pretty
.(Note: Replace
^...
with actual installed versions if needed, butnpm install
handles this.)5. Create Project Structure:
Organize the project for clarity:
src/
: Contains the main application code.src/routes/
: Will hold our API route definitions.src/server.js
: The main entry point for the Fastify application..env
: Stores sensitive information like API keys (DO NOT commit this file)..gitignore
: Specifies intentionally untracked files that Git should ignore.6. Configure
.gitignore
:Add
node_modules
and.env
to your.gitignore
file to prevent committing them.7. Set Up Environment Variables:
Open the
.env
file and add your Vonage API credentials and a brand name for the SMS message.Replace
YOUR_API_KEY
andYOUR_API_SECRET
with the actual credentials from your Vonage API Dashboard.8. Basic Fastify Server Setup:
Create the initial server configuration in
src/server.js
.Explanation:
require('dotenv').config();
: Loads variables from.env
intoprocess.env
. Crucially done before other modules might need them.fastify({ logger: true })
: Initializes Fastify with logging enabled. Pino is used by default.fastify.listen()
: Starts the server. We listen on0.0.0.0
to make it accessible within Docker containers or VMs if needed later.You can now run
npm run dev
in your terminal. You should see log output indicating the server is running, likely on port 3000. Stop the server withCtrl+C
.2. Integrating with Vonage
Now, let's initialize the Vonage SDK client and make it available within our Fastify application.
1. Initialize Vonage Client:
Update
src/server.js
to create and configure the Vonage client using the environment variables. We'll use Fastify'sdecorate
utility to make the client accessible within route handlers viarequest.vonage
orfastify.vonage
.Explanation:
Vonage
class from the SDK.Vonage
using the API key and secret loaded from.env
.fastify.decorate('vonage', vonage)
adds thevonage
instance to Fastify's application context, making it easily accessible in routes and plugins.3. Implementing the OTP Flow (API Layer)
We'll create two API endpoints within a dedicated route file:
POST /request-otp
: Initiates the OTP process.POST /verify-otp
: Verifies the submitted OTP.1. Create Route File:
Create a new file
src/routes/otp.js
.2. Define Routes:
Add the route logic to
src/routes/otp.js
. We'll use Fastify's schema validation for request bodies.Explanation:
otpRoutes(fastify, options)
: Standard Fastify plugin structure.requestOtpSchema
,verifyOtpSchema
): Define the expected structure and types for the request bodies. Fastify automatically validates incoming requests against these schemas and returns a 400 Bad Request error if validation fails.additionalProperties: false
prevents unexpected fields./request-otp
:phoneNumber
from the validatedrequest.body
.vonage
client.vonage.verify.start()
with the phone number and brand name.response.status
. Astatus
of'0'
means Vonage accepted the request and will attempt to send the SMS. Other statuses mean an error occurred before sending (e.g., invalid number format, throttling).requestId
on success, which the client needs for the verification step.try...catch
for network/SDK errors./verify-otp
:requestId
andcode
from the validatedrequest.body
.vonage.verify.check()
with therequestId
and the user-providedcode
.response.status
:'0'
means the code was correct.try...catch
for network/SDK errors during the check.3. Register Routes in Server:
Now, register these routes in
src/server.js
.Explanation:
otpRoutes
function.fastify.register(otpRoutes, { prefix: '/api/v1' })
: Registers all routes defined inotp.js
under the/api/v1
path prefix (e.g.,/api/v1/request-otp
). Using a prefix is good practice for API versioning.At this point, you have the core API functionality. You can test it using
curl
or a tool like Postman after starting the server (npm run dev
).Testing with
curl
:Replace
+1XXXXXXXXXX
with a real phone number you can receive SMS on (use E.164 format).Request OTP:
Expected Response (Success):
You should receive an SMS with a 4-digit code (by default).
Verify OTP: Replace
YOUR_UNIQUE_REQUEST_ID
with the ID from the previous step andYOUR_OTP_CODE
with the code from the SMS.Expected Response (Success):
Expected Response (Incorrect Code):
(Note: The HTTP status code will be 400 for incorrect code)
4. Implementing Proper Error Handling and Logging
Fastify's built-in logger (Pino) is already active. Let's add a centralized error handler to catch unhandled exceptions and format error responses consistently.
1. Add Custom Error Handler:
Update
src/server.js
to includesetErrorHandler
.Explanation:
fastify.setErrorHandler
: Registers a function to handle errors that occur during request processing after the initial routing but before a reply is sent, or errors explicitly passed toreply.send(error)
.request.log.error(error)
.error.validation
) and returns a structured 400 response.statusCode
andmessage
if available (falling back to 500).NODE_ENV
is not 'production' to avoid leaking sensitive information.Logging:
request.log.info
,request.log.error
,request.log.warn
) to provide context about the OTP flow.npm run dev
),pino-pretty
formats these JSON logs nicely. In production, you'd typically pipe the raw JSON logs to a log management system (e.g., Datadog, ELK stack, Splunk) for analysis and alerting.5. Adding Security Features
Security is paramount for an authentication mechanism.
1. Rate Limiting:
Protect against brute-force attacks on both requesting and verifying OTPs.
Install Rate Limiter Plugin:
Register and Configure: Add this to the ""Plugin Registration"" section in
src/server.js
.Apply Specific Limits (Optional but Recommended): You can override the global settings within specific route options in
src/routes/otp.js
. It's wise to have stricter limits on OTP requests than general API usage.Explanation:
@fastify/rate-limit
: Provides robust rate limiting based on IP address by default.max
,timeWindow
: Control how many requests are allowed within a specific timeframe.errorResponseBuilder
: Customizes the response when the limit is hit.keyGenerator
: Allows using factors other than IP (like phone number,requestId
, or authenticated user ID) for more granular limiting. Use with caution, as poorly chosen keys can block legitimate users. IP-based is often the simplest starting point.config.rateLimit
overrides the global settings for finer control.2. Input Validation and Sanitization:
google-libphonenumber
on the backend before sending to Vonage could provide earlier, more specific feedback to the user about formatting issues.3. Secure Handling of Secrets:
.env
and loaded viadotenv
..gitignore
: The.env
file is in.gitignore
to prevent accidental commits..env
files directly to production servers.4. Other Considerations:
@fastify/helmet
to set various security-related HTTP headers (likeX-Frame-Options
,Strict-Transport-Security
).src/server.js
within the ""Plugin Registration"" section:6. Testing
Writing automated tests is crucial for ensuring reliability. We'll use
tap
, Fastify's default test runner.1. Install
tap
as a Dev Dependency:2. Update
test
script inpackage.json
:(Note: Added
@fastify/helmet
,@fastify/rate-limit
to dependencies andtap
,pino-pretty
to devDependencies based on previous steps. Ensure versions match your installation.)3. Create Test File Structure:
4. Write Basic Tests:
Here's an example testing the success path for
/request-otp
, mocking the Vonage API call. You would typically create a test helper (test/helper.js
) to build the Fastify app instance for tests, potentially injecting mocks.5. Create Test Helper (Conceptual):
A file like
test/helper.js
would typically contain a function to build and configure the Fastify app instance for testing, allowing injection of mocks and handling teardown. Creating a full helper is beyond the scope of this rewrite, but the test file structure assumes its existence.Run tests using
npm test
. Add comprehensive tests covering success paths, error paths (Vonage errors, validation errors), and security features like rate limiting.