code examples
code examples
Build OTP/2FA with MessageBird Verify API: Complete Vite + React/Vue Guide
Implement secure SMS-based OTP two-factor authentication using MessageBird Verify API with Node.js backend and Vite (React/Vue) frontend. Complete tutorial with code generation, verification, and security best practices.
Build Secure OTP/2FA with MessageBird Verify API: Complete Vite + React/Vue Integration Guide
Build a production-ready SMS-based One-Time Password (OTP) two-factor authentication (2FA) system using MessageBird's Verify API with a Node.js/Express backend and modern Vite-powered frontend (React or Vue). Implement secure code generation, verification flows, and handle edge cases for real-world applications.
What is OTP/2FA and Why MessageBird?
One-Time Password (OTP) authentication sends a temporary numeric code (typically 4–10 digits) to a user's mobile phone via SMS. Users must enter this code within a limited timeframe (usually 30 seconds to 2 days) to verify their identity. Two-Factor Authentication (2FA) adds this second verification layer beyond username/password, significantly reducing unauthorized access even if passwords are compromised (NIST SP 800-63B recommends 2FA for sensitive applications as of 2023).
MessageBird's Verify API simplifies OTP implementation by:
- Automatically generating secure random tokens (6–10 digits, configurable)
- Handling message delivery via SMS, voice (TTS), or email
- Managing token expiration (30 seconds to 2 days)
- Tracking verification attempts (1–10 configurable limit)
- Providing status tracking (sent, verified, expired, failed)
According to MessageBird's official Verify API documentation, the service handles token generation server-side, eliminating client-side security risks and simplifying implementation compared to manual OTP solutions.
Project Overview and Goals
Goal: Build a full-stack OTP/2FA system with:
- Backend (Node.js/Express): Three API endpoints for generating OTP codes (
/api/otp/generate), verifying codes (/api/otp/verify), and viewing verification status (/api/otp/status) - Frontend (Vite + React or Vue): Phone number input form, OTP code entry interface with auto-focus, and verification status display
- Security: Rate limiting, attempt tracking, secure token comparison, and proper error handling
Problem Solved: Adds robust two-factor authentication to protect user accounts, secure transactions, verify phone numbers during registration, and meet compliance requirements for financial services, healthcare, or e-commerce applications.
Technologies Used:
- Node.js: JavaScript runtime for backend server
- Express: Web application framework for API routing
messagebirdnpm package: Official MessageBird SDK for Node.js (v4.0.1 as of January 2022, Apache-2.0 license, GitHub repository)dotenv: Environment variable management- Vite: Next-generation frontend build tool (faster than webpack/CRA)
- React or Vue 3: Modern frontend framework for UI components
axios: HTTP client for API requests from frontendexpress-rate-limit: Protection against brute-force attacks
System Architecture:
The complete OTP flow involves both frontend and backend coordination:
sequenceDiagram
participant User
participant Frontend as Vite Frontend<br/>(React/Vue)
participant Backend as Node.js/Express API
participant MessageBird as MessageBird Verify API
participant Phone as User's Phone
User->>Frontend: 1. Enter phone number
Frontend->>Backend: 2. POST /api/otp/generate {phone}
Backend->>MessageBird: 3. Create Verify object
MessageBird->>Phone: 4. Send SMS with OTP code
MessageBird-->>Backend: 5. Return verify ID, status
Backend-->>Frontend: 6. Return {verifyId, expiry}
Frontend->>User: 7. Show OTP input form
User->>Frontend: 8. Enter OTP code
Frontend->>Backend: 9. POST /api/otp/verify {verifyId, token}
Backend->>MessageBird: 10. Verify token
MessageBird-->>Backend: 11. Return status (verified/failed)
Backend-->>Frontend: 12. Return success/error
Frontend->>User: 13. Show result (access granted/denied)Prerequisites:
- Node.js and npm: Version 14.x or higher (nodejs.org)
- MessageBird Account: Free account with API key from MessageBird Dashboard (includes free test credits)
- MessageBird Live API Key: Required for Verify API (test keys don't support verification). Create at Dashboard → Developers → API access (REST)
- Phone Number Format: E.164 international format required (e.g., +14155552671 for US, +447700900000 for UK)
- Basic Knowledge: JavaScript, async/await, RESTful APIs, React or Vue basics
- (Optional) Testing Tools: Postman or curl for backend API testing
Expected Outcome:
- Backend server with three OTP endpoints handling generation, verification, and status checks
- Frontend application with phone input form and OTP code entry interface
- Complete security implementation with rate limiting, attempt tracking, and expiration handling
- Production-ready error handling for common failures (invalid numbers, expired codes, max attempts exceeded)
1. Setting up the Backend Project
Initialize the Node.js backend with MessageBird SDK and security dependencies.
Backend Setup Steps
-
Create Backend Directory:
bashmkdir messagebird-otp-backend cd messagebird-otp-backend npm init -y -
Enable ES Modules: Edit
package.jsonto add:json{ "type": "module", "scripts": { "dev": "nodemon src/server.js", "start": "node src/server.js" } } -
Install Backend Dependencies:
bashnpm install express messagebird dotenv express-rate-limit cors npm install -D nodemonmessagebird: Official SDK (v4.0.1, npm package)express-rate-limit: Prevent brute-force OTP guessing attackscors: Enable frontend-backend communication during development
-
Create Project Structure:
bashmkdir src touch src/server.js src/messagebirdService.js .env .gitignoreStructure:
messagebird-otp-backend/ ├── node_modules/ ├── src/ │ ├── server.js # Express server with OTP endpoints │ └── messagebirdService.js # MessageBird Verify API wrapper ├── .env # API keys (DO NOT COMMIT) ├── .gitignore ├── package.json └── package-lock.json -
Configure
.gitignore:textnode_modules .env .DS_Store -
Configure
.env:env# MessageBird Credentials MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY_HERE # OTP Configuration OTP_TIMEOUT=300 # 5 minutes (300 seconds) OTP_TOKEN_LENGTH=6 # 6-digit codes OTP_MAX_ATTEMPTS=3 # Maximum verification attempts # Server Configuration PORT=3001 NODE_ENV=developmentImportant: MessageBird Verify API requires a live API key (not test mode). Get yours at MessageBird Dashboard → Developers → API access. According to MessageBird's Verify API documentation, test keys do not support verification functionality (effective as of 2024).
2. Implementing MessageBird Verify Service
Create a service module to interact with MessageBird's Verify API for generating and verifying OTP codes.
Edit src/messagebirdService.js:
// src/messagebirdService.js
import messagebird from 'messagebird';
import dotenv from 'dotenv';
dotenv.config();
// Validate API key presence
if (!process.env.MESSAGEBIRD_API_KEY) {
console.error('Error: MESSAGEBIRD_API_KEY is not set in .env file');
process.exit(1);
}
// Initialize MessageBird client
const client = messagebird(process.env.MESSAGEBIRD_API_KEY);
// Configuration from environment variables with defaults
const OTP_CONFIG = {
timeout: parseInt(process.env.OTP_TIMEOUT) || 300, // Default: 5 minutes
tokenLength: parseInt(process.env.OTP_TOKEN_LENGTH) || 6, // Default: 6 digits
maxAttempts: parseInt(process.env.OTP_MAX_ATTEMPTS) || 3 // Default: 3 attempts
};
/**
* Generate and send OTP code to phone number using MessageBird Verify API
* @param {string} phoneNumber - E.164 format phone number (e.g., +14155552671)
* @returns {Promise<object>} - Verify object with id, status, validUntil
* @throws {Error} - If verification creation fails
*/
export const generateOTP = async (phoneNumber) => {
return new Promise((resolve, reject) => {
// Create verification request with MessageBird
client.verify.create(phoneNumber, {
originator: 'YourApp', // Sender ID (max 11 chars, alphanumeric not supported in US)
template: 'Your verification code is %token.', // %token replaced with generated code
type: 'sms', // Options: sms, flash, tts (voice), email
timeout: OTP_CONFIG.timeout, // Expiration time in seconds (30-172800)
tokenLength: OTP_CONFIG.tokenLength, // Code length (6-10 digits)
datacoding: 'plain' // Options: plain (GSM), unicode, auto
}, (error, response) => {
if (error) {
console.error('MessageBird Verify creation error:', error);
// Common errors: invalid number format, insufficient balance, API key issues
return reject(new Error(error.errors?.[0]?.description || 'Failed to generate OTP'));
}
console.log('OTP generated successfully:', {
id: response.id,
recipient: response.recipient,
status: response.status,
validUntil: response.validUntilDatetime
});
resolve({
verifyId: response.id,
status: response.status,
validUntil: response.validUntilDatetime,
recipient: response.recipient
});
});
});
};
/**
* Verify OTP code entered by user
* @param {string} verifyId - The verification ID from generateOTP
* @param {string} token - The code entered by the user
* @returns {Promise<object>} - Verification result with status
* @throws {Error} - If verification fails (wrong code, expired, max attempts)
*/
export const verifyOTP = async (verifyId, token) => {
return new Promise((resolve, reject) => {
client.verify.verify(verifyId, token, (error, response) => {
if (error) {
console.error('MessageBird verification error:', error);
// Common errors: token mismatch, expired verification, too many attempts
const errorMsg = error.errors?.[0]?.description || 'Verification failed';
return reject(new Error(errorMsg));
}
console.log('OTP verified successfully:', {
id: response.id,
status: response.status
});
resolve({
verifyId: response.id,
status: response.status, // Will be "verified" on success
recipient: response.recipient
});
});
});
};
/**
* Get current status of a verification (for checking expiry, attempts)
* @param {string} verifyId - The verification ID
* @returns {Promise<object>} - Verification object with current status
*/
export const getVerifyStatus = async (verifyId) => {
return new Promise((resolve, reject) => {
client.verify.view(verifyId, (error, response) => {
if (error) {
console.error('MessageBird verify status error:', error);
return reject(new Error(error.errors?.[0]?.description || 'Failed to get status'));
}
resolve({
verifyId: response.id,
status: response.status, // sent, verified, expired, failed
createdAt: response.createdDatetime,
validUntil: response.validUntilDatetime,
recipient: response.recipient
});
});
});
};
/**
* Delete/cancel a verification (useful for cleanup or resend flows)
* @param {string} verifyId - The verification ID to delete
*/
export const deleteVerification = async (verifyId) => {
return new Promise((resolve, reject) => {
client.verify.delete(verifyId, (error) => {
if (error) {
console.error('MessageBird verification deletion error:', error);
return reject(new Error('Failed to delete verification'));
}
resolve({ deleted: true });
});
});
};Key Implementation Notes:
-
Timeout Range: MessageBird supports 30–172,800 seconds (30s to 2 days). For security, 5–10 minutes is recommended for most applications (NIST SP 800-63B suggests short-lived OTP tokens).
-
Token Length: 6 digits provides 1 million combinations. With
maxAttempts=3, brute-force probability is 0.0003% per verification session (3/1,000,000). -
Originator Restrictions: Alphanumeric sender IDs (e.g., "YourApp") are not supported in the United States and require pre-registration in many countries. Use a MessageBird virtual number for reliable delivery (MessageBird sender ID guide).
3. Building the API Endpoints
Create Express server with three OTP endpoints: generate, verify, and status.
Edit src/server.js:
// src/server.js
import express from 'express';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import dotenv from 'dotenv';
import { generateOTP, verifyOTP, getVerifyStatus } from './messagebirdService.js';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(express.json());
app.use(cors({
origin: process.env.NODE_ENV === 'production'
? 'https://your-frontend-domain.com'
: 'http://localhost:5173' // Vite default port
}));
// Rate limiting for OTP generation (prevent SMS bombing)
const otpGenerateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Max 5 OTP requests per phone per window
message: { success: false, message: 'Too many OTP requests. Try again later.' },
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.body.phoneNumber || req.ip // Rate limit by phone number
});
// Rate limiting for OTP verification (prevent brute force)
const otpVerifyLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // Max 10 verification attempts per window
message: { success: false, message: 'Too many verification attempts. Try again later.' },
standardHeaders: true,
legacyHeaders: false
});
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
service: 'MessageBird OTP API'
});
});
// --- OTP Generation Endpoint ---
app.post('/api/otp/generate', otpGenerateLimiter, async (req, res) => {
const { phoneNumber } = req.body;
// Validate phone number presence
if (!phoneNumber) {
return res.status(400).json({
success: false,
message: 'Phone number is required'
});
}
// Validate E.164 format (starts with +, followed by 10-15 digits)
const e164Regex = /^\+[1-9]\d{10,14}$/;
if (!e164Regex.test(phoneNumber)) {
return res.status(400).json({
success: false,
message: 'Invalid phone number format. Use E.164 format (e.g., +14155552671)'
});
}
try {
const result = await generateOTP(phoneNumber);
res.status(200).json({
success: true,
message: 'OTP sent successfully',
data: {
verifyId: result.verifyId,
validUntil: result.validUntil,
status: result.status
}
});
} catch (error) {
console.error('OTP generation error:', error.message);
res.status(500).json({
success: false,
message: 'Failed to send OTP',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
// --- OTP Verification Endpoint ---
app.post('/api/otp/verify', otpVerifyLimiter, async (req, res) => {
const { verifyId, token } = req.body;
// Validate inputs
if (!verifyId || !token) {
return res.status(400).json({
success: false,
message: 'Verification ID and token are required'
});
}
// Validate token format (should be numeric, length matches config)
const tokenLength = parseInt(process.env.OTP_TOKEN_LENGTH) || 6;
if (!/^\d+$/.test(token) || token.length !== tokenLength) {
return res.status(400).json({
success: false,
message: `Token must be ${tokenLength} digits`
});
}
try {
const result = await verifyOTP(verifyId, token);
res.status(200).json({
success: true,
message: 'Phone number verified successfully',
data: {
verifyId: result.verifyId,
status: result.status,
recipient: result.recipient
}
});
} catch (error) {
console.error('OTP verification error:', error.message);
// Provide specific error messages for common failures
let statusCode = 400;
let message = 'Verification failed';
if (error.message.includes('token does not match') || error.message.includes('invalid')) {
message = 'Invalid verification code';
} else if (error.message.includes('expired')) {
message = 'Verification code has expired';
statusCode = 410; // 410 Gone
} else if (error.message.includes('attempts')) {
message = 'Maximum verification attempts exceeded';
statusCode = 429; // 429 Too Many Requests
}
res.status(statusCode).json({
success: false,
message,
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
// --- OTP Status Check Endpoint ---
app.get('/api/otp/status/:verifyId', async (req, res) => {
const { verifyId } = req.params;
if (!verifyId) {
return res.status(400).json({
success: false,
message: 'Verification ID is required'
});
}
try {
const result = await getVerifyStatus(verifyId);
res.status(200).json({
success: true,
data: result
});
} catch (error) {
console.error('Status check error:', error.message);
res.status(404).json({
success: false,
message: 'Verification not found or expired'
});
}
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({
success: false,
message: 'Internal server error'
});
});
// Start server
app.listen(PORT, () => {
console.log(`✅ MessageBird OTP API running on http://localhost:${PORT}`);
console.log(`📱 Endpoints available:`);
console.log(` POST http://localhost:${PORT}/api/otp/generate`);
console.log(` POST http://localhost:${PORT}/api/otp/verify`);
console.log(` GET http://localhost:${PORT}/api/otp/status/:verifyId`);
});Security Implementation Details:
-
Phone-based Rate Limiting: The generate endpoint limits requests by phone number (5 per 15 min) to prevent SMS bombing attacks where attackers flood a number with verification codes.
-
Attempt Limiting: MessageBird tracks verification attempts server-side (configurable 1–10 via
maxAttempts). Combined with API rate limiting (10 verify requests per 15 min), this provides defense-in-depth against brute-force attacks. -
Specific Error Responses: Different HTTP status codes help clients handle failures appropriately (400 for invalid input, 410 for expired codes, 429 for rate limits).
4. Frontend Setup with Vite
Create a modern frontend using Vite with either React or Vue for the OTP interface.
Choose Your Framework:
Option A: React with Vite
cd ..
npm create vite@latest messagebird-otp-frontend -- --template react
cd messagebird-otp-frontend
npm install
npm install axiosOption B: Vue 3 with Vite
cd ..
npm create vite@latest messagebird-otp-frontend -- --template vue
cd messagebird-otp-frontend
npm install
npm install axiosFrontend Project Structure
messagebird-otp-frontend/
├── node_modules/
├── public/
├── src/
│ ├── components/
│ │ ├── PhoneInput.jsx (or .vue) # Phone number entry form
│ │ ├── OTPInput.jsx (or .vue) # OTP code entry with auto-focus
│ │ └── StatusMessage.jsx (or .vue) # Success/error display
│ ├── services/
│ │ └── api.js # Axios API client
│ ├── App.jsx (or .vue) # Main application component
│ └── main.jsx (or .js)
├── .env
├── package.json
└── vite.config.js
Configure API Endpoint
Create .env in frontend root:
VITE_API_URL=http://localhost:3001Note: Vite requires VITE_ prefix for environment variables to be exposed to client code (Vite env documentation).
5. Building the Frontend Components
API Service Layer
Create src/services/api.js:
// src/services/api.js
import axios from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json'
},
timeout: 10000 // 10 second timeout
});
export const otpAPI = {
/**
* Request OTP code for phone number
* @param {string} phoneNumber - E.164 format
* @returns {Promise<{verifyId, validUntil, status}>}
*/
generateOTP: async (phoneNumber) => {
const response = await apiClient.post('/api/otp/generate', { phoneNumber });
return response.data;
},
/**
* Verify OTP code entered by user
* @param {string} verifyId - Verification ID from generateOTP
* @param {string} token - User-entered code
* @returns {Promise<{status, recipient}>}
*/
verifyOTP: async (verifyId, token) => {
const response = await apiClient.post('/api/otp/verify', { verifyId, token });
return response.data;
},
/**
* Check verification status
* @param {string} verifyId
* @returns {Promise<{status, validUntil, createdAt}>}
*/
getStatus: async (verifyId) => {
const response = await apiClient.get(`/api/otp/status/${verifyId}`);
return response.data;
}
};React Implementation
React: PhoneInput Component
Create src/components/PhoneInput.jsx:
// src/components/PhoneInput.jsx
import { useState } from 'react';
export default function PhoneInput({ onSubmit, loading }) {
const [phoneNumber, setPhoneNumber] = useState('');
const [error, setError] = useState('');
const validatePhone = (phone) => {
// E.164 format validation
const e164Regex = /^\+[1-9]\d{10,14}$/;
return e164Regex.test(phone);
};
const handleSubmit = (e) => {
e.preventDefault();
setError('');
if (!phoneNumber) {
setError('Phone number is required');
return;
}
if (!validatePhone(phoneNumber)) {
setError('Invalid format. Use E.164 (e.g., +14155552671)');
return;
}
onSubmit(phoneNumber);
};
return (
<div className="phone-input-container">
<h2>Enter Your Phone Number</h2>
<p>We'll send you a verification code via SMS</p>
<form onSubmit={handleSubmit}>
<input
type="tel"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
placeholder="+1 415 555 2671"
disabled={loading}
autoFocus
className="phone-input"
/>
{error && <p className="error-message">{error}</p>}
<button type="submit" disabled={loading} className="submit-button">
{loading ? 'Sending…' : 'Send Code'}
</button>
</form>
<p className="help-text">
Format: +[country code][number] (e.g., +14155552671 for US)
</p>
</div>
);
}React: OTPInput Component
Create src/components/OTPInput.jsx:
// src/components/OTPInput.jsx
import { useState, useRef, useEffect } from 'react';
export default function OTPInput({
length = 6,
onComplete,
onResend,
loading,
expiryTime
}) {
const [otp, setOtp] = useState(new Array(length).fill(''));
const [timeLeft, setTimeLeft] = useState(null);
const inputRefs = useRef([]);
// Calculate time remaining
useEffect(() => {
if (!expiryTime) return;
const interval = setInterval(() => {
const now = new Date().getTime();
const expiry = new Date(expiryTime).getTime();
const remaining = Math.max(0, Math.floor((expiry - now) / 1000));
setTimeLeft(remaining);
if (remaining === 0) {
clearInterval(interval);
}
}, 1000);
return () => clearInterval(interval);
}, [expiryTime]);
const handleChange = (index, value) => {
// Only allow digits
if (!/^\d*$/.test(value)) return;
const newOtp = [...otp];
newOtp[index] = value.slice(-1); // Only last character
setOtp(newOtp);
// Auto-focus next input
if (value && index < length - 1) {
inputRefs.current[index + 1]?.focus();
}
// Auto-submit when complete
if (newOtp.every(digit => digit !== '') && !loading) {
onComplete(newOtp.join(''));
}
};
const handleKeyDown = (index, e) => {
// Handle backspace to move to previous input
if (e.key === 'Backspace' && !otp[index] && index > 0) {
inputRefs.current[index - 1]?.focus();
}
};
const handlePaste = (e) => {
e.preventDefault();
const pastedData = e.clipboardData.getData('text').slice(0, length);
if (!/^\d+$/.test(pastedData)) return;
const newOtp = pastedData.split('').concat(new Array(length).fill('')).slice(0, length);
setOtp(newOtp);
// Focus last filled input or next empty
const nextIndex = Math.min(pastedData.length, length - 1);
inputRefs.current[nextIndex]?.focus();
if (newOtp.every(digit => digit !== '')) {
onComplete(newOtp.join(''));
}
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
return (
<div className="otp-input-container">
<h2>Enter Verification Code</h2>
<p>We sent a {length}-digit code to your phone</p>
<div className="otp-inputs">
{otp.map((digit, index) => (
<input
key={index}
ref={(el) => (inputRefs.current[index] = el)}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(index, e)}
onPaste={handlePaste}
disabled={loading || timeLeft === 0}
className="otp-digit"
autoFocus={index === 0}
/>
))}
</div>
{timeLeft !== null && (
<p className={`timer ${timeLeft < 30 ? 'warning' : ''}`}>
{timeLeft > 0
? `Code expires in ${formatTime(timeLeft)}`
: 'Code has expired'}
</p>
)}
<button
onClick={onResend}
disabled={loading || timeLeft > 0}
className="resend-button"
>
{timeLeft > 0 ? 'Resend available after expiry' : 'Resend Code'}
</button>
</div>
);
}React: Main App Component
Edit src/App.jsx:
// src/App.jsx
import { useState } from 'react';
import PhoneInput from './components/PhoneInput';
import OTPInput from './components/OTPInput';
import { otpAPI } from './services/api';
import './App.css';
function App() {
const [step, setStep] = useState('phone'); // 'phone', 'otp', 'success', 'error'
const [phoneNumber, setPhoneNumber] = useState('');
const [verifyId, setVerifyId] = useState('');
const [validUntil, setValidUntil] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const handlePhoneSubmit = async (phone) => {
setLoading(true);
setMessage('');
try {
const response = await otpAPI.generateOTP(phone);
if (response.success) {
setPhoneNumber(phone);
setVerifyId(response.data.verifyId);
setValidUntil(response.data.validUntil);
setStep('otp');
setMessage('Code sent! Check your phone.');
}
} catch (error) {
setMessage(error.response?.data?.message || 'Failed to send code. Try again.');
} finally {
setLoading(false);
}
};
const handleOTPComplete = async (code) => {
setLoading(true);
setMessage('');
try {
const response = await otpAPI.verifyOTP(verifyId, code);
if (response.success) {
setStep('success');
setMessage('✅ Phone number verified successfully!');
}
} catch (error) {
const errorMsg = error.response?.data?.message || 'Verification failed';
setMessage(`❌ ${errorMsg}`);
// Show error state but stay on OTP screen for retry
if (error.response?.status === 410 || error.response?.status === 429) {
// Expired or max attempts – go back to phone input
setTimeout(() => {
setStep('phone');
setMessage('');
}, 3000);
}
} finally {
setLoading(false);
}
};
const handleResend = () => {
setStep('phone');
setMessage('');
// Optionally pre-fill phone number
};
const handleReset = () => {
setStep('phone');
setPhoneNumber('');
setVerifyId('');
setValidUntil('');
setMessage('');
};
return (
<div className="app-container">
<div className="card">
<h1>🔐 Phone Verification</h1>
{message && (
<div className={`message ${step === 'success' ? 'success' : 'error'}`}>
{message}
</div>
)}
{step === 'phone' && (
<PhoneInput onSubmit={handlePhoneSubmit} loading={loading} />
)}
{step === 'otp' && (
<OTPInput
length={6}
onComplete={handleOTPComplete}
onResend={handleResend}
loading={loading}
expiryTime={validUntil}
/>
)}
{step === 'success' && (
<div className="success-container">
<div className="success-icon">✓</div>
<h2>Verification Successful!</h2>
<p>Your phone number {phoneNumber} has been verified.</p>
<button onClick={handleReset} className="button">
Verify Another Number
</button>
</div>
)}
</div>
</div>
);
}
export default App;Vue 3 Implementation
Vue: PhoneInput Component
Create src/components/PhoneInput.vue:
<template>
<div class="phone-input-container">
<h2>Enter Your Phone Number</h2>
<p>We'll send you a verification code via SMS</p>
<form @submit.prevent="handleSubmit">
<input
v-model="phoneNumber"
type="tel"
placeholder="+1 415 555 2671"
:disabled="loading"
autofocus
class="phone-input"
/>
<p v-if="error" class="error-message">{{ error }}</p>
<button type="submit" :disabled="loading" class="submit-button">
{{ loading ? 'Sending…' : 'Send Code' }}
</button>
</form>
<p class="help-text">
Format: +[country code][number] (e.g., +14155552671 for US)
</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
loading: Boolean
});
const emit = defineEmits(['submit']);
const phoneNumber = ref('');
const error = ref('');
const validatePhone = (phone) => {
const e164Regex = /^\+[1-9]\d{10,14}$/;
return e164Regex.test(phone);
};
const handleSubmit = () => {
error.value = '';
if (!phoneNumber.value) {
error.value = 'Phone number is required';
return;
}
if (!validatePhone(phoneNumber.value)) {
error.value = 'Invalid format. Use E.164 (e.g., +14155552671)';
return;
}
emit('submit', phoneNumber.value);
};
</script>Vue: OTPInput Component
Create src/components/OTPInput.vue:
<template>
<div class="otp-input-container">
<h2>Enter Verification Code</h2>
<p>We sent a {{ length }}-digit code to your phone</p>
<div class="otp-inputs">
<input
v-for="(digit, index) in otp"
:key="index"
:ref="el => inputRefs[index] = el"
v-model="otp[index]"
type="text"
inputmode="numeric"
maxlength="1"
@input="handleInput(index, $event)"
@keydown="handleKeyDown(index, $event)"
@paste="handlePaste"
:disabled="loading || timeLeft === 0"
class="otp-digit"
:autofocus="index === 0"
/>
</div>
<p v-if="timeLeft !== null" :class="['timer', { warning: timeLeft < 30 }]">
{{ timeLeft > 0 ? `Code expires in ${formatTime(timeLeft)}` : 'Code has expired' }}
</p>
<button
@click="$emit('resend')"
:disabled="loading || timeLeft > 0"
class="resend-button"
>
{{ timeLeft > 0 ? 'Resend available after expiry' : 'Resend Code' }}
</button>
</div>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue';
const props = defineProps({
length: { type: Number, default: 6 },
loading: Boolean,
expiryTime: String
});
const emit = defineEmits(['complete', 'resend']);
const otp = ref(new Array(props.length).fill(''));
const timeLeft = ref(null);
const inputRefs = ref([]);
let timerInterval = null;
const handleInput = (index, event) => {
const value = event.target.value;
if (!/^\d*$/.test(value)) {
otp.value[index] = '';
return;
}
otp.value[index] = value.slice(-1);
if (value && index < props.length - 1) {
inputRefs.value[index + 1]?.focus();
}
if (otp.value.every(d => d !== '') && !props.loading) {
emit('complete', otp.value.join(''));
}
};
const handleKeyDown = (index, event) => {
if (event.key === 'Backspace' && !otp.value[index] && index > 0) {
inputRefs.value[index - 1]?.focus();
}
};
const handlePaste = (event) => {
event.preventDefault();
const pastedData = event.clipboardData.getData('text').slice(0, props.length);
if (!/^\d+$/.test(pastedData)) return;
const newOtp = pastedData.split('').concat(new Array(props.length).fill('')).slice(0, props.length);
otp.value = newOtp;
const nextIndex = Math.min(pastedData.length, props.length - 1);
inputRefs.value[nextIndex]?.focus();
if (newOtp.every(d => d !== '')) {
emit('complete', newOtp.join(''));
}
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
watch(() => props.expiryTime, (newExpiry) => {
if (!newExpiry) return;
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(() => {
const now = new Date().getTime();
const expiry = new Date(newExpiry).getTime();
const remaining = Math.max(0, Math.floor((expiry - now) / 1000));
timeLeft.value = remaining;
if (remaining === 0) {
clearInterval(timerInterval);
}
}, 1000);
}, { immediate: true });
onUnmounted(() => {
if (timerInterval) clearInterval(timerInterval);
});
</script>Vue: Main App Component
Edit src/App.vue:
<template>
<div class="app-container">
<div class="card">
<h1>🔐 Phone Verification</h1>
<div v-if="message" :class="['message', step === 'success' ? 'success' : 'error']">
{{ message }}
</div>
<PhoneInput
v-if="step === 'phone'"
:loading="loading"
@submit="handlePhoneSubmit"
/>
<OTPInput
v-if="step === 'otp'"
:length="6"
:loading="loading"
:expiry-time="validUntil"
@complete="handleOTPComplete"
@resend="handleResend"
/>
<div v-if="step === 'success'" class="success-container">
<div class="success-icon">✓</div>
<h2>Verification Successful!</h2>
<p>Your phone number {{ phoneNumber }} has been verified.</p>
<button @click="handleReset" class="button">
Verify Another Number
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import PhoneInput from './components/PhoneInput.vue';
import OTPInput from './components/OTPInput.vue';
import { otpAPI } from './services/api';
const step = ref('phone');
const phoneNumber = ref('');
const verifyId = ref('');
const validUntil = ref('');
const loading = ref(false);
const message = ref('');
const handlePhoneSubmit = async (phone) => {
loading.value = true;
message.value = '';
try {
const response = await otpAPI.generateOTP(phone);
if (response.success) {
phoneNumber.value = phone;
verifyId.value = response.data.verifyId;
validUntil.value = response.data.validUntil;
step.value = 'otp';
message.value = 'Code sent! Check your phone.';
}
} catch (error) {
message.value = error.response?.data?.message || 'Failed to send code. Try again.';
} finally {
loading.value = false;
}
};
const handleOTPComplete = async (code) => {
loading.value = true;
message.value = '';
try {
const response = await otpAPI.verifyOTP(verifyId.value, code);
if (response.success) {
step.value = 'success';
message.value = '✅ Phone number verified successfully!';
}
} catch (error) {
const errorMsg = error.response?.data?.message || 'Verification failed';
message.value = `❌ ${errorMsg}`;
if (error.response?.status === 410 || error.response?.status === 429) {
setTimeout(() => {
step.value = 'phone';
message.value = '';
}, 3000);
}
} finally {
loading.value = false;
}
};
const handleResend = () => {
step.value = 'phone';
message.value = '';
};
const handleReset = () => {
step.value = 'phone';
phoneNumber.value = '';
verifyId.value = '';
validUntil.value = '';
message.value = '';
};
</script>6. Adding Styles
Create src/App.css (works for both React and Vue):
/* src/App.css */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.app-container {
width: 100%;
max-width: 500px;
}
.card {
background: white;
border-radius: 16px;
padding: 40px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 24px;
}
h2 {
color: #444;
margin-bottom: 10px;
font-size: 20px;
}
p {
color: #666;
margin-bottom: 20px;
line-height: 1.5;
}
.message {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: 500;
}
.message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.phone-input-container,
.otp-input-container {
text-align: center;
}
.phone-input {
width: 100%;
padding: 14px 16px;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 8px;
margin-bottom: 10px;
transition: border-color 0.3s;
}
.phone-input:focus {
outline: none;
border-color: #667eea;
}
.phone-input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.otp-inputs {
display: flex;
gap: 12px;
justify-content: center;
margin: 20px 0;
}
.otp-digit {
width: 50px;
height: 60px;
font-size: 24px;
text-align: center;
border: 2px solid #ddd;
border-radius: 8px;
transition: all 0.3s;
font-weight: 600;
}
.otp-digit:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.otp-digit:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.timer {
font-size: 14px;
margin: 15px 0;
font-weight: 500;
}
.timer.warning {
color: #dc3545;
}
.submit-button,
.resend-button,
.button {
width: 100%;
padding: 14px;
font-size: 16px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
margin-top: 10px;
}
.submit-button,
.button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.submit-button:hover:not(:disabled),
.button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.submit-button:disabled,
.resend-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.resend-button {
background: white;
color: #667eea;
border: 2px solid #667eea;
}
.resend-button:hover:not(:disabled) {
background: #f8f9ff;
}
.error-message {
color: #dc3545;
font-size: 14px;
margin-top: -10px;
margin-bottom: 15px;
}
.help-text {
font-size: 13px;
color: #999;
margin-top: 10px;
}
.success-container {
text-align: center;
padding: 20px 0;
}
.success-icon {
width: 80px;
height: 80px;
background: #28a745;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
margin: 0 auto 20px;
}
@media (max-width: 600px) {
.card {
padding: 30px 20px;
}
.otp-digit {
width: 40px;
height: 50px;
font-size: 20px;
}
.otp-inputs {
gap: 8px;
}
}7. Testing the Complete Application
Backend Testing
-
Start Backend Server:
bashcd messagebird-otp-backend npm run dev -
Test with curl:
Generate OTP:
bashcurl -X POST http://localhost:3001/api/otp/generate \ -H "Content-Type: application/json" \ -d '{"phoneNumber":"+14155552671"}'Response:
json{ "success": true, "message": "OTP sent successfully", "data": { "verifyId": "4e213b01155d1e35a9d9571v00162985", "validUntil": "2024-01-15T14:35:00Z", "status": "sent" } }Verify OTP:
bashcurl -X POST http://localhost:3001/api/otp/verify \ -H "Content-Type: application/json" \ -d '{"verifyId":"4e213b01155d1e35a9d9571v00162985","token":"123456"}'
Frontend Testing
-
Start Frontend Dev Server:
bashcd messagebird-otp-frontend npm run devOpens at
http://localhost:5173 -
Test Flow:
- Enter your real phone number in E.164 format (e.g., +14155552671)
- Click "Send Code"
- Check your phone for SMS (arrives in 5–30 seconds typically)
- Enter the 6-digit code in the OTP inputs
- Verify success message displays
Common Testing Issues
| Issue | Cause | Solution |
|---|---|---|
| "Failed to generate OTP" | Invalid API key | Verify MESSAGEBIRD_API_KEY is live key (not test) in backend .env |
| "Invalid phone number format" | Wrong format | Use E.164: +[country code][number] without spaces |
| SMS not received | Trial account restrictions | MessageBird trials may have destination restrictions (check dashboard) |
| "Code has expired" | Default 30s timeout too short | Increase OTP_TIMEOUT in backend .env to 300 (5 minutes) |
| CORS errors | Frontend/backend mismatch | Verify VITE_API_URL in frontend .env matches backend PORT |
| "Too many requests" | Rate limiter triggered | Wait 15 minutes or adjust limits in server.js for testing |
8. Security Best Practices
Production Security Checklist
-
Rate Limiting Configuration:
- Generation endpoint: 3–5 requests per phone number per hour (prevents SMS bombing)
- Verification endpoint: 10 attempts per IP per 15 minutes (prevents brute force)
- Use Redis-backed rate limiter for distributed systems (rate-limit-redis)
-
OTP Configuration:
- Token length: Minimum 6 digits (1M combinations)
- Timeout: 5–10 minutes for UX balance (per OWASP Authentication Cheat Sheet)
- Max attempts: 3 attempts per verification session
- One-time use: MessageBird Verify API automatically invalidates tokens after successful verification
-
HTTPS Requirements:
- Production: Always use HTTPS for both frontend and backend (prevent MITM attacks)
- Development: Use mkcert or similar for local HTTPS testing
- Enforce HTTPS redirects in production (use Helmet.js
hstsmiddleware)
-
API Key Protection:
- Never expose MessageBird API keys in frontend code
- Use environment variables on server
- Rotate keys quarterly or after suspected compromise
- Use separate keys for staging/production environments
-
Logging and Monitoring:
- Log failed verification attempts (security event monitoring)
- Alert on unusual patterns (e.g., 100+ OTP requests from single IP)
- Never log full phone numbers in production (use masking: +1415***2671)
- Track metrics: generation success rate, verification success rate, average time to verify
-
GDPR/Privacy Compliance:
- Obtain user consent before sending OTP messages (GDPR Article 6)
- Implement data retention policies (delete verification records after 30–90 days)
- Provide opt-out mechanisms for SMS communications
- Honor "right to be forgotten" requests
Known Security Limitations
SMS-based OTP vulnerabilities (documented by NIST SP 800-63B):
- SIM swapping attacks: Attacker ports victim's number to their SIM
- SMS interception: Possible via SS7 protocol vulnerabilities
- Phishing: Users may be tricked into revealing codes
Mitigation strategies:
- Use MessageBird Verify API alongside app-based TOTP (e.g., Google Authenticator) for high-security applications
- Implement device fingerprinting to detect suspicious logins
- Monitor for rapid verification attempts from new locations
- Consider MessageBird's WhatsApp or voice verification for higher security channels
9. Deployment Considerations
Backend Deployment (Node.js/Express)
Platform Options:
- Heroku: Easy deployment with buildpacks (guide)
- AWS Elastic Beanstalk: Managed Node.js hosting
- DigitalOcean App Platform: Simple Node.js deployments
- Railway: Modern platform with Git-based deployments
Environment Variables Setup:
# Production environment variables
MESSAGEBIRD_API_KEY=your_live_key_here
OTP_TIMEOUT=300
OTP_TOKEN_LENGTH=6
OTP_MAX_ATTEMPTS=3
PORT=3001
NODE_ENV=productionProcess Management: Use PM2 for production resilience:
npm install -g pm2
pm2 start src/server.js --name "messagebird-otp-api"
pm2 save
pm2 startupFrontend Deployment (Vite)
Platform Options:
- Vercel: Automatic Vite detection, zero config (vercel.com)
- Netlify: Drag-and-drop or Git-based (netlify.com)
- Cloudflare Pages: Fast global CDN
- AWS S3 + CloudFront: Full control, CDN included
Build and Deploy:
# Build production bundle
npm run build
# Output in dist/ directory
# Deploy dist/ to hosting platformProduction Environment Variables:
Create .env.production:
VITE_API_URL=https://your-backend-domain.comCORS Configuration for Production
Update backend src/server.js CORS settings:
app.use(cors({
origin: process.env.NODE_ENV === 'production'
? ['https://your-frontend-domain.com', 'https://www.your-frontend-domain.com']
: 'http://localhost:5173',
credentials: true
}));10. Advanced Enhancements
Database Integration for Audit Trail
For production applications, track verification attempts in a database:
PostgreSQL Schema Example:
CREATE TABLE otp_verifications (
id SERIAL PRIMARY KEY,
phone_number VARCHAR(20) NOT NULL,
verify_id VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL, -- sent, verified, expired, failed
attempts INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
verified_at TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
ip_address INET,
user_agent TEXT
);
CREATE INDEX idx_phone_created ON otp_verifications(phone_number, created_at);
CREATE INDEX idx_verify_id ON otp_verifications(verify_id);Benefits:
- Audit trail for compliance (GDPR Article 30)
- Security analytics (detect abuse patterns)
- User support (verify code was sent to correct number)
Resend OTP Functionality
Add cooldown period to prevent abuse:
// Backend: Track last send time per phone number
const sendCooldown = new Map(); // Use Redis in production
app.post('/api/otp/resend', otpGenerateLimiter, async (req, res) => {
const { phoneNumber } = req.body;
const lastSend = sendCooldown.get(phoneNumber);
if (lastSend && Date.now() - lastSend < 60000) {
return res.status(429).json({
success: false,
message: 'Wait 60 seconds before requesting another code'
});
}
// Generate new OTP (same logic as /generate)
const result = await generateOTP(phoneNumber);
sendCooldown.set(phoneNumber, Date.now());
res.status(200).json({ success: true, data: result });
});Multi-Language Support
MessageBird Verify API supports 35+ languages for voice OTP (API documentation):
client.verify.create(phoneNumber, {
type: 'tts', // Text-to-speech voice call
language: 'es-mx', // Spanish (Mexico)
voice: 'female', // male or female
// ... other options
});Supported languages include: English (en-us, en-gb), Spanish (es-es, es-mx), French (fr-fr), German (de-de), Mandarin (zh-cn), Japanese (ja-jp), and many more.
Frequently Asked Questions
How does MessageBird Verify API generate OTP codes?
MessageBird Verify API automatically generates cryptographically secure random numeric codes (6–10 digits configurable) server-side when you create a verification object. The API handles token generation, storage, expiration, and delivery—you never see or manage the actual code in your backend, which eliminates implementation vulnerabilities (MessageBird Verify API docs).
What's the difference between MessageBird SMS API and Verify API?
The SMS API (client.messages.create()) sends arbitrary text messages and requires you to manually generate OTP codes, track expiration, and manage verification attempts. The Verify API (client.verify.create()) is purpose-built for OTP/2FA, automatically handling token generation, validation, expiration, and attempt limiting. Use Verify API for authentication flows and SMS API for notifications or marketing messages.
How long should OTP codes be valid?
According to NIST SP 800-63B guidelines (2023), OTP tokens should expire quickly to minimize attack windows. Recommended ranges: 5–10 minutes for standard authentication (balances security and UX), 30–60 seconds for high-security transactions (banking, medical records), 15–30 minutes for account recovery flows (users may need to check email for password reset). MessageBird Verify API supports 30 seconds to 2 days.
Can I use MessageBird Verify API with free trial accounts?
Yes, MessageBird provides free trial credits for testing, but you must use a live API key (not test mode key) for Verify API functionality. Trial accounts may have destination restrictions—verify that you can send to your test phone numbers in the MessageBird Dashboard. As of 2024, most trial accounts include €10–20 in free credits sufficient for 100–200 SMS verifications depending on destination country.
How do I prevent SMS bombing attacks on my OTP endpoint?
Implement multi-layer rate limiting: (1) Phone-number-based limits (3–5 OTP requests per number per hour) using express-rate-limit with custom keyGenerator, (2) IP-based limits (10 requests per IP per hour) as backup, (3) Consider CAPTCHA after 2 failed generation attempts, (4) Monitor for unusual patterns (alerts when >50 requests/hour from single source). MessageBird also applies account-level rate limits for additional protection.
What happens if a user enters the wrong OTP code multiple times?
MessageBird Verify API tracks verification attempts (configurable 1–10 via maxAttempts, default 3). After exceeding max attempts, the verification status changes to "failed" and the API returns an error on subsequent verify requests. The verification ID becomes invalid and the user must request a new OTP code. This prevents brute-force attacks (3 attempts = 0.0003% success probability with 6-digit codes).
Is SMS-based OTP secure enough for sensitive applications?
SMS OTP provides moderate security suitable for most applications (account login, e-commerce checkouts, identity verification). However, NIST SP 800-63B deprecates SMS for high-security contexts due to vulnerabilities (SIM swapping, SS7 interception, phishing). For banking, healthcare, or government applications, consider: (1) App-based TOTP (Google Authenticator, Authy), (2) Hardware tokens (YubiKey), (3) MessageBird's WhatsApp verification (more secure than SMS), or (4) Multi-factor authentication combining SMS + another factor.
How do I handle users in countries with poor SMS delivery?
MessageBird Verify API supports alternative channels: (1) Voice OTP (TTS): Call user and speak code aloud (set type: 'tts'), useful for areas with SMS blocking, (2) WhatsApp: More reliable delivery in developing markets (requires WhatsApp Business API integration), (3) Email: For non-mobile users (set type: 'email'). Monitor delivery rates by country in MessageBird Dashboard and automatically fallback to voice for countries with <95% SMS delivery success.
Can I customize the OTP message template?
Yes, MessageBird Verify API allows custom templates via the template parameter. The string must include %token placeholder which MessageBird replaces with the generated code. Example templates: "Your verification code is %token." (default), "Use code %token to log in to YourApp", "%token is your one-time password (valid 5 minutes)". Keep messages under 160 characters (GSM-7) or 70 characters (Unicode) to avoid multi-part SMS charges.
How do I implement OTP for user registration vs. login?
Registration flow: (1) User enters phone number during signup, (2) Send OTP via Verify API, (3) Verify code, (4) Create account with verified phone number in database. Login flow: (1) User enters username/password, (2) If credentials valid, send OTP to registered phone, (3) Verify code, (4) Issue session token/JWT. For enhanced security, require OTP on every login (2FA) or only for high-risk events (new device, unusual location, password change).
Related Resources
For comprehensive guides on SMS authentication and security:
- MessageBird Verify API Official Documentation: Complete API reference with parameters, response formats, and error codes (updated 2024)
- MessageBird Node.js SDK GitHub: Open-source SDK with code examples and contribution guidelines
- NIST SP 800-63B Digital Identity Guidelines: Authoritative guidance on authentication and OTP security (June 2023 revision)
- OWASP Authentication Cheat Sheet: Best practices for implementing secure authentication flows
- E.164 Phone Number Format Standard: International telecommunication numbering plan specification (ITU-T)
- Vite Frontend Tooling Guide: Official documentation for Vite build tool
- SMS vs. TOTP for 2FA: Security Comparison: Analysis of SMS OTP vulnerabilities and alternatives (check security blogs like Krebs on Security)
- React Hook Form for OTP Inputs: Advanced form validation patterns for production applications
- Vue 3 Composition API Best Practices: Modern Vue patterns for component architecture
Frequently Asked Questions
How to send SMS with Node.js and Express?
Use the Vonage Messages API with the Node.js SDK and Express. Create an API endpoint that accepts recipient details and message content, then uses the SDK to send the SMS via Vonage.
What is the Vonage Messages API?
It's a cloud-based API by Vonage that allows developers to send and receive SMS messages programmatically. This guide focuses on sending SMS using the Messages API via their Node.js Server SDK.
Why use Express for sending SMS in Node?
Express simplifies creating API endpoints to handle SMS requests, manage middleware, and handle HTTP requests/responses. This guide uses Express to build a /api/send-sms endpoint.
When should I use Vonage private key content vs. path?
Use the private key path for local development, as specified in your .env file with `VONAGE_PRIVATE_KEY_PATH`. Use the content variable, `VONAGE_PRIVATE_KEY_CONTENT`, in production environments for security, and populating the variable through a deployment pipeline, avoiding the need to store the key file directly on the server.
Can I use alphanumeric sender ID with Vonage?
Yes, you can use an alphanumeric sender ID (e.g., 'MyBrand') instead of a phone number. Note that support varies by country, and replies may not be possible. Use the `from` parameter in your API request as outlined in section 8. For this project, we'll default to your `VONAGE_NUMBER`.
How to set up Vonage application for SMS?
Log in to the Vonage Dashboard, create a new application, generate your keys (saving the private key securely), and link your Vonage number to the application. Then, set your credentials in your project's .env file. Be sure the default SMS API is set to "Messages API".
What is nodemon used for in this project?
Nodemon automatically restarts the Node.js server during development when code changes are detected, streamlining the development process. It's installed as a development dependency.
How to handle Vonage SMS errors in Node.js?
Implement try...catch blocks to handle errors during API calls. The Vonage SDK might provide specific error codes/messages you can use for more detailed error handling. For production, map these to user-friendly responses.
What is the purpose of a health check endpoint?
A health check endpoint (e.g., /health) allows monitoring systems to quickly check the status of your application. It typically returns a simple response like { status: 'OK' } if the server is running.
Why separate Vonage logic into a service module?
Separating concerns makes your code more organized, testable, and maintainable. It isolates the Vonage API interaction, simplifying your main server file (server.js) and allowing independent testing of the Vonage service logic.
How to secure Vonage SMS API endpoint in Node.js?
Use API keys, JWTs, or other authentication/authorization methods to protect your API endpoint. Also, implement input validation, rate limiting (e.g., with express-rate-limit), and use security headers (e.g., with helmet).
What are DLRs and how to handle them with Vonage?
DLRs (Delivery Receipts) provide delivery status updates (delivered, failed, etc.). Configure a 'Status URL' in your Vonage application settings. Implement an endpoint to receive POST requests from Vonage containing DLR information, using the message_uuid to correlate responses.
How to handle SMS character encoding and limits with Vonage?
Vonage largely handles encoding automatically, switching between GSM-7 (160 chars/segment) and UCS-2 (70 chars/segment) as needed. Be aware of these limits, especially for non-GSM characters (emojis, accented letters), as messages are split into segments and billing is per segment.
How to manage private key securely during Node.js deployment?
Never include the private key file (.env or private.key) in version control. In production, store the *content* of `private.key` in a secure environment variable like `VONAGE_PRIVATE_KEY_CONTENT` using your platform's secrets management features (e.g., Heroku Config Vars).