Frequently Asked Questions
Implement 2FA using the Vonage Verify API with Node.js and the Fastify framework. This involves accepting a user's phone number, sending an OTP via SMS using the Vonage API, and then verifying the user-entered OTP against the Vonage request to enhance login security.
The Vonage Verify API simplifies the process of sending and verifying one-time passwords (OTPs) within Node.js applications. It handles the complex logic of OTP delivery and management via SMS, voice, and other channels, enhancing security beyond simple passwords.
Fastify is a high-performance Node.js web framework chosen for its speed and developer-friendly experience. Its extensibility makes it ideal for integrating services like the Vonage Verify API, and its low overhead contributes to application efficiency.
Two-factor authentication (2FA) should be added to your Node.js application anytime you need to strengthen user authentication beyond relying solely on potentially vulnerable passwords. This is especially important during login and other sensitive actions.
Install the Vonage Server SDK using npm or yarn with the command: npm install @vonage/server-sdk
. This SDK allows your Node.js application to interact with the Vonage Verify API for sending and verifying OTPs.
libphonenumber-js
provides robust phone number validation and formatting in your 2FA implementation. This ensures phone numbers are in the correct international format before sending OTP requests to Vonage.
You'll need your Vonage API Key and API Secret, both found on your Vonage Dashboard. These credentials are essential for authenticating with the Vonage API and are used when initializing the Vonage SDK within your application.
Create a .env
file in your project root and store sensitive information like API keys there. Install dotenv
with npm, require it in server.js
with require('dotenv').config()
, then access via process.env.VARIABLE_NAME
.
The .env
file contains sensitive data like API keys which should never be exposed publicly. Add .env
to your .gitignore
file to prevent it from being accidentally committed to version control.
Check the status
and error_text
fields in Vonage API responses. Provide user-friendly error messages based on common status codes like invalid numbers, expired requests, or too many attempts. For SDK or network errors, use try...catch
blocks and log errors server-side.
The requestId
is a unique identifier returned by vonage.verify.start()
. It's crucial for tracking the verification process. It's passed to vonage.verify.check()
along with the OTP to verify the user's input.
Use the libphonenumber-js
library to validate international phone numbers before sending them to the Vonage Verify API. Parse the input with parsePhoneNumberFromString
and check validity with phoneNumber.isValid()
. Use E.164 formatting for consistency.
Create separate API endpoints (e.g., /api/otp/request
, /api/otp/verify
) in your Fastify application. Use request.body
to handle JSON payloads and send responses with reply.send()
and appropriate status codes (e.g., 200 OK, 400 Bad Request).
Rate limiting prevents abuse such as SMS spamming and brute-force attacks on OTP codes. Implement rate limiting on your 2FA routes (/request-otp
, /verify-otp
) using Fastify plugins or middleware to limit requests per phone number or IP within a timeframe.
Learn how to implement secure two-factor authentication (2FA) using Twilio Verify API with Node.js and Fastify. This step-by-step tutorial shows you how to send SMS verification codes, validate phone numbers, and verify OTP codes to add a critical security layer to your application.
Build a web application with two core functions: request an OTP sent via SMS to a user's phone number, then verify the OTP they enter. This ensures users possess the registered phone number during login or sensitive operations.
What You'll Build: Twilio OTP Authentication System
What We're Building:
A Node.js web application using the Fastify framework that integrates with the Twilio Verify API to:
Problem Solved:
This implementation addresses the need for stronger user authentication beyond simple passwords, mitigating risks associated with compromised credentials. It provides a practical example of adding SMS-based OTP verification to any Node.js application.
Technologies Used:
twilio
: The official Twilio Node.js SDK for interacting with the API.1@fastify/view
&ejs
: For server-side rendering of simple HTML templates.@fastify/formbody
: To parseapplication/x-www-form-urlencoded
request bodies (standard HTML form submissions).dotenv
: To manage environment variables securely.libphonenumber-js
: For robust phone number validation and formatting.System Architecture:
Prerequisites:
Security Considerations (2025 NIST Guidelines):
NIST SP 800-63B classifies SMS-based authentication as "RESTRICTED" due to vulnerabilities including SIM swapping and number porting attacks.4 Organizations using SMS for 2FA should:
Expected Outcome:
A functional web application running locally that demonstrates the complete Twilio OTP request and verification flow using Fastify.
1. Setting up Your Node.js Project with Twilio
Initialize your Node.js project and install the necessary dependencies.
Create Project Directory:
Open your terminal, create a new directory for the project, then navigate into it.
Initialize Node.js Project:
Create a
package.json
file to manage dependencies and project metadata.Install Dependencies:
Install Fastify, the Twilio SDK, template rendering, form body parsing, environment variable management, and phone number validation.
Install Development Dependency (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 configuration, server logic, and views.
src/server.js
: Main application logic.views/
: Directory for HTML templates..env
: Stores sensitive credentials (API keys). Never commit this file..env.example
: Example structure for.env
(safe to commit)..gitignore
: Specifies files/directories Git should ignore.Configure
.gitignore
:Add
node_modules
and.env
to prevent committing them.Configure Environment Variables:
Add placeholders to
.env.example
and add your actual credentials to.env
.Now open
.env
and replace the placeholders with your actual Twilio credentials from the Twilio Console. CustomizeTWILIO_BRAND_NAME
if desired.Add
package.json
Scripts:Open
package.json
and add scripts for starting the server normally and withnodemon
.(Note: These versions are current as of 2025. Fastify v5.6.x includes performance improvements and better TypeScript support. Ensure the versions in your
package.json
reflect whatnpm install
added, as newer versions may be available.)5Now the basic project structure and dependencies are set up.
2. Implementing the Twilio Verify API Integration
Build the Fastify server and integrate the Twilio Verify logic.
Basic Server Setup (
src/server.js
):Initialize Fastify, load environment variables, register necessary plugins, and import the phone number library.
parsePhoneNumberFromString
.dotenv.config()
loads variables from.env
.@fastify/formbody
to parse POST request bodies from HTML forms.@fastify/view
withejs
as the templating engine, pointing to theviews
directory.Create HTML Templates:
Populate the
.ejs
files with simple HTML forms.views/request-form.ejs
: Form to enter the phone number.views/verify-form.ejs
: Form to enter the received OTP. It includes a hidden field for therequestId
.views/success.ejs
: Success message page.views/error.ejs
: Generic error message page.Implement Routes (
src/server.js
):Add the Fastify routes to handle the OTP flow. Place this code before the
start()
function call insrc/server.js
.GET /
: Renders the initialrequest-form.ejs
.POST /request-otp
:number
from the form body.libphonenumber-js
to parse and validate the number.request-form
.+
as often expected by Twilio Verify.twilio.verify.v2.services(process.env.TWILIO_VERIFY_SERVICE_SID).verifications.create()
with the validated number and brand name. Explicitly requests a 6-digit code (code_length: '6'
).result.status
. Ifpending
, rendersverify-form.ejs
, passing theresult.sid
. If non-pending, shows an error on the initial form, potentially more specific if the status code is known (like '3' for invalid number).try...catch
for SDK/network errors.POST /verify-otp
:code
andrequestId
from the form body. Performs basic format validation on the code.twilio.verify.v2.services(process.env.TWILIO_VERIFY_SERVICE_SID).verificationChecks.create()
with therequestId
andcode
.result.status
. Ifapproved
, renderssuccess.ejs
. If non-approved, rendersverify-form.ejs
again with a user-friendly error message derived fromresult.error_message
and common status codes.try...catch
for SDK/network errors.setErrorHandler
: A basic global error handler catches unhandled exceptions and displays a generic error page.Run the Application:
Open your browser and navigate to
http://localhost:3000
(or the port you configured). You should see the form to enter your phone number.3. Building a RESTful API for OTP Verification
While this guide focuses on server-rendered HTML, the core Twilio logic can easily be exposed via a JSON API.
Example API Endpoints:
POST /api/otp/request
{ "phoneNumber": "+14155551212" }
{ "requestId": "a1b2c3d4e5f6…" }
{ "error": "Invalid phone number format" }
or{ "error": "Failed to initiate verification" }
POST /api/otp/verify
{ "requestId": "a1b2c3d4e5f6…", "code": "123456" }
{ "status": "verified" }
{ "error": "Invalid code or request expired." }
{ "error": "Verification check failed" }
Implementation Sketch (in
src/server.js
):Testing API Endpoints (using
curl
):4. Configuring Twilio Verify Service Credentials
API Credentials:
TWILIO_ACCOUNT_SID
: Your public API key from the Twilio Console.TWILIO_AUTH_TOKEN
: Your private API secret from the Twilio Console. Treat this like a password..env
file.Environment Variables:
TWILIO_ACCOUNT_SID
: (String) Required for authentication. Format: Typically 34 alphanumeric characters.TWILIO_AUTH_TOKEN
: (String) Required for authentication. Format: Typically 32 alphanumeric characters.TWILIO_VERIFY_SERVICE_SID
: (String) Required for specifying the Verify Service. Format: Typically 34 alphanumeric characters.TWILIO_BRAND_NAME
: (String, Optional) The name displayed in the SMS message (e.g., "Your code from [Brand Name] is…"). Max 11 alphanumeric characters or 16 digits for numeric sender ID. Defaults to 'MyApp' in our code if not set.PORT
: (Number, Optional) The port the Fastify server listens on. Defaults to 3000.Secure Storage: Using
.env
anddotenv
keeps credentials out of your source code. Ensure.env
is listed in your.gitignore
file. In production environments (like Heroku, AWS, etc.), use the platform's mechanism for setting environment variables securely – do not deploy.env
files.Fallback Mechanisms: The Twilio Verify API itself handles retries and channel fallbacks (e.g., SMS -> Text-to-Speech call) based on the chosen
workflow_id
(default is workflow 1). You generally don't need to implement complex fallback logic on the client-side for delivery itself, but robust application-level error handling (as shown in the route examples) is essential.5. Error Handling and Logging Best Practices
Error Handling Strategy:
status
anderror_message
fields in the Twilio API responses (verify.create
andverificationChecks.create
). Provide user-friendly messages based on common statuses (e.g., invalid code, expired request). Link to Twilio Verify API Errors. Our code examples show basic mapping for common errors.try...catch
blocks around Twilio SDK calls to handle network issues, timeouts, or configuration errors. Log these errors server-side and provide generic user messages.libphonenumber-js
and basic checks.setErrorHandler
for unexpected/unhandled errors to prevent crashes and provide a generic error page/response.Logging:
fastify.log
) provides basic request logging and methods likeinfo
,warn
,error
.Retry Mechanisms (Application Level):
/
). Consider adding a "Resend Code" button on theverify-form
that navigates back or triggers/request-otp
again (with rate limiting).verify.create
orverificationChecks.create
calls immediately is usually not recommended, as the issue might be persistent (e.g., invalid API key, Twilio outage, invalid number). Log the error and inform the user. If temporary network issues are suspected, a cautious retry with exponential backoff could be considered for specific error types, but often informing the user to try again later is safer. Twilio handles SMS delivery retries internally.6. Database Integration for User Authentication
While this guide doesn't implement a database, in a real-world application, you would integrate this OTP flow with your user management system.
Schema: You'd typically have a
users
table. You might add fields like:phone_number
(VARCHAR, UNIQUE) - Store in E.164 format.phone_verified_at
(TIMESTAMP, NULLABLE)two_factor_enabled
(BOOLEAN, DEFAULT FALSE)last_verify_request_id
temporarily if needed for specific flows, but Twilio manages the core state.Data Layer:
libphonenumber-js
, store the E.164 format in theusers
table./verify-otp
success), update the corresponding user record: setphone_verified_at
to the current time and potentiallytwo_factor_enabled
to true.7. Production Security Features
Input Validation:
libphonenumber-js
for robust international phone number validation and formatting (as implemented in Section 2). Always validate before sending to Twilio./verify-otp
route)./api/*
) for stronger type and format enforcement.Rate Limiting: Crucial to prevent abuse (SMS spamming/toll fraud) and brute-force attacks.
@fastify/rate-limit
. Apply limits to both/request-otp
(prevent spamming SMS) and/verify-otp
(prevent brute-forcing codes). Also apply to API equivalents.Frequently Asked Questions About Twilio OTP Implementation
How do I get started with Twilio Verify API for OTP?
Sign up for a Twilio account at https://www.twilio.com/try-twilio, obtain your Account SID and Auth Token from the Console dashboard, then create a Verify Service under Verify > Services. Use these credentials in your
.env
file to authenticate API requests.What is the difference between Twilio Verify API and SMS API?
Twilio Verify API is purpose-built for OTP and 2FA workflows, handling code generation, delivery retries, rate limiting, and expiration automatically. The SMS API requires you to manually implement these features. Verify API supports multiple channels (SMS, voice, email, WhatsApp) with automatic fallback.
Why does NIST classify SMS authentication as "RESTRICTED"?
NIST SP 800-63B classifies SMS as RESTRICTED due to vulnerabilities including SIM swapping attacks, number porting exploits, and SS7 protocol weaknesses.4 Organizations should offer alternative authenticators like TOTP apps, hardware tokens, or passkeys, and monitor for risk indicators like device swaps.
How do I validate international phone numbers in Node.js?
Use the
libphonenumber-js
library to parse and validate phone numbers. CallparsePhoneNumberFromString(number)
and checkisValid()
. Format validated numbers to E.164 standard usingformat('E.164')
before sending to Twilio Verify API. This ensures consistent formatting across all countries.What is the default OTP code length in Twilio Verify?
Twilio Verify API defaults to 4-digit verification codes. Specify
code_length: '6'
in theverifications.create()
call to use 6-digit codes for enhanced security. Longer codes provide better protection against brute-force attacks while remaining user-friendly.How do I implement rate limiting for OTP endpoints in Fastify?
Install
@fastify/rate-limit
and register it with your Fastify instance. Apply route-specific limits using theconfig.rateLimit
option: limit/request-otp
to 5 requests per phone number per hour to prevent SMS abuse, and limit/verify-otp
to 10 attempts per verification ID per 5 minutes to prevent brute-forcing.Can I use Twilio Verify API with Fastify v5?
Yes, Twilio Verify API works with Fastify v5.6.x (current as of 2025). The
twilio
Node.js SDK (v5.3.0+) supports Node.js 14, 16, 18, 20, and LTS 22. Fastify v5 includes performance improvements and better TypeScript support without breaking changes to plugin APIs used in this guide.What happens if the user doesn't receive the OTP code?
Twilio Verify API automatically handles SMS delivery retries. If delivery fails, the API can fall back to voice calls based on your workflow configuration. Implement a "Resend Code" button that calls
/request-otp
again with rate limiting. Check Twilio Console logs for delivery status and carrier-specific issues.Related Tutorials and Resources
Footnotes
https://github.com/twilio/twilio-node ↩ ↩2
https://fastify.dev ↩
https://www.twilio.com/docs/verify/api ↩
https://pages.nist.gov/800-63-3/sp800-63b.html ↩ ↩2
https://fastify.dev ↩