code examples
code examples
Implementing SMS OTP Authentication with Infobip in Node.js and Express
Complete guide to building SMS-based two-factor authentication using Infobip 2FA API, Node.js, and Express with rate limiting and security best practices.
Two-Factor Authentication (2FA) adds a crucial layer of security to user accounts by requiring a second form of verification beyond just a password. One of the most common 2FA methods is One-Time Passwords (OTP) delivered via SMS.
This guide shows you how to implement SMS-based OTP verification in a Node.js application using the Express framework and the Infobip 2FA API. You'll build a simple Express application with endpoints to request an OTP via SMS and verify the OTP entered by the user.
What You'll Learn:
- How to implement SMS OTP authentication in Node.js with Express
- Integrate Infobip's 2FA API for sending and verifying one-time passwords
- Build secure OTP workflows with rate limiting and session management
- Handle OTP verification errors and expiration scenarios
- Deploy production-ready SMS authentication to your application
Project Goals:
- Secure user actions (like login or registration) with SMS-based OTP.
- Integrate with Infobip's 2FA API for sending and verifying OTPs.
- Build a robust Node.js/Express backend to handle the OTP lifecycle.
- Implement essential security measures like rate limiting.
- Provide clear steps for setup, implementation, testing, and deployment.
Technologies Used:
- Node.js: JavaScript runtime environment.
- Express: Minimalist web framework for Node.js.
- Infobip 2FA API: Service for sending and verifying OTPs via SMS, Voice, or Email. This guide focuses on SMS.
- axios: Promise-based HTTP client for making requests to the Infobip API.
- dotenv: Module to load environment variables from a
.envfile. - express-rate-limit: Middleware for rate-limiting requests in Express.
- (Optional) Database: For persisting user data and potentially linking
pinId. This guide uses a simple in-memory store for demonstration, but provides schema guidance for a real database.
System Architecture:
sequenceDiagram
participant User
participant Browser/Client
participant ExpressApp as Node.js/Express App
participant InfobipAPI as Infobip 2FA API
participant SMSGateway as SMS Gateway
User->>Browser/Client: Initiates action requiring OTP (e.g., Login)
Browser/Client->>+ExpressApp: POST /api/send-otp (phoneNumber)
ExpressApp->>+InfobipAPI: Send PIN Request (apiKey, appId, msgId, phoneNumber)
InfobipAPI->>+SMSGateway: Send SMS with OTP
SMSGateway->>User: Delivers SMS with OTP
InfobipAPI-->>-ExpressApp: Success Response (pinId)
Note over ExpressApp: Store pinId securely server-side (e.g., session, temp DB record) associated with user/phone number. DO NOT expose to client.
ExpressApp-->>-Browser/Client: Success Response ({ message: 'OTP sent successfully.' })
User->>Browser/Client: Enters received OTP
Browser/Client->>+ExpressApp: POST /api/verify-otp (phoneNumber, otpCode)
Note over ExpressApp: Retrieve stored pinId associated with user/phone number from server-side store.
ExpressApp->>+InfobipAPI: Verify PIN Request (apiKey, pinId, otpCode)
InfobipAPI-->>-ExpressApp: Verification Response (verified: true/false)
alt Verification Successful
ExpressApp->>ExpressApp: Mark user/action as verified
ExpressApp-->>-Browser/Client: Success Response ({ verified: true, message: 'OTP verified successfully.' })
else Verification Failed
ExpressApp-->>-Browser/Client: Failure Response ({ verified: false, error: 'Invalid or expired OTP.' })
endPrerequisites:
- Install Node.js and npm (or yarn).
- Create a free trial account at Infobip.
- Understand the basics of Node.js, Express, REST APIs, and asynchronous JavaScript.
- Use a tool for testing APIs (like Postman or
curl).
1. Setting Up Your Node.js OTP Project
Initialize your Node.js project and install the necessary dependencies for SMS authentication.
1.1 Create Project Directory
Open your terminal and create a new directory for the project:
mkdir node-infobip-otp
cd node-infobip-otp1.2 Initialize npm
npm init -yThis creates a package.json file.
1.3 Install Dependencies
npm install express dotenv axios express-rate-limitexpress: The web framework.dotenv: Manages environment variables for API keys and configurations.axios: Makes HTTP requests to the Infobip API.express-rate-limit: Prevents abuse of OTP endpoints.
1.4 Project Structure
Create the following basic structure:
node-infobip-otp/
├── node_modules/
├── .env # Stores environment variables (API keys, etc.) - DO NOT COMMIT
├── .gitignore # Specifies intentionally untracked files that Git should ignore
├── server.js # Main application file
├── infobipService.js # Handles interaction with Infobip API
├── package.json
└── package-lock.json
1.5 Configure .gitignore
Create a .gitignore file in the root directory and add node_modules and .env to prevent committing them to version control:
# .gitignore
node_modules
.env
1.6 Create .env File
Create a .env file in the root directory. Populate this with your Infobip credentials and configuration obtained in Section 4.
# .env - Replace placeholder values with your actual credentials.
# See Section 4 of the guide for details on obtaining these values.
# Infobip Credentials (See Section 4.1)
INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL # Found on Infobip portal homepage/API section
INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY # Generated in Infobip portal API Key management
# Infobip 2FA Configuration (See Section 4.2)
INFOBIP_2FA_APP_ID=YOUR_INFOBIP_2FA_APP_ID # ID from created 2FA Application
INFOBIP_2FA_MSG_ID=YOUR_INFOBIP_2FA_MSG_ID # ID from created 2FA Message Template
# Application Port
PORT=3000Purpose of Configuration: Using environment variables (dotenv) is crucial for security and flexibility. It keeps sensitive credentials like API keys out of your source code and allows different configurations for development, staging, and production environments.
2. Building the Infobip SMS OTP Service
Create functions to interact with the Infobip 2FA API: sending the OTP and verifying it. Encapsulating third-party API interactions follows best practices.
Create a new file, infobipService.js:
// infobipService.js
const axios = require('axios');
// Ensure environment variables are loaded (usually done in server.js, but safe to re-require)
require('dotenv').config();
const INFOBIP_BASE_URL = process.env.INFOBIP_BASE_URL;
const INFOBIP_API_KEY = process.env.INFOBIP_API_KEY;
const INFOBIP_2FA_APP_ID = process.env.INFOBIP_2FA_APP_ID;
const INFOBIP_2FA_MSG_ID = process.env.INFOBIP_2FA_MSG_ID;
// Configure Axios instance for Infobip API calls
const infobipAxios = axios.create({
baseURL: INFOBIP_BASE_URL,
headers: {
'Authorization': `App ${INFOBIP_API_KEY}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
}
});
/**
* Sends an OTP PIN via SMS using Infobip 2FA API.
* @param {string} phoneNumber - The recipient's phone number in E.164 format.
* @returns {Promise<string>} - A promise that resolves with the pinId.
* @throws {Error} - Throws an error if the API call fails or configuration is missing.
*/
async function sendOtp(phoneNumber) {
if (!INFOBIP_BASE_URL || !INFOBIP_API_KEY || !INFOBIP_2FA_APP_ID || !INFOBIP_2FA_MSG_ID) {
console.error('Infobip configuration is missing or incomplete in .env file.');
throw new Error('Server configuration error: Infobip details missing.');
// In a real app, you might have more robust config validation on startup.
// See Section 4 for obtaining these values.
}
try {
console.log(`Sending OTP to ${phoneNumber} via Infobip...`);
const response = await infobipAxios.post(`/2fa/2/pin`, {
applicationId: INFOBIP_2FA_APP_ID,
messageId: INFOBIP_2FA_MSG_ID,
to: phoneNumber,
// 'from' can often be omitted if set in the message template or app config
// from: 'YourAppName'
});
if (response.data && response.data.pinId) {
console.log(`OTP sent successfully. Pin ID: ${response.data.pinId}`); // Log pinId server-side for debugging
return response.data.pinId; // Return pinId for server-side use
} else {
console.error('Infobip send OTP response missing pinId:', response.data);
throw new Error('Failed to send OTP: Invalid response from Infobip.');
}
} catch (error) {
const errorMessage = error.response?.data?.requestError?.serviceException?.text || error.message || 'Failed to send OTP via Infobip.';
console.error(`Error sending Infobip OTP to ${phoneNumber}:`, errorMessage, error.response?.data || '');
// Re-throw a more generic error or the specific Infobip text
throw new Error(error.response?.data?.requestError?.serviceException?.text || 'Failed to send OTP via Infobip.');
}
}
/**
* Verifies an OTP PIN using Infobip 2FA API.
* @param {string} pinId - The ID of the PIN received when sending the OTP (kept server-side).
* @param {string} otpCode - The OTP code entered by the user.
* @returns {Promise<boolean>} - A promise that resolves with true if verified, false otherwise.
* @throws {Error} - Throws an error if the API call fails unexpectedly (not for standard verification failures like wrong pin).
*/
async function verifyOtp(pinId, otpCode) {
if (!pinId || !otpCode) {
throw new Error('Missing pinId or otpCode for verification.');
}
if (!INFOBIP_BASE_URL || !INFOBIP_API_KEY) {
console.error('Infobip configuration is missing or incomplete in .env file.');
throw new Error('Server configuration error: Infobip details missing.');
}
try {
console.log(`Verifying OTP for Pin ID: ${pinId}`);
const response = await infobipAxios.post(`/2fa/2/pin/${pinId}/verify`, {
pin: otpCode
});
// Check if 'verified' field exists in the response data
if (response.data && typeof response.data.verified !== 'undefined') {
console.log(`OTP Verification result for ${pinId}: ${response.data.verified}`);
return response.data.verified; // Returns true or false based on Infobip's check
} else {
// Handle cases where verification fails gracefully (e.g., wrong PIN)
// Infobip API should return verified: false in the response data for known failures.
// If the structure is unexpected, log an error.
console.error('Infobip verify OTP response missing expected structure:', response.data);
throw new Error('Failed to verify OTP: Invalid response structure from Infobip.');
}
} catch (error) {
// Check if the error is an expected verification failure (like WRONG_PIN, TOO_MANY_ATTEMPTS)
const serviceException = error.response?.data?.requestError?.serviceException;
if (serviceException && (serviceException.messageId === 'WRONG_PIN' || serviceException.messageId === 'PIN_EXPIRED' || serviceException.messageId === 'TOO_MANY_ATTEMPTS')) {
console.warn(`Verification failed for ${pinId}: ${serviceException.text}`);
return false; // Return false for these expected verification failures
}
// For other unexpected errors (network, config, other Infobip errors), log and throw
const errorMessage = serviceException?.text || error.message || 'Failed to verify OTP via Infobip.';
console.error(`Error verifying Infobip OTP for pinId ${pinId}:`, errorMessage, error.response?.data || '');
throw new Error(serviceException?.text || 'Failed to verify OTP via Infobip.');
}
}
module.exports = {
sendOtp,
verifyOtp
};Explanation:
- We import
axiosand load necessary environment variables usingdotenv.config(). - An
axiosinstance (infobipAxios) is created with the base URL and default headers (Authorization, Content-Type, Accept) required by Infobip. The API key is included in theAuthorizationheader. sendOtp:- Takes the
phoneNumberas input. - Includes checks for necessary environment variables.
- Makes a POST request to Infobip's
/2fa/2/pinendpoint. - Requires
applicationId,messageId, andto(phone number). - Returns the
pinIdfrom the response. ThispinIdmust be kept server-side and associated securely with the user's session or verification attempt. - Includes improved error handling and logs relevant information.
- Takes the
verifyOtp:- Takes the
pinId(retrieved from server-side storage) and theotpCodeentered by the user. - Includes checks for necessary environment variables.
- Makes a POST request to Infobip's
/2fa/2/pin/{pinId}/verifyendpoint. - Sends the
pin(user's code) in the request body. - Returns
trueifresponse.data.verifiedis true. - Crucially, it catches specific Infobip errors like
WRONG_PIN,PIN_EXPIRED,TOO_MANY_ATTEMPTSand returnsfalseinstead of throwing an error, as these are expected verification outcomes. - Throws an error only for unexpected issues (network errors, configuration problems, other API errors).
- Takes the
3. Creating Express API Endpoints for OTP Verification
Set up the Express server and create the API endpoints for sending and verifying SMS codes.
Update server.js:
// server.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet'); // Basic security headers
const { sendOtp, verifyOtp } = require('./infobipService');
const app = express();
const port = process.env.PORT || 3000;
// --- Middleware ---
// Basic security headers
app.use(helmet());
// Enable JSON body parsing
app.use(express.json());
// --- Rate Limiting ---
// Apply rate limiting to OTP endpoints to prevent abuse
const otpSendLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 5, // Limit each IP to 5 OTP send requests per windowMs
message: { error: 'Too many OTP requests from this IP, please try again after 5 minutes' },
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
const otpVerifyLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // Limit each IP to 10 verification attempts per windowMs
message: { error: 'Too many verification attempts from this IP, please try again after 15 minutes' },
standardHeaders: true,
legacyHeaders: false,
});
// --- Temporary Storage (Replace with Database/Session in Production) ---
// WARNING: In-memory storage is NOT suitable for production. It's not scalable
// and data is lost on restart. Use a database (like Redis, PostgreSQL, MongoDB)
// or proper session management (e.g., express-session with a persistent store).
const otpStore = {}; // Store { phoneNumber: { pinId: '...', timestamp: ... } }
// --- Secure Storage Association (Conceptual - using express-session) ---
/*
// If using express-session:
const session = require('express-session');
// Configure session middleware with a secure secret and persistent store (e.g., connect-redis)
app.use(session({
secret: process.env.SESSION_SECRET, // MUST be a strong, random secret stored in .env
resave: false,
saveUninitialized: false,
// store: new RedisStore({ client: redisClient }), // Example using Redis
cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 15 * 60 * 1000 } // 15 min
}));
// Inside /api/send-otp, after getting pinId:
// req.session.otpPinId = pinId;
// req.session.otpPhoneNumber = phoneNumber; // Store phone number for association
// req.session.otpTimestamp = Date.now();
// Inside /api/verify-otp:
// const pinId = req.session.otpPinId;
// const associatedPhoneNumber = req.session.otpPhoneNumber;
// const timestamp = req.session.otpTimestamp;
// // Validate timestamp, ensure associatedPhoneNumber matches req.body.phoneNumber
// // ... proceed with verifyOtp(pinId, otpCode) ...
// delete req.session.otpPinId; // Clean up session on success/failure/expiry
// delete req.session.otpPhoneNumber;
// delete req.session.otpTimestamp;
*/
// --- API Routes ---
// Endpoint to request an OTP
app.post('/api/send-otp', otpSendLimiter, async (req, res) => {
const { phoneNumber } = req.body;
// Input validation (Essential!)
// Use a robust library like libphonenumber-js for production (See Section 8)
if (!phoneNumber || !/^\+?[1-9]\d{1,14}$/.test(phoneNumber)) { // Basic E.164-like format check
return res.status(400).json({ error: 'Valid phone number in E.164 format (e.g., +14155552671) is required.' });
}
try {
const pinId = await sendOtp(phoneNumber);
// Securely store the pinId server-side, associated with the phone number/user session.
// DO NOT send pinId back to the client.
// Using temporary in-memory store for demonstration ONLY:
otpStore[phoneNumber] = { pinId: pinId, timestamp: Date.now() };
console.log(`Stored OTP info for ${phoneNumber}:`, otpStore[phoneNumber]); // For debugging
// Consider using req.session as described above for a more robust approach.
res.status(200).json({ message: 'OTP sent successfully.' }); // Do NOT return pinId
} catch (error) {
console.error(`Error in /api/send-otp route for ${phoneNumber}:`, error.message);
// Return a generic error message to the client
res.status(500).json({ error: error.message || 'Failed to send OTP. Please try again later.' });
}
});
// Endpoint to verify an OTP
app.post('/api/verify-otp', otpVerifyLimiter, async (req, res) => {
const { phoneNumber, otpCode } = req.body;
// Basic validation
if (!phoneNumber || !otpCode || !/^\d{4,8}$/.test(otpCode)) { // Adjust regex based on your PIN length
return res.status(400).json({ error: 'Phone number and a valid numeric OTP code are required.' });
}
// Re-validate phone number format if needed
if (!/^\+?[1-9]\d{1,14}$/.test(phoneNumber)) {
return res.status(400).json({ error: 'Valid phone number in E.164 format is required.' });
}
// Retrieve the stored pinId securely from server-side storage
// Using temporary in-memory store for demonstration ONLY:
const storedOtpData = otpStore[phoneNumber];
// If using sessions: const pinId = req.session.otpPinId; (plus other checks)
if (!storedOtpData) {
return res.status(400).json({ verified: false, error: 'No OTP request found for this number or it may have expired. Please request a new OTP.' });
}
// Optional: Check timestamp for expiry (Infobip handles expiry too via pinTimeToLive)
const otpRequestTime = storedOtpData.timestamp;
const fifteenMinutes = 15 * 60 * 1000; // Example: 15 minute validity window on our side
if (Date.now() - otpRequestTime > fifteenMinutes) {
delete otpStore[phoneNumber]; // Clean up expired entry
return res.status(400).json({ verified: false, error: 'OTP has expired. Please request a new one.' });
}
const pinId = storedOtpData.pinId; // Get pinId from storage
try {
const isVerified = await verifyOtp(pinId, otpCode);
if (isVerified) {
// OTP is correct - Proceed with user action (e.g., complete login, grant access)
delete otpStore[phoneNumber]; // Clean up storage after successful verification
// If using sessions: delete req.session.otpPinId; etc.
// In a real app: Mark user as verified in your DB/session, complete login, etc.
res.status(200).json({ verified: true, message: 'OTP verified successfully.' });
} else {
// OTP is incorrect or expired on Infobip's side (verifyOtp returns false for these)
// Optional: Implement attempt counting here or rely on Infobip's limits configured in the Application
res.status(400).json({ verified: false, error: 'Invalid or expired OTP code.' });
// Consider deleting from otpStore here too, or after max attempts are reached
}
} catch (error) {
// This catch block handles unexpected errors from verifyOtp (e.g., network, config errors)
console.error(`Error in /api/verify-otp route for ${phoneNumber} (PinID: ${pinId}):`, error.message);
// Don't expose detailed internal errors unless necessary
res.status(500).json({ verified: false, error: 'Failed to verify OTP due to a server error. Please try again later.' });
}
});
// Basic root route
app.get('/', (req, res) => {
res.send('Infobip OTP Service is running!');
});
// Global error handler (optional basic example)
app.use((err, req, res, next) => {
console.error('Unhandled error:', err.stack);
res.status(500).send({ error: 'Something went wrong!' });
});
// --- Start Server ---
const server = app.listen(port, () => {
console.log(`Server listening on port ${port}`);
console.log(`Ensure your .env file is configured correctly (see Section 1.6 & 4).`);
if (!process.env.INFOBIP_BASE_URL || !process.env.INFOBIP_API_KEY || !process.env.INFOBIP_2FA_APP_ID || !process.env.INFOBIP_2FA_MSG_ID) {
console.warn('WARNING: Infobip environment variables are not fully set! OTP functionality will likely fail.');
}
});
// Export app for testing purposes (See Section 13)
module.exports = app;Explanation:
- Middleware:
helmet()adds basic security headers.express.json()parses JSON bodies. - Rate Limiting: Separate
express-rate-limitinstances (otpSendLimiter,otpVerifyLimiter) are applied to the respective routes. This is crucial for security. Adjust limits as needed. - Temporary Storage (
otpStore): An in-memory objectotpStoreis used for demonstration only. This is NOT production-ready. It linkspinIdtophoneNumber. - Secure Storage Association: A commented-out section illustrates conceptually how
express-sessionwould be used in a real application. The key steps are:- Configure
express-sessionwith a secret and persistent store. - After
sendOtpsucceeds, storepinId, associatedphoneNumber, andtimestampinreq.session. - In
/verify-otp, retrieve these details fromreq.session, perform necessary checks (e.g., doesreq.body.phoneNumbermatchreq.session.otpPhoneNumber?), and then callverifyOtp. - Clean up session variables after verification (success or failure) or expiry.
- Configure
/api/send-otpRoute (POST):- Applies
otpSendLimiter. - Extracts and validates
phoneNumber(emphasizing need for better validation). - Calls
infobipService.sendOtp. - Stores the returned
pinIdserver-side (usingotpStoredemo orreq.session). - Returns only a success message, not the
pinId. - Includes error handling.
- Applies
/api/verify-otpRoute (POST):- Applies
otpVerifyLimiter. - Extracts and validates
phoneNumberandotpCode. - Retrieves the corresponding
pinIdfrom server-side storage (otpStoredemo orreq.session). - Checks if an OTP request exists and hasn't expired locally (optional timestamp check).
- Calls
infobipService.verifyOtpwith the retrievedpinIdand the user-providedotpCode. - If
isVerifiedis true, cleans up storage and returns success. This is the point to grant access/complete the action. - If
isVerifiedis false (wrong code, expired on Infobip), returns a400error. - Includes error handling for unexpected errors during verification.
- Applies
- Server Start: Loads
.env, starts the Express server, logs essential information, including a warning if Infobip variables aren't set. - App Export:
module.exports = app;is added to allow importing theappinstance for automated testing (see Section 13).
4. Configuring Infobip API Credentials for SMS OTP
Get your credentials from Infobip and set up the necessary 2FA Application and Message Template.
4.1 Obtain Infobip API Key and Base URL
-
Log in to your Infobip Portal.
-
Your Base URL is usually displayed prominently on the homepage after login, often within an ""API Key Management"" or ""Developer Tools"" section, or mentioned in the API documentation landing page specific to your account. It will look something like
xxxxxx.api.infobip.com. Find the correct URL for your account region. -
Navigate to the API Key management section (typically found under your account settings, developer tools, or a dedicated ""API"" menu item).
-
Create a new API key. Give it a descriptive name (e.g., ""Node OTP App Key"").
-
Securely copy the generated API Key immediately. You will not be able to view it again after closing the creation dialog. Store it safely.
-
Update your
.envfile (created in Section 1.6) with these values:dotenv# .env (Update these lines) INFOBIP_BASE_URL=YOUR_COPIED_BASE_URL # e.g., abcde.api.infobip.com INFOBIP_API_KEY=YOUR_COPIED_API_KEY # ... other variables remain ...
4.2 Create Infobip 2FA Application and Message Template
You need an Application and a Message Template within Infobip to define the behavior (PIN length, expiry, attempts) and content of your OTP messages. You can usually do this via the Infobip API or potentially through their web portal (the availability and location of UI configuration for 2FA might change, check their documentation or portal interface). We outline the API approach here.
-
Using Infobip Portal (If Available): Explore the portal for sections like ""Apps"", ""Channels"", ""Verify"", or ""2FA"". Look for options to create a new ""Application"" (specifically for 2FA/Verify) and associated ""Message Templates"". Configure settings like PIN type, PIN length, validity time (
pinTimeToLive), allowed attempts (pinAttempts), and the message text (critically, include the{{pin}}placeholder where the code should appear). If you create them via the UI, carefully note down the generated Application ID and Message ID. -
Using API (Recommended for Automation/Consistency): Use
curl, Postman, or an HTTP client in your preferred language with the Base URL and API Key obtained in Section 4.1. Note: Always refer to the latest Infobip 2FA API Documentation for the most current endpoints and request/response structures.a) Create 2FA Application: Send a POST request to
https://<YOUR_BASE_URL>/2fa/2/applications. Replace<YOUR_BASE_URL>with the value you added to your.envfile.Request (
curlexample - replace placeholders!):bash# Replace YOUR_BASE_URL and YOUR_API_KEY with your actual values curl -X POST https://YOUR_BASE_URL/2fa/2/applications \ --header 'Authorization: App YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data-raw '{ ""name"": ""My Nodejs App OTP"", ""configuration"": { ""pinAttempts"": 5, ""allowMultiplePinVerifications"": false, ""pinTimeToLive"": ""10m"", ""verifyPinLimit"": ""3/10s"", ""sendPinPerApplicationLimit"": ""5000/1d"", ""sendPinPerPhoneNumberLimit"": ""5/1d"" }, ""enabled"": true }'Adjust configuration values (like
pinAttempts,pinTimeToLive, rate limits) based on your security requirements.Example Successful Response:
json{ ""applicationId"": ""HJ675435E3A6EA43432G5F37A635KJ8B"", // <-- Copy this Application ID ""name"": ""My Nodejs App OTP"", ""configuration"": { // ... configuration details reflected ... }, ""enabled"": true }Copy the
applicationIdfrom the response.b) Create 2FA Message Template: Send a POST request to
https://<YOUR_BASE_URL>/2fa/2/applications/<YOUR_APPLICATION_ID>/messages. Replace<YOUR_BASE_URL>and<YOUR_APPLICATION_ID>with your Base URL and the Application ID you just received.Request (
curlexample - replace placeholders!):bash# Replace YOUR_BASE_URL, YOUR_APPLICATION_ID, and YOUR_API_KEY curl -X POST https://YOUR_BASE_URL/2fa/2/applications/YOUR_APPLICATION_ID/messages \ --header 'Authorization: App YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data-raw '{ ""messageText"": ""Your MyApp verification code is: {{pin}}. It expires in 10 minutes. Do not share this code."", ""pinLength"": 6, ""pinType"": ""NUMERIC"", ""language"": ""en"", ""senderId"": ""InfoSMS"" # ""senderId"": ""YourBrand"" # Use a registered Alphanumeric Sender ID if available/required # ""regional"": { /* Optional regional settings like content templates for India */ } }'Ensure
{{pin}}is included inmessageText. AdjustpinLength,pinTypeas needed.senderIdmight need configuration/approval depending on the destination country.Example Successful Response:
json{ ""messageId"": ""0130269F44AFD07AEBC2FEFEB30398A0"", // <-- Copy this Message ID ""pinType"": ""NUMERIC"", ""messageText"": ""Your MyApp verification code is: {{pin}}. It expires in 10 minutes. Do not share this code."", ""pinLength"": 6, ""language"": ""en"", ""senderId"": ""InfoSMS"" // ... other details ... }Copy the
messageIdfrom the response.
4.3 Update .env File
Add the obtained applicationId and messageId to your .env file:
# .env (Update these lines)
INFOBIP_BASE_URL=YOUR_COPIED_BASE_URL
INFOBIP_API_KEY=YOUR_COPIED_API_KEY
# Infobip 2FA Configuration (Obtained in Section 4.2)
INFOBIP_2FA_APP_ID=YOUR_COPIED_APPLICATION_ID # From step 4.2a response
INFOBIP_2FA_MSG_ID=YOUR_COPIED_MESSAGE_ID # From step 4.2b response
# Application Port
PORT=3000Explanation of Variables:
INFOBIP_BASE_URL: The unique API endpoint URL provided by Infobip for your account.INFOBIP_API_KEY: Your secret key for authenticating API requests. Treat this like a password.INFOBIP_2FA_APP_ID: Identifies the specific 2FA application configuration (rate limits, attempts, expiry) to use.INFOBIP_2FA_MSG_ID: Identifies the specific message template (text, PIN length, sender ID) to use when sending the OTP SMS.
Now your application is configured to communicate with the correct Infobip resources using the settings you defined.
5. OTP Error Handling and Security Best Practices
Error Handling Strategy:
- Specific Infobip Errors: The
infobipService.jsfunctions now attempt to catch specific API errors from Infobip (inspectingerror.response.data). Expected failures likeWRONG_PINorPIN_EXPIREDduring verification result inverifyOtpreturningfalse, which the route handler translates into a user-friendly 400 response. Other API errors (config, auth, network) are caught and logged server-side, resulting in a 500 response with a generic message. - General Application Errors:
try...catchblocks are used in route handlers and service functions to catch unexpected JavaScript errors or issues within the application logic. A basic global error handler is added toserver.jsas a fallback. - User Feedback: API responses provide clear status codes (200, 400, 429, 500) and JSON bodies with
messageorerrorfields suitable for client-side display (e.g., ""Invalid or expired OTP code."", ""Too many requests...""). Sensitive internal details are not exposed to the client.
(Code in infobipService.js and server.js includes improved error handling.)
Logging:
Use a dedicated logging library in production (like winston or pino) for structured, leveled logging (info, warn, error) and easy integration with log management systems. For this guide, we use console.log, console.warn, and console.error.
- Log Key Events: OTP request received, OTP sent success (log
pinIdserver-side only), verification attempt, verification result (success/failure). - Log Errors: Log detailed error information on the server, including error messages, stack traces (where available), and potentially relevant request details (excluding sensitive data like full phone numbers or OTP codes unless absolutely necessary and secured).
Frequently Asked Questions
How to implement SMS OTP 2FA in Node.js?
Implement SMS OTP 2FA using Express.js, the Infobip 2FA API, and environment variables for API keys. This setup allows you to send and verify one-time passwords, enhancing security for actions like login or registration by requiring a second verification step beyond a password.
What is the Infobip 2FA API used for?
The Infobip 2FA API is used for sending and verifying one-time passwords (OTPs) via various channels like SMS, voice calls, or email. This guide focuses on SMS OTPs, adding an extra layer of security to user accounts.
Why use express-rate-limit with Infobip OTP?
The `express-rate-limit` middleware helps protect your Node.js application from abuse by limiting the number of OTP requests and verification attempts from a single IP address within a timeframe, enhancing security.
When should I use environment variables for API keys?
Always use environment variables for sensitive information like API keys. The dotenv module in Node.js helps manage environment variables, ensuring your credentials are not exposed in your source code.
How to send an OTP with Infobip API?
Send a POST request to Infobip's /2fa/2/pin endpoint using a library like axios. Provide your application ID, message ID, and user's phone number. The response contains a pinId, which should be stored securely on the server.
How to verify an OTP from Infobip in Node?
Use the pinId received from the send OTP request, store it securely on the server, and the user-provided OTP to make a POST request to Infobip's /2fa/2/pin/{pinId}/verify endpoint. This process confirms if the OTP is valid.
What is the purpose of pinId in Infobip 2FA?
The pinId is a unique identifier for each OTP sent by Infobip's 2FA API. It's crucial for server-side tracking and verifying the correct OTP against the user's attempt, maintaining security and preventing unauthorized access.
Why does Infobip OTP need rate limiting?
Rate limiting protects against brute-force attacks and prevents abuse. It limits how many OTP requests can be made within a certain timeframe, enhancing security.
When should I clean up stored pinId after verification?
Clean up the stored `pinId` and other OTP-related data immediately after successful verification or after a defined number of failed attempts, or upon expiry. This is important for security best practices.
Can I store pinId in an in-memory object for production?
No, storing pinId in an in-memory object like otpStore is NOT suitable for production. Use a database (like Redis, PostgreSQL, MongoDB) or proper session management (like express-session) with a persistent store instead.
What Node.js libraries are required for SMS OTP?
Essential libraries include express for the web framework, dotenv for managing environment variables, axios for making API requests, and express-rate-limit for security. Optionally, a database connector or session management library is needed for production.
How to handle Infobip OTP errors in Node?
Handle errors by checking the error response from the Infobip API. For expected errors like WRONG_PIN, return false. For other errors, log details and return generic error messages to the client, protecting sensitive information.
What is the project structure for Node.js SMS OTP?
The project includes server.js for the main application, infobipService.js for API interaction logic, .env for configuration, .gitignore for version control, node_modules, package.json, and package-lock.json. This structure organizes the core components of the application.