This guide provides a step-by-step walkthrough for building a production-ready One-Time Password (OTP) Two-Factor Authentication (2FA) system using SMS delivery. We'll leverage Node.js with the Express framework and the Vonage Verify API for a robust and simplified implementation.
By the end of this tutorial, you will have a functional application that can:
- Accept a user's phone number.
- Initiate an OTP request to that number via SMS using the Vonage Verify API.
- Allow the user to submit the received OTP code.
- Verify the submitted code against the Vonage Verify API.
- Provide feedback on the verification status (success or failure).
This guide focuses on the core backend logic and API endpoints necessary for SMS OTP verification.
Project Overview and Goals
Goal: To implement a secure and reliable SMS-based 2FA mechanism for user verification within a Node.js/Express application.
Problem Solved: Standard password authentication is vulnerable. Adding 2FA via SMS OTP significantly enhances security by requiring users to possess their registered phone (""something they have"") in addition to their password (""something they know""). This guide provides the backend infrastructure for this second factor.
Technologies Used:
- Node.js: A JavaScript runtime environment ideal for building scalable network applications.
- Express.js: A minimal and flexible Node.js web application framework, providing robust features for web and mobile applications.
- Vonage Verify API: A purpose-built API by Vonage that handles OTP generation, delivery (including SMS, voice fallbacks, and retries), and verification logic, simplifying the developer's task.
@vonage/server-sdk
: The official Vonage Node.js SDK for interacting with Vonage APIs, including the Verify API.dotenv
: A module to load environment variables from a.env
file intoprocess.env
.body-parser
: Node.js middleware for parsing incoming request bodies. While newer versions of Express (4.16+) have built-in middleware (express.json()
,express.urlencoded()
), includingbody-parser
explicitly ensures compatibility or can make parsing setup clearer for some developers.
Why Vonage Verify API?
Instead of manually generating codes, managing delivery attempts, handling expiry, and implementing complex verification logic, the Vonage Verify API encapsulates this entire workflow. It provides:
- Secure code generation.
- Multi-channel delivery attempts (SMS primary, often with voice fallback).
- Built-in code expiry and retry logic.
- Simplified verification checks via API calls.
- Protection against certain types of abuse.
This significantly reduces development time and potential security pitfalls compared to building an OTP system from scratch.
System Architecture:
<!-- [Insert System Architecture Diagram Image Here - e.g., PNG or SVG] -->
Diagram Description:
- The user initiates the 2FA process (e.g., after login) by providing their phone number to the Node.js application via an API call.
- The Node.js application calls the Vonage Verify API's
start
endpoint with the user's number. - Vonage generates an OTP and sends it via SMS to the user's phone. Vonage returns a
request_id
to the Node.js application. - The Node.js application prompts the user (via the frontend) to enter the OTP code they received.
- The user submits the OTP code and implicitly identifies the verification attempt (e.g., via their session or phone number) back to the Node.js application via another API call.
- The Node.js application retrieves the relevant
request_id
and calls the Vonage Verify API'scheck
endpoint with therequest_id
and the submitted code. - Vonage verifies the code and returns the result (success or failure) to the Node.js application.
- The Node.js application informs the user of the outcome.
Prerequisites:
- Node.js and npm (or yarn): Installed on your development machine. (Download Node.js)
- Vonage API Account: A free account is sufficient to start. (Sign up for Vonage)
- You will need your API Key and API Secret found on the Vonage API Dashboard homepage after signing in.
- (Optional) Basic understanding of Express.js: Familiarity with routing and middleware helps.
- (Optional)
curl
or Postman: For testing the API endpoints.
1. Setting up the project
Let's create the basic project structure and install the necessary dependencies.
1.1 Create Project Directory:
Open your terminal or command prompt and create a new directory for your project.
mkdir vonage-otp-guide
cd vonage-otp-guide
1.2 Initialize Node.js Project:
Initialize the project using npm. The -y
flag accepts the default settings.
npm init -y
This creates a package.json
file.
1.3 Install Dependencies:
Install Express, the Vonage Server SDK, dotenv
for environment variables, and body-parser
.
npm install express @vonage/server-sdk dotenv body-parser
express
: The web framework.@vonage/server-sdk
: To interact with the Vonage APIs.dotenv
: To manage sensitive credentials like API keys.body-parser
: Middleware to parse JSON request bodies. (Note: While newer Express versions includeexpress.json()
andexpress.urlencoded()
, explicitly includingbody-parser
remains common for clarity or compatibility reasons.)
1.4 Create Project Structure:
Create the following basic file structure:
vonage-otp-guide/
├── node_modules/
├── .env # For environment variables (API keys) - DO NOT COMMIT TO GIT
├── .gitignore # To exclude node_modules and .env from Git
├── index.js # Main application file
├── package.json
└── package-lock.json
1.5 Configure .gitignore
:
Create a .gitignore
file in the root directory and add the following lines to prevent committing sensitive information and unnecessary files:
# .gitignore
node_modules
.env
1.6 Set up Environment Variables (.env
):
Create a file named .env
in the root directory. Find your API Key and API Secret on the Vonage API Dashboard.
# .env
VONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET
# Optional: Set a default port
PORT=3000
Replace YOUR_API_KEY
and YOUR_API_SECRET
with your actual credentials from the Vonage dashboard.
Purpose: Using environment variables is crucial for security. It prevents hardcoding sensitive credentials directly into your source code. dotenv
loads these variables into process.env
when the application starts.
2. Implementing Core Functionality (Vonage Verify API)
Now, let's write the core logic in index.js
to interact with the Vonage Verify API.
2.1 Initialize Express and Vonage SDK:
Open index.js
and add the following setup code:
// index.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const bodyParser = require('body-parser');
const { Vonage } = require('@vonage/server-sdk');
// --- Configuration ---
const app = express();
const port = process.env.PORT || 3000; // Use port from .env or default to 3000
// --- Vonage Client Initialization ---
// IMPORTANT: Ensure VONAGE_API_KEY and VONAGE_API_SECRET are set in your .env file
if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET) {
console.error('_ Error: VONAGE_API_KEY or VONAGE_API_SECRET not found in .env file.');
console.error('Please add them to your .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 ---
// Parse JSON bodies (as sent by API clients)
app.use(bodyParser.json());
// Parse URL-encoded bodies (as sent by HTML forms)
app.use(bodyParser.urlencoded({ extended: true }));
// In-memory storage for demo purposes.
// Production apps should use a database or persistent cache (e.g., Redis).
const verificationRequests = {}; // Store { phoneNumber: requestId } temporarily
// --- Routes (will be added below) ---
// ...
// --- Start Server ---
app.listen(port, () => {
console.log(`_ Server running on http://localhost:${port}`);
});
Explanation:
require('dotenv').config();
: Loads variables from the.env
file. Crucial to do this early.- Imports
express
,body-parser
, and theVonage
class from the SDK. - Initializes the
express
app and sets theport
. - Crucially, initializes the
Vonage
client using the API key and secret loaded fromprocess.env
. Includes a check to ensure the keys are present. - Sets up
bodyParser
middleware to correctly parse incoming JSON and URL-encoded request bodies. verificationRequests
: A simple JavaScript object used as temporary storage to map phone numbers to therequest_id
returned by Vonage. Note: This is for demonstration only. In a production system, you would typically store thisrequest_id
associated with the user's session or in a more persistent store like a database or Redis cache, linked to the user attempting verification.- Starts the Express server listening on the configured port.
2.2 Implement OTP Request Route (/request-otp
):
Add the following route handler in index.js
within the // --- Routes ---
section:
// index.js (continued)
// --- Routes ---
/**
* @route POST /request-otp
* @description Initiates an OTP verification request via SMS.
* @param {string} phoneNumber - The user's phone number in E.164 format (e.g., +14155552671).
* @returns {object} 200 - Success message (OTP sent).
* @returns {object} 400 - Bad request (e.g., missing phone number).
* @returns {object} 500 - Server error or Vonage API error.
*/
app.post('/request-otp', async (req, res) => {
const { phoneNumber } = req.body;
if (!phoneNumber) {
return res.status(400).json({ error: 'Phone number is required.' });
}
console.log(`__ Requesting OTP for: ${phoneNumber}`);
try {
const result = await vonage.verify.start({
number: phoneNumber,
brand: 'YourAppName' // IMPORTANT: Customize this! Appears in the SMS message.
// Optional parameters like 'code_length', 'workflow_id' can be added here
// See Vonage Docs: https://developer.vonage.com/api/verify#startVerification
});
if (result.status === '0') { // Status '0' indicates success
// Store the request_id temporarily, associated with the phone number
// In production, associate this with a user session or DB record
verificationRequests[phoneNumber] = result.request_id;
console.log(`_ OTP request successful. Request ID: ${result.request_id}`);
// IMPORTANT: Do NOT send the request_id to the client-side directly for security reasons
// The client only needs to know the request was initiated.
res.status(200).json({ message: 'OTP requested successfully. Check your phone.' });
} else {
// Handle Vonage API errors (returned in the result object)
console.error(`_ Vonage Verify API Error: Status ${result.status} - ${result.error_text}`);
// Map Vonage status to appropriate HTTP status if desired, otherwise use 500 for upstream issues
res.status(500).json({ error: `Failed to request OTP. ${result.error_text}` });
}
} catch (error) {
// Handle errors thrown by the SDK or network issues
console.error('_ Server Error requesting OTP:', error);
res.status(500).json({ error: 'An unexpected server error occurred while requesting the OTP.' });
}
});
// ... (other routes and server start)
Explanation:
- Defines a
POST
route at/request-otp
. - Extracts
phoneNumber
from the JSON request body (req.body
). Validates its presence. - Calls
vonage.verify.start()
:number
: The destination phone number (ideally in E.164 format, e.g.,+14155552671
).brand
: A name that identifies your application in the SMS message (e.g., ""YourAppName Code is 1234""). You MUST customize this value.
- Uses
async/await
to handle the promise returned by the SDK. - Checks
result.status
:'0'
means success. Other statuses indicate errors (see Vonage Verify API documentation for details). - On Success (
status === '0'
):- Stores the returned
result.request_id
in our temporaryverificationRequests
object, keyed by the phone number. - Logs the success and
request_id
server-side. - Returns a generic success message to the client. Crucially, do not send the
request_id
back to the client in this response. The client doesn't need it yet, and exposing it unnecessarily could be a minor security risk. The frontend should prompt the user for the code based on this success response.
- Stores the returned
- On Vonage Error (
status !== '0'
): Logs the specific error text from Vonage and returns a 500 status (or a more specific one if you map Vonage errors) with the error message. - On Server/SDK Error (e.g., network issue, SDK exception): Catches exceptions thrown during the API call and returns a generic 500 error.
2.3 Implement OTP Check Route (/check-otp
):
Add the route handler for verifying the code submitted by the user:
// index.js (continued)
/**
* @route POST /check-otp
* @description Verifies the OTP code submitted by the user.
* @param {string} phoneNumber - The user's phone number (used to retrieve request_id).
* @param {string} code - The OTP code entered by the user.
* @returns {object} 200 - Verification successful.
* @returns {object} 400 - Bad request (e.g., missing fields, invalid code format).
* @returns {object} 401 - Unauthorized (verification failed - wrong code, expired, etc.).
* @returns {object} 404 - Not Found (request ID not found for the phone number).
* @returns {object} 500 - Server error or Vonage API error.
*/
app.post('/check-otp', async (req, res) => {
const { phoneNumber, code } = req.body;
if (!phoneNumber || !code) {
return res.status(400).json({ error: 'Phone number and code are required.' });
}
// Retrieve the request_id associated with the phone number
const requestId = verificationRequests[phoneNumber];
if (!requestId) {
// This could happen if the request never started or expired from our temporary store
console.warn(`__ No pending verification found for: ${phoneNumber}`);
return res.status(404).json({ error: 'No pending verification found for this number, or it has expired. Please request a new code.' });
}
console.log(`__ Verifying OTP for: ${phoneNumber} with code: ${code} (Request ID: ${requestId})`);
try {
const result = await vonage.verify.check(requestId, code);
if (result.status === '0') { // Status '0' indicates successful verification
console.log(`_ Verification successful for: ${phoneNumber}`);
// Verification successful - clear the stored request ID
delete verificationRequests[phoneNumber];
// In a real app: Update user status to verified, grant access, etc.
res.status(200).json({ message: 'Verification successful!' });
} else {
// Handle Vonage verification failures (wrong code, expired, etc.)
console.warn(`__ Verification failed for ${phoneNumber}: Status ${result.status} - ${result.error_text}`);
// Map non-zero status codes to 401 Unauthorized or other relevant client errors
res.status(401).json({ error: `Verification failed. ${result.error_text}` });
// Optionally, clear the request ID on failure too, depending on your retry logic
// e.g., if status is '6' (wrong code), maybe keep it for retries?
// if status is '16' (expired) or '17' (too many attempts), definitely clear it:
if (['16', '17'].includes(result.status)) {
delete verificationRequests[phoneNumber];
}
}
} catch (error) {
// Handle errors thrown by the SDK (e.g., invalid request_id format) or network issues
// The Vonage SDK might throw errors with specific properties, or generic Errors.
// Log the actual error for diagnostics.
console.error('_ Server Error checking OTP:', error.message || error);
// Check if it looks like a Vonage API error response structure, though this might vary
if (error.body && error.body.error_text) {
console.error(`_ Vonage Check API Error Detail: ${error.statusCode} - ${error.body.error_text}`);
res.status(error.statusCode || 500).json({ error: `Verification check failed. ${error.body.error_text}` });
} else {
// Generic server error
res.status(500).json({ error: 'An unexpected server error occurred during verification.' });
}
// Optionally, clear the request ID on server error too, although the state might be uncertain
// delete verificationRequests[phoneNumber];
}
});
// ... (server start)
Explanation:
- Defines a
POST
route at/check-otp
. - Extracts
phoneNumber
and the submittedcode
from the request body. Validates their presence. - Retrieves
requestId
: Looks up therequest_id
from our temporaryverificationRequests
storage using thephoneNumber
. Returns a 404 if not found (the user might be trying to verify without requesting first, or the temporary entry expired/was cleared). - Calls
vonage.verify.check()
:- Passes the retrieved
requestId
. - Passes the
code
submitted by the user.
- Passes the retrieved
- Uses
async/await
for the promise. - Checks
result.status
:'0'
means the code was correct and the verification succeeded. - On Success (
status === '0'
):- Logs the success.
- Important: Removes the
requestId
entry fromverificationRequests
to prevent reuse. - Returns a success message. In a real application, this is where you'd flag the user's session as verified, grant access to protected resources, etc.
- On Vonage Failure (
status !== '0'
):- Logs the failure reason (e.g., wrong code, request expired).
- Returns a 401 Unauthorized status (appropriate for failed authentication). Provides the Vonage error text.
- Includes logic to potentially clear the
requestId
on certain final failures like expiry or too many attempts.
- On Server/SDK Error: Catches exceptions. Logs the error message. Attempts to provide specific Vonage error details if the caught error object seems to contain them, otherwise returns a generic 500.
3. Building a complete API layer
We have already defined the core API endpoints:
POST /request-otp
: Initiates the verification process.POST /check-otp
: Verifies the submitted code.
API Documentation & Testing Examples:
Here are examples using curl
to test the endpoints. Replace placeholders accordingly.
3.1 Test Requesting an OTP:
(Replace +14155552671
with a real phone number you can receive SMS on, in E.164 format)
curl -X POST http://localhost:3000/request-otp \
-H ""Content-Type: application/json"" \
-d '{""phoneNumber"": ""+14155552671""}'
Expected Success Response (200 OK):
{
""message"": ""OTP requested successfully. Check your phone.""
}
(You should receive an SMS on the target phone)
Example Error Response (400 Bad Request - Missing phone number):
curl -X POST http://localhost:3000/request-otp \
-H ""Content-Type: application/json"" \
-d '{}'
{
""error"": ""Phone number is required.""
}
3.2 Test Checking an OTP:
(Use the same phone number as above and the code received via SMS)
curl -X POST http://localhost:3000/check-otp \
-H ""Content-Type: application/json"" \
-d '{""phoneNumber"": ""+14155552671"", ""code"": ""1234""}' # Replace 1234 with the actual code
Expected Success Response (200 OK):
{
""message"": ""Verification successful!""
}
Example Error Response (401 Unauthorized - Wrong Code):
(Submit an incorrect code)
curl -X POST http://localhost:3000/check-otp \
-H ""Content-Type: application/json"" \
-d '{""phoneNumber"": ""+14155552671"", ""code"": ""0000""}'
{
""error"": ""Verification failed. The code provided does not match the expected value.""
}
Example Error Response (404 Not Found - No pending request):
(Try to verify without requesting first, or after a successful verification)
curl -X POST http://localhost:3000/check-otp \
-H ""Content-Type: application/json"" \
-d '{""phoneNumber"": ""+14155559999"", ""code"": ""1234""}' # Use a number not requested or already verified
{
""error"": ""No pending verification found for this number, or it has expired. Please request a new code.""
}
Authentication/Authorization:
This basic guide doesn't implement user authentication before allowing 2FA setup/verification. In a real application:
- The
/request-otp
endpoint should likely be protected. A user should typically be logged in (first factor authenticated) before they can request an OTP for their registered phone number. - The
/check-otp
endpoint also needs context, usually tied to the user's session who initiated the request. You wouldn't want one user verifying another user's OTP attempt. - Use standard authentication middleware (e.g., JWT, sessions) to protect these routes and ensure the actions are performed by the correct, authenticated user.
4. Integrating with Vonage (Third-Party Service)
We've already integrated the Vonage SDK. This section details the configuration aspects.
Configuration Steps:
- Sign up for Vonage: Create an account at Vonage.
- Get API Credentials: Navigate to your Vonage API Dashboard. Your API Key and API Secret are displayed prominently near the top.
- Store Credentials Securely: Copy the API Key and Secret into the
.env
file in your project root, as shown in Step 1.6.# .env VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET PORT=3000
- Load Credentials in Code: Use the
dotenv
package (require('dotenv').config();
) at the start of yourindex.js
to load these intoprocess.env
. - Initialize SDK: Instantiate the Vonage client using these environment variables:
const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET });
Environment Variables Explained:
VONAGE_API_KEY
: Your public Vonage API key. Identifies your account when making API requests.- How to obtain: From the main page of the Vonage API Dashboard.
- Format: Alphanumeric string (e.g.,
a1b2c3d4
).
VONAGE_API_SECRET
: Your private Vonage API secret. Used to authenticate your API requests. Keep this confidential.- How to obtain: From the main page of the Vonage API Dashboard.
- Format: Longer alphanumeric string (e.g.,
e5f6g7h8i9j0k1l2
).
PORT
(Optional): The port your Express server will listen on.- Format: Number (e.g.,
3000
).
- Format: Number (e.g.,
Fallback Mechanisms:
The Vonage Verify API has built-in fallback mechanisms. By default (workflow_id: 1
), it tries:
- SMS
- Text-To-Speech (TTS) call after a delay
- Another TTS call after a further delay
If the user doesn't receive the SMS, they might receive a phone call reading out the code. You can customize this behavior using different workflow_id
values when calling vonage.verify.start()
. Consult the Vonage Verify API documentation for available workflows. For service outages on Vonage's side, you would need broader application-level error handling or potentially integrate a secondary provider (which adds significant complexity).
5. Error Handling, Logging, and Retry Mechanisms
Error Handling Strategy:
- Input Validation: Check for required fields (
phoneNumber
,code
) early and return 400 Bad Request errors. Use validation libraries for robustness (see Security section). - API Call Errors: Use
try...catch
blocks around allvonage
SDK calls to handle network issues or unexpected SDK exceptions. - Vonage Status Codes: Inspect the
result.status
andresult.error_text
from Vonage responses within thetry
block. Map non-'0' statuses to appropriate HTTP error codes (e.g., 401 for failed verification, 500 or 502 for upstream Vonage issues). - Server Errors: The
catch
block handles unexpected exceptions. Log detailed error information and return a generic 500 Internal Server Error.
Logging:
-
We are using basic
console.log
,console.warn
, andconsole.error
. -
Production Logging: Use a dedicated logging library like
winston
orpino
.- Configure different log levels (info, warn, error, debug).
- Log in a structured format (like JSON) for easier parsing by log analysis tools (e.g., ELK Stack, Datadog, Splunk).
- Include timestamps, potentially correlation IDs (if tracing requests), and relevant context (like
phoneNumber
orrequestId
where appropriate, being mindful of PII). - Log key events: OTP requested, OTP verification attempt, success, failure (with Vonage status code).
// Example using console (basic structured logging concept) console.info(JSON.stringify({ timestamp: new Date().toISOString(), level: 'info', message: 'OTP Requested', phoneNumber: phoneNumber })); console.warn(JSON.stringify({ timestamp: new Date().toISOString(), level: 'warn', message: 'Verification failed', phoneNumber: phoneNumber, vonageStatus: result.status, vonageError: result.error_text })); console.error(JSON.stringify({ timestamp: new Date().toISOString(), level: 'error', message: 'Server Error checking OTP', error: error.message, stack: error.stack }));
Retry Mechanisms:
- Vonage Handles Delivery Retries: The Verify API itself handles retrying SMS/voice delivery according to the chosen workflow. You generally do not need to implement retries in your application for the delivery of the code via SMS/voice.
- API Call Retries (Your App -> Vonage): For transient network errors when your server calls the Vonage API (
start
orcheck
), you could implement a retry mechanism (e.g., using a library likeasync-retry
or a simple loop with exponential backoff) within yourcatch
blocks for specific error types (like network timeouts). However:- Retrying
start
: Be cautious, as this might trigger multiple OTPs (though Vonage might cancel previous ones). Usually better to let the user retry manually. - Retrying
check
: Generally safer, but ensure idempotency if possible. - Given the Verify API's robustness, manual retries for API calls are often unnecessary unless you observe frequent transient network issues between your server and Vonage.
- Retrying
- User Retries: The primary ""retry"" is handled by the user requesting a new code if the first one fails, doesn't arrive, or expires. Implement rate limiting (see Security section) to prevent abuse of the ""request new code"" feature (
/request-otp
).
Testing Error Scenarios:
- Provide no phone number to
/request-otp
-> Expect 400. - Provide an invalid phone number format to
/request-otp
-> Expect Vonage API error (e.g., status '3') returned as 500 or mapped error. - Provide no code or phone number to
/check-otp
-> Expect 400. - Provide the wrong code to
/check-otp
-> Expect 401 (Vonage status '6'). - Wait for the code to expire (default 5 mins) and try
/check-otp
-> Expect 401 (Vonage status '16'). - Try to verify a
request_id
that doesn't exist (e.g., after successful check) -> Expect 404 from your API. - Try to verify with an invalid
request_id
format -> Expect SDK error caught and returned as 500, or potentially a specific Vonage error (status '101'). - Temporarily disable network or use invalid Vonage credentials -> Expect 500 server error or specific auth errors (status '4') from Vonage.
6. Database Schema and Data Layer
For this basic implementation, we used a simple in-memory JavaScript object (verificationRequests
). This is not suitable for production.
Why a Persistent Store is Needed:
- Statelessness: Web servers should ideally be stateless. If the server restarts, the in-memory object is lost, and pending verifications cannot be completed.
- Scalability: If you run multiple instances of your application behind a load balancer, an in-memory object on one instance won't be accessible by others.
- Association with User: You need to link the
request_id
to the specific user session or user account attempting verification, not just the phone number (as multiple users might share a number temporarily, or one user might have multiple sessions).
Recommended Approach (using a Database or Cache):
-
Database Schema (Conceptual Example): If you have a
Users
table, you might add columns to temporarily store verification details, or use a separate table.Adding to User Table (Example - PostgreSQL): Note:
SERIAL
is PostgreSQL-specific; useAUTO_INCREMENT
(MySQL) or equivalent for other databases.-- Example PostgreSQL Schema Snippet CREATE TABLE Users ( user_id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, phone_number VARCHAR(50) UNIQUE, -- Store verified number password_hash VARCHAR(255) NOT NULL, -- ... other user fields pending_verification_request_id VARCHAR(50), -- Store the Vonage request_id verification_request_expires_at TIMESTAMPTZ -- Optional: Store expiry based on Vonage TTL );
-
Data Layer Logic:
- On
/request-otp
:- Authenticate the user (ensure they are logged in). Identify the
user_id
. - Call
vonage.verify.start()
with the user's registered phone number. - Store the returned
request_id
(and optionally calculate/store expiry time) in thepending_verification_request_id
column for that specificuser_id
. Clear any previous pending ID for that user.
- Authenticate the user (ensure they are logged in). Identify the
- On
/check-otp
:- Authenticate the user, get
user_id
. - Retrieve the
pending_verification_request_id
from the user's record in the database based onuser_id
. If not found, return 404/error. - (Optional but Recommended): Check
verification_request_expires_at
if stored, return error if expired before calling Vonage. - Call
vonage.verify.check()
with the retrievedrequest_id
and the user-submittedcode
. - On Success: Clear the
pending_verification_request_id
(set toNULL
) and expiry in the database. Update the user's session/state to indicate 2FA completion. - On Failure: Handle the error. Consider clearing the
pending_verification_request_id
after too many failed attempts or expiry reported by Vonage.
- Authenticate the user, get
- On
-
Alternative (Cache - e.g., Redis):
- Store the mapping in Redis. A key could be
verification:user_id:<user_id>
containing therequest_id
, orverification:request_id:<request_id>
containing theuser_id
. - Set an expiry time on the Redis key (e.g., 5-10 minutes) slightly longer than the Vonage code validity to handle clock skew.
- This avoids modifying the main user table schema and leverages Redis's speed and built-in expiry. Lookups would typically be by
user_id
to retrieve the expectedrequest_id
.
- Store the mapping in Redis. A key could be
Migrations/Data Access:
Use an ORM (like Sequelize, Prisma, TypeORM) or a query builder (like Knex.js) to manage database interactions and schema migrations in a structured way.
- Prisma Example: Define schema in
schema.prisma
, useprisma migrate dev
to apply changes, use Prisma Client for type-safe data access. - Sequelize Example: Define models, use Sequelize CLI for migrations (
sequelize db:migrate
), use model methods.