---
# === IDENTITY ===
id: software/patterns/mfa-implementation/2026
canonical_question: "How do I implement Multi-Factor Authentication?"
aliases:
  - "MFA implementation guide"
  - "two-factor authentication setup"
  - "2FA implementation Node.js Python"
  - "TOTP implementation"
  - "WebAuthn FIDO2 integration"
  - "add MFA to web application"
  - "multi-factor authentication best practices"
  - "OTP verification flow"
entity_type: software_reference
domain: software > patterns > Multi-Factor Authentication
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-24
confidence: 0.92
version: 1.0
first_published: 2026-02-24

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "NIST SP 800-63-4 superseded 800-63-3 (2025-08); deprecated email OTP and downgraded SMS"
  next_review: 2026-08-23
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "TOTP shared secrets must be encrypted at rest (AES-256-GCM or equivalent) -- never store plaintext"
  - "Rate-limit verification attempts to 3-5 per minute per user to prevent brute-force (TOTP has only 1M combinations)"
  - "Backup/recovery codes are one-time-use -- hash them like passwords (bcrypt/argon2), never store plaintext"
  - "SMS OTP must not be the sole second factor -- NIST SP 800-63B deprecated SMS as a standalone authenticator"
  - "WebAuthn relying party ID (rpID) must match the effective domain -- misconfigured rpID silently fails registration"
  - "TOTP time window tolerance should be +/- 1 step (30s) maximum -- wider windows increase brute-force surface"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need passwordless-only login (no password at all)"
    use_instead: "WebAuthn/passkeys-only flow -- MFA assumes a primary factor already exists"
  - condition: "Need SSO federation (SAML/OIDC) with MFA delegated to IdP"
    use_instead: "OAuth2/OIDC with IdP-enforced MFA -- do not implement MFA in the relying party"
  - condition: "Need machine-to-machine API authentication"
    use_instead: "Mutual TLS or API key + HMAC signing -- MFA is for interactive human authentication"

# === AGENT HINTS ===
inputs_needed:
  - key: mfa_method
    question: "Which MFA method do you want to implement?"
    type: choice
    options: ["TOTP (Google Authenticator, Authy)", "WebAuthn/FIDO2 (passkeys, security keys)", "SMS OTP", "Push notification", "Multiple methods"]
  - key: framework
    question: "What backend framework/language are you using?"
    type: choice
    options: ["Node.js (Express/Fastify)", "Python (Django/Flask/FastAPI)", "Go", "Java (Spring Boot)", "Ruby on Rails", "Other"]
  - key: recovery_flow
    question: "What recovery mechanism do you need for lost authenticators?"
    type: choice
    options: ["Backup codes", "Admin-initiated reset", "Alternative MFA method fallback", "Account recovery via verified email + identity verification"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/patterns/mfa-implementation/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-24)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/patterns/session-management/2026"
      label: "Session Management"
    - id: "software/patterns/jwt-implementation/2026"
      label: "JWT Implementation"
    - id: "software/patterns/rbac-implementation/2026"
      label: "Role-Based Access Control"
  solves: []
  alternative_to:
    - id: "software/patterns/oauth2-authorization-code-pkce/2026"
      label: "OAuth2 Authorization Code with PKCE (delegates MFA to IdP)"
  often_confused_with:
    - id: "software/patterns/passwordless-authentication/2026"
      label: "Passwordless Authentication (replaces passwords; MFA supplements them)"

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "NIST SP 800-63B: Digital Identity Guidelines -- Authentication and Lifecycle Management"
    author: NIST
    url: https://pages.nist.gov/800-63-3/sp800-63b.html
    type: official_docs
    published: 2020-03-02
    reliability: authoritative
  - id: src2
    title: "NIST SP 800-63B-4: Digital Identity Guidelines (2025 revision)"
    author: NIST
    url: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63B-4.pdf
    type: official_docs
    published: 2025-08-01
    reliability: authoritative
  - id: src3
    title: "RFC 6238: TOTP -- Time-Based One-Time Password Algorithm"
    author: IETF
    url: https://datatracker.ietf.org/doc/html/rfc6238
    type: rfc_spec
    published: 2011-05-01
    reliability: authoritative
  - id: src4
    title: "Web Authentication: An API for accessing Public Key Credentials (Level 2)"
    author: W3C
    url: https://www.w3.org/TR/webauthn-2/
    type: official_docs
    published: 2021-04-08
    reliability: authoritative
  - id: src5
    title: "OWASP Multifactor Authentication Cheat Sheet"
    author: OWASP
    url: https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html
    type: community_resource
    published: 2024-01-15
    reliability: high
  - id: src6
    title: "FIDO2: Web Authentication (WebAuthn) Specifications"
    author: FIDO Alliance
    url: https://fidoalliance.org/specifications/
    type: official_docs
    published: 2024-06-01
    reliability: authoritative
  - id: src7
    title: "SimpleWebAuthn: WebAuthn, Simplified"
    author: Matthew Miller
    url: https://github.com/MasterKale/SimpleWebAuthn
    type: community_resource
    published: 2025-01-10
    reliability: moderate_high
---

# Multi-Factor Authentication (MFA): Complete Reference

## TL;DR

- **Bottom line**: Implement TOTP (RFC 6238) as the default MFA method for broad compatibility, and add WebAuthn/FIDO2 for phishing-resistant authentication on modern platforms.
- **Key tool/command**: `otplib` (Node.js) / `pyotp` (Python) for TOTP; `@simplewebauthn/server` for WebAuthn.
- **Watch out for**: Storing TOTP secrets in plaintext -- always encrypt at rest with AES-256-GCM and rate-limit verification to 3-5 attempts/minute.
- **Works with**: Any web framework; TOTP works on all platforms; WebAuthn requires HTTPS and a modern browser (Chrome 67+, Firefox 60+, Safari 14+).

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- TOTP shared secrets must be encrypted at rest (AES-256-GCM or equivalent) -- never store in plaintext [src1]
- Rate-limit verification attempts to 3-5 per minute per user -- TOTP codes have only 10^6 combinations per 30-second window [src3]
- Backup/recovery codes are one-time-use -- hash them with bcrypt/argon2, never store plaintext [src5]
- SMS OTP must not be the sole second factor -- NIST SP 800-63-4 deprecated SMS as a standalone authenticator due to SIM-swap and SS7 attacks [src2]
- WebAuthn relying party ID (rpID) must exactly match the effective domain or a registrable suffix -- misconfiguration causes silent registration failure [src4]
- TOTP clock drift tolerance should be +/- 1 step (30 seconds) maximum -- wider windows significantly increase brute-force attack surface [src3]

## Quick Reference

| MFA Method | Security Level | UX Friction | Phishing Resistant | Implementation Complexity | NIST AAL | Key Standard |
|---|---|---|---|---|---|---|
| TOTP (Authenticator App) | High | Low | No | Low | AAL2 | RFC 6238 |
| WebAuthn/FIDO2 (Passkeys) | Very High | Very Low | Yes | Medium | AAL2-AAL3 | W3C WebAuthn L2 |
| SMS OTP | Low | Medium | No | Low | Restricted (deprecated) | -- |
| Email OTP | Low | High | No | Very Low | Deprecated in SP 800-63-4 | -- |
| Push Notification | High | Very Low | Partial (fatigue attacks) | High | AAL2 | Proprietary |
| Hardware Security Key (U2F) | Very High | Low | Yes | Medium | AAL3 | FIDO U2F / CTAP2 |
| Backup Codes | N/A (recovery only) | Low | No | Very Low | N/A | -- |

**Factor Categories** (must combine factors from different categories for true MFA):

| Factor Type | Examples | Storage/Transmission |
|---|---|---|
| Knowledge (something you know) | Password, PIN, security question | Hashed (argon2id) |
| Possession (something you have) | Phone (TOTP app), security key, smart card | Challenge-response / HMAC |
| Inherence (something you are) | Fingerprint, face, iris | Biometric 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]

```javascript
// 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);
// secret example: "JBSWY3DPEHPK3PXP" (base32)

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

// Store in DB: { user_id, mfa_secret_encrypted, mfa_enabled: false }
```

**Verify**: Generate a test token with `authenticator.generate(secret)` and confirm it is 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]

```javascript
const QRCode = require('qrcode');

const otpauthUrl = authenticator.keyuri(
  user.email,           // account label
  'YourAppName',        // issuer
  secret                // base32 secret
);
// otpauth://totp/YourAppName:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=YourAppName

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

**Verify**: Scan the QR code with Google Authenticator -- it should show the app name and generate 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]

```javascript
const isValid = authenticator.check(userSubmittedToken, secret);
// check() validates current window +/- 1 step by default

if (isValid) {
  // Generate 10 backup codes
  const backupCodes = Array.from({ length: 10 }, () =>
    crypto.randomBytes(4).toString('hex') // 8-char hex codes
  );
  // 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 the authenticator app -- should return `isValid === true`.

### 4. Enforce MFA on login flow

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

```javascript
app.post('/login', async (req, res) => {
  const { email, password, mfaToken } = req.body;

  // Step 1: Verify password
  const user = await verifyPassword(email, password);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  // Step 2: Check if MFA is enabled
  if (user.mfa_enabled) {
    if (!mfaToken) {
      // Return partial auth -- client must prompt for MFA
      return res.status(200).json({
        mfa_required: true,
        mfa_challenge_token: signTempToken(user.id, '5m')
      });
    }

    // Step 3: Rate-limit MFA attempts (3 per minute)
    const attempts = await getMfaAttempts(user.id, 60); // last 60s
    if (attempts >= 3) {
      return res.status(429).json({ error: 'Too many attempts. Try again later.' });
    }
    await recordMfaAttempt(user.id);

    // Step 4: Verify TOTP
    const secret = decryptSecret(user.mfa_secret_encrypted, ENCRYPTION_KEY);
    const isValid = authenticator.check(mfaToken, secret);
    if (!isValid) {
      return res.status(401).json({ error: 'Invalid MFA code' });
    }
  }

  // Step 5: Issue session
  const session = await createSession(user.id, { mfa_verified: true });
  res.json({ session_token: session.token });
});
```

**Verify**: Login with valid password but no MFA token returns `{ mfa_required: true }`. Submit valid TOTP code to complete login.

### 5. Implement WebAuthn registration (optional upgrade)

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

```javascript
// Server -- using @simplewebauthn/server v11+
const {
  generateRegistrationOptions,
  verifyRegistrationResponse,
} = require('@simplewebauthn/server');

const rpName = 'YourAppName';
const rpID = 'yourdomain.com';
const origin = 'https://yourdomain.com';

app.get('/webauthn/register/options', async (req, res) => {
  const user = req.authenticatedUser;
  const existingDevices = await getUserAuthenticators(user.id);

  const options = await generateRegistrationOptions({
    rpName,
    rpID,
    userID: user.id,
    userName: user.email,
    attestationType: 'none', // 'none' is simplest; use 'direct' for enterprise
    excludeCredentials: existingDevices.map(dev => ({
      id: dev.credentialID,
      type: 'public-key',
    })),
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'preferred',
    },
  });

  // Store challenge in session for verification
  req.session.currentChallenge = options.challenge;
  res.json(options);
});
```

**Verify**: The `/webauthn/register/options` endpoint returns a JSON object with `challenge`, `rp`, `user`, and `pubKeyCredParams` fields.

### 6. Implement WebAuthn assertion (login verification)

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

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

app.get('/webauthn/authenticate/options', async (req, res) => {
  const user = req.authenticatedUser; // from password step
  const devices = await getUserAuthenticators(user.id);

  const options = await generateAuthenticationOptions({
    rpID,
    allowCredentials: devices.map(dev => ({
      id: dev.credentialID,
      type: 'public-key',
    })),
    userVerification: 'preferred',
  });

  req.session.currentChallenge = options.challenge;
  res.json(options);
});

app.post('/webauthn/authenticate/verify', async (req, res) => {
  const expectedChallenge = req.session.currentChallenge;
  const authenticator = await getAuthenticatorById(req.body.id);

  const verification = await verifyAuthenticationResponse({
    response: req.body,
    expectedChallenge,
    expectedOrigin: origin,
    expectedRPID: rpID,
    authenticator,
  });

  if (verification.verified) {
    // Update sign count to detect cloned authenticators
    await updateAuthenticatorCounter(
      authenticator.credentialID,
      verification.authenticationInfo.newCounter
    );
    // Issue session with mfa_verified: true
  }
});
```

**Verify**: Authenticate with a registered security key or passkey -- server returns `verification.verified === true`.

## Code Examples

### Node.js (otplib): TOTP Setup and Verification

> Full script: [node-js-otplib-totp-setup-and-verification.js](scripts/node-js-otplib-totp-setup-and-verification.js) (25 lines)

```javascript
// Input:  User email, app name
// Output: QR code data URL, encrypted secret, backup codes
const { authenticator } = require('otplib'); // v12.0.1
const QRCode = require('qrcode');            // v1.5.3
const crypto = require('crypto');
# ... (see full script)
```

### Python (pyotp): TOTP Setup and Verification

> Full script: [python-pyotp-totp-setup-and-verification.py](scripts/python-pyotp-totp-setup-and-verification.py) (25 lines)

```python
# 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:
# ... (see full script)
```

### WebAuthn (SimpleWebAuthn): Browser-Side Registration

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

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

async function registerWebAuthn() {
  // 1. Get options from server
  const optionsRes = await fetch('/webauthn/register/options');
  const options = await optionsRes.json();

  // 2. Prompt user for authenticator (biometric, security key, etc.)
  const registration = await startRegistration(options);

  // 3. Send response to server for verification
  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

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

### Correct: Encrypting TOTP secrets at rest

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

### Wrong: No rate limiting on MFA verification

```javascript
// BAD -- unlimited TOTP attempts allow brute-force
app.post('/verify-mfa', (req, res) => {
  const isValid = authenticator.check(req.body.token, user.secret);
  // Attacker can try all 1,000,000 combinations in the 30s window
  res.json({ valid: isValid });
});
```

### Correct: Rate-limited MFA verification

```javascript
// GOOD -- rate limit to 3 attempts per 60 seconds
app.post('/verify-mfa', rateLimit({ windowMs: 60000, max: 3 }), (req, res) => {
  const isValid = authenticator.check(req.body.token, user.secret);
  if (!isValid) {
    // Log failed attempt for security monitoring
    auditLog('mfa_failed', { userId: user.id, ip: req.ip });
  }
  res.json({ valid: isValid });
});
```

### Wrong: Using SMS as the sole second factor

```javascript
// BAD -- SMS-only 2FA 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';
  // No fallback, no alternative method -- NIST deprecated SMS-only MFA
}
```

### Correct: SMS as fallback with TOTP as primary

```javascript
// GOOD -- TOTP primary, SMS only as fallback with additional controls
async function verifyMFA(user, code, method) {
  if (method === 'totp') {
    return authenticator.check(code, decryptSecret(user.totp_secret));
  }
  if (method === 'sms' && user.totp_enabled === false) {
    // SMS fallback only when TOTP is not set up
    // Additional fraud checks: device fingerprint, geolocation
    const result = await verifySmsOtp(user.phone, code);
    auditLog('sms_mfa_used', { userId: user.id, reason: 'totp_not_enrolled' });
    return result;
  }
  return false;
}
```

## Common Pitfalls

- **Clock drift breaks TOTP**: Server and user device clocks diverge by more than 30 seconds, causing valid codes to be rejected. Fix: Use `window: 1` in otplib (accepts +/- 1 step) and ensure server NTP is synced. [src3]
- **QR code encoding errors**: Using the wrong `otpauth://` URI format causes authenticator apps to reject the secret. Fix: Always use the library's `keyuri()` method; never construct the URI manually. [src3]
- **Backup codes stored unhashed**: If the database is compromised, attackers can bypass MFA using recovery codes. Fix: Hash each backup code with bcrypt (cost 12+) or argon2id before storage. [src5]
- **MFA bypass via session fixation**: Attacker reuses a pre-MFA session token after user completes MFA on a different session. Fix: Regenerate session ID after MFA verification and bind MFA status to the new session. [src5]
- **WebAuthn rpID mismatch**: Setting `rpID` to `www.example.com` when the user authenticates from `example.com` (or vice versa) silently fails. Fix: Set `rpID` to the registrable domain (`example.com`), which covers all subdomains. [src4]
- **TOTP replay attack**: Accepting the same TOTP code twice within its validity window. Fix: Track last-used timestamp per user and reject any code with a timestamp equal to or earlier than the previous one. [src3]
- **Push notification fatigue attack**: Attackers repeatedly trigger push MFA prompts until the user accidentally approves. Fix: Implement number matching (user must enter a number shown on screen) instead of simple approve/deny. [src2]

## Diagnostic Commands

```bash
# 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"
# or on macOS:
sntp -S time.apple.com

# 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/Library | Version | Status | Key Changes |
|---|---|---|---|
| NIST SP 800-63-4 | 4.0 | Current (Aug 2025) | Deprecated email OTP, downgraded SMS, added passkey support at AAL2 |
| NIST SP 800-63-3 | 3.0 | Superseded | Original MFA assurance levels (AAL1-AAL3) |
| RFC 6238 (TOTP) | 1.0 | Active (2011) | Stable -- no revisions planned |
| W3C WebAuthn | Level 2 | Current (2021) | Added cross-origin iframes, large blob storage |
| W3C WebAuthn | Level 3 | Draft (2024) | Conditional mediation, related origin requests |
| otplib (Node.js) | 12.x | Current | Modular architecture, tree-shaking support |
| pyotp (Python) | 2.9.x | Current | Steam guard support, type hints |
| SimpleWebAuthn | 11.x | Current | ESM-first, Deno/Bun support |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Protecting user accounts with sensitive data (financial, health, PII) | Machine-to-machine API authentication | Mutual TLS or API key + HMAC signing |
| Compliance requires MFA (PCI DSS, HIPAA, SOC 2, NIST 800-171) | Single-use anonymous interactions (public content, no account) | No authentication needed |
| Login from untrusted devices or networks | Users are exclusively on shared/kiosk devices without personal authenticators | Device certificates or SSO with IdP-managed MFA |
| Step-up authentication for high-risk operations (wire transfers, admin actions) | Internal microservice calls behind a service mesh | Service mesh mTLS (Istio, Linkerd) |
| Enterprise or B2B applications with compliance requirements | Friction-sensitive consumer onboarding where MFA would cause drop-off | Progressive MFA (offer after first login, require after risk signal) |

## Important Caveats

- NIST SP 800-63-4 (August 2025) significantly changed the MFA landscape: email OTP is deprecated, SMS is restricted, and passkeys are now explicitly accepted at AAL2. Implementations built to SP 800-63-3 may need updates. [src2]
- WebAuthn requires HTTPS and a secure context. It will not work on `http://localhost` in production-like testing -- use `localhost` (special-cased by browsers) or a proper TLS certificate. [src4]
- TOTP is not phishing-resistant: a real-time phishing proxy (e.g., Evilginx) can capture and replay TOTP codes within their validity window. For phishing resistance, WebAuthn is the only standards-based option. [src6]
- Biometric data (fingerprints, face) in WebAuthn never leaves the user's device -- the server only stores public keys and credential IDs. Do not attempt to store or transmit biometric templates. [src4]
- Backup code UX matters: users who do not save their backup codes during enrollment will be locked out when they lose their authenticator device. Consider requiring confirmation of at least one backup code during setup. [src5]
- Push notification MFA is vulnerable to "MFA fatigue" attacks (repeated prompts until user approves). Microsoft and Okta now recommend number matching as the default. [src2]

## Related Units

- [Session Management](/software/patterns/session-management/2026)
- [JWT Implementation](/software/patterns/jwt-implementation/2026)
- [Role-Based Access Control](/software/patterns/rbac-implementation/2026)
- [OAuth2 Authorization Code with PKCE](/software/patterns/oauth2-authorization-code-pkce/2026)
