Specific MFA implementations: how they work under the hood

Technical mechanics of the most-used MFA implementations — TOTP, SMS OTP, push, FIDO2/WebAuthn, passkeys, magic links — with their flows, specific vulnerabilities, and fallback patterns. Plus CAPTCHA: why it's not authentication but is a useful complement.

Why this article is more technical than the previous one

In the previous article we went through the five categories of authentication factors and mentioned several implementations in passing. Now we open the hood. Every implementation has a concrete mechanic — a protocol, a flow, a set of specific vulnerabilities, design decisions that creep into real code. If you’re going to implement authentication in an app or audit someone else’s, this is the level of detail you need.


I’ll assume you have technical background (you’re a developer or IAM engineer). You’ll see RFC references, formulas, pseudocode, and sequence diagrams with protocol-level detail. The goal is that you finish this article able to:

  • Explain how a TOTP code is calculated.
  • Diagram the WebAuthn registration and authentication ceremonies.
  • Identify when SMS OTP is acceptable and when it isn’t.
  • Design reasonable fallbacks for when a user loses their primary factor.


TOTP and HOTP: the code that changes every 30 seconds

One-Time Password algorithms are the workhorse of enterprise MFA since the mid-2000s. They’re simple, open, and work offline.


HOTP: the predecessor (RFC 4226)

HOTP (HMAC-based One-Time Password) generates codes from a shared seed plus a counter. Each time a code is generated, the counter advances:

HOTP(K, C) = Truncate(HMAC-SHA-1(K, C))
 
where:
  K = secret key (shared seed)
  C = 8-byte counter (increments with each code)

Server and client maintain the same counter. When the user presents a code, the server recalculates with its current counter. If it matches, it validates and advances the counter.


Problem: if the counter desynchronizes (the user generated a code but didn’t use it), everything breaks. That’s why almost nobody uses pure HOTP today — they use TOTP.


TOTP: the current standard (RFC 6238)

TOTP (Time-based One-Time Password) replaces the counter with time divided into 30-second windows:

T = floor((current_unix_time - T0) / X)
 
TOTP(K) = HOTP(K, T)
 
where:
  T0 = typically 0 (Unix epoch)
  X  = typically 30 seconds

Server and client share a clock (UTC) and the same seed. Every 30 seconds they generate the same code. When the user presents it, the server recalculates and compares.


TOTP enrollment

sequenceDiagram
  accTitle: TOTP enrollment ceremony
  accDescr: The user activates TOTP; the server generates a 160-bit secret and returns it as an otpauth QR code; the authenticator app scans and stores the secret locally; the user enters a test code and the server verifies it before marking TOTP enrolled.
  actor U as User
  participant App as Auth app
  participant Srv as Server
  U->>Srv: Activates TOTP on the account
  Srv->>Srv: Generates secret K (160 bits)
  Srv->>U: Returns QR with otpauth://totp/...?secret=K
  U->>App: Scans QR (Google Auth, Authy)
  App->>App: Stores K locally
  U->>Srv: Enters test code
  Srv->>Srv: Verifies the code matches
  Srv->>U: TOTP enrolled
Enrollment binds a shared secret to the app via a QR — after one test code, both sides generate the same 30-second codes.

The otpauth:// URI is the standard format from Google Authenticator, accepted by almost all authentication apps.


Handling clock drift

Clocks drift. If the user’s is 15 seconds behind, their code will be invalid on the server. The standard solution is to accept a tolerance window — typically ±1 window, giving 90 effective seconds:

def validate_totp(K, code_received, current_time):
    for offset in [-1, 0, 1]:
        T = (current_time / 30) + offset
        expected = HOTP(K, T)
        if expected == code_received:
            return True
    return False

Too much tolerance breaks security; too little breaks usability.


TOTP vulnerabilities

  • Real-time phishing: an attacker presents a fake site, the user types the TOTP, the attacker replays it in seconds to the real site. TOTP doesn’t protect against this.
  • Secret K theft: if the user’s app is compromised (e.g., phone malware), the attacker extracts K and generates codes forever. That’s why seeds shouldn’t be easily exportable.
  • Insecure backup: apps like Authy used to allow cloud backup. If the Authy account is compromised, the TOTPs of every service are exposed.

SMS OTP: convenient but indefensible for high value

How it works

The flow is the simplest of all the implementations here: you log in with username + password, the server generates a 6-digit code and hands it to an SMS gateway, the gateway delivers it to your registered number, and you type it back into the app, which checks that it matches and hasn’t expired. The code has a typical TTL of 5-10 minutes.


Specific vulnerabilities

  • SIM swapping: the attacker convinces the carrier (with social engineering, bribery, or fake documents) to port your number to their SIM. Takes minutes. Famous case: the Twitter CEO’s account compromise in 2019.
  • SS7 attacks: the SS7 protocol carriers use to route SMS has known vulnerabilities that allow intercepting messages from other carriers. Demonstrated in production.
  • Real-time phishing: same as TOTP, vulnerable.
  • Compromised carriers: internal carrier employees can see SMS.
  • Number recycling: when a number is released and reassigned, the new owner can receive SMS intended for the previous one.

When it (still) makes sense

Although NIST SP 800-63B discourages it for AAL2, SMS OTP is still useful when:

  • It’s better than SFA: having some MFA, even weak, is better than just a password.
  • Your user base doesn’t have modern smartphones and you need something universal.
  • It’s a secondary fallback channel, not the primary one.


Email OTP: only as secure as the email

How it works

Identical to SMS but via email: the app generates a code, sends it to the registered email, the user types it.


The fundamental problem

The security of email OTP reduces to the security of the email itself. If the user’s email account is protected only with a password (no MFA), “MFA with email OTP” is essentially a single factor in disguise. An attacker with email access can:

  1. Initiate the login flow on the target app.
  2. Receive the code in the compromised email.
  3. Complete the login.

And if the email has “forgot password” as a recovery mechanism, the email alone is enough to take over accounts on any service where you use that email.


When it’s reasonable

  • Low-value apps where TOTP friction isn’t justified.
  • As a fallback channel when the TOTP device is lost, with additional verifications.
  • In CIAM for email verification during signup (not for recurring login).

Push notifications: convenient but caught Uber’s CEO

How it works

sequenceDiagram
  accTitle: Push-notification authentication flow
  accDescr: After the user signs in with email and password, the IdP asks a push service to notify the registered mobile app; the user approves with one tap; the mobile app confirms with a device signature; the IdP returns a token and the session starts.
  actor U as User
  participant App as Web app
  participant IdP as IdP
  participant PushSvc as Push service
  participant Phone as Mobile app
  U->>App: Login with email + password
  App->>IdP: Authenticates first factor
  IdP->>PushSvc: Requests push to registered device
  PushSvc->>Phone: Notification: "Is it you?"
  U->>Phone: Approves (one tap)
  Phone->>IdP: Confirms approval with device signature
  IdP->>App: Token with claims
  App->>U: Session started
One tap to approve — the same convenience that MFA-bombing attacks abuse.

Great UX: one tap and you’re in. But it introduced a new risk.


MFA fatigue / push bombing

The attacker, with your password already stolen, fires repeated push notifications. The victim — tired, distracted, half-asleep — eventually approves one. Famous case: the 2022 Uber breach, where an attacker from Lapsus$ got in this way.


Modern mitigations

  • Number matching: instead of approving with a tap, the push shows a number (e.g., 47) that the user must type/select in the originating app. Eliminates automatic approval.
  • Geo + context in the push: the push shows “Login from Tokyo. Is it you?”. If the user is in Madrid, they see the inconsistency and reject.
  • Rate limiting on pushes: if more than 3 pushes fire in 5 minutes, the account is automatically blocked and an alert fires.
  • Biometric confirmation: the push requires Face ID or fingerprint in the mobile app to approve. A tap alone is not enough.

Microsoft Authenticator, Duo, and Okta Verify implement number matching by default since 2023.


FIDO2 / WebAuthn: the phishing-resistant standard

FIDO2 is the set of standards (WebAuthn + CTAP) that replaces passwords with asymmetric cryptography anchored to the user’s device. It’s the strongest consumer authentication that exists today.


Why it’s phishing-resistant

The key difference with everything previous: the site’s domain is included in the signature. The key doesn’t sign “I’m María”; it signs “I’m María at bank.com.” If you connect to banc.com (with one character changed), the signature includes the fake domain and the real server rejects it.


The two WebAuthn ceremonies

WebAuthn has two flows: registration (the first time) and authentication (every login).


Registration (Attestation)

sequenceDiagram
  accTitle: WebAuthn registration (attestation) ceremony
  accDescr: The server sends a challenge and options; the browser calls navigator.credentials.create; the authenticator asks for a gesture, generates a key pair and stores the private key locally, then returns the public key with a signed attestation; the browser forwards the public key and the server stores it tied to the user.
  participant U as User
  participant Browser as Browser
  participant Auth as Authenticator (YubiKey, TPM, Secure Enclave)
  participant Srv as Server
  Srv->>Browser: Challenge + options
  Browser->>Auth: navigator.credentials.create()
  Auth->>U: Requests gesture (tap, biometric)
  Auth->>Auth: Generates key pair
  Auth->>Auth: Stores private key locally
  Auth->>Browser: Public + signed attestation
  Browser->>Srv: Sends public key
  Srv->>Srv: Stores public key tied to user
Registration: the private key is generated on the authenticator and never leaves it — the server only ever sees the public key.

The server stores the public key (not the private one — which never leaves the authenticator) and is ready for the next login.


Authentication (Assertion)

sequenceDiagram
  accTitle: WebAuthn authentication (assertion) ceremony
  accDescr: The browser requests a challenge; the server returns a random challenge and the origin; the browser calls navigator.credentials.get including the origin; the authenticator asks for a gesture and signs the challenge plus origin with the private key; the browser sends the signature and the server verifies it with the stored public key before starting the session.
  participant U as User
  participant Browser as Browser
  participant Auth as Authenticator
  participant Srv as Server
  U->>Browser: Initiates login
  Browser->>Srv: Requests challenge
  Srv->>Browser: Random challenge + origin
  Browser->>Auth: navigator.credentials.get() — includes origin
  Auth->>U: Requests gesture
  Auth->>Auth: Signs challenge + origin with private key
  Auth->>Browser: Signature
  Browser->>Srv: Sends signature
  Srv->>Srv: Verifies signature with public key
  Srv->>Browser: Session started
Authentication: the signature covers the challenge and the origin, so a look-alike domain can't reuse it.

Why the origin matters

In the “Signs challenge + origin” step: if the fake site has origin banc.com and the browser sends it to the authenticator, the resulting signature includes banc.com. The real server (bank.com) receives that signature and verifies against its public key, but detects that the signed origin doesn’t match its own domain. It rejects the authentication.


That means even if the user is fooled and “approves” on a fake site, the attacker can’t use the signature to access the real site. That’s what no traditional OTP/push achieves.


Authenticator types

  • Roaming authenticators: separate devices connecting via USB/NFC/Bluetooth (YubiKey, Google Titan).
  • Platform authenticators: built into the device (Touch ID, Face ID, Windows Hello, Android biometric).

Passkeys: WebAuthn synced across devices

Passkeys are the consumer-friendly evolution of WebAuthn. They solve two limitations of classic WebAuthn:

  1. If you lose the device, you lose the credential — terrible UX for end users.
  2. Each device needs independent registration — friction when changing phones.

Discoverable credentials

Passkeys are discoverable credentials: the authenticator stores both the private key and a user identifier. At login, the user doesn’t even have to type their email — the browser offers “sign in with María García’s passkey” directly.


Cloud sync

The main operational difference: the private key is synced via iCloud Keychain, Google Password Manager, or Windows Hello across the same user’s devices. If you have iPhone + Mac + iPad, the passkey works on all three without enrolling each one.


flowchart LR
  accTitle: Passkeys synced across a user's devices
  accDescr: A passkey created on an iPhone syncs through an end-to-end encrypted iCloud Keychain to the same user's Mac and iPad, and can fall back to a Windows device via a cross-device QR code.
  iPhone[iPhone<br/>original passkey] --> iCloud[(iCloud Keychain<br/>E2E encrypted)]
  iCloud --> Mac[Mac<br/>synced passkey]
  iCloud --> iPad[iPad<br/>synced passkey]
  iCloud -.fallback.-> Windows[Windows with QR<br/>cross-device]
One passkey, every device — synced end-to-end through the platform's keychain, with a cross-device QR as the escape hatch.

Account recovery

When you lose all your devices:

  • Recovery via cloud account: if you recover your Apple ID or Google account, you recover every passkey.
  • Recovery via cross-device: on another device, you scan a QR to temporarily use the passkey.

Current limitations

  • Soft vendor lock-in: your iCloud passkeys don’t easily export to Google. Switching ecosystems is painful.
  • Heterogeneous adoption: not every app supports passkeys yet. Improving fast.
  • For regulated B2B: some regulations require FIDO2 with a hardware authenticator (not synced), for fear of cloud account compromise.


A magic link is a unique URL sent to the user’s email that, when clicked, completes the login. Adopted by Slack, Notion, Medium, and many modern products.


How it works

sequenceDiagram
  accTitle: Magic-link login flow
  accDescr: The user enters their email; the server generates a random token tied to the email with a TTL and emails a link containing it; the user clicks the link, which navigates to an auth endpoint; the server validates the token, marks it used so it can't be replayed, and starts the session.
  actor U as User
  participant App as Web app
  participant Srv as Server
  participant Mail as Email
  U->>App: Enters email
  App->>Srv: Requests magic link
  Srv->>Srv: Generates random token
  Srv->>Srv: Associates with email + TTL
  Srv->>Mail: Sends link with token
  U->>Mail: Clicks the link
  Mail->>App: Navigates to /auth?token=...
  App->>Srv: Validates token
  Srv->>Srv: Marks token as used (not reusable)
  Srv->>App: Session started
A single-use, short-lived token round-trips through the inbox — so the security reduces to the email's.

Important security patterns

  • Single-use token: once consumed, it doesn’t work again. Defense against replay.
  • Short TTL: typically 10-15 minutes. If it expires, the user requests a new one.
  • Optional IP/device validation: if the link opens from a country or device different from the one that requested it, require additional confirmation.
  • Anti-CSRF: the token must be long (256 bits ideally), random, and unpredictable.

Vulnerabilities

  • Compromised email = compromised account: same as email OTP, security reduces to the email’s.
  • Misconfigured email forwarding: if the email auto-forwards to another account, the forward can read the link.
  • Phishing: the attacker sends an email identical to the real magic link with their own link, and the user clicks it. (Mitigation: standards like BIMI help verify the sender.)

Combining them

Common pattern in modern apps: magic link as primary factor + push or TOTP as secondary. You combine the simplicity of the magic link with the security of an independent second factor.


CAPTCHA, reCAPTCHA, hCaptcha: anti-bot, not authentication

Critical clarification

CAPTCHA is not an authentication factor. It doesn’t prove you’re you; it proves you’re human (or that you act like one). It’s a complement to login, not a substitute. People confuse them because they appear on login forms next to the password.


What they actually do

CAPTCHAs protect against:

  • Credential stuffing: an attacker with millions of leaked credentials tries them in bulk. CAPTCHA raises the cost of each attempt.
  • Account-scraping bots: registering 10,000 fake accounts in an hour.
  • Brute force: if rate limiting fails, CAPTCHA adds friction.

Modern types

TypeHow it works
reCAPTCHA v2 (“I’m not a robot”)Checkbox; analyzes mouse movement and behavior
reCAPTCHA v3Invisible; returns a 0.0-1.0 score without user interaction
hCaptchaAlternative that pays sites for their training data
Cloudflare TurnstileModern anti-bot, privacy-first, no Google tracker
Proof-of-Work CAPTCHAThe browser solves a cryptographic puzzle (e.g., Anubis)

Privacy

Google reCAPTCHA collects massive behavioral data, which bothers many operators. Cloudflare Turnstile and hCaptcha position themselves as privacy-first alternatives.



Fallback and recovery patterns

No matter how good your primary authentication is, users lose devices, forget passwords, change numbers. You need recovery plans — and they’re the weakest point of many systems, because for the user to recover their account, the attacker can also exploit the same path.


Table of common patterns

Recovery patternProsCons / threats
Email “forgot password”Universal, easyOnly as secure as the email
Backup codes (10 single-use OTPs)Independent of devicesUsers rarely save them well
Manual identity proofing (photo + document)RobustExpensive, slow, exposes PII to the human verifier
Trusted device listGood UXIf every device is lost, doesn’t work
Recovery by verified contact (another person)Good for Apple FamilyVery new, requires prior setup

The fundamental principle

Your recovery path must not be weaker than your primary authentication. If your primary is passkey + biometric but you recover with SMS OTP, you’ve reduced the effective security of the system to SMS level. An attacker will go through the weakest path.


How to choose: three practical scenarios

The practical question I get most often in consulting is: “what factor do I implement for my case?”. There’s no single answer. It depends on risk, audience, and the team’s technical maturity. Three typical scenarios:


Scenario 1: consumer mobile banking app in Central America

  • Audience: customers of all ages, many with mid-range phones.
  • Constraints: local regulation requires real MFA; UX must be smooth.
  • Recommendation: passkey as primary factor (where the device supports it) + TOTP in app + local biometric. SMS OTP fallback only for customers with older phones. Step-up with transaction PIN for operations over $200.
  • Why: passkeys eliminate phishing (the dominant vector in banking), TOTP covers cases where passkey isn’t viable yet, and SMS only appears when there’s no alternative.

Scenario 2: internal HR app for 200 employees

  • Audience: employees connecting from corporate laptops or BYOD.
  • Constraints: low budget, small team maintaining the app.
  • Recommendation: SSO against the corporate IdP (Microsoft Entra ID or Google Workspace) that already has MFA configured. The app doesn’t implement its own MFA.
  • Why: reusing the corporate IdP’s MFA investment is the most efficient. Implementing custom MFA in every internal app duplicates work and multiplies attack surfaces.

Scenario 3: admin panel for critical operations (K8s cluster admin)

  • Audience: 8 platform engineers with production access.
  • Constraints: one compromised credential = catastrophe.
  • Recommendation: mandatory FIDO2 hardware key (a YubiKey for each engineer) + step-up for destructive actions (delete cluster, modify IAM policies) requiring approval from a second engineer.
  • Why: this is the case where the cost and friction of a hardware key is justified. Synced passkeys are insufficient — you want the private key to never leave a physical chip.


Comparative table: when to use what

FactorPhishing-resistantVulnerable toGood primary for
PasswordNoPhishing, leaks, reuseNothing in 2026
TOTPNoReal-time phishingInternal apps, where passkey doesn’t apply
SMS OTPNoSIM swap, SS7Fallback only
Email OTPNoCompromised emailFallback only
Push notificationPartialMFA bombing (no number matching)Workforce with number matching
FIDO2 hardwareYesPhysical loss, costAdmins, critical accounts
PasskeysYesCloud account compromiseConsumers in general
Magic linkNoCompromised emailSimple apps + 2nd factor
CAPTCHAn/a — not a factorAdvanced botsComplementary anti-bot

Recap

MFA implementations aren’t interchangeable. Each one solves a concrete problem and has its specific threat model:

  • TOTP/HOTP: simple, offline, vulnerable to real-time phishing.
  • SMS OTP: convenient, but vulnerable to SIM swap and SS7. Not for high value.
  • Email OTP: only as secure as the email. Useful as a bounded fallback.
  • Push: great UX, but requires number matching to avoid MFA bombing.
  • FIDO2/WebAuthn: the phishing-resistant gold standard for critical accounts.
  • Passkeys: the consumer-friendly version of WebAuthn, ideal for modern CIAM.
  • Magic links: simple, but email-dependent.
  • CAPTCHAs: anti-bot, not authentication. Don’t confuse them.

And an iron rule for any design: your recovery path must not be weaker than your primary authentication.



Three questions to test yourself

  1. Design the recovery flow for a user who lost their iPhone (where they had passkeys + TOTP). What steps do you put, what validations do you do, and why each one?
  2. Your CTO says “let’s implement passkeys and eliminate passwords in a week.” What three warnings would you give about transition and compatibility?
  3. An app allows login with a magic link without a second factor for every action. Design three step-ups you’d recommend adding and for what types of operation.

Frequently asked questions

How is a TOTP code calculated?

TOTP applies HOTP to a time counter: T = floor((current_unix_time - T0) / X), where T0 is usually 0 and X is a 30-second window, then TOTP(K) = HOTP(K, T) = Truncate(HMAC-SHA-1(K, T)). Server and client share the secret K and a UTC clock, so both compute the same 6-digit code every 30 seconds. Defined in RFC 6238.

Is SMS OTP safe enough for MFA?

Only as a last resort. SMS OTP is vulnerable to SIM swapping, SS7 interception, real-time phishing, and number recycling, and NIST SP 800-63B has discouraged it for AAL2 since 2017. It is still better than a password alone, so it is acceptable as a secondary fallback or for users without smartphones — but never as the primary factor for money, sensitive data, or admin access.

What makes FIDO2/WebAuthn phishing-resistant?

The site's origin (domain) is included in the signed challenge. The authenticator signs 'I am this user at bank.com,' not just 'I am this user,' so a look-alike domain like banc.com produces a signature the real server rejects. Even if a user is tricked into approving on a fake site, the attacker cannot replay that signature against the real one — something no OTP or push achieves.

What is the difference between a passkey and a FIDO2 hardware key?

Both use WebAuthn asymmetric cryptography, but a passkey is a discoverable credential synced across a user's devices through iCloud Keychain, Google Password Manager, or Windows Hello, while a FIDO2 hardware key (YubiKey, Titan) keeps the private key on a physical chip that never syncs. Passkeys are best for consumers; hardware keys are best for admins and regulated accounts where cloud-account compromise is the bigger fear.

Is CAPTCHA an authentication factor?

No. CAPTCHA proves you are human, not that you are a specific person, so it is anti-bot tooling, not authentication. It complements login by raising the cost of credential stuffing and scraping, but it must never replace a factor and must never be your sole defense against brute force — rate limiting and progressive lockout remain the primary controls.