code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

Node.js SMS OTP Verification: Complete 2FA Implementation Guide with Vonage

Learn how to implement secure SMS OTP two-factor authentication in Node.js using Express and Vonage Verify API. Step-by-step tutorial with code examples, security best practices, and error handling.

Node.js SMS OTP Verification: Complete 2FA Implementation Guide with Vonage

Modern web applications need security beyond passwords. Two-Factor Authentication (2FA), implemented using One-Time Passwords (OTP) sent via SMS, verifies user identity by requiring both something they know (password) and something they have (their phone).

This guide provides a complete, step-by-step walkthrough for implementing SMS OTP verification in a Node.js application using the Express framework and the Vonage Verify API. Build a simple application that prompts users for their phone number, sends them an OTP via SMS, and allows them to verify the code.

What is SMS OTP Two-Factor Authentication?

SMS OTP (One-Time Password) two-factor authentication is a security method that sends a temporary verification code to a user's mobile phone via text message. This two-factor authentication approach combines something users know (their password) with something they have (their phone), providing an additional security layer beyond traditional password-only authentication.

Project Overview and Goals

What You'll Build: A Node.js/Express web application with three core pages:

  1. A form to input a phone number to initiate OTP verification.
  2. A form to enter the OTP code received via SMS.
  3. A success page upon correct code verification.

Problem Solved: This implementation secures user actions (like login, password reset, or sensitive transactions) by adding a second verification factor delivered via SMS, significantly reducing the risk of unauthorized access.

Technologies Used:

  • Node.js: A JavaScript runtime for building the backend server.
  • Express: A minimal and flexible Node.js web application framework for handling routing and requests.
  • Vonage Verify API: A service specifically designed to handle OTP generation, delivery (SMS, voice), retries, code validation, and expiration, simplifying the 2FA implementation.
  • EJS: A simple templating engine for rendering basic HTML views.
  • dotenv: A module to load environment variables from a .env file.
  • libphonenumber-js: (Optional but Recommended) For parsing and validating phone numbers.

Architecture:

mermaid
graph LR
    A[User's Browser] -- 1. Enters Phone Number --> B(Node.js/Express App);
    B -- 2. Calls Verify API --> C(Vonage Verify API);
    C -- 3. Sends OTP --> D(User's Phone via SMS);
    A -- 4. Enters OTP Code --> B;
    B -- 5. Calls Check API --> C;
    C -- 6. Returns Verification Result --> B;
    B -- 7. Displays Success/Failure --> A;

Prerequisites:

  • Node.js and npm (or yarn) installed on your machine.
  • A Vonage API account. Sign up here if you don't have one (free credit is available for new accounts).
  • Your Vonage API Key and API Secret, found on the Vonage API Dashboard.

Final Outcome: A functional web application demonstrating the core Vonage Verify OTP flow, ready to integrate into a larger authentication system.


How SMS OTP Verification Works

Before diving into implementation, understanding the OTP verification flow is crucial:

  1. User initiates verification by entering their phone number
  2. Server requests OTP from Vonage Verify API
  3. Vonage sends SMS with a time-limited code to the user's phone
  4. User enters the code they received
  5. Server validates the code through Vonage's check endpoint
  6. Authentication completes if the code matches

This SMS verification process typically takes 30 seconds to 2 minutes, providing a balance between security and user experience.


1. Setting up the Project

Initialize your Node.js project and install the necessary dependencies.

  1. Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.

    bash
    mkdir vonage-otp-express
    cd vonage-otp-express
  2. Initialize Node.js Project: Create a package.json file.

    bash
    npm init -y
  3. Install Dependencies: Install Express for the web server, the Vonage Server SDK for interacting with the Verify API, EJS for simple HTML templates, dotenv for managing API credentials securely, and optionally libphonenumber-js for robust phone number validation.

    bash
    npm install express @vonage/server-sdk dotenv ejs libphonenumber-js
    • express: Web framework.
    • @vonage/server-sdk: Official Vonage SDK for Node.js.
    • dotenv: Loads environment variables from a .env file.
    • ejs: Templating engine for views.
    • libphonenumber-js: (Recommended) For parsing, validating, and formatting phone numbers.
  4. Create Project Structure: Set up a basic structure for your files.

    bash
    mkdir views
    touch index.js .env .gitignore
    • views/: Directory to store EJS template files.
    • index.js: Main application file containing server logic.
    • .env: File to store sensitive API keys (will not be committed to Git).
    • .gitignore: Specifies files and directories Git should ignore.
  5. Configure .gitignore: Prevent committing sensitive files and dependencies. Add the following to .gitignore:

    text
    # Dependencies
    node_modules
    
    # Environment Variables
    .env
    
    # Logs
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
  6. Configure Environment Variables (.env): You need your Vonage API Key and Secret. Find these on your Vonage API Dashboard. Add the following to your .env file, replacing the placeholders with your actual credentials:

    dotenv
    # .env
    VONAGE_API_KEY=YOUR_API_KEY_HERE
    VONAGE_API_SECRET=YOUR_API_SECRET_HERE
    VONAGE_BRAND_NAME=MyApp # Name shown in the SMS message (max 11 chars)
    • VONAGE_API_KEY: Your public Vonage API key.
    • VONAGE_API_SECRET: Your private Vonage API secret.
    • VONAGE_BRAND_NAME: The brand name associated with the verification request (appears in the SMS). Keep it short.
  7. Basic Server Setup (index.js): Set up the initial Express server structure and load environment variables.

    javascript
    // index.js
    require('dotenv').config(); // Load environment variables from .env file
    const express = require('express');
    const { Vonage } = require('@vonage/server-sdk');
    // Optional: const parsePhoneNumber = require('libphonenumber-js');
    
    const app = express();
    const port = process.env.PORT || 3000; // Use port from env or default to 3000
    
    // Initialize Vonage SDK
    const vonage = new Vonage({
      apiKey: process.env.VONAGE_API_KEY,
      apiSecret: process.env.VONAGE_API_SECRET
    });
    
    // Middleware
    app.use(express.json()); // Parse JSON bodies (built-in)
    app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies (form data – built-in)
    app.set('view engine', 'ejs'); // Set EJS as the view engine
    app.set('views', './views'); // Specify the views directory
    
    // Routes will go here
    
    // Start the server
    app.listen(port, () => {
      console.log(`Server listening at http://localhost:${port}`);
    });
    • Load dotenv first to ensure environment variables are available.
    • Initialize Express and the Vonage SDK using credentials from .env.
    • Use built-in Express middleware (express.json, express.urlencoded) to handle request bodies.
    • Configure EJS as the template engine.
    • Start the server listening on the specified port.

2. Creating the User Interface

Create the user interface elements for OTP request and verification.

  1. Create the Phone Number Input View (views/request.ejs): This simple form asks the user for their phone number.

    html
    <!DOCTYPE html>
    <html>
    <head>
        <title>Request OTP</title>
        <style>
            body { font-family: sans-serif; padding: 20px; }
            label, input, button { display: block; margin-bottom: 10px; }
            .error { color: red; margin-bottom: 10px; }
        </style>
    </head>
    <body>
        <h1>Enter Your Phone Number</h1>
        <% if (typeof error !== 'undefined' && error) { %>
            <p class="error"><%= error %></p>
        <% } %>
        <form method="post" action="/request-otp">
            <label for="number">Phone Number (e.g., 14155552671):</label>
            <input type="tel" id="number" name="number" required placeholder="Include country code">
            <button type="submit">Send OTP</button>
        </form>
    </body>
    </html>
    • Includes a basic form posting to /request-otp.
    • Displays an error message if one is passed from the server.
  2. Create the OTP Input View (views/verify.ejs): This form allows the user to enter the code they received.

    html
    <!DOCTYPE html>
    <html>
    <head>
        <title>Verify OTP</title>
         <style>
            body { font-family: sans-serif; padding: 20px; }
            label, input, button { display: block; margin-bottom: 10px; }
            .error { color: red; margin-bottom: 10px; }
        </style>
    </head>
    <body>
        <h1>Enter OTP Code</h1>
        <p>An OTP code has been sent to your phone.</p>
         <% if (typeof error !== 'undefined' && error) { %>
            <p class="error"><%= error %></p>
        <% } %>
        <form method="post" action="/verify-otp">
            <input type="hidden" name="requestId" value="<%= requestId %>">
            <label for="code">OTP Code:</label>
            <input type="text" id="code" name="code" required pattern="\d{4,6}" title="Enter the 4 or 6 digit code">
            <button type="submit">Verify Code</button>
        </form>
    </body>
    </html>
    • Includes a hidden input field to pass the requestId back to the server.
    • The code input uses a pattern to suggest 4–6 digits (Vonage default is 4, but you can configure it).
  3. Create the Success View (views/success.ejs): A simple page displayed after successful verification.

    html
    <!DOCTYPE html>
    <html>
    <head>
        <title>Verification Success</title>
         <style>
            body { font-family: sans-serif; padding: 20px; color: green; }
        </style>
    </head>
    <body>
        <h1>Verification Successful!</h1>
        <p>Your phone number has been verified.</p>
        <p><a href="/">Start Over</a></p>
    </body>
    </html>

3. Building the OTP Verification API

Implement the routes and logic for requesting and verifying the OTP.

  1. Create the Root Route (GET /): This route renders the phone number input form. Add this to index.js before the app.listen call.

    javascript
    // index.js (add this route)
    app.get('/', (req, res) => {
      res.render('request', { error: null }); // Render request.ejs, initially no error
    });
  2. Implement the OTP Request Route (POST /request-otp): This route handles the phone number submission, calls the Vonage Verify API to start the verification process, and renders the OTP input form upon success. This is where your Node.js application initiates SMS OTP delivery.

    javascript
    // index.js (add this route)
    app.post('/request-otp', async (req, res) => {
      let phoneNumber = req.body.number;
      const brand = process.env.VONAGE_BRAND_NAME;
    
      // Basic validation (enhance in production)
      if (!phoneNumber) {
        return res.render('request', { error: 'Phone number is required.' });
      }
    
      // IMPORTANT: Add E.164 validation/normalization here in production!
      // Example using libphonenumber-js (conceptual):
      // try {
      //   const parsedNumber = parsePhoneNumber(phoneNumber, 'US'); // Default country if needed
      //   if (parsedNumber && parsedNumber.isValid()) {
      //     phoneNumber = parsedNumber.number; // Use E.164 format
      //   } else {
      //     return res.render('request', { error: 'Invalid phone number format.' });
      //   }
      // } catch (e) {
      //    return res.render('request', { error: 'Invalid phone number format.' });
      // }
      // Ensure the phone number is in E.164 format (e.g., 14155552671) for best results with Vonage.
    
      console.log(`Requesting OTP for ${phoneNumber} with brand ${brand}`);
    
      try {
        const result = await vonage.verify.start({
          number: phoneNumber,
          brand: brand,
          // workflow_id: 6 // Optional: Specify workflow (e.g., 6 for 6-digit code)
          // code_length: 6 // Optional: Specify code length (4 or 6)
        });
    
        console.log('Vonage Verify Start Result:', result);
    
        if (result.status === '0') {
          // Successfully initiated verification
          const requestId = result.request_id;
          res.render('verify', { requestId: requestId, error: null }); // Render verify.ejs
        } else {
          // Handle Vonage API errors
          console.error('Vonage Verify Start Error:', result.error_text);
          res.render('request', { error: `Error starting verification: ${result.error_text}` });
        }
      } catch (error) {
        // Handle network or other unexpected errors
        console.error('Error calling Vonage Verify API:', error);
        res.render('request', { error: 'An unexpected error occurred. Please try again.' });
      }
    });
    • Retrieve the phone number from the form data (req.body.number).
    • Crucially, in production, add robust validation and normalization here to ensure the phoneNumber is in E.164 format (e.g., 14155552671) before sending it to Vonage. Use libphonenumber-js (see commented example).
    • Call vonage.verify.start() with the (ideally validated) phone number and brand name.
    • The vonage.verify.start method handles sending the SMS (and potential voice fallbacks depending on the workflow).
    • If result.status is '0', the request was successful. Extract the request_id (crucial for the next step) and render the verify.ejs view.
    • If result.status is not '0', display the error_text from Vonage.
    • A try…catch block handles potential network errors when calling the Vonage API.
  3. Implement the OTP Verification Route (POST /verify-otp): This route takes the requestId and the user-entered code, calls the Vonage Verify API to check the code, and renders the success or failure view.

    javascript
    // index.js (add this route)
    app.post('/verify-otp', async (req, res) => {
      const requestId = req.body.requestId;
      const code = req.body.code;
    
      // Basic validation
      if (!requestId || !code) {
         // This shouldn't happen with the hidden field, but good practice
         return res.render('request', { error: 'Missing request ID or code.' });
      }
    
      console.log(`Verifying OTP for request ID ${requestId} with code ${code}`);
    
      try {
        const result = await vonage.verify.check(requestId, code);
    
        console.log('Vonage Verify Check Result:', result);
    
        if (result.status === '0') {
          // Verification successful
          res.render('success'); // Render success.ejs
        } else {
          // Verification failed (e.g., wrong code, expired)
           console.error('Vonage Verify Check Error:', result.error_text);
           // Render the verify page again with an error message
           res.render('verify', {
                requestId: requestId, // Pass requestId back to the view
                error: `Verification failed: ${result.error_text}. Please try again.`
           });
        }
      } catch (error) {
        // Handle network or other unexpected errors
        console.error('Error calling Vonage Check API:', error);
        res.render('verify', {
          requestId: requestId, // Pass requestId back
          error: 'An unexpected error occurred during verification. Please try again.'
        });
      }
    });
    • Retrieve requestId and code from the form data.
    • Call vonage.verify.check() with these two parameters.
    • If result.status is '0', the code is correct; render the success page.
    • If not '0', the code is incorrect, expired, or another issue occurred. Render the verify.ejs view again, passing back the requestId and displaying the error_text.
    • Again, a try…catch handles potential network errors.

4. Integrating with Vonage

Integration primarily involves:

  1. Account Setup: Sign up for a Vonage account.

  2. API Credentials: Obtain your API Key and Secret from the Vonage API Dashboard.

    • Navigate to the dashboard after logging in.
    • The API Key and Secret are usually displayed prominently near the top.
  3. Secure Storage: Store these credentials securely using environment variables (.env file) as demonstrated in the setup section. Never commit your .env file or hardcode secrets directly in your source code.

  4. SDK Initialization: Use the @vonage/server-sdk and initialize it with the credentials from environment variables:

    javascript
    // index.js (Initialization part)
    const vonage = new Vonage({
      apiKey: process.env.VONAGE_API_KEY,
      apiSecret: process.env.VONAGE_API_SECRET
    });
  5. API Calls: Use the SDK methods (verify.start, verify.check) as shown in the route implementations.

Environment Variables Explained:

  • VONAGE_API_KEY: (String) Your public Vonage API identifier. Required for authentication. Obtain from Vonage Dashboard.
  • VONAGE_API_SECRET: (String) Your private Vonage API secret key. Required for authentication. Obtain from Vonage Dashboard. Keep this highly confidential.
  • VONAGE_BRAND_NAME: (String, Max 11 Alphanumeric Chars) The name displayed as the sender in the SMS message (actual sender ID may vary by country/carrier). Define this in your .env.

5. Implementing Error Handling and Logging

Robust error handling and logging are essential for production.

  • Consistent Error Handling Strategy:

    • Use try…catch blocks around all external API calls (Vonage SDK calls).
    • Check the result.status from Vonage API responses. A status of '0' indicates success. Any other status indicates an error.
    • Use the result.error_text provided by Vonage for specific error details.
    • Render user-friendly error messages on the frontend (as shown in the .ejs templates), passing specific error details when appropriate but avoiding exposure of sensitive system information.
    • For unexpected errors (network issues, code bugs), provide a generic error message to the user while logging detailed information on the server.
  • Logging:

    • Use console.log for basic informational messages during development (e.g., "Requesting OTP for…", "Verifying OTP…").
    • Use console.error for logging actual errors, including the error object or Vonage error_text.
    • Production Logging: For production, replace console.log/console.error with a dedicated logging library like Winston or Pino. Configure appropriate log levels (e.g., INFO, WARN, ERROR), output formats (e.g., JSON), and direct logs to persistent storage or a log management service.
    javascript
    // Example logging within a catch block
    catch (error) {
      console.error({
        message: 'Error calling Vonage Check API',
        requestId: requestId, // Log relevant context
        error: error.message, // Log the error message
        stack: error.stack // Log stack trace for debugging
      });
      res.render('verify', { /* … render error view … */ });
    }
  • Retry Mechanisms:

    • Vonage Internal Retries: The Vonage Verify API itself handles retries for delivering the OTP (e.g., SMS → Voice Call → Voice Call) according to the chosen workflow (default is workflow 1). You generally don't need to implement application-level retries for code delivery.
    • API Call Retries: For transient network errors when calling the Vonage API (verify.start or verify.check), you could implement a simple retry mechanism with exponential backoff using libraries like async-retry. However, for this basic OTP flow, failing the request and asking the user to try again might be acceptable.

6. Database Schema and Data Layer (Integration Context)

While this standalone example doesn't require a database, integrating OTP verification into a real application typically involves linking the verification status to a user account.

  • Concept: After a successful OTP check (vonage.verify.check returns status '0'), update a flag in your user database record to indicate that the phone number associated with that user has been verified.

  • Example Schema (Conceptual – using Prisma syntax):

    prisma
    // Example schema.prisma (Conceptual)
    model User {
      id              String   @id @default(cuid())
      email           String   @unique
      passwordHash    String
      phoneNumber     String?  @unique // Store phone number used for OTP (E.164 format)
      isPhoneVerified Boolean  @default(false) // Flag set after successful OTP check
      createdAt       DateTime @default(now())
      updatedAt       DateTime @updatedAt
    }
  • Workflow:

    1. User initiates an action requiring phone verification (e.g., sign-up, profile update).
    2. Store the user's phone number (in E.164 format) in the User record.
    3. Initiate the Vonage Verify flow (/request-otp).
    4. User submits the code (/verify-otp).
    5. If vonage.verify.check is successful:
      • Find the user record associated with the phone number (or the logged-in user session).
      • Update isPhoneVerified to true.
      • Persist the change to the database.
    6. Proceed with the user's original action (e.g., complete sign-up, allow profile change).
  • Data Access: Use an ORM like Prisma or Sequelize to manage database interactions.

  • Migrations: Use the migration tools provided by your ORM (e.g., prisma migrate dev) to manage schema changes.


7. Adding Security Features for OTP Authentication

Security is paramount for authentication flows. When implementing SMS authentication, these security measures protect against common attack vectors.

  • Input Validation and Sanitization:

    • Phone Number: Validate the format rigorously before sending to Vonage. Use a library like libphonenumber-js to parse, validate, and normalize phone numbers to E.164 format. This prevents errors and potential abuse.

      javascript
      // Conceptual Example in /request-otp route using libphonenumber-js
      const parsePhoneNumber = require('libphonenumber-js');
      // … inside the route handler …
      let phoneNumberInput = req.body.number;
      let phoneNumberE164;
      
      try {
        const parsedNumber = parsePhoneNumber(phoneNumberInput); // Can add default country e.g., 'US'
        if (parsedNumber && parsedNumber.isValid()) {
          phoneNumberE164 = parsedNumber.number; // Get E.164 format (e.g., +14155552671)
          console.log(`Validated number: ${phoneNumberE164}`);
        } else {
          console.warn(`Invalid phone number input: ${phoneNumberInput}`);
          return res.render('request', { error: 'Invalid phone number format.' });
        }
      } catch (e) {
         console.error(`Error parsing phone number ${phoneNumberInput}:`, e);
         return res.render('request', { error: 'Invalid phone number format.' });
      }
      // Now use phoneNumberE164 when calling vonage.verify.start
      // const result = await vonage.verify.start({ number: phoneNumberE164, brand });
      // … rest of the route …
    • OTP Code: Ensure the code consists only of digits and matches the expected length (typically 4 or 6). Reject any non-numeric input immediately on the client-side (using pattern) and server-side.

  • Rate Limiting: Crucial to prevent abuse and control costs.

    • Limit OTP Requests: Prevent users (identified by IP address or potentially phone number prefix after validation) from requesting too many OTP codes in a short period. This mitigates SMS pumping fraud and annoyance.
    • Limit Verification Attempts: Prevent brute-force attacks on the OTP code itself. Limit the number of check attempts per requestId or IP address. Vonage has some built-in limits, but application-level limiting adds an extra layer.
    • Implementation: Use middleware like express-rate-limit.
    javascript
    // Example using express-rate-limit
    const rateLimit = require('express-rate-limit');
    
    const otpRequestLimiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 5, // Limit each IP to 5 OTP requests per windowMs
      message: 'Too many OTP 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
    });
    
    const verifyAttemptLimiter = rateLimit({
      windowMs: 5 * 60 * 1000, // 5 minutes
      max: 5, // Limit each IP to 5 verification attempts per windowMs (adjust as needed)
      message: 'Too many verification attempts, please try again later.',
      standardHeaders: true,
      legacyHeaders: false,
    });
    
    // Apply to routes in index.js BEFORE the route handlers
    app.post('/request-otp', otpRequestLimiter, async (req, res) => { /* … */ });
    app.post('/verify-otp', verifyAttemptLimiter, async (req, res) => { /* … */ });
  • HTTPS: Always use HTTPS in production to encrypt communication between the client and server. Configure your hosting environment or reverse proxy (like Nginx) accordingly.

  • Secrets Management: Use environment variables for API keys and secrets. Do not hardcode them. Use your deployment platform's secrets management features.

  • Session Management (if integrated): If used within a login flow, ensure secure session handling practices (e.g., using express-session with a secure store and appropriate cookie settings).


8. Handling Special Cases

Real-world scenarios often involve edge cases.

  • Phone Number Formatting: Always convert numbers to E.164 format (+ followed by country code and number, e.g., +14155552671) before sending to Vonage. The SDK might handle some variations, but explicit formatting using libraries like libphonenumber-js (as shown in Section 7) is much safer and more reliable.
  • Code Expiration: Vonage Verify codes expire (default 5 minutes, configurable). If a user tries to verify an expired code, the vonage.verify.check call will fail with an appropriate error status/text (e.g., status '16', error_text "The code provided does not match the expected value" or status '17' for request not found). Inform the user clearly that the code has expired and they may need to request a new one.
  • Cancellation: A user might want to cancel an ongoing verification request (e.g., they entered the wrong number). Implement this using vonage.verify.cancel(requestId). Provide a "Cancel" or "Resend Code" button in the UI that triggers a route calling this method. Note that calling verify.start again for the same number usually implicitly cancels the previous request.
  • Multiple Requests: If a user requests a code multiple times for the same number before verifying, Vonage handles this by invalidating previous codes when a new one is sent for the same requestId. Ensure your UI guides the user to use the latest code received.
  • SMS Delivery Delays: SMS is not instantaneous. Inform users that the code might take a minute or two to arrive. Vonage's fallback to voice calls (depending on workflow) helps mitigate delivery failures.
  • International Numbers: Vonage Verify supports global numbers, but ensure your UI correctly captures country codes (or use libphonenumber-js to help parse) and that your Vonage account has permissions/funds for international SMS if needed. E.164 formatting is essential here.

9. Implementing Performance Optimizations

For a standard OTP flow, performance bottlenecks are less common unless handling extremely high volume.

  • Asynchronous Operations: Node.js is naturally asynchronous. Ensure all I/O operations (like Vonage API calls, database interactions) use async/await or Promises correctly to avoid blocking the event loop. The provided code examples use async/await.
  • Efficient Database Queries: If integrating with a database (Section 6), ensure queries to find users or update verification status are properly indexed (e.g., on phoneNumber or userId).
  • Minimize Payload: Keep request/response payloads minimal.
  • Caching: Caching is generally not applicable or recommended for the OTP codes or verification status itself due to their short-lived and sensitive nature. Caching user session data might be relevant if integrating into a login flow.
  • Load Testing: For high-traffic applications, use tools like k6, Artillery, or JMeter to simulate load and identify potential bottlenecks in your routes or dependencies (especially rate limiting).

10. Adding Monitoring, Observability, and Analytics

Gain insights into how your OTP flow performs.

  • Logging: (Covered in Section 5) Ensure comprehensive logging of requests, successes, failures (with reasons), and errors using a structured logger in production.

  • Metrics: Track key performance indicators (KPIs):

    • Number of OTP requests (/request-otp calls).
    • Number of verification attempts (/verify-otp calls).
    • Verification success rate (successful checks / total checks).
    • Verification failure rate (broken down by error type, e.g., "wrong code," "expired," "throttled," "invalid number").
    • Latency of Vonage API calls (verify.start, verify.check).
    • Rate limit triggers.
    • Implementation: Use libraries like prom-client for Prometheus metrics or integrate with Application Performance Monitoring (APM) tools (Datadog, New Relic, Dynatrace).
  • Health Checks: Implement a simple /health endpoint that returns a 200 OK status if the server is running and basic checks pass (e.g., can initialize Vonage SDK).

    javascript
    // index.js (add health check route)
    app.get('/health', (req, res) => {
      // Add more sophisticated checks if needed (e.g., DB connection)
      res.status(200).send('OK');
    });
  • Error Tracking: Integrate services like Sentry, Bugsnag, or APM tools to automatically capture and report unhandled exceptions and errors in real-time, providing context like request IDs.

  • Dashboards: Create dashboards (e.g., in Grafana, Kibana, or your APM tool) visualizing the key metrics defined above to monitor the health, performance, and potential abuse patterns of the OTP system.


11. Troubleshooting and Caveats

Common issues and their solutions:

  • Error: Authentication failed / Status 1 or 2 (from Start/Check): Double-check VONAGE_API_KEY and VONAGE_API_SECRET in your environment variables. Ensure they are correct, have no extra spaces, and are loaded properly by dotenv or your platform's secret management.
  • Error: Invalid number / Status 3 (from Start): The phone number format is likely incorrect or invalid as perceived by Vonage. Ensure it's normalized to E.164 format (14155552671 – Vonage often prefers without the +) and is a valid, reachable number. Use libphonenumber-js for validation before calling Vonage. Check Vonage dashboard logs for the exact number sent.
  • Error: Your account does not have sufficient credit / Status 9 (from Start): Your Vonage account balance is too low. Add funds via the Vonage dashboard.
  • Error: Throttled / Status 6 (from Start) or 1 (from Check): You've hit Vonage's API rate limits or internal velocity checks. Implement application-level rate limiting (Section 7) and ensure it's working. If legitimate traffic is throttled, analyze patterns and potentially contact Vonage support.
  • Error: The code provided does not match the expected value / Status 16 (from Check): The user entered the wrong OTP code. Render the verify form again with an error message.
  • Error: Request '…' was not found or it has been verified already / Status 17 (from Check): The requestId is invalid, has expired (default 5 mins), or was already successfully verified. This can happen if the user takes too long, tries to reuse an old link/request, or double-submits the form. Handle by prompting the user to start the process over.
  • SMS Not Received:
    • Verify the phone number is correct (E.164 format) and reachable. Log the number being sent.
    • Check carrier filtering (less common with Vonage Verify's shared shortcodes/numbers, but possible).
    • Check Vonage account settings for any country restrictions.
    • Inform the user about potential delays and consider UI options like a "Resend Code" button (which triggers /request-otp again).

Frequently Asked Questions

How to implement two-factor authentication with Node.js?

Implement 2FA using SMS OTP by integrating the Vonage Verify API into your Node.js and Express application. This involves sending an OTP to the user's phone and verifying it upon entry, securing actions like logins and transactions beyond just passwords by adding the user's phone as a second factor.

What is the Vonage Verify API used for?

The Vonage Verify API simplifies OTP-based 2FA by handling code generation, delivery via SMS or voice, retries, code validation, and expiration. It streamlines the entire process within your Node.js application, providing a secure way to confirm user identity.

Why use SMS OTP for two-factor authentication?

SMS OTP enhances security by requiring something users know (password) and something they have (phone). This mitigates risks of unauthorized access even if passwords are compromised, as the OTP acts as a temporary, second verification factor.

When should I add phone number verification to my web app?

Add phone verification for actions needing enhanced security, such as login, password reset, sensitive transactions, or profile changes. This provides an extra layer of identity assurance, reducing the risk of fraud and unauthorized access.

Can I use libphonenumber-js with Vonage Verify?

Yes, using `libphonenumber-js` is highly recommended for parsing, validating, and normalizing phone numbers to E.164 format before sending them to the Vonage Verify API. This ensures compatibility, reduces errors, and mitigates security risks from incorrectly formatted numbers.

How to set up Vonage Verify API in Node.js?

Install the `@vonage/server-sdk`, `dotenv` package for Node.js. Store your API Key, Secret, and Brand Name in a `.env` file. Initialize the Vonage SDK with these credentials. The SDK then enables you to easily interact with the Verify API methods like `verify.start` and `verify.check`.

What is the request ID in Vonage Verify API?

The request ID, returned by `vonage.verify.start`, is a unique identifier for each OTP verification request. It is crucial for checking the entered OTP against the correct request using `vonage.verify.check`, ensuring code validity.

How to handle Vonage Verify API errors in Express?

Use `try...catch` blocks around Vonage API calls. Check the `result.status` from API responses (status '0' means success). Display user-friendly error messages from `result.error_text` in your views while logging detailed error information on the server for debugging.

How to resend OTP code with Vonage Verify API?

Resending an OTP involves calling `vonage.verify.start` again with the same phone number. This automatically cancels the previous request and sends a new code, ensuring users always use the latest received code.

What happens if Vonage Verify SMS is not received?

Vonage has fallback workflows (like voice calls) to manage SMS delivery failures. You can implement a "Resend Code" mechanism in your app and inform users that the code may take a few minutes to arrive due to potential delays.

How to prevent Vonage Verify API abuse?

Implement rate limiting using `express-rate-limit` middleware to restrict OTP requests and verification attempts per IP address or other identifiers. Validate phone number formats and sanitize user input to minimize incorrect requests.

What are best practices for storing Vonage API credentials?

Store Vonage API Key and Secret securely as environment variables in a `.env` file, which should be added to `.gitignore` to prevent it from being committed to version control. Do not hardcode API credentials in your application code.