Multi-Factor Authentication Cheat Sheet
Covers MFA factor types, common protocols like TOTP and WebAuthn, and implementation guidance for adding MFA to an application.
Authentication Factor Types
The categories combined to form multi-factor authentication.
- Something you know- Password, PIN, security question
- Something you have- Hardware token, authenticator app, smart card, phone
- Something you are- Biometrics: fingerprint, face, iris
- Somewhere you are- Location-based signals, sometimes used as an additional risk factor
- True MFA- Requires factors from at least two different categories, not two of the same type
TOTP Generation (Python)
Generating a time-based one-time password, as used by authenticator apps.
import pyotp# Generate a new secret when enrolling a usersecret = pyotp.random_base32()# Provisioning URI for QR code (scan in Google Authenticator, etc.)totp = pyotp.TOTP(secret)uri = totp.provisioning_uri(name="[email protected]", issuer_name="SkillVeris")# Verify a code entered by the useris_valid = totp.verify("123456")
Common MFA Methods, Ranked by Strength
Relative security of different MFA delivery mechanisms.
- Hardware security keys (FIDO2/WebAuthn)- Strongest; phishing-resistant, cryptographic challenge-response
- Authenticator app (TOTP)- Strong; offline-generated codes, but still phishable via fake login pages
- Push notification- Convenient, vulnerable to MFA fatigue/push-bombing attacks if not paired with number matching
- SMS/voice OTP- Weakest; vulnerable to SIM swapping and SS7 interception, avoid for high-value accounts
Implementation Considerations
Practical points when adding MFA to a system.
- Backup codes- Provide one-time recovery codes in case the primary factor is unavailable
- Rate limiting- Throttle verification attempts to prevent brute-forcing OTP codes
- Time window tolerance- Allow a small clock drift window (e.g. ±1 step) when validating TOTP codes
- Number matching- Require the user to enter a displayed number in push notifications to defeat fatigue attacks
WebAuthn/FIDO2 Registration Flow
Server-side steps for enrolling a phishing-resistant security key or platform authenticator.
// 1. Server generates a registration challengeconst options = { challenge: randomBytes(32), rp: { name: 'SkillVeris', id: 'skillveris.com' }, user: { id: userId, name: '[email protected]', displayName: 'Jane' }, pubKeyCredParams: [{ alg: -7, type: 'public-key' }], // ES256 authenticatorSelection: { userVerification: 'required', residentKey: 'preferred' }, attestation: 'none',};// 2. Browser: navigator.credentials.create({ publicKey: options })// 3. Server verifies the attestation response and stores the public key// bound to the exact origin — this binding is why WebAuthn can't be phished// by a lookalike domain, unlike a TOTP codeconst verification = await verifyRegistrationResponse({ response: attestationResponse, expectedChallenge: options.challenge, expectedOrigin: 'https://skillveris.com', expectedRPID: 'skillveris.com',});await storeCredential(userId, verification.registrationInfo.credentialPublicKey);
HOTP vs TOTP: Counter-Based vs Time-Based OTP
The underlying algorithm difference and why TOTP dominates modern authenticator apps.
import hmac, hashlib, struct, timedef hotp(secret: bytes, counter: int, digits=6) -> str: msg = struct.pack('>Q', counter) h = hmac.new(secret, msg, hashlib.sha1).digest() offset = h[-1] & 0x0F code = (struct.unpack('>I', h[offset:offset+4])[0] & 0x7FFFFFFF) % (10 ** digits) return str(code).zfill(digits)# HOTP: counter increments per use -> server/client can desync if a code is skipped# TOTP: counter = floor(unix_time / 30) -> self-syncs as long as clocks agree,# which is why virtually every authenticator app uses TOTP, not raw HOTPdef totp(secret: bytes, step=30) -> str: return hotp(secret, counter=int(time.time()) // step)
Adaptive / Risk-Based MFA Trigger Logic
Deciding when to demand step-up authentication instead of prompting MFA on every login.
def requires_step_up_mfa(login_event) -> bool: risk_score = 0 if login_event.ip_reputation == "suspicious": risk_score += 40 if login_event.new_device: risk_score += 25 if login_event.impossible_travel: risk_score += 50 if login_event.resource_sensitivity == "high": risk_score += 20 if login_event.time_since_last_mfa_minutes < 15: risk_score -= 30 # recent successful MFA reduces friction return risk_score >= 50# Trusted, recent, low-sensitivity logins skip a redundant prompt;# anomalous or high-value ones get stepped up -- this is what separates# adaptive MFA from a static "MFA on every login" policy
Hashing Backup/Recovery Codes at Rest
Backup codes must never be stored in plaintext or reused after redemption.
import secrets, hashlibdef generate_backup_codes(n=10): codes = [secrets.token_hex(5) for _ in range(n)] # shown to user once hashed = [hashlib.sha256(c.encode()).hexdigest() for c in codes] store_hashed_codes(user_id, hashed) # only hashes persisted return codesdef redeem_backup_code(user_id, submitted_code): h = hashlib.sha256(submitted_code.encode()).hexdigest() if not consume_if_unused(user_id, h): # single-use, mark burned raise InvalidCodeError() return True
MFA Attack Vectors and Mitigations
How each MFA method actually gets bypassed in the wild, and the corresponding defense.
- Real-time phishing proxy (AiTM)- Attacker-in-the-middle relays login + OTP live to hijack the session cookie; mitigate with WebAuthn's origin binding, which can't be relayed
- MFA fatigue / push bombing- Spamming push approvals until the user taps accept; mitigate with number matching and rate-limited push attempts
- SIM swapping- Attacker ports the victim's number to intercept SMS OTP; mitigate by not offering SMS as an MFA option for privileged accounts
- SS7 interception- Telecom protocol weaknesses allow SMS/voice OTP interception without a SIM swap at all
- OTP secret theft- Malware exfiltrating the TOTP seed during enrollment (e.g. QR code screenshot) lets an attacker generate valid codes indefinitely
- Session token theft post-MFA- Once MFA succeeds, stealing the resulting session cookie/token bypasses MFA entirely; mitigate with token binding and short session lifetimes
Prefer FIDO2/WebAuthn security keys for admin and privileged accounts — unlike TOTP or SMS, the cryptographic challenge is bound to the origin domain, so it can't be phished by a lookalike login page.