Secure your web app with ease—learn how to add multi-factor authentication in this simple, step-by-step guide.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Why MFA Matters: The 30-Second Pitch
Adding multi-factor authentication to your web application isn't just a security checkbox—it's an insurance policy that costs pennies but saves millions. When implemented properly, MFA blocks over 99.9% of automated attacks according to Microsoft's security research. Let's turn this seemingly complex security feature into something your team can implement in days, not months.
Multi-factor authentication requires users to verify their identity using two or more of these factors:
Step 1: Choose Your MFA Methods
Select the right combination of methods based on your user base and security needs:
Step 2: Set Up Your Authentication Flow
Here's a typical MFA authentication flow:
// Simplified authentication flow pseudocode
async function loginWithMFA(username, password) {
// Step 1: Validate primary credentials
const user = await validateCredentials(username, password);
if (!user) {
return { success: false, message: 'Invalid username or password' };
}
// Step 2: Check if MFA is enabled for this user
if (user.mfaEnabled) {
// Step 3: Generate and send MFA challenge
const mfaToken = generateMFAToken(user.id);
// Send the token via SMS, email, or store for app verification
await sendMFAChallenge(user.contactMethod, mfaToken);
// Return session token that requires MFA completion
return {
success: true,
requiresMFA: true,
tempSessionId: createTempSession(user.id)
};
}
// No MFA required, complete login
return {
success: true,
requiresMFA: false,
sessionToken: generateSessionToken(user.id)
};
}
Step 3: Add MFA Verification Endpoint
// MFA verification endpoint
async function verifyMFAToken(tempSessionId, providedToken) {
// Step 1: Retrieve the temporary session
const session = await getTempSession(tempSessionId);
if (!session || isExpired(session)) {
return { success: false, message: 'Session expired or invalid' };
}
// Step 2: Validate the provided token
const isValid = await validateToken(session.userId, providedToken);
if (!isValid) {
// Log failed attempt and implement rate limiting
await logFailedAttempt(session.userId);
// Check for too many failed attempts
if (await tooManyFailedAttempts(session.userId)) {
await lockAccount(session.userId);
return { success: false, message: 'Account locked due to too many failed attempts' };
}
return { success: false, message: 'Invalid verification code' };
}
// Step 3: Complete authentication
await clearTempSession(tempSessionId);
return {
success: true,
sessionToken: generateSessionToken(session.userId)
};
}
Step 4: Implement Time-Based One-Time Password (TOTP)
For authenticator apps like Google Authenticator or Authy:
// Server-side TOTP implementation
const crypto = require('crypto');
const base32 = require('hi-base32');
function generateSecret() {
// Generate a random 20-byte (160-bit) secret
const buffer = crypto.randomBytes(20);
// Convert to Base32 for QR code and manual entry
return base32.encode(buffer).replace(/=/g, '');
}
function generateTOTP(secret, window = 0) {
// Get current Unix time in seconds
const now = Math.floor(Date.now() / 1000);
// Default 30-second time step
const timeStep = 30;
// Calculate the time counter (number of time steps since Unix epoch)
const counter = Math.floor(now / timeStep) + window;
// Convert counter to buffer
const counterBuffer = Buffer.alloc(8);
for (let i = 0; i < 8; i++) {
counterBuffer[7 - i] = counter & 0xff;
counter = counter >> 8;
}
// Decode the Base32 secret
const secretBuffer = base32.decode.asBytes(secret);
// Generate HMAC-SHA1
const hmac = crypto.createHmac('sha1', secretBuffer);
hmac.update(counterBuffer);
const hmacResult = hmac.digest();
// Dynamic truncation
const offset = hmacResult[hmacResult.length - 1] & 0xf;
// Generate 4-byte binary code
let code = (hmacResult[offset] & 0x7f) << 24 |
(hmacResult[offset + 1] & 0xff) << 16 |
(hmacResult[offset + 2] & 0xff) << 8 |
(hmacResult[offset + 3] & 0xff);
// Generate 6-digit code
code = code % 1000000;
// Add leading zeros if necessary
return code.toString().padStart(6, '0');
}
function verifyTOTP(secret, token) {
// Try current time window and adjacent windows to account for time drift
for (let window = -1; window <= 1; window++) {
const calculatedToken = generateTOTP(secret, window);
if (calculatedToken === token) {
return true;
}
}
return false;
}
Step 5: Generate QR Codes for Authenticator Apps
function generateQRCodeUrl(username, appName, secret) {
// Create the URI according to the otpauth:// protocol
const uri = encodeURIComponent(
`otpauth://totp/${encodeURIComponent(appName)}:${encodeURIComponent(username)}?` +
`secret=${secret}&issuer=${encodeURIComponent(appName)}`
);
// Use Google's Chart API to generate a QR code (or use a library like qrcode)
return `https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=${uri}`;
}
Step 6: Implement SMS-based MFA
Using Twilio as an example:
// Install Twilio: npm install twilio
const twilio = require('twilio');
async function sendSMSVerificationCode(phoneNumber) {
// Generate a random 6-digit code
const verificationCode = Math.floor(100000 + Math.random() * 900000);
// Store the code in your database with expiration time
await storeVerificationCode(phoneNumber, verificationCode, Date.now() + 600000); // 10-minute expiration
// Initialize Twilio client
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
// Send SMS
await client.messages.create({
body: `Your verification code is: ${verificationCode}`,
to: phoneNumber,
from: process.env.TWILIO_PHONE_NUMBER
});
return true;
}
async function verifySMSCode(phoneNumber, code) {
// Retrieve stored code
const storedData = await getVerificationCode(phoneNumber);
// Check if code exists and is not expired
if (!storedData || Date.now() > storedData.expiresAt) {
return false;
}
// Verify the code
const isValid = storedData.code === code;
// If valid, remove the code to prevent reuse
if (isValid) {
await removeVerificationCode(phoneNumber);
}
return isValid;
}
Step 7: Add Backup Codes
function generateBackupCodes(userId, numberOfCodes = 10) {
const codes = [];
for (let i = 0; i < numberOfCodes; i++) {
// Generate a random 8-character code
const code = crypto.randomBytes(4).toString('hex');
codes.push(code);
}
// Store hashed versions of these codes in the database
const hashedCodes = codes.map(code => {
return {
userId,
codeHash: crypto.createHash('sha256').update(code).digest('hex'),
used: false
};
});
// Save the hashed codes
saveBackupCodes(userId, hashedCodes);
// Return plain text codes to show to the user only once
return codes;
}
async function verifyBackupCode(userId, providedCode) {
// Get all unused backup codes for the user
const backupCodes = await getUnusedBackupCodes(userId);
// Hash the provided code
const providedCodeHash = crypto.createHash('sha256').update(providedCode).digest('hex');
// Find the matching code
const matchingCode = backupCodes.find(code => code.codeHash === providedCodeHash);
if (matchingCode) {
// Mark this code as used
await markBackupCodeAsUsed(userId, matchingCode.id);
return true;
}
return false;
}
Step 8: Implement Remember This Device
function generateDeviceToken(userId) {
// Generate a unique token
const token = crypto.randomBytes(32).toString('hex');
// Store token with user ID and creation timestamp
const expirationDate = new Date();
expirationDate.setDate(expirationDate.getDate() + 30); // 30-day expiration
storeDeviceToken({
userId,
token,
createdAt: new Date(),
expiresAt: expirationDate,
deviceInfo: {
userAgent: getUserAgent(),
ipAddress: getIpAddress()
}
});
return token;
}
function validateDeviceToken(userId, token) {
// Retrieve the token from storage
const storedToken = getDeviceToken(userId, token);
// Check if token exists and is not expired
if (!storedToken || new Date() > storedToken.expiresAt) {
return false;
}
// Validate device info to prevent token theft
const currentDeviceInfo = {
userAgent: getUserAgent(),
ipAddress: getIpAddress()
};
// Basic device fingerprinting check
if (currentDeviceInfo.userAgent !== storedToken.deviceInfo.userAgent) {
// Suspicious activity detected
logSecurityEvent(userId, 'device_mismatch');
return false;
}
return true;
}
Security Considerations
User Experience Best Practices
Testing Your MFA Implementation
WebAuthn and FIDO2
The future of authentication is passwordless with WebAuthn:
// Basic WebAuthn registration pseudocode
async function registerWebAuthn(username) {
// 1. Server generates challenge
const challenge = crypto.randomBytes(32);
// 2. Create credential options
const credentialOptions = {
challenge,
rp: {
name: "Your App Name",
id: "yourapp.com"
},
user: {
id: crypto.randomBytes(16),
name: username,
displayName: username
},
pubKeyCredParams: [
{ type: "public-key", alg: -7 } // ES256
],
timeout: 60000,
attestation: "direct"
};
// 3. Store challenge in session for verification
storeRegistrationChallenge(username, challenge);
// 4. Return options to client for navigator.credentials.create()
return credentialOptions;
}
// Verify WebAuthn registration
async function verifyWebAuthnRegistration(username, credential) {
// 1. Retrieve stored challenge
const expectedChallenge = await getRegistrationChallenge(username);
// 2. Verify attestation
const verification = verifyAttestation(credential, expectedChallenge);
if (verification.verified) {
// 3. Store credential for future authentications
await storeCredential(username, {
credentialId: verification.credentialId,
publicKey: verification.publicKey
});
return { success: true };
}
return { success: false, message: verification.error };
}
Risk-Based Authentication
For a more sophisticated approach, implement adaptive MFA based on risk factors:
async function assessLoginRisk(user, loginContext) {
let riskScore = 0;
// Check for IP address change
if (user.lastLoginIp && user.lastLoginIp !== loginContext.ipAddress) {
riskScore += 10;
// GeoIP lookup to check distance
const lastLocation = await geoLocate(user.lastLoginIp);
const currentLocation = await geoLocate(loginContext.ipAddress);
const distance = calculateDistance(lastLocation, currentLocation);
// Add points based on geographic distance
if (distance > 1000) { // More than 1000km
riskScore += 30;
}
}
// Check for unusual login time
const userLocalTime = getUserLocalTime(loginContext.ipAddress);
if (isUnsualTime(user, userLocalTime)) {
riskScore += 20;
}
// Check for unusual device
if (!isKnownDevice(user, loginContext.deviceFingerprint)) {
riskScore += 15;
}
// Determine authentication requirements based on risk score
if (riskScore >= 40) {
return { requireMFA: true, requireAdditionalFactors: true };
} else if (riskScore >= 20) {
return { requireMFA: true, requireAdditionalFactors: false };
} else {
return { requireMFA: user.mfaEnabled, requireAdditionalFactors: false };
}
}
For teams without security expertise, consider using identity-as-a-service:
Example integration with Auth0:
// Install: npm install @auth0/auth0-spa-js
import createAuth0Client from '@auth0/auth0-spa-js';
const auth0 = await createAuth0Client({
domain: 'your-domain.auth0.com',
client_id: 'YOUR_AUTH0_CLIENT_ID',
redirect_uri: window.location.origin
});
// Handle login with MFA
async function login() {
try {
// Redirects to Auth0 login page which handles MFA
await auth0.loginWithRedirect({
// Enable MFA
acr_values: 'http://schemas.openid.net/pape/policies/2007/06/multi-factor'
});
} catch (error) {
console.error("Login failed:", error);
}
}
// After redirect back to your app
async function handleRedirectCallback() {
try {
// Process the authentication result
await auth0.handleRedirectCallback();
// Get the user
const user = await auth0.getUser();
console.log("User is authenticated:", user);
// Get token to use for API calls
const token = await auth0.getTokenSilently();
// Use token for your API calls
} catch (error) {
console.error("Error handling redirect:", error);
}
}
Implementing MFA doesn't have to be overwhelming. By focusing on a phased approach—starting with high-risk users and gradually expanding—you can quickly see the security benefits while managing user adoption.
Quick wins:
Remember that even a basic MFA implementation is significantly better than password-only authentication. You can always refine your approach over time as user feedback comes in and security requirements evolve.
Explore the top 3 practical multi-factor authentication use cases to secure your web app effectively.
Adding MFA for wire transfers, large purchases, or account settings changes creates a critical security checkpoint that prevents costly fraud. When millions in assets are at stake, the minor friction of a second verification factor becomes insignificant compared to the risk reduction.
When employees can access sensitive company data, customer information, or critical infrastructure, MFA creates an essential security barrier. This is particularly vital for admin accounts, database access, and cloud infrastructure management where a single compromised credential could lead to catastrophic data breaches.
Many industries face strict authentication requirements from regulations like PCI DSS (payment processing), HIPAA (healthcare), GDPR (EU data protection), and SOC2 (service organizations). MFA implementation often serves as a cornerstone control that satisfies multiple compliance requirements simultaneously.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.