Skip to content

Multi-Factor Authentication (MFA) — Complete Implementation Guide

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Multi. We cover key concepts, practical examples, and best practices to help you master this topic.

Multi-Factor Authentication (MFA) requires users to provide two or more verification factors to gain access, combining something you know (password), something you have (phone, hardware key), or something you are (biometric) for defense-in-depth security.

What You'll Learn

By the end of this lesson, you will implement TOTP-based MFA with authenticator apps, configure backup codes for account recovery, implement step-up authentication for sensitive actions, and understand WebAuthn/FIDO2 hardware keys.

Why It Matters

Password-only authentication is insufficient for modern security. MFA prevents 99.9% of account compromise attacks. Durga Antivirus Pro requires MFA for all administrator accounts. Doda Browser offers optional MFA for user accounts and enforces it for enterprise SSO.

Real-World Use

A user logs into their email account. After entering their password, the server sends a push notification to their phone. They approve it. Later, they try to change their recovery email. The server requires MFA again for this sensitive action.

MFA Enrollment Flow

sequenceDiagram
    participant User
    participant Server
    participant Authenticator

    User->>Server: Enable MFA in settings
    Server->>Server: Generate secret key
    Server-->>User: Show QR code (otpauth:// URL)
    User->>Authenticator: Scan QR code
    Authenticator->>Authenticator: Store secret, generate TOTP
    User->>Server: Enter current TOTP code
    Server->>Server: Verify TOTP matches secret
    Server-->>User: MFA enabled
    Server-->>User: Show backup codes (single use)

TOTP Implementation

import pyotp
import qrcode
from io import BytesIO
import base64

class TOTPManager:
    def __init__(self, issuer="DodaTech"):
        self.issuer = issuer

    def generate_secret(self):
        secret = pyotp.random_base32()
        print(f"[TOTP] Secret generated: {secret}")
        return secret

    def get_provisioning_uri(self, secret, email):
        totp = pyotp.TOTP(secret)
        uri = totp.provisioning_uri(name=email, issuer_name=self.issuer)
        print(f"[TOTP] Provisioning URI generated")
        return uri

    def verify_code(self, secret, code):
        totp = pyotp.TOTP(secret)
        is_valid = totp.verify(code)
        print(f"[TOTP] Code verification: {'PASSED' if is_valid else 'FAILED'}")
        return is_valid

    def generate_backup_codes(self, count=8):
        import secrets
        codes = []
        for _ in range(count):
            code = secrets.token_hex(5).upper()
            codes.append(code)
        print(f"[TOTP] Generated {len(codes)} backup codes")
        return codes

manager = TOTPManager()
secret = manager.generate_secret()
uri = manager.get_provisioning_uri(secret, "alice@example.com")
print(f"Scan QR in authenticator app:\n{uri}")

test_code = pyotp.TOTP(secret).now()
print(f"Current TOTP: {test_code}")
manager.verify_code(secret, test_code)

Expected output:

[TOTP] Secret generated: JBSWY3DPEHPK3PXP
[TOTP] Provisioning URI generated
Scan QR in authenticator app:
otpauth://totp/DodaTech:alice@example.com?secret=...
[TOTP] Code verification: PASSED

MFA Middleware with Step-Up Auth

const express = require("express");
const speakeasy = require("speakeasy");

const app = express();
app.use(express.json());

const users = {
  1: {
    email: "alice@example.com",
    password: "hashed-password",
    mfaSecret: speakeasy.generateSecret({ length: 20 }).base32,
    mfaEnabled: true,
    role: "user",
  },
};

function verifyTOTP(secret, token) {
  return speakeasy.totp.verify({
    secret,
    encoding: "base32",
    token,
    window: 1,
  });
}

app.post("/api/auth/login", (req, res) => {
  const { email, password, mfaCode } = req.body;
  const user = Object.values(users).find(u => u.email === email);

  if (!user || user.password !== password) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  if (user.mfaEnabled) {
    if (!mfaCode) {
      return res.json({ requireMfa: true, message: "MFA code required" });
    }
    if (!verifyTOTP(user.mfaSecret, mfaCode)) {
      return res.status(401).json({ error: "Invalid MFA code" });
    }
  }

  console.log(`[MFA] User ${user.email} fully authenticated`);
  res.json({ session: "session-token", user: { email: user.email } });
});

app.post("/api/auth/enable-mfa", (req, res) => {
  const secret = speakeasy.generateSecret({ length: 20 });
  console.log(`[MFA] New secret generated for enrollment`);
  res.json({
    secret: secret.base32,
    qrCode: `otpauth://totp/DodaTech:${req.body.email}?secret=${secret.base32}&issuer=DodaTech`,
  });
});

app.post("/api/auth/verify-mfa-setup", (req, res) => {
  const { secret, token } = req.body;
  if (verifyTOTP(secret, token)) {
    const backupCodes = Array.from({ length: 8 }, () =>
      require("crypto").randomBytes(4).toString("hex").toUpperCase()
    );
    console.log(`[MFA] Setup verified, backup codes generated`);
    res.json({ success: true, backupCodes });
  } else {
    res.status(400).json({ error: "Invalid code, try again" });
  }
});

app.listen(3000);

Common Mistakes

  1. Not providing backup codes locks users out if they lose their authenticator device.
  2. Implementing TOTP without time drift tolerance causes valid codes to be rejected.
  3. Requiring MFA on every request instead of using step-up authentication for sensitive actions only.
  4. Storing TOTP secrets in the same database as passwords without encryption.
  5. Not rate-limiting MFA verification attempts allows brute force of short TOTP codes.
  6. Failing to handle clock synchronization issues between server and authenticator app.

Practice Questions

  1. How does TOTP generate time-based codes?

TOTP combines a shared secret with the current Unix timestamp divided into 30-second intervals. The HMAC-SHA1 hash of the secret and time counter produces a 6-8 digit code. Both server and authenticator compute the same code from the same secret and time.

  1. What is the purpose of backup codes?

Backup codes are single-use codes generated during MFA enrollment. Users store them safely (e.g., print or password manager). If the authenticator device is lost, backup codes provide a recovery method without needing to contact support.

  1. What is step-up authentication?

Step-up auth requires additional verification only for sensitive actions. A user with MFA already authenticated can access their profile, but changing the password or initiating a wire transfer triggers an additional MFA challenge.

  1. Challenge: Build a complete MFA system with TOTP authenticator app support, backup codes, step-up authentication for sensitive endpoints, Rate Limiting on MFA verification (3 attempts per 5 minutes), and device trust (remember this device for 30 days).

FAQ

What is the difference between 2FA and MFA?

2FA (Two-Factor Authentication) uses exactly two factors. MFA can use two or more. In practice, the terms are used interchangeably. All MFA implementations require at least two factors.

Is SMS-based MFA secure?

SMS MFA is better than no MFA but vulnerable to SIM swapping attacks. TOTP with authenticator apps or hardware security keys (FIDO2/WebAuthn) provide stronger security.

How do I handle users who lose their MFA device?

Provide backup codes during enrollment. Allow users to regenerate backup codes from their profile. Implement an account recovery Process with email verification and a waiting period for sensitive changes.

What is WebAuthn?

WebAuthn is a web standard for passwordless authentication using public key cryptography. It supports platform authenticators (fingerprint, FaceID) and external authenticators (YubiKey). It replaces passwords entirely.

Mini Project: MFA Code Validator

Build a CLI tool that can generate TOTP codes from a secret, validate codes, generate QR code URIs, and manage backup codes.

import pyotp
import sys
import json
from pathlib import Path

class MFATool:
    def __init__(self, storage_path=".mfa_secrets.json"):
        self.storage_path = Path(storage_path)
        self.secrets = self._load()

    def _load(self):
        if self.storage_path.exists():
            return json.loads(self.storage_path.read_text())
        return {}

    def _save(self):
        self.storage_path.write_text(json.dumps(self.secrets, indent=2))

    def generate(self, email):
        secret = pyotp.random_base32()
        self.secrets[email] = secret
        self._save()
        totp = pyotp.TOTP(secret)
        uri = totp.provisioning_uri(email, issuer="DodaTech")
        print(f"Secret: {secret}")
        print(f"Current code: {totp.now()}")
        print(f"Provisioning URI: {uri}")

    def code(self, email):
        secret = self.secrets.get(email)
        if not secret:
            print(f"No secret for {email}")
            return
        totp = pyotp.TOTP(secret)
        print(f"Current code: {totp.now()}")
        print(f"Remaining: {30 - (int(__import__('time').time()) % 30)}s")

    def verify(self, email, code):
        secret = self.secrets.get(email)
        if not secret:
            print(f"No secret for {email}")
            return
        totp = pyotp.TOTP(secret)
        if totp.verify(code, valid_window=1):
            print("Code VALID")
        else:
            print("Code INVALID")

if __name__ == "__main__":
    tool = MFATool()
    if len(sys.argv) < 2:
        print("Commands: generate <email>, code <email>, verify <email> <code>")
    elif sys.argv[1] == "generate":
        tool.generate(sys.argv[2])
    elif sys.argv[1] == "code":
        tool.code(sys.argv[2])
    elif sys.argv[1] == "verify":
        tool.verify(sys.argv[2], sys.argv[3])

What's Next

Learn about passwordless authentication for modern login experiences, then explore social login for third-party identity integration.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro