Specific MFA implementations: how they work under the hood
The technical mechanics of the most used MFA implementations, TOTP, SMS codes, push, FIDO2 and WebAuthn, passkeys, and magic links, with their flows, specific weaknesses, and fallback patterns. Plus CAPTCHA, and why it isn't authentication but is a useful complement.
The phone in the taxi
Sara López leaves her phone in a taxi on a Thursday night. It held her passkeys, her authenticator app with a dozen TOTP codes, and the SIM her bank sends codes to. On Friday morning she calls the help desk from her laptop and asks the question that every MFA design eventually has to answer: “How do I get back into my accounts?” The answer depends on decisions engineers made months earlier, often without thinking about this moment. If the way back in is weaker than the way in, the attacker who found the phone, or anyone who can impersonate Sara on the phone with the help desk, will take that path.
In the previous article we went through the five categories of factors and mentioned several implementations in passing. Now we open the hood. Every implementation has its own mechanics: a protocol, a flow, specific weaknesses, design decisions that end up in real code. If you’re going to build authentication into an app or audit someone else’s, this is the level of detail you need. Expect RFC references, formulas, pseudocode, and sequence diagrams at protocol level. By the end you should be able to explain how a TOTP code is calculated, sketch the WebAuthn registration and authentication ceremonies, say when SMS codes are acceptable and when they aren’t, and design sensible fallbacks for someone like Sara.
This is a map, not an implementation manual. If you’re about to build authentication for production, also read the RFCs cited along the way and the OWASP Authentication Cheat Sheet.
TOTP and HOTP: the code that changes every 30 seconds
One-time password algorithms have been the workhorse of enterprise MFA since the mid-2000s. They’re simple, open, and work offline.
HOTP, the predecessor
HOTP (HMAC-based One-Time Password, RFC 4226) generates codes from a shared seed and a counter that moves forward each time a code is generated:
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 keep the same counter. When the user presents a code, the server computes it with its own counter, and if they match, it accepts the code and advances the counter. The trouble is that if the counters fall out of step, for instance because the user generated a code and never used it, everything breaks. That’s why almost nobody uses plain HOTP any more.
TOTP, the current standard
TOTP (Time-based One-Time Password, RFC 6238) replaces the counter with time sliced 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 secondsServer and client share a UTC clock and the same seed, so every 30 seconds they produce the same code. When the user types it, the server computes its own and compares.
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
The otpauth:// URI follows the format Google Authenticator defined, which almost every authenticator app accepts.
Clocks drift, and if the user’s is 15 seconds behind, their code fails on the server. The usual fix is a tolerance window, typically one window either side, which gives about 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 FalseToo much tolerance weakens security; too little hurts usability.
TOTP’s weaknesses are well known. Real-time phishing defeats it: a fake site asks for the code, and the attacker replays it on the real site within seconds. If the authenticator app is compromised, for example by malware on the phone, the attacker can extract the seed and generate codes indefinitely, which is why seeds shouldn’t be easy to export. And cloud backups of authenticator apps move the risk to that cloud account: if it falls, every service’s codes fall with it. For Sara, though, a backup is exactly what would make Friday easier, which shows the tension recovery always involves.
SMS codes: convenient, indefensible for anything valuable
The SMS flow is the simplest here. You sign in with a username and password, the server generates a six-digit code and passes 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. A typical lifetime is five to ten minutes.
Its weaknesses are serious. In a SIM swap, an attacker uses social engineering, bribery, or forged documents to get the carrier to move your number to their SIM, which takes minutes; the Twitter CEO’s account was taken over this way in 2019. The SS7 protocol that carriers use to route messages has known flaws that allow interception, demonstrated in the wild. Real-time phishing works just as it does against TOTP. Carrier insiders can read messages. And when a number is released and reassigned, the new owner can receive messages meant for the old one. In Sara’s case, whoever found her phone in the taxi now receives her bank’s codes, at least until she has the SIM blocked.
NIST SP 800-63B discourages SMS codes for AAL2, but they still have a place. Some MFA beats none, so an SMS code is better than a password alone. It reaches users without modern smartphones. And it can serve as a secondary fallback channel. What it shouldn’t be is the main factor for anything involving money, sensitive personal data, or administrator access. Several countries’ banking rules already forbid SMS as the only factor for high-value operations.
Email codes: only as safe as the inbox
Email codes work like SMS codes, through email instead. Their security reduces entirely to the security of the email account. If that account is protected by a password alone, “MFA with an email code” is a single factor in disguise: an attacker with access to the mailbox starts the sign-in, receives the code, and finishes. And if the email is also the “forgot password” channel, the mailbox alone is enough to take over every service registered with it. Email codes are reasonable for low-value apps where TOTP’s friction isn’t justified, as a fallback when the TOTP device is lost, with extra checks, and for confirming an address at sign-up, not for recurring sign-in.
Push notifications
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: Sign in 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
The experience is excellent: one tap and you’re in. It also introduced a new risk, MFA fatigue or push bombing. An attacker who already has the password sends notification after notification until the victim, tired, distracted, or half asleep, approves one. That’s how a Lapsus$ attacker got into Uber in 2022.
Modern platforms answer with several measures. Number matching replaces the simple approve button with a number, say 47, that the user has to enter or pick in the app where they’re signing in, which rules out approving by reflex. Showing location and context in the notification (“Sign-in from Tokyo. Is it you?”) lets a user in Madrid spot the mismatch and reject it. Rate limits lock the account and raise an alert if, say, more than three notifications fire in five minutes. And requiring Face ID or a fingerprint inside the authenticator app means a bare tap isn’t enough. Microsoft Authenticator, Duo, and Okta Verify have shipped number matching by default since 2023.
FIDO2 and WebAuthn: the phishing-resistant standard
FIDO2 is the pair of standards, WebAuthn and CTAP, that replaces shared secrets with asymmetric cryptography anchored to the user’s device. It’s the strongest consumer authentication available today. What sets it apart from everything above is that the site’s domain is part of what gets signed. The key doesn’t sign “I’m Sara”; it signs “I’m Sara at bank.com.” If you’re lured to banc.com, one letter off, the signature names the fake domain and the real server rejects it.
WebAuthn has two ceremonies: registration, the first time, and authentication, every sign-in after that.
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 key + signed attestation Browser->>Srv: Sends public key Srv->>Srv: Stores public key tied to user
The server keeps the public key; the private key never leaves the authenticator. On the next sign-in, the authentication ceremony runs.
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: Starts sign-in 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
The step that matters is the signature over the challenge and the origin. If the fake site’s origin is banc.com, the browser passes that to the authenticator and the resulting signature contains banc.com. The real server checks the signature with its stored public key, sees that the signed origin isn’t its own domain, and rejects it. So even if the user is fooled into approving on a fake site, the attacker can’t use that signature on the real one. No one-time code or push notification can say that.
Authenticators come in two kinds. Roaming authenticators are separate devices that connect over USB, NFC, or Bluetooth, such as a YubiKey or Google Titan. Platform authenticators are built into the device: Touch ID, Face ID, Windows Hello, Android biometrics.
Passkeys: WebAuthn synced across devices
Passkeys are the consumer-friendly evolution of WebAuthn, and they fix two problems with the classic version: losing the device meant losing the credential, and every device needed its own registration, which made changing phones painful.
Passkeys are discoverable credentials. The authenticator stores both the private key and a user identifier, so at sign-in the user doesn’t even type an email; the browser simply offers “sign in with Sara López’s passkey.” And the private key is synced through iCloud Keychain, Google Password Manager, or Windows Hello across the same person’s devices, so a passkey created on an iPhone works on the Mac and the iPad 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]
This is what saves Sara’s Friday. Her passkeys weren’t only on the phone in the taxi; they were also on her laptop, synced through her cloud account. If she had lost every device, recovering the cloud account would bring the passkeys back, and on a borrowed device she could scan a QR code with another of her devices to use a passkey temporarily.
Passkeys have limits too. Moving between ecosystems is awkward, since passkeys in iCloud don’t export easily to Google. Not every app supports them yet, though that’s changing quickly. And some regulated B2B settings require FIDO2 with a hardware authenticator rather than a synced one, because a compromised cloud account would expose every synced passkey. For consumers, passkeys are the best option in 2026. For administrators and critical enterprise accounts, consider a separate hardware key whose private key never leaves the physical chip.
Magic links
A magic link is a one-time URL sent to the user’s email that completes the sign-in when clicked. Slack, Notion, Medium, and many modern products use them.
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: Ties it to the 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 few patterns keep magic links safe. The token is single use, so once consumed it never works again. Its lifetime is short, typically 10 to 15 minutes. It’s long, ideally 256 bits, random, and unpredictable. And optionally, if the link is opened from a different country or device than the one that asked for it, the app asks for extra confirmation. The weaknesses are the same as for email codes: whoever controls the mailbox controls the account, a careless auto-forwarding rule can send the link to another mailbox, and a phishing email can imitate the real one with the attacker’s link (sender verification such as BIMI helps here). A common pattern in modern apps is a magic link as the first factor plus push or TOTP as the second, combining the simplicity of the link with an independent factor.
CAPTCHA: anti-bot, not authentication
CAPTCHA isn’t an authentication factor. It doesn’t prove you’re you; it suggests you’re human, or at least behaving like one. People confuse the two because CAPTCHAs appear on sign-in forms next to the password. What a CAPTCHA does is make automated abuse more expensive: credential stuffing, where an attacker tries millions of leaked credentials; bots registering ten thousand fake accounts in an hour; and brute force when rate limiting falls short.
There are several kinds today. reCAPTCHA v2 is the “I’m not a robot” checkbox, which analyzes mouse movement and behavior. reCAPTCHA v3 is invisible and returns a score between 0.0 and 1.0 without user interaction. hCaptcha is an alternative that pays sites for the training data it collects. Cloudflare Turnstile is a privacy-focused anti-bot check without Google’s tracking. And proof-of-work CAPTCHAs, such as Anubis, make the browser solve a cryptographic puzzle. Google’s reCAPTCHA gathers a lot of behavioral data, which bothers many operators, and Turnstile and hCaptcha position themselves as the privacy-friendly options.
Never rely on a CAPTCHA as your only defense against brute force. The main defense remains rate limiting, progressive lockout, and monitoring for patterns, and a CAPTCHA is never an authentication factor.
Fallback and recovery
However good the main authentication is, people lose devices, forget passwords, and change numbers, as Sara did. You need recovery plans, and they’re the weakest point of many systems, because any path that lets the user back in is also a path an attacker can try.
| Recovery pattern | Strengths | Weaknesses and threats |
|---|---|---|
| ”Forgot password” by email | Universal, easy | Only as secure as the email |
| Backup codes (ten single-use codes) | Independent of any device | Users rarely store them well |
| Manual identity proofing (photo + document) | Robust | Expensive, slow, exposes personal data to the human checker |
| Trusted device list | Good experience | Fails if every device is lost |
| Recovery contact (another person) | Works well in family plans | New, needs setting up in advance |
The principle behind all of them is simple: the recovery path must not be weaker than the main authentication. If you sign in with a passkey and a biometric but recover with an SMS code, the effective security of the whole system is that of SMS, because an attacker will always take the weakest door. If Sara’s help desk would reset her MFA after a phone call where she gave her date of birth and employee number, everything else about her setup would be decoration.
How to choose: three scenarios
The question that comes up most in practice is “which factor should I implement for my case?” There’s no single answer; it depends on risk, audience, and the team’s maturity. Three typical cases illustrate the reasoning.
A consumer mobile banking app in Central America serves customers of all ages, many with mid-range phones, under local regulation that requires real MFA and with an expectation of a smooth experience. A good design uses a passkey as the main factor where the device supports it, plus TOTP and on-device biometrics, with SMS codes as a fallback only for customers on older phones, and step-up with a transaction PIN for operations over $200. Passkeys remove phishing, the dominant attack in banking; TOTP covers the cases where passkeys aren’t possible yet; and SMS appears only when there’s no alternative.
An internal HR app for 200 employees, maintained by a small team on a small budget, should rely on single sign-on with the corporate identity provider, such as Microsoft Entra ID or Google Workspace, which already enforces MFA. The app shouldn’t implement its own MFA at all. Reusing the company’s existing investment is the efficient choice, and building custom MFA into every internal app duplicates work and multiplies the attack surface.
An administration panel for critical operations, say Kubernetes cluster administration for eight platform engineers with production access, is the case where one compromised credential is a catastrophe. Here a FIDO2 hardware key per engineer is mandatory, and destructive actions such as deleting a cluster or changing IAM policies require step-up plus approval from a second engineer. This is where the cost and friction of a hardware key pay off, and synced passkeys aren’t enough, because you want the private key never to leave a physical chip.
A pattern shows up across projects: teams tend to over-engineer for end users, adding too much friction, and under-engineer for administrators, assuming they know what they’re doing. It should be the other way round. End users tolerate less friction, and administrators need more controls. When you design, treat the two audiences separately.
Which factor for which job
| Factor | Phishing-resistant | Vulnerable to | Good main factor for |
|---|---|---|---|
| Password | No | Phishing, leaks, reuse | Nothing in 2026 |
| TOTP | No | Real-time phishing | Internal apps where passkeys don’t apply |
| SMS code | No | SIM swap, SS7 | Fallback only |
| Email code | No | Compromised mailbox | Fallback only |
| Push notification | Partly | MFA bombing (without number matching) | Workforce, with number matching |
| FIDO2 hardware key | Yes | Physical loss, cost | Administrators, critical accounts |
| Passkey | Yes | Compromised cloud account | Consumers in general |
| Magic link | No | Compromised mailbox | Simple apps, plus a second factor |
| CAPTCHA | Not a factor | Advanced bots | Complementary anti-bot measure |
Recap
MFA implementations aren’t interchangeable; each solves a particular problem and has its own threat model. TOTP and HOTP are simple and offline but fall to real-time phishing. SMS codes are convenient but exposed to SIM swaps and SS7, so not for anything valuable. Email codes are only as safe as the mailbox and work as a bounded fallback. Push is pleasant to use but needs number matching to resist MFA bombing. FIDO2 and WebAuthn are the phishing-resistant standard for critical accounts, and passkeys are their consumer-friendly form, ideal for modern customer identity. Magic links are simple and depend on email. CAPTCHAs fight bots and aren’t authentication. And above all of them sits one rule: the recovery path must never be weaker than the main authentication.
Three questions to test yourself
- Design the recovery flow for Sara, who lost the iPhone that held her passkeys and TOTP codes. Which steps would you include, which checks would you make, and why each one?
- Your CTO says, “Let’s roll out passkeys and get rid of passwords in a week.” What three warnings would you give about the transition and compatibility?
- An app lets users sign in with a magic link and no second factor for every action. Design three step-ups you’d recommend adding and the kinds of operation each would cover.
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, and 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 six-digit code every 30 seconds. It's defined in RFC 6238.
Is SMS OTP safe enough for MFA?
Only as a last resort. SMS codes are vulnerable to SIM swapping, SS7 interception, real-time phishing, and recycled numbers, and NIST SP 800-63B has discouraged them for AAL2 since 2017. They're still better than a password alone, so they're acceptable as a secondary fallback or for users without smartphones, but never as the main factor for money, sensitive data, or administrator access.
What makes FIDO2/WebAuthn phishing-resistant?
The site's origin, its domain, is included in what gets signed. The authenticator signs 'I am this user at bank.com,' not just 'I am this user,' so a look-alike domain such as banc.com produces a signature the real server rejects. Even if a user is tricked into approving on a fake site, the attacker can't replay that signature against the real one, which no one-time code or push notification achieves.
What is the difference between a passkey and a FIDO2 hardware key?
Both use WebAuthn's asymmetric cryptography, but a passkey is a discoverable credential synced across a person's devices through iCloud Keychain, Google Password Manager, or Windows Hello, while a FIDO2 hardware key such as a YubiKey or Titan keeps the private key on a physical chip that never syncs. Passkeys suit consumers best; hardware keys suit administrators and regulated accounts, where a compromised cloud account is the bigger worry.
Is CAPTCHA an authentication factor?
No. CAPTCHA shows that you're human, not that you're a specific person, so it's anti-bot tooling, not authentication. It complements sign-in by making credential stuffing and scraping more expensive, but it must never replace a factor or be your only defense against brute force; rate limiting and progressive lockout remain the main controls.