Multi-Factor Authentication (MFA): Complete Reference

Type: Software Reference Confidence: 0.92 Sources: 7 Verified: 2026-02-24 Freshness: 2026-02-24

TL;DR

Constraints

Quick Reference

MFA MethodSecurity LevelUX FrictionPhishing ResistantImplementation ComplexityNIST AALKey Standard
TOTP (Authenticator App)HighLowNoLowAAL2RFC 6238
WebAuthn/FIDO2 (Passkeys)Very HighVery LowYesMediumAAL2-AAL3W3C WebAuthn L2
SMS OTPLowMediumNoLowRestricted (deprecated)--
Email OTPLowHighNoVery LowDeprecated in SP 800-63-4--
Push NotificationHighVery LowPartial (fatigue attacks)HighAAL2Proprietary
Hardware Security Key (U2F)Very HighLowYesMediumAAL3FIDO U2F / CTAP2
Backup CodesN/A (recovery only)LowNoVery LowN/A--

Factor Categories

Factor TypeExamplesStorage/Transmission
Knowledge (something you know)Password, PIN, security questionHashed (argon2id)
Possession (something you have)Phone (TOTP app), security key, smart cardChallenge-response / HMAC
Inherence (something you are)Fingerprint, face, irisBiometric template (on-device)

Decision Tree

START
+-- Need phishing resistance?
|   +-- YES --> WebAuthn/FIDO2 (passkeys or hardware keys)
|   +-- NO v
+-- Users have smartphones?
|   +-- YES --> TOTP via authenticator app (Google Authenticator, Authy, 1Password)
|   +-- NO v
+-- Enterprise with MDM?
|   +-- YES --> Push notification (Duo, Okta Verify, Microsoft Authenticator)
|   +-- NO v
+-- SMS/Email only as last resort?
|   +-- YES --> SMS OTP (always pair with rate limiting + fraud detection)
|   +-- NO v
+-- DEFAULT --> TOTP as primary + WebAuthn as optional upgrade path

Step-by-Step Guide

1. Generate and store TOTP secret

Create a cryptographically random 160-bit (20-byte) secret per user. Encrypt it before storing in your database. [src3]

// Node.js -- using otplib v12+
const { authenticator } = require('otplib');
const crypto = require('crypto');

// Generate a base32-encoded secret (160-bit)
const secret = authenticator.generateSecret(20);

// Encrypt before storing (AES-256-GCM)
function encryptSecret(plaintext, encryptionKey) {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey, iv);
  let encrypted = cipher.update(plaintext, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const tag = cipher.getAuthTag().toString('hex');
  return `${iv.toString('hex')}:${tag}:${encrypted}`;
}

Verify: authenticator.generate(secret) returns a 6-digit string.

2. Generate QR code for authenticator app enrollment

Build an otpauth:// URI and render it as a QR code for the user to scan. [src3]

const QRCode = require('qrcode');

const otpauthUrl = authenticator.keyuri(
  user.email,           // account label
  'YourAppName',        // issuer
  secret                // base32 secret
);

const qrCodeDataUrl = await QRCode.toDataURL(otpauthUrl);
// Render qrCodeDataUrl in <img src="..."> for user to scan

Verify: Scan the QR code with Google Authenticator -- it shows the app name and generates 6-digit codes.

3. Verify TOTP code and enable MFA

Accept the user's first TOTP code to confirm enrollment before enabling MFA on their account. [src3]

const isValid = authenticator.check(userSubmittedToken, secret);

if (isValid) {
  // Generate 10 backup codes
  const backupCodes = Array.from({ length: 10 }, () =>
    crypto.randomBytes(4).toString('hex')
  );
  // Hash each backup code before storing
  const hashedCodes = await Promise.all(
    backupCodes.map(code => bcrypt.hash(code, 12))
  );
  // Store hashedCodes, set mfa_enabled = true
  // Show backupCodes to user ONCE (never again)
}

Verify: Submit a valid 6-digit code from authenticator -- returns isValid === true.

4. Enforce MFA on login flow

After password verification, require MFA verification before issuing a session. [src5]

app.post('/login', async (req, res) => {
  const { email, password, mfaToken } = req.body;
  const user = await verifyPassword(email, password);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  if (user.mfa_enabled) {
    if (!mfaToken) {
      return res.status(200).json({
        mfa_required: true,
        mfa_challenge_token: signTempToken(user.id, '5m')
      });
    }
    // Rate-limit: 3 attempts per 60 seconds
    const attempts = await getMfaAttempts(user.id, 60);
    if (attempts >= 3) {
      return res.status(429).json({ error: 'Too many attempts' });
    }
    await recordMfaAttempt(user.id);

    const secret = decryptSecret(user.mfa_secret_encrypted, KEY);
    if (!authenticator.check(mfaToken, secret)) {
      return res.status(401).json({ error: 'Invalid MFA code' });
    }
  }
  const session = await createSession(user.id, { mfa_verified: true });
  res.json({ session_token: session.token });
});

Verify: Login without MFA token returns { mfa_required: true }. Valid TOTP completes login.

5. Implement WebAuthn registration (optional upgrade)

Add WebAuthn as a phishing-resistant second factor using SimpleWebAuthn. [src4] [src7]

const { generateRegistrationOptions, verifyRegistrationResponse } = require('@simplewebauthn/server');

app.get('/webauthn/register/options', async (req, res) => {
  const user = req.authenticatedUser;
  const existingDevices = await getUserAuthenticators(user.id);
  const options = await generateRegistrationOptions({
    rpName: 'YourAppName',
    rpID: 'yourdomain.com',
    userID: user.id,
    userName: user.email,
    attestationType: 'none',
    excludeCredentials: existingDevices.map(dev => ({
      id: dev.credentialID, type: 'public-key',
    })),
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'preferred',
    },
  });
  req.session.currentChallenge = options.challenge;
  res.json(options);
});

Verify: Endpoint returns JSON with challenge, rp, user, and pubKeyCredParams fields.

6. Implement WebAuthn assertion (login verification)

Verify the WebAuthn assertion during login after password authentication. [src4] [src7]

const { generateAuthenticationOptions, verifyAuthenticationResponse } = require('@simplewebauthn/server');

app.post('/webauthn/authenticate/verify', async (req, res) => {
  const expectedChallenge = req.session.currentChallenge;
  const authn = await getAuthenticatorById(req.body.id);
  const verification = await verifyAuthenticationResponse({
    response: req.body,
    expectedChallenge,
    expectedOrigin: 'https://yourdomain.com',
    expectedRPID: 'yourdomain.com',
    authenticator: authn,
  });
  if (verification.verified) {
    await updateAuthenticatorCounter(authn.credentialID, verification.authenticationInfo.newCounter);
    // Issue session with mfa_verified: true
  }
});

Verify: Authenticate with registered security key -- verification.verified === true.

Code Examples

Node.js (otplib): TOTP Setup and Verification

// Input:  User email, app name, encryption key
// Output: QR code data URL, encrypted secret

const { authenticator } = require('otplib'); // v12.0.1
const QRCode = require('qrcode');            // v1.5.3
const crypto = require('crypto');

authenticator.options = { digits: 6, step: 30, window: 1 };

async function setupTOTP(userEmail, appName, encryptionKey) {
  const secret = authenticator.generateSecret(20);
  const uri = authenticator.keyuri(userEmail, appName, secret);
  const qr = await QRCode.toDataURL(uri);
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey, iv);
  let enc = cipher.update(secret, 'utf8', 'hex') + cipher.final('hex');
  const tag = cipher.getAuthTag().toString('hex');
  return { qrCodeDataUrl: qr, encryptedSecret: `${iv.toString('hex')}:${tag}:${enc}` };
}

function verifyTOTP(token, secret) {
  return authenticator.check(token, secret);
}

Python (pyotp): TOTP Setup and Verification

# Input:  User email, issuer name
# Output: Provisioning URI, base32 secret, verification result

import pyotp  # v2.9.0
from cryptography.fernet import Fernet  # v42.0.0

def setup_totp(user_email: str, issuer: str) -> dict:
    secret = pyotp.random_base32(length=32)
    totp = pyotp.TOTP(secret)
    uri = totp.provisioning_uri(name=user_email, issuer_name=issuer)
    return {"secret": secret, "provisioning_uri": uri}

def verify_totp(token: str, secret: str) -> bool:
    totp = pyotp.TOTP(secret)
    return totp.verify(token, valid_window=1)

def encrypt_secret(secret: str, key: bytes) -> str:
    return Fernet(key).encrypt(secret.encode()).decode()

def decrypt_secret(encrypted: str, key: bytes) -> str:
    return Fernet(key).decrypt(encrypted.encode()).decode()

WebAuthn (SimpleWebAuthn): Browser-Side Registration

// Input:  Registration options from server
// Output: Registration response to send back to server

import { startRegistration } from '@simplewebauthn/browser'; // v11+

async function registerWebAuthn() {
  const optionsRes = await fetch('/webauthn/register/options');
  const options = await optionsRes.json();
  const registration = await startRegistration(options);
  const verifyRes = await fetch('/webauthn/register/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(registration),
  });
  const result = await verifyRes.json();
  if (result.verified) console.log('WebAuthn registration successful');
}

Anti-Patterns

Wrong: Storing TOTP secrets in plaintext

// BAD -- plaintext secret; if DB is breached, all MFA is compromised
await db.query(
  'INSERT INTO users (email, totp_secret) VALUES ($1, $2)',
  [email, secret]
);

Correct: Encrypting TOTP secrets at rest

// GOOD -- encrypted; DB breach does not expose secrets
const encryptedSecret = encryptSecret(secret, ENCRYPTION_KEY);
await db.query(
  'INSERT INTO users (email, totp_secret_encrypted) VALUES ($1, $2)',
  [email, encryptedSecret]
);

Wrong: No rate limiting on MFA verification

// BAD -- unlimited attempts allow brute-force of 1M combinations
app.post('/verify-mfa', (req, res) => {
  const isValid = authenticator.check(req.body.token, user.secret);
  res.json({ valid: isValid });
});

Correct: Rate-limited MFA verification

// GOOD -- 3 attempts per 60 seconds + audit logging
app.post('/verify-mfa', rateLimit({ windowMs: 60000, max: 3 }), (req, res) => {
  const isValid = authenticator.check(req.body.token, user.secret);
  if (!isValid) auditLog('mfa_failed', { userId: user.id, ip: req.ip });
  res.json({ valid: isValid });
});

Wrong: Using SMS as the sole second factor

// BAD -- SMS-only is vulnerable to SIM swap and SS7 interception
async function verifyMFA(user, code) {
  const sent = await twilioClient.verify.services(sid)
    .verificationChecks.create({ to: user.phone, code });
  return sent.status === 'approved';
}

Correct: SMS as fallback with TOTP as primary

// GOOD -- TOTP primary, SMS only as fallback with audit trail
async function verifyMFA(user, code, method) {
  if (method === 'totp') {
    return authenticator.check(code, decryptSecret(user.totp_secret));
  }
  if (method === 'sms' && !user.totp_enabled) {
    const result = await verifySmsOtp(user.phone, code);
    auditLog('sms_mfa_used', { userId: user.id, reason: 'totp_not_enrolled' });
    return result;
  }
  return false;
}

Common Pitfalls

Diagnostic Commands

# Test TOTP generation locally (Node.js)
node -e "const {authenticator}=require('otplib'); const s=authenticator.generateSecret(); console.log('Secret:',s,'Token:',authenticator.generate(s))"

# Verify server NTP synchronization (critical for TOTP)
timedatectl status | grep "synchronized"

# Check otplib / pyotp version
npm list otplib 2>/dev/null || pip show pyotp 2>/dev/null

# Test WebAuthn endpoint availability
curl -s https://yourdomain.com/webauthn/register/options -H "Authorization: Bearer TOKEN" | jq '.challenge'

# Verify HTTPS is enabled (required for WebAuthn)
curl -sI https://yourdomain.com | grep -i "strict-transport-security"

# Check rate limit headers on MFA endpoint
curl -sI -X POST https://yourdomain.com/verify-mfa | grep -i "x-ratelimit"

Version History & Compatibility

Standard/LibraryVersionStatusKey Changes
NIST SP 800-63-44.0Current (Aug 2025)Deprecated email OTP, downgraded SMS, added passkey support at AAL2
NIST SP 800-63-33.0SupersededOriginal MFA assurance levels (AAL1-AAL3)
RFC 6238 (TOTP)1.0Active (2011)Stable -- no revisions planned
W3C WebAuthnLevel 2Current (2021)Added cross-origin iframes, large blob storage
W3C WebAuthnLevel 3Draft (2024)Conditional mediation, related origin requests
otplib (Node.js)12.xCurrentModular architecture, tree-shaking support
pyotp (Python)2.9.xCurrentSteam guard support, type hints
SimpleWebAuthn11.xCurrentESM-first, Deno/Bun support

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Protecting user accounts with sensitive data (financial, health, PII)Machine-to-machine API authenticationMutual TLS or API key + HMAC signing
Compliance requires MFA (PCI DSS, HIPAA, SOC 2, NIST 800-171)Single-use anonymous interactions (no account)No authentication needed
Login from untrusted devices or networksUsers on shared/kiosk devices without personal authenticatorsDevice certificates or SSO with IdP-managed MFA
Step-up authentication for high-risk operationsInternal microservice calls behind a service meshService mesh mTLS (Istio, Linkerd)
Enterprise or B2B applications with compliance requirementsFriction-sensitive consumer onboardingProgressive MFA (offer after first login, require after risk signal)

Important Caveats

Related Units