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 provides a complete walkthrough for implementing SMS-based OTP verification in a Node.js application using the Express framework and the Infobip 2FA API. We will build a simple Express application with endpoints to request an OTP via SMS and verify the OTP entered by the user.
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. We'll focus on SMS.
- axios: Promise-based HTTP client for making requests to the Infobip API.
- dotenv: Module to load environment variables from a
.env
file. - express-rate-limit: Middleware for rate-limiting requests in Express.
- (Optional) Database: For persisting user data and potentially linking
pinId
. We'll use a simple in-memory store for demonstration, but provide 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.' })
end
Prerequisites:
- Node.js and npm (or yarn) installed.
- An Infobip account (Create a free trial account).
- Basic understanding of Node.js, Express, REST APIs, and asynchronous JavaScript.
- A tool for testing APIs (like Postman or
curl
).
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
1.1 Create Project Directory
Open your terminal and create a new directory for the project:
mkdir node-infobip-otp
cd node-infobip-otp
1.2 Initialize npm
npm init -y
This creates a package.json
file.
1.3 Install Dependencies
npm install express dotenv axios express-rate-limit
express
: The web framework.dotenv
: To manage environment variables for API keys and configurations.axios
: To make HTTP requests to the Infobip API.express-rate-limit
: To prevent 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
.gitignore
1.5 Configure 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
.env
File
1.6 Create 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=3000
Purpose 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. Implementing Core Functionality (Infobip Interaction)
We need functions to interact with the Infobip 2FA API: sending the OTP and verifying it. Let's create helper functions for these actions. It's good practice to encapsulate third-party API interactions.
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
axios
and load necessary environment variables usingdotenv.config()
. - An
axios
instance (infobipAxios
) is created with the base URL and default headers (Authorization, Content-Type, Accept) required by Infobip. The API key is included in theAuthorization
header. sendOtp
:- Takes the
phoneNumber
as input. - Includes checks for necessary environment variables.
- Makes a POST request to Infobip's
/2fa/2/pin
endpoint. - Requires
applicationId
,messageId
, andto
(phone number). - Returns the
pinId
from the response. ThispinId
must 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 theotpCode
entered by the user. - Includes checks for necessary environment variables.
- Makes a POST request to Infobip's
/2fa/2/pin/{pinId}/verify
endpoint. - Sends the
pin
(user's code) in the request body. - Returns
true
ifresponse.data.verified
is true. - Crucially, it catches specific Infobip errors like
WRONG_PIN
,PIN_EXPIRED
,TOO_MANY_ATTEMPTS
and returnsfalse
instead 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. Building the API Layer (Express Routes)
Now, let's set up the Express server and create the API endpoints.
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-limit
instances (otpSendLimiter
,otpVerifyLimiter
) are applied to the respective routes. This is crucial for security. Adjust limits as needed. - Temporary Storage (
otpStore
): An in-memory objectotpStore
is used for demonstration only. This is NOT production-ready. It linkspinId
tophoneNumber
. - Secure Storage Association: A commented-out section illustrates conceptually how
express-session
would be used in a real application. The key steps are:- Configure
express-session
with a secret and persistent store. - After
sendOtp
succeeds, storepinId
, associatedphoneNumber
, andtimestamp
inreq.session
. - In
/verify-otp
, retrieve these details fromreq.session
, perform necessary checks (e.g., doesreq.body.phoneNumber
matchreq.session.otpPhoneNumber
?), and then callverifyOtp
. - Clean up session variables after verification (success or failure) or expiry.
- Configure
/api/send-otp
Route (POST):- Applies
otpSendLimiter
. - Extracts and validates
phoneNumber
(emphasizing need for better validation). - Calls
infobipService.sendOtp
. - Stores the returned
pinId
server-side (usingotpStore
demo orreq.session
). - Returns only a success message, not the
pinId
. - Includes error handling.
- Applies
/api/verify-otp
Route (POST):- Applies
otpVerifyLimiter
. - Extracts and validates
phoneNumber
andotpCode
. - Retrieves the corresponding
pinId
from server-side storage (otpStore
demo orreq.session
). - Checks if an OTP request exists and hasn't expired locally (optional timestamp check).
- Calls
infobipService.verifyOtp
with the retrievedpinId
and the user-providedotpCode
. - If
isVerified
is true, cleans up storage and returns success. This is the point to grant access/complete the action. - If
isVerified
is false (wrong code, expired on Infobip), returns a400
error. - 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 theapp
instance for automated testing (see Section 13).
4. Integrating with Infobip (Configuration Details)
This step involves getting your credentials from Infobip and setting 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
.env
file (created in Section 1.6) with these values:# .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.env
file.Request (
curl
example - replace placeholders!):# 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:
{ ""applicationId"": ""HJ675435E3A6EA43432G5F37A635KJ8B"", // <-- Copy this Application ID ""name"": ""My Nodejs App OTP""_ ""configuration"": { // ... configuration details reflected ... }_ ""enabled"": true }
Copy the
applicationId
from 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 (
curl
example - replace placeholders!):# 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
,pinType
as needed.senderId
might need configuration/approval depending on the destination country.Example Successful Response:
{ ""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
messageId
from the response.
.env
File
4.3 Update 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=3000
Explanation 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. Implementing Error Handling, Logging, and Retries
Error Handling Strategy:
- Specific Infobip Errors: The
infobipService.js
functions now attempt to catch specific API errors from Infobip (inspectingerror.response.data
). Expected failures likeWRONG_PIN
orPIN_EXPIRED
during verification result inverifyOtp
returningfalse
, 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...catch
blocks 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.js
as a fallback. - User Feedback: API responses provide clear status codes (200, 400, 429, 500) and JSON bodies with
message
orerror
fields 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
pinId
server-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).