/web-app-features

How to Add Multi-Factor Authentication to Your Web App

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

Book a free  consultation
4.9
Clutch rating 🌟
600+
Happy partners
17+
Countries served
190+
Team members
Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

How to Add Multi-Factor Authentication to Your Web App

How to Add Multi-Factor Authentication to Your Web App

 

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.

 

Understanding MFA: Beyond Just Passwords

 

Multi-factor authentication requires users to verify their identity using two or more of these factors:

 

  • Something you know - passwords, security questions, PINs
  • Something you have - mobile phone, hardware token, authentication app
  • Something you are - fingerprint, face scan, voice recognition

 

MFA Implementation: A Step-by-Step Approach

 

Step 1: Choose Your MFA Methods

 

Select the right combination of methods based on your user base and security needs:

 

  • SMS/Voice codes: Easy to implement but vulnerable to SIM swapping
  • Authenticator apps: More secure than SMS and works offline (Google Authenticator, Authy)
  • Email verification: Familiar but less secure than dedicated methods
  • Push notifications: User-friendly but requires a dedicated app
  • Hardware tokens: Highest security but requires physical distribution
  • Biometrics: Convenient but requires specific device capabilities

 

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;
}

 

MFA Best Practices for Your Implementation

 

Security Considerations

 

  • Rate limiting: Implement limits on verification attempts to prevent brute-force attacks
  • Session expiration: Set short lifetimes for MFA sessions (5-10 minutes)
  • Secrets storage: Never store TOTP secrets or verification codes in plaintext
  • Account recovery: Have a secure process for users who lose their authentication devices

 

User Experience Best Practices

 

  • Gradual rollout: Start with optional MFA, then make it mandatory for sensitive operations, and finally for all users
  • Clear instructions: Provide step-by-step guides with screenshots
  • Backup options: Always give users multiple recovery methods
  • Remember device: Allow users to bypass MFA on trusted devices

 

Testing Your MFA Implementation

 

  • Test regular flows: Successful logins with correct MFA codes
  • Test edge cases: Expired codes, incorrect codes, locked accounts
  • Test recovery paths: Backup codes, lost device scenarios
  • Security testing: Try to bypass MFA through session manipulation

 

Going Beyond Basic MFA: Advanced Options

 

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 };
  }
}

 

Integration with Identity Providers

 

For teams without security expertise, consider using identity-as-a-service:

 

  • Auth0: Comprehensive identity platform with built-in MFA
  • Okta: Enterprise-focused identity with extensive MFA options
  • Firebase Authentication: Google's authentication system with MFA capabilities
  • AWS Cognito: Amazon's user management service with MFA support

 

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);
  }
}

 

Conclusion: The ROI of MFA

 

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:

 

  • Start with SMS verification—it's familiar to users and quick to implement
  • Add TOTP support next for improved security
  • Make MFA optional initially but incentivize adoption
  • Track failed login attempts before and after implementation to demonstrate value

 

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.

Ship Multi-Factor Authentication 10x Faster with RapidDev

Connect with our team to unlock the full potential of code solutions with a no-commitment consultation!

Book a Free Consultation

Top 3 Multi-Factor Authentication Usecases

Explore the top 3 practical multi-factor authentication use cases to secure your web app effectively.

 

High-Value Financial Transactions

 

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.

 
  • Business Impact: Reduces fraud-related losses by 76-99% according to Microsoft's security research, directly protecting both revenue and reputation.
  • Implementation Consideration: Consider contextual MFA that only triggers above certain transaction thresholds to balance security with user experience.

 

Privileged Access Management

 

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.

 
  • Business Impact: Mitigates insider threats and significantly reduces the attack surface that hackers can exploit. Over 80% of data breaches involve compromised credentials.
  • Implementation Consideration: Integrate with role-based access control (RBAC) to ensure MFA requirements scale appropriately with access privileges.

 

Regulatory Compliance

 

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.

 
  • Business Impact: Avoids costly compliance penalties while streamlining audit processes. A single compliance violation can cost organizations millions in fines and remediation.
  • Implementation Consideration: Document your MFA implementation thoroughly, including risk assessments and exception handling, to satisfy auditor requirements.


Recognized by the best

Trusted by 600+ businesses globally

From startups to enterprises and everything in between, see for yourself our incredible impact.

RapidDev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with.

They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

Arkady
CPO, Praction
Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost.

He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Donald Muir
Co-Founder, Arc
RapidDev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space.

They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Mat Westergreen-Thorne
Co-CEO, Grantify
RapidDev is an excellent developer for custom-code solutions.

We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Emmanuel Brown
Co-Founder, Church Real Estate Marketplace
Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 

This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Samantha Fekete
Production Manager, Media Production Company
The pSEO strategy executed by RapidDev is clearly driving meaningful results.

Working with RapidDev has delivered measurable, year-over-year growth. Comparing the same period, clicks increased by 129%, impressions grew by 196%, and average position improved by 14.6%. Most importantly, qualified contact form submissions rose 350%, excluding spam.

Appreciation as well to Matt Graham for championing the collaboration!

Michael W. Hammond
Principal Owner, OCD Tech

We put the rapid in RapidDev

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.