code examples
code examples
Implement Node.js Express OTP/2FA SMS with Vonage Verify API
A guide on implementing SMS-based OTP/2FA in a Node.js Express application using the Vonage Verify API, covering setup, API interaction, and error handling.
This guide provides a complete walkthrough for implementing One-Time Password (OTP) or Two-Factor Authentication (2FA) via SMS in a Node.js application using Express and the Vonage Verify API. We will build a simple web application that requests a user's phone number, sends a verification code via SMS using Vonage, and then verifies the code entered by the user.
This approach simplifies the complexities of OTP generation, delivery retries (including voice call fallbacks), code expiry, and verification checks by leveraging the robust features of the Vonage Verify API.
Project Overview and Goals
What We're Building:
- A Node.js web application using the Express framework.
- A simple frontend with three stages:
- Enter phone number.
- Enter the received OTP code.
- Success/Failure indication.
- Backend logic to interact with the Vonage Verify API for sending and checking OTP codes via SMS.
Problem Solved: Securely verifying user identity or actions by confirming possession of a specific phone number, a common requirement for sign-ups, logins, or sensitive transactions.
Technologies Used:
- Node.js: JavaScript runtime environment.
- Express: Minimalist web framework for Node.js.
- Vonage Server SDK for Node.js (
@vonage/server-sdk): Official library to interact with Vonage APIs. - Vonage Verify API: Handles the OTP lifecycle (generation, sending via SMS/voice, checking).
- EJS: Templating engine for rendering simple HTML views.
- dotenv: Module to load environment variables from a
.envfile. - body-parser: Middleware to parse incoming request bodies.
- express-validator: Middleware for input validation.
Architecture:
The flow is straightforward:
- User: Accesses the web application via a browser.
- Express App (Node.js):
- Serves the HTML pages (EJS templates).
- Receives user input (phone number, OTP code).
- Communicates with the Vonage Verify API using the Vonage SDK.
- Renders appropriate responses based on API results.
- Vonage Verify API:
- Receives requests from the Express app.
- Generates and sends OTP codes via SMS (or voice fallback).
- Manages code validity and retries.
- Verifies submitted codes against its records.
- Returns verification results to the Express app.
(Diagrammatically):
[Browser] <--> [Express App (Node.js + Vonage SDK)] <--> [Vonage Verify API] --> [User's Phone (SMS)]
Prerequisites:
- Node.js and npm (or yarn): Installed on your development machine. Download Node.js
- Vonage API Account: Required to access the Verify API. Sign up for a free Vonage account – you'll get free credit to start.
- Vonage API Key and Secret: Found on the Vonage API Dashboard after signing up.
Expected Outcome: A functional web application demonstrating the OTP/2FA flow using SMS verification powered by Vonage.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal or command prompt and create a new directory for your project, then navigate into it:
bashmkdir vonage-otp-express cd vonage-otp-express -
Initialize npm: Create a
package.jsonfile to manage project dependencies and scripts:bashnpm init -y -
Install Dependencies: Install Express, the Vonage SDK, EJS for templating,
dotenvfor environment variables,body-parserfor handling form data, andexpress-validatorfor input validation:bashnpm install express @vonage/server-sdk dotenv ejs body-parser express-validatorexpress: Web application framework.@vonage/server-sdk: To interact with Vonage APIs.dotenv: Loads environment variables from a.envfile intoprocess.env. Crucial for keeping secrets out of code.ejs: Simple templating engine for rendering HTML views with dynamic data.body-parser: Middleware needed to parse JSON and URL-encoded request bodies (like form submissions).express-validator: Middleware for validating incoming request data (like phone numbers and codes).
-
Create Project Structure: Set up a basic structure for organization:
bashmkdir views public touch index.js .env .gitignoreviews/: Directory to store our EJS template files (.ejs).public/: Directory for static assets like CSS or client-side JavaScript (optional for this guide).index.js: Main application file where our Express server logic will reside..env: File to store sensitive credentials like API keys (will be ignored by Git)..gitignore: Specifies intentionally untracked files that Git should ignore.
-
Configure
.gitignore: Addnode_modulesand.envto your.gitignorefile to prevent committing dependencies and secrets:text# .gitignore node_modules/ .env -
Configure Environment Variables (
.env): Open the.envfile and add your Vonage API Key and Secret.How to get Vonage API Key and Secret: a. Log in to your Vonage API Dashboard. b. Your API Key and API Secret are displayed prominently on the main dashboard page under "API settings". c. Copy these values into your
.envfile.dotenv# .env VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET VONAGE_BRAND_NAME=YourAppName # Optional: Customize the brand name in the SMS messageReplace
YOUR_API_KEYandYOUR_API_SECRETwith your actual credentials.VONAGE_BRAND_NAMEwill be used in the SMS message ("YourAppName code is 1234").
2. Implementing Core Functionality (Verify API Workflow)
Now, let's build the Express application logic and the views for our OTP flow.
-
Initialize Express and Vonage SDK (
index.js): Openindex.jsand set up the basic Express app, configure EJS, load environment variables, initialize the Vonage SDK, and requireexpress-validator.javascript// index.js require('dotenv').config(); // Load .env variables into process.env const express = require('express'); const bodyParser = require('body-parser'); const { Vonage } = require('@vonage/server-sdk'); const { check, validationResult } = require('express-validator'); // For input validation const app = express(); const port = process.env.PORT || 3000; // Initialize Vonage SDK // IMPORTANT: Ensure VONGAGE_API_KEY and VONAGE_API_SECRET are in your .env file if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET) { console.error('Error: VONAGE_API_KEY and VONAGE_API_SECRET must be set in .env file.'); process.exit(1); // Exit if keys are missing } const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET }); // Middleware Setup app.set('view engine', 'ejs'); // Set EJS as the template engine app.use(bodyParser.json()); // Parse JSON bodies app.use(bodyParser.urlencoded({ extended: true })); // Parse URL-encoded bodies (form submissions) app.use(express.static(__dirname + '/public')); // Serve static files (optional) // --- Routes will go here --- // Start the server app.listen(port, () => { console.log(`Server listening at http://localhost:${port}`); });require('dotenv').config(): Loads variables from.env. Must be called early.- We initialize
Vonagewith the API key and secret fromprocess.env. - Basic check added to ensure keys are present before proceeding.
app.set('view engine', 'ejs'): Tells Express to use EJS for rendering files from theviewsdirectory.bodyParsermiddleware is configured to handle form submissions.express-validatorfunctions (check,validationResult) are imported.
-
Page 1: Request Verification Code (View and Routes):
-
Create View (
views/index.ejs): This page will contain the form to enter the phone number. It includes a placeholder for displaying messages and pre-fills the number if an error occurred.html<!-- views/index.ejs --> <!DOCTYPE html> <html> <head> <title>Enter Phone Number</title> <!-- Add basic styling if desired --> </head> <body> <h1>Enter Your Phone Number for Verification</h1> <% if (typeof message !== 'undefined' && message) { %> <p style=""color: <%= messageType === 'error' ? 'red' : 'green' %>;""><%= message %></p> <% } %> <form method=""POST"" action=""/request-verification""> <label for=""number"">Phone Number (e.g., 14155552671):</label><br> <input type=""tel"" id=""number"" name=""number"" required placeholder=""Include country code"" value=""<%= typeof phoneNumber !== 'undefined' ? phoneNumber : '' %>""><br><br> <button type=""submit"">Send Verification Code</button> </form> </body> </html>- It displays a
messageif passed from the server (used for success/error feedback). - The form
POSTs to the/request-verificationendpoint. - The
valueattribute of the input field is set tophoneNumberif it's passed to the template (useful on error). - We recommend instructing users to include the country code (E.164 format is best for Vonage, e.g.,
14155552671for US).
- It displays a
-
Create GET Route
/(index.js): This route simply renders theindex.ejsview when the user visits the root URL. Add this insideindex.jsbeforeapp.listen():javascript// index.js (continued) // --- Routes --- app.get('/', (req, res) => { // Render the initial page with the phone number form res.render('index', { message: null, phoneNumber: null }); // Pass null message and number initially }); // --- More routes below --- -
Create POST Route
/request-verification(index.js): This route handles the form submission fromindex.ejs. It takes the phone number, calls the Vonage Verify API to start the verification process, and then renders the next page (verify.ejs) for code entry. If an error occurs, it re-renders theindex.ejspage with the error message and the previously entered phone number.javascript// index.js (continued) app.post('/request-verification', async (req, res) => { const phoneNumber = req.body.number; const brand = process.env.VONAGE_BRAND_NAME || ""MyApp""; // Use brand from .env or default // Basic validation: Check if number exists if (!phoneNumber) { return res.render('index', { message: 'Phone number is required.', messageType: 'error', phoneNumber: phoneNumber // Pass back the (empty) number }); } console.log(`Requesting verification for number: ${phoneNumber}`); try { const response = await vonage.verify.start({ number: phoneNumber, brand: brand // workflow_id: 6 // Optional: To use SMS only workflow. Default includes SMS -> TTS -> TTS }); console.log('Vonage Verify Start Response:', response); if (response.status === '0') { // Successfully started verification // Render the page to enter the code, passing the request_id and phone number res.render('verify', { requestId: response.request_id, phoneNumber: phoneNumber, // Pass number for display/resend/cancel message: 'Verification code sent. Please check your phone.', messageType: 'success' }); } else { // Handle Vonage API errors console.error('Vonage Verify Start Error:', response.error_text); res.render('index', { message: `Error starting verification: ${response.error_text}`, messageType: 'error', phoneNumber: phoneNumber // Pass number back to the form }); } } catch (error) { // Handle network or other unexpected errors console.error('Error calling Vonage Verify API:', error); res.render('index', { message: 'An unexpected error occurred. Please try again later.', messageType: 'error', phoneNumber: phoneNumber // Pass number back to the form }); } }); // --- More routes below ---- It retrieves the
numberfrom the request body (req.body). - It calls
vonage.verify.start(). - Success (
response.status === '0'): It rendersverify.ejs(created next), passing the crucialrequest_idand thephoneNumber. - Failure: It renders
index.ejsagain with an error message and thephoneNumberto repopulate the form. - A
try...catchblock handles potential network errors during the API call.
- It retrieves the
-
-
Page 2: Check Verification Code (View and Route):
-
Create View (
views/verify.ejs): This page allows the user to enter the code they received via SMS. It includes hidden fields to pass therequest_idandphoneNumberback to the server.html<!-- views/verify.ejs --> <!DOCTYPE html> <html> <head> <title>Enter Verification Code</title> </head> <body> <h1>Enter Verification Code</h1> <p>Sent to: <%= phoneNumber %></p> <%# Display the number for confirmation %> <% if (typeof message !== 'undefined' && message) { %> <p style=""color: <%= messageType === 'error' ? 'red' : 'green' %>;""><%= message %></p> <% } %> <form method=""POST"" action=""/check-verification""> <input type=""hidden"" name=""requestId"" value=""<%= requestId %>""> <%# Crucial hidden field %> <input type=""hidden"" name=""phoneNumber"" value=""<%= phoneNumber %>""> <%# Pass phone number back %> <label for=""code"">Verification Code:</label><br> <input type=""text"" id=""code"" name=""code"" required minlength=""4"" maxlength=""6""><br><br> <%# OTPs are typically 4-6 digits %> <button type=""submit"">Verify Code</button> </form> <!-- Optional: Add a cancel button/link here --> <form method=""POST"" action=""/cancel-verification"" style=""margin-top: 10px;""> <input type=""hidden"" name=""requestId"" value=""<%= requestId %>""> <input type=""hidden"" name=""phoneNumber"" value=""<%= phoneNumber %>""> <button type=""submit"">Cancel Verification</button> </form> <!-- Optional: Add a resend button/link here (would likely POST to /request-verification again) --> </body> </html>- It displays the
phoneNumberit was sent to. - It uses hidden inputs to send both
requestIdandphoneNumberback. This is essential for Vonage to check the correct attempt and for re-rendering the page on failure. - The form
POSTs to the/check-verificationendpoint. - Includes an example ""Cancel Verification"" form posting to
/cancel-verification.
- It displays the
-
Create POST Route
/check-verification(index.js): This route handles the submission fromverify.ejs. It receives therequestId,code, andphoneNumber, then calls the Vonage Verify API to check the code. If the check fails, it re-rendersverify.ejswith the context.javascript// index.js (continued) app.post('/check-verification', async (req, res) => { const requestId = req.body.requestId; const code = req.body.code; const phoneNumber = req.body.phoneNumber; // Retrieve phone number passed from hidden input // Basic validation if (!requestId || !code || !phoneNumber) { return res.render('verify', { // Re-render verify page with error requestId: requestId, phoneNumber: phoneNumber, // Pass back what we received message: 'Missing information. Please try entering the code again.', messageType: 'error' }); } console.log(`Checking verification for request ID: ${requestId} with code: ${code}`); try { const response = await vonage.verify.check(requestId, code); console.log('Vonage Verify Check Response:', response); if (response.status === '0') { // Verification Successful! // Render the success page res.render('success', { message: 'Phone number verified successfully!' }); // In a real app: Mark user as verified, log them in, proceed with action, etc. } else { // Verification Failed // Render the verify page again with an error message res.render('verify', { requestId: requestId, phoneNumber: phoneNumber, // Pass number back for display and hidden field message: `Verification failed: ${response.error_text} (Status: ${response.status})`, messageType: 'error' }); } } catch (error) { // Handle network or other unexpected errors console.error('Error calling Vonage Check API:', error); res.render('verify', { // Re-render verify page requestId: requestId, phoneNumber: phoneNumber, // Pass number back message: 'An unexpected error occurred during verification. Please try again.', messageType: 'error' }); } }); // --- Success page route below ---- It retrieves
requestId,code, and cruciallyphoneNumberfromreq.body. - It calls
vonage.verify.check(). - Success (
response.status === '0'): Renderssuccess.ejs(created next). - Failure: Renders
verify.ejsagain, passing back therequestId,phoneNumber, and displaying an error message. - Includes
try...catchfor network errors.
- It retrieves
-
-
Page 3: Success Page (View):
-
Create View (
views/success.ejs): A simple confirmation page shown after successful verification.html<!-- views/success.ejs --> <!DOCTYPE html> <html> <head> <title>Verification Successful</title> </head> <body> <h1>Success!</h1> <% if (typeof message !== 'undefined' && message) { %> <p style=""color: green;""><%= message %></p> <% } %> <p>Your phone number has been successfully verified.</p> <!-- Add a link back to the start or to the next step in your application --> <a href=""/"">Start Over</a> </body> </html>
-
3. Building a Complete API Layer
While this example is a simple web application, the Express routes (/request-verification, /check-verification) effectively act as an API layer if you were building a backend service for a separate frontend (like a React or Vue app) or mobile application.
Considerations for a dedicated API:
- Authentication/Authorization: Protect these endpoints if they are part of a larger system. Ensure only authenticated users can initiate verification for their own associated phone numbers. This typically involves session management or token-based authentication (e.g., JWT).
- Request Validation: Implement robust validation using libraries like
express-validatorto check input formats (e.g., ensuring the phone number follows E.164, the code is numeric and has the expected length).(Remember:javascript// Example using express-validator in index.js routes // Add validation middleware to the /request-verification route app.post('/request-verification', check('number').isMobilePhone('any', { strictMode: false }).withMessage('Invalid phone number format.'), // Basic phone format check async (req, res) => { const errors = validationResult(req); const phoneNumber = req.body.number; // Get phone number for re-rendering on error if (!errors.isEmpty()) { return res.status(400).render('index', { // Use 400 status for bad request message: errors.array()[0].msg, // Show first validation error messageType: 'error', phoneNumber: phoneNumber // Pass number back }); } // ... rest of the route logic from Section 2 ... } ); // Add validation middleware to the /check-verification route app.post('/check-verification', check('code').isLength({ min: 4, max: 6 }).isNumeric().withMessage('Invalid code format.'), check('requestId').notEmpty().withMessage('Request ID is missing.'), check('phoneNumber').notEmpty().withMessage('Phone number is missing.'), // Also check hidden field async (req, res) => { const errors = validationResult(req); const requestId = req.body.requestId; // Get these to re-render verify page correctly const phoneNumber = req.body.phoneNumber; if (!errors.isEmpty()) { return res.status(400).render('verify', { requestId: requestId, phoneNumber: phoneNumber, message: errors.array()[0].msg, messageType: 'error' }); } // ... rest of the route logic from Section 2 ... } );express-validatorwas installed in Section 1, Step 3) - API Documentation: Use tools like Swagger/OpenAPI to document endpoints, request/response formats (JSON), and status codes if building a true API.
- Testing with
curlor Postman:- Request Verification:
bash
curl -X POST http://localhost:3000/request-verification \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "number=YOUR_PHONE_NUMBER" # Replace YOUR_PHONE_NUMBER with a valid E.164 number # Expect HTML response rendering the verify page (if successful) - Check Verification: (Requires a valid
requestIdandphoneNumberfrom a previous step)bashcurl -X POST http://localhost:3000/check-verification \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "requestId=YOUR_REQUEST_ID&code=YOUR_OTP_CODE&phoneNumber=YOUR_PHONE_NUMBER" # Replace YOUR_REQUEST_ID, YOUR_OTP_CODE, and YOUR_PHONE_NUMBER # Expect HTML response rendering success or verify page
- Request Verification:
4. Integrating with Necessary Third-Party Services (Vonage)
- Configuration: Done via the
.envfile (VONAGE_API_KEY,VONAGE_API_SECRET,VONAGE_BRAND_NAME). - API Keys/Secrets Security:
- Use
dotenvto load keys from.env. - Crucially: Add
.envto your.gitignorefile to prevent accidentally committing secrets to version control. - On deployment platforms (Heroku, AWS, etc.), use their mechanisms for setting environment variables securely – do not store
.envfiles on production servers.
- Use
- Dashboard Navigation for Credentials:
- Go to the Vonage API Dashboard.
- Log in to your account.
- The API Key and API Secret are displayed directly on the landing page under the ""API settings"" section. Copy these values.
- Fallback Mechanisms: The Vonage Verify API handles fallbacks automatically within its defined workflows (e.g., SMS -> Voice Call). You generally don't need to implement fallback logic in your application for OTP delivery, unless you want to offer alternative 2FA methods entirely (like authenticator apps).
5. Implementing Proper Error Handling, Logging, and Retry Mechanisms
- Error Handling Strategy:
- Use
try...catchblocks around all Vonage API calls to handle network issues or unexpected exceptions. - Check the
statusproperty in the Vonage API response.status === '0'indicates success. Any other status indicates an error specific to the API call (e.g., invalid number, wrong code, insufficient funds). - Use the
error_textproperty from the Vonage response for specific error details. - Provide user-friendly error messages on the frontend (don't expose raw API error details unless necessary for debugging). Pass error messages, types, and necessary context (like
phoneNumber,requestId) to the EJS templates for re-rendering forms correctly. - Use appropriate HTTP status codes for API-style responses (e.g.,
400for bad input,500for server errors,429for rate limiting). Even in this web app, settingres.status(400)before rendering an error page due to invalid input (like in theexpress-validatorexample) is good practice.
- Use
- Logging:
- Use
console.logfor basic informational messages during development (e.g., which number is being verified). - Use
console.errorfor logging errors caught incatchblocks or when Vonage API status is non-zero. Include the Vonageerror_textandstatuscode in the log. - Production Logging: For production, replace
console.log/console.errorwith a dedicated logging library like Winston or Pino. These offer structured logging (JSON), different log levels (info, warn, error), and transport options (writing to files, external services).
javascript// Example: Logging Vonage errors if (response.status !== '0') { console.error(`Vonage API Error: Status ${response.status}, Text: ${response.error_text}, RequestID: ${response.request_id || requestId || 'N/A'}`); // Render error page... } - Use
- Retry Mechanisms:
- Vonage Handles Delivery Retries: The Verify API automatically handles retrying SMS delivery and falling back to voice calls based on the selected workflow. You don't need to implement retries for sending the OTP.
- Application-Level Retries: You might implement retries in your
catchblocks for transient network errors when calling the Vonage API, potentially using exponential backoff. However, for OTP verification, it's often simpler to report the error and let the user try again manually. - User-Initiated Resend: Consider adding a "Resend Code" button on the
verify.ejspage. This button would trigger the/request-verificationroute again with the same phone number. Implement rate limiting on this feature.
6. Creating a Database Schema and Data Layer
For this specific implementation using the Vonage Verify API, no database schema is required on your end to manage the OTP state. Vonage handles:
- Generating the code.
- Storing the code securely.
- Tracking the
request_id. - Managing expiry times.
- Recording verification attempts.
You would need a database if:
- You were building the OTP logic manually using the Vonage Messages API. You'd need to store the generated OTP, its expiry timestamp, the associated user/phone number, and its verification status.
- You need to link the verified phone number to a user account in your application's main database. You'd typically have a
userstable with columns likeid,email,password_hash,phone_number,is_phone_verified(boolean), etc. After a successful verification via/check-verification, you would update theis_phone_verifiedflag for the corresponding user.
7. Adding Security Features
- Input Validation and Sanitization:
- Use
express-validator(shown in Section 3) or similar libraries to validate phone number format and code format/length. This prevents invalid data from reaching the Vonage API and reduces error handling complexity. - Sanitization (removing potentially harmful characters) is less critical for phone numbers and numeric codes but is vital for other user inputs in a larger application.
- Use
- Protection Against Common Vulnerabilities:
- Cross-Site Scripting (XSS): EJS automatically escapes HTML content by default when using
<%= ... %>, providing basic XSS protection for data displayed in views. Be cautious if using<%- ... %>(unescaped output). - Cross-Site Request Forgery (CSRF): For web applications with sessions, use CSRF tokens (e.g., with the
csurfmiddleware) to prevent malicious sites from forcing users to submit requests to your application. Less critical for stateless APIs using tokens but still good practice if sessions are used.
- Cross-Site Scripting (XSS): EJS automatically escapes HTML content by default when using
- Rate Limiting: Essential to prevent abuse and control costs.
- Apply rate limiting to the
/request-verificationendpoint to prevent users from spamming phone numbers with codes. - Apply rate limiting to the
/check-verificationendpoint to prevent brute-force guessing of OTP codes. - Use middleware like
express-rate-limit.
bashnpm install express-rate-limitjavascript// index.js - Apply rate limiting const rateLimit = require('express-rate-limit'); const requestVerificationLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // Limit each IP to 5 verification requests per windowMs message: 'Too many verification requests from this IP, please try again after 15 minutes', standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers // Store configuration can be added for distributed environments }); const checkVerificationLimiter = rateLimit({ windowMs: 5 * 60 * 1000, // 5 minutes max: 10, // Limit each IP to 10 check attempts per windowMs (allows for typos) message: 'Too many code check attempts from this IP, please try again after 5 minutes', standardHeaders: true, legacyHeaders: false, }); // Apply to specific routes *before* the route handlers app.use('/request-verification', requestVerificationLimiter); app.use('/check-verification', checkVerificationLimiter); // If cancel route is heavily used, apply rate limiting there too // app.use('/cancel-verification', checkVerificationLimiter); // Example // ... (rest of your routes) - Apply rate limiting to the
- Brute Force Protection: The Vonage Verify API has built-in mechanisms (like limiting check attempts per
request_id). Server-side rate limiting (above) adds another layer. - Secure Key Storage: Reiterating: Use
.envand.gitignorelocally, and secure environment variable management in production. Never hardcode API keys/secrets.
8. Handling Special Cases Relevant to the Domain
- International Phone Numbers: Always aim to collect numbers in E.164 format (e.g.,
+14155552671, although Vonage often handles numbers without the+). Inform users clearly about the required format. Use frontend libraries (likelibphonenumber-js) for input formatting and validation if possible. - Number Formatting: Be prepared for users entering numbers with spaces, dashes, or parentheses. Sanitize the input on the backend before sending it to Vonage (e.g., remove all non-numeric characters except a leading
+if using strict E.164).express-validator'sisMobilePhonecan help, but additional stripping of characters might be needed. - SMS Delivery Issues: SMS is generally reliable but not guaranteed. Vonage's fallback to voice calls helps mitigate this. Inform users that delivery might take a moment and provide a ""Resend Code"" option (with rate limiting).
- Code Expiry: Vonage manages code expiry (typically 5 minutes). If a user enters an expired code, the
/check-verificationcall will fail with an appropriate error (status16: ""The code provided does not match the expected value""). Your error handling should inform the user the code expired and prompt them to request a new one. - Cancel Verification: The Vonage Verify API provides a
cancelendpoint (vonage.verify.cancel(requestId)). Implement a route (e.g.,/cancel-verification) triggered by a ""Cancel"" button on theverify.ejspage. This invalidates therequest_idand prevents further checks against it.javascript// index.js - Add Cancel Route app.post('/cancel-verification', async (req, res) => { const requestId = req.body.requestId; const phoneNumber = req.body.phoneNumber; // Get phone number for potential redirect/message if (!requestId) { // Redirect back to start or show generic error console.error('Cancel attempt without request ID.'); return res.redirect('/'); } console.log(`Cancelling verification for request ID: ${requestId}`); try { const response = await vonage.verify.cancel(requestId); console.log('Vonage Verify Cancel Response:', response); if (response.status === '0') { // Successfully cancelled res.render('index', { // Render start page with message message: 'Verification cancelled.', messageType: 'info', phoneNumber: null // Clear phone number field }); } else if (response.status === '19') { // Status 19: Verification request cannot be cancelled now (already verified or expired) console.warn(`Attempted to cancel already completed/invalid request: ${requestId}`); res.render('index', { message: 'Verification process already completed or expired.', messageType: 'warn', phoneNumber: null }); } else { // Handle other Vonage API errors during cancel console.error('Vonage Verify Cancel Error:', response.error_text); // Maybe redirect back to verify page with error? Or start page? res.render('verify', { // Re-render verify page with cancel error requestId: requestId, phoneNumber: phoneNumber, message: `Error cancelling verification: ${response.error_text}`, messageType: 'error' }); } } catch (error) { // Handle network or other unexpected errors console.error('Error calling Vonage Cancel API:', error); res.render('verify', { // Re-render verify page with generic error requestId: requestId, phoneNumber: phoneNumber, message: 'An unexpected error occurred while cancelling. Please try again.', messageType: 'error' }); } }); - User Account Linking: In a real application, after successful verification (
/check-verificationsuccess), you need logic to associate the verified phone number with the correct user account in your database. This might involve looking up the user by an ID stored in the session or passed via a token.
9. Testing the Implementation
- Unit Tests: Test individual functions, especially any utility functions for number sanitization or validation logic you add. Use mocking libraries (like
jest.mock) to mock the Vonage SDK calls and test your route handlers' logic without making actual API calls. - Integration Tests: Test the interaction between your Express routes and the (mocked) Vonage API. Ensure the correct data is passed between routes (e.g.,
requestId,phoneNumber) and that responses are handled correctly. Tools likesupertestcan be used to make HTTP requests to your running Express app during tests. - End-to-End (E2E) Tests: Use tools like Cypress or Playwright to automate browser interactions. Test the full user flow: entering a phone number, receiving a real SMS (requires a test Vonage account and phone number), entering the code, and verifying success/failure/cancellation. E2E tests are crucial but slower and require managing real credentials/costs.
- Manual Testing: Thoroughly test the flow manually with different scenarios:
- Valid phone number, valid code.
- Valid phone number, invalid code.
- Valid phone number, expired code (wait > 5 mins).
- Invalid phone number format.
- Attempting to check code without requesting first.
- Cancelling a request.
- Hitting rate limits (if implemented).
- Using international numbers.
10. Deployment Considerations
- Environment Variables: Use the hosting provider's mechanism for setting environment variables (
VONAGE_API_KEY,VONAGE_API_SECRET,VONAGE_BRAND_NAME,PORT,NODE_ENV=production). Do not commit.envfiles or hardcode secrets. - Process Management: Use a process manager like PM2 or rely on the platform's built-in service management (e.g., systemd, Heroku dynos, Docker container orchestration) to keep the Node.js application running, manage logs, and handle restarts.
- HTTPS: Ensure your application is served over HTTPS in production to protect data in transit. Use a reverse proxy like Nginx or Caddy, or leverage platform features (like Heroku Automated Certificate Management, AWS Load Balancer listeners).
- Logging: Configure production-level logging (e.g., Winston, Pino) to output structured logs to files or a logging service for monitoring and debugging.
- Dependencies: Run
npm install --production(or equivalent) to install only necessary production dependencies. - Build Step (if applicable): If using TypeScript or a frontend build process, ensure the build is run before deployment.
- Database (if applicable): If linking to a user database, ensure connection strings and credentials are secure and configured via environment variables. Set up database migrations.
- Rate Limiting Storage: For distributed deployments (multiple server instances), configure
express-rate-limit(or similar) to use a shared store (like Redis or Memcached) so limits are applied across all instances.
Conclusion
By following this guide, you have implemented a secure SMS-based OTP/2FA verification flow in a Node.js Express application using the Vonage Verify API. This leverages Vonage's infrastructure for reliable code generation, delivery (with fallbacks), expiry management, and verification, simplifying your backend development. Remember to prioritize security through input validation, rate limiting, and secure credential management, especially when deploying to production.
Frequently Asked Questions
How to implement OTP SMS verification in Node.js?
Implement OTP SMS verification using Express, the Vonage Verify API, and the Vonage Server SDK. This setup allows you to collect a user's phone number, send a verification code via SMS, and verify the code entered by the user upon its return, leveraging Vonage's robust API for OTP management.
What is the Vonage Verify API used for?
The Vonage Verify API simplifies the complexities of OTP/2FA by handling OTP generation, delivery with retries, code expiry, and verification checks. It's a core component for securely verifying user identity during sign-ups, logins, or sensitive transactions.
Why does Vonage Verify API simplify 2FA implementation?
Vonage Verify API handles the entire OTP lifecycle, from code generation and SMS/voice delivery to verification and expiry. This offloads the burden of managing these complex processes from the developer, allowing for a simpler and more secure implementation.
When should I use the Vonage Verify API for OTP?
Use the Vonage Verify API whenever you need to verify a user's phone number for security purposes, such as during user registration, login, or when authorizing sensitive transactions. It provides a robust and reliable solution for two-factor authentication.
How to install Vonage Server SDK for Node.js?
Install the Vonage Server SDK using npm or yarn with the command 'npm install @vonage/server-sdk'. This SDK provides the necessary functions to interact with the Vonage Verify API and other Vonage services within your Node.js application.
What is the purpose of the .env file in this project?
The .env file stores sensitive credentials like your Vonage API Key and Secret, keeping them separate from your codebase. It's crucial for security best practices. The 'dotenv' module loads these variables into 'process.env'.
How to set up Vonage API key and secret?
Obtain your Vonage API Key and Secret from the Vonage API Dashboard after signing up for an account. Create a .env file in your project's root directory and add the keys as VONAGE_API_KEY and VONAGE_API_SECRET. Make sure to add .env to your .gitignore file.
How to handle errors with the Vonage Verify API?
Use try...catch blocks around Vonage API calls and check the 'status' property of the response. A '0' status indicates success, while other statuses signal errors detailed in the 'error_text' property. Provide user-friendly feedback without exposing raw API error details.
What are the Vonage Verify API costs?
The article doesn't mention any specific Vonage API costs. You would need to consult the official Vonage pricing documentation to get details of costs involved.
How to test Vonage Verify API integration?
Testing involves unit tests for individual functions, integration tests for interactions between routes and the (mocked) API, and end-to-end tests for the entire user flow. Manual testing with various scenarios, including valid and invalid inputs and edge cases, is also recommended.
Why use express-validator with Vonage Verify API?
Express-validator provides input validation and sanitization to ensure data integrity and security before sending it to the Vonage API. This helps prevent issues such as invalid phone number formats or malicious code injections.
What is the role of body-parser in Vonage OTP project?
Body-parser is middleware that parses incoming request bodies (like form submissions) in JSON or URL-encoded format. It makes the submitted data accessible in req.body within your Express routes, enabling you to process user inputs.
How to handle SMS delivery failures with Vonage?
The Vonage Verify API automatically handles SMS delivery retries and potential fallbacks to voice calls, minimizing the need for custom retry logic. Inform users about potential delays and consider a "Resend Code" option with appropriate rate limiting.
How to resend verification code with Vonage?
Implement a "Resend Code" functionality by triggering the /request-verification route again with the same phone number, allowing users to request a new code if the original wasn't received. Be sure to add rate limiting to this endpoint to prevent abuse.