Frequently Asked Questions
Use Express for the backend, Vite with React (or Vue) for the frontend, and the Vonage Verify API for sending and verifying OTPs. This setup provides a secure way to verify user phone numbers, adding 2FA for logins or other critical actions.
The Vonage Verify API simplifies OTP implementation by handling OTP generation, delivery via SMS or voice, and the verification process. This offloads the complex security logic to a dedicated service.
2FA adds a crucial security layer by requiring users to verify their identity with something they know (password) and something they have (phone). This helps prevent unauthorized access and automated attacks.
Implement OTP verification for actions requiring high security, such as login, password resets, transaction confirmations, or sensitive profile changes. This helps protect user accounts and data.
Yes, the provided frontend code using Vite is easily adaptable to Vue.js or other frontend frameworks. The core logic for interacting with the backend API remains the same.
Send a POST request to the `/api/request-otp` endpoint with the user's phone number in E.164 format (e.g., +14155552671). The backend will interact with the Vonage API and return a `request_id`.
The `request_id`, returned by the Vonage API after requesting an OTP, is a unique identifier for that verification attempt. It's used to verify the OTP code entered by the user against the correct request.
Send a POST request to the `/api/verify-otp` endpoint, providing the `request_id` and the user-entered OTP code. The backend will check the code against the Vonage Verify API and return the verification result.
Use the E.164 format, which includes a plus sign (+) followed by the country code and phone number (e.g., +14155552671). Ensure consistent formatting to avoid errors.
The in-memory store (the `verificationRequests` object) is only for demonstration purposes. In production, use a persistent database like Redis or PostgreSQL to store request IDs and associate them with users.
Implement input validation, rate limiting, secure API key storage, HTTPS, and proper request ID handling to prevent abuse and ensure the security of your OTP system. Consider CSRF protection as well.
The backend handles expired codes and request IDs (Vonage status '6'). The frontend should allow users to request a new code after a timeout or if an error indicates expiry. Proper error messages guide users.
Double-check your `.env` file to ensure the `VONAGE_API_KEY` and `VONAGE_API_SECRET` are correct and that the `.env` file is loaded properly using `dotenv.config()` early in your backend code.
Ensure the `cors` middleware in your backend's `server.js` file is set up correctly, especially the `origin` option. During development, set it to your frontend's development URL. In production, configure to match frontend domain.
Two-factor authentication (2FA), often implemented via One-Time Passwords (OTP) sent over SMS, adds a critical layer of security to applications. It verifies user identity by combining something they know (password) with something they have (their phone).
This guide provides a complete walkthrough for adding SMS OTP verification to your web application using Node.js with Express for the backend, Vite (with React, though adaptable for Vue) for the frontend, and the Vonage Verify API for sending and checking OTPs.
Project Goal: Build a simple web application where a user can:
Problem Solved: Securely verify user phone numbers, enabling functionalities like 2FA for login, password resets, or transaction confirmations, significantly reducing automated attacks and unauthorized access.
Technologies Used:
dotenv
: For managing environment variables securely.@vonage/server-sdk
: The official Vonage Node.js library.cors
: To handle Cross-Origin Resource Sharing between the frontend and backend during development.System Architecture:
Note: Ensure your publishing platform supports Mermaid diagrams. If not, replace the code block with a static image of the diagram and provide descriptive alt text.
Prerequisites:
curl
or Postman).Final Outcome: A working application demonstrating the OTP request and verification flow, with separate frontend and backend components.
1. Setting up the Project
We'll create two main directories:
backend
for our Node.js API andfrontend
for our Vite/React application.A. Backend Setup (Node.js/Express)
Create Project Directory & Initialize:
Install Dependencies:
express
: Web framework.dotenv
: Loads environment variables from a.env
file.@vonage/server-sdk
: Vonage API client library.cors
: Enables Cross-Origin Resource Sharing (needed for frontend/backend communication on different ports).body-parser
: Parses incoming request bodies. Update: Modern Express (4.16+
) includesexpress.json()
andexpress.urlencoded()
, makingbody-parser
less critical but harmless to include.Create Core Files:
Configure
.gitignore
: Addnode_modules
and.env
to prevent committing them.Set up Environment Variables (
.env
): You need your Vonage API Key and Secret. Find these on the Vonage API Dashboard after signing up.VONAGE_API_KEY
andVONAGE_API_SECRET
authenticate your application with the Vonage API.PORT
defines where your backend server listens. Storing these in.env
keeps secrets out of your code repository.B. Frontend Setup (Vite/React)
Create Project Directory (from the root folder, outside
backend
):(You can replace
react
withvue
if preferred).Install Dependencies: Vite handles the initial setup.
Run Development Server:
This usually starts the Vite development server on
http://localhost:5173
(or the next available port).Project Structure:
2. Implementing Core Functionality (Backend API)
We'll build two API endpoints in
backend/server.js
:POST /api/request-otp
: Takes a phone number_ requests Vonage to send an OTP_ and returns arequest_id
.POST /api/verify-otp
: Takes therequest_id
and the user-entered OTP code_ asks Vonage to verify them_ and returns the result.backend/server.js
:Explanation:
.env
, configure Express middleware (CORS, JSON parsing).Vonage
client and specifically theVerify
client using credentials from.env
. Added crucial checks for missing keys.verificationRequests
is used to temporarily store therequest_id
received from Vonage. This is NOT suitable for production. In a real application, you must use a database (like Redis for caching/session data or PostgreSQL/MongoDB linked to user accounts) to store this information persistently and associate it with the user initiating the request.phoneNumber
from the request body.vonageVerify.start()
with the number and a custombrand
name (this appears in the SMS message).requestId
locally (in memory for now) and sends it back to the client.try...catch
error handling, logging Vonage errors and returning appropriate HTTP status codes (500 for server errors, 400 for bad input, 429 for rate limiting).requestId
andcode
from the request body.requestId
exists in the local store (simulating checking a database).vonageVerify.check()
with therequestId
andcode
.result.status
. Vonage uses'0'
for success.'16'
for wrong code,'6'
for expired/not found,'17'
for too many attempts) with user-friendly messages and appropriate HTTP status codes (400, 410, 429).try...catch
for network or unexpected Vonage errors.console.log
for simplicity. In production, replace these with a structured logger (like Winston or Pino, as mentioned in Section 5) for better log management and analysis.3. Building the Frontend (React Example)
Now, let's create the user interface in the
frontend
directory.frontend/src/App.jsx
:Note: Optional basic styling can be placed in
src/App.css
(which is already imported by the component above) or a similar CSS file. Example CSS:Explanation:
useState
to manage the phone number input, OTP code input, therequestId
received from the backend, loading state, UI status (verificationStatus
), and feedback messages.handleRequestOtp
:POST
request to/api/request-otp
with the phone number.fetch
for the API call.requestId
and updating the UI state to show the OTP input field.handleVerifyOtp
:POST
request to/api/verify-otp
with therequestId
and the enteredcode
.verified
and showing a success message.verificationStatus
:pending
, shows the OTP code input form.verified
, shows a success confirmation.4. Running the Application
Start the Backend:
The API server should start, likely on
http://localhost:3001
.Start the Frontend:
The Vite dev server should start, likely on
http://localhost:5173
.Test:
http://localhost:5173
).+14155552671
- use your actual number).Request OTP
. Check your phone for an SMS containing the code (it might take a few seconds).Verify Code
.5. Error Handling and Logging
server.js
includestry...catch
blocks for Vonage API calls. It logs errors to the console and attempts to return meaningful JSON error responses with appropriate HTTP status codes (400, 404, 410, 429, 500). Vonage-specific error statuses (like'6'
or'16'
) are mapped to clearer user messages. For production, integrate a dedicated logging library (like Winston or Pino) and an error tracking service (like Sentry).App.jsx
catches errors fromfetch
calls and displays error messages returned from the backend API.6. Database Schema and Data Layer (Production Considerations)
The provided example uses an in-memory object (
verificationRequests
) which is unsuitable for production. You must integrate a database.phone_verifications
) to store:verification_id
(Primary Key)user_id
(Foreign Key to your users table, if applicable)phone_number
(The number being verified)vonage_request_id
(The ID from Vonage, indexed for lookups)status
('pending', 'verified', 'failed', 'expired', 'cancelled')created_at
(Timestamp)expires_at
(Timestamp, e.g., 5 minutes fromcreated_at
)attempts
(Integer, track incorrect code entries)/request-otp
is called.vonage_request_id
during/verify-otp
.7. Security Features
libphonenumber-js
for comprehensive phone number validation instead of basic regex. Sanitize all inputs./request-otp
(e.g., 1 request per minute per number/IP) and/verify-otp
(e.g., 5 attempts perrequest_id
). Use libraries likeexpress-rate-limit
. Vonage also applies its own rate limits..env
files or hardcode keys. Use environment variables in your deployment environment.vonage_request_id
for verification checks.csurf
middleware). This is less critical if using token-based authentication (like JWTs) stored in localStorage/sessionStorage, but still good practice.8. Handling Special Cases
+
followed by country code and number) before sending to Vonage.status: '6'
).10
). The backend example doesn't explicitly handle this, but a production implementation should catch this error (429 Too Many Requests
often), potentially inform the user, or manage the flow via the database state.fetch
, you might need to implement this manually or use afetch
wrapper library that supports retries. The backend SDK might have some internal retry logic, but explicit handling can be beneficial.9. Performance Optimizations
vonage_request_id
column in your database table for fast lookups during verification.async/await
or Promises correctly to avoid blocking the event loop.10. Monitoring, Observability, and Analytics
requestId
) in logs./health
endpoint provides a basic check. Expand this to verify database connectivity and Vonage API reachability if possible./request-otp
,/verify-otp
).11. Troubleshooting and Caveats
401 Unauthorized
) Double-check keys in.env
and ensure the file is loaded correctly (dotenv.config()
is called early).3
or backend 400) Ensure E.164 format (+1...
). Use validation libraries.1
or HTTP 429) You're sending requests too frequently. Implement backend rate limiting and inform the user. Check Vonage account limits.16
or backend 400) User entered the wrong code. Handled in the/verify-otp
logic.6
or backend 410/404) The code expired (usually 5 mins), or therequestId
is wrong/not found. The user needs to request a new code.17
or backend 429) User tried the wrong code too many times for a givenrequestId
. Invalidate the request and force requesting a new one.cors
middleware inserver.js
is configured correctly, especially theorigin
option (use*
for testing, but restrict it to your frontend domain in production)..env
Not Loading: Ensuredotenv.config()
is called at the very top ofserver.js
before accessingprocess.env
variables.12. Deployment and CI/CD
VONAGE_API_KEY
,VONAGE_API_SECRET
,PORT
, and the production frontendorigin
URL for CORS securely in your deployment environment (e.g., using platform secrets management).npm run build
in thefrontend
directory. Serve the static files from thedist
folder using a static file server (like Nginx, Vercel, Netlify, or integrated into your backend).