Contents

TOTP and WebAuthn: Two-Factor Authentication Without Authy

How second factors actually work, and how to own yours

Contents

There was a moment, somewhere around the third time Authy decided my desktop app should no longer exist, that I realised I had handed the keys to my entire digital life to a company whose roadmap I had no say in. The desktop client was deprecated, the cloud backup was a black box, and exporting my tokens turned out to be deliberately awkward. That’s the wrong feeling to have about the thing standing between an attacker and every account I own.

So let’s talk about what two-factor authentication actually is under the hood, because once you understand the mechanism, the question “which app should I use” mostly answers itself — and the answer is rarely a closed cloud service.

TOTP is just a shared secret and a clock

Advertisement

The codes that tick over every thirty seconds are not magic. TOTP (Time-based One-Time Password, RFC 6238) is built on a stupidly simple idea: the server and your authenticator share a secret, you both look at the current time, and you both run an HMAC over it. The six digits are just the truncated output of that HMAC. When you scan a QR code during setup, you’re copying that shared secret — and that’s the whole game.

You can prove this to yourself with a one-liner, given the base32 secret the site handed you:

1
2
3
# secret is the base32 string from the QR code
oathtool --totp -b JBSWY3DPEHPK3PXP
# 282760  (changes every 30s)

That’s it. No server contacted, no internet required, just maths over a secret and the wall clock. Which means whoever holds the secret can generate your codes. If that’s a cloud service you can’t export from, you don’t really hold it — they do.

The practical upshot: store the secret somewhere you control. I keep mine in a password manager that supports TOTP, so the secret lives in the same encrypted vault I already back up myself. If you self-host that vault — I moved to Vaultwarden years ago precisely so nobody but me holds the master key — then your TOTP seeds live on hardware you own, backed up on your terms. Some people prefer a dedicated open-source app that exports a plain, portable file. Both are fine. What’s not fine is a service that can lock you out of your own seeds.

There’s a legitimate debate about whether TOTP belongs in the same vault as the password it protects. Purists argue it collapses two factors into one: crack the vault and you have both. It’s a fair point. My counter is that a compromised vault is game over regardless — if an attacker has decrypted my password manager, my accounts are gone whether the TOTP lives there or not, so I optimise for the far more common failure mode, which is losing access to my own seeds. If that argument bothers you, keep TOTP in a separate app on a separate device. The important thing is that you can export it.

Actually reading a QR code with your eyes

When a site shows you a TOTP QR code, there’s almost always a “can’t scan it?” link that reveals the raw secret and the otpauth:// URI behind it. That URI is worth understanding, because it’s the whole enrolment in one line:

1
otpauth://totp/GitHub:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=GitHub&algorithm=SHA1&digits=6&period=30

Every parameter matters. secret is the shared key. algorithm, digits and period are the knobs both sides must agree on — almost everyone uses SHA1, 6 digits, 30 seconds, but a handful of services (Steam, some banks) deviate, which is exactly why a code that “should” work sometimes doesn’t. Save that URI or the base32 secret when you enrol and you can re-import into any authenticator you like, forever. That’s ownership.

TOTP’s weakness is the bit nobody mentions

TOTP is enormously better than nothing. It defeats password reuse and credential-stuffing outright. But it has one fatal flaw: it’s phishable. If a fake login page asks for your password and then your six digits, you’ll happily type both, and the attacker relays them to the real site within the thirty-second window. The code doesn’t know which site it’s being typed into. Neither do you, when the domain is paypa1-secure.com at 7am.

This is not theoretical. Real-time phishing kits that proxy your TOTP straight through to the genuine site are a commodity now. Which is exactly the problem WebAuthn was designed to kill.

WebAuthn binds the login to the actual website

Advertisement

WebAuthn (the web-facing half of FIDO2) replaces the shared secret with public-key cryptography, and — crucially — it cryptographically ties every credential to the origin it was created for. Your authenticator, whether that’s a YubiKey, your phone’s secure enclave, or a Windows Hello chip, generates a unique key pair per site. The private key never leaves the device. To log in, the site sends a challenge, the device signs it, the site verifies it against the public key it stored at registration.

Here’s the part that makes it unphishable: the browser includes the real origin in what gets signed. A credential registered for github.com simply will not respond to a request claiming to be github.com when the page is actually githubb.com. There’s nothing for the user to get wrong, because the user isn’t in the loop. The browser refuses.

This is the same principle that makes mutual TLS between services trustworthy: identity is proven by possession of a private key that never travels, not by a secret typed over the wire that anyone downstream can replay. WebAuthn is that idea applied to human logins. The key stays put; only signatures move.

The trade-off is recovery. A YubiKey can be lost or left in a hotel. The honest discipline is to register at least two keys on every important account — one on your keyring, one in a drawer at home — and to keep recovery codes printed somewhere offline.

1
2
3
4
5
Account recovery checklist (do this once, thank yourself later):
  [ ] Hardware key #1 registered  (daily carry)
  [ ] Hardware key #2 registered  (backup, stays home)
  [ ] Recovery codes printed and stored offline
  [ ] TOTP seed in a vault YOU can export from

Passkeys are WebAuthn with the awkward bits smoothed off

You’ll have noticed “passkeys” everywhere lately. A passkey is just a WebAuthn credential that can be discoverable and, optionally, synced across your devices by a platform vault rather than locked to a single hardware token. The cryptography and the anti-phishing guarantee are identical. The difference is convenience versus the absolute, hardware-bound assurance of a key that physically cannot be copied. For most accounts a synced passkey is plenty; for the handful that really matter — your email, your password manager, your domain registrar — I still prefer a hardware key that can’t be exfiltrated by malware on a synced device.

The subtlety worth understanding is where the passkey lives. An Apple passkey syncs through iCloud Keychain; a Google one through your Google account; a 1Password or Bitwarden passkey through that vault. Each is only as trustworthy as the vault backing it, and each ties you to that ecosystem. A hardware key answers to nobody’s cloud — which is precisely why I keep one for the accounts that can reset everything else. The pragmatic split I’ve settled on: synced passkeys for convenience on the long tail, at least one hardware key for the crown-jewel accounts, and recovery codes on paper as the backstop for all of them.

One more thing people miss: passkeys don’t have to replace your password, they can sit alongside it, and on many sites they currently do. That’s a transitional awkwardness. The end state, where the passkey is the only credential and there’s no phishable password to steal at all, is genuinely better security — but until a site removes the password fallback, an attacker can often just target that instead. Check whether “passwordless” actually means passwordless before you trust it to.

When it goes wrong: the troubleshooting the docs skip

Second factors fail in a small number of predictable ways, and knowing them turns a panic into a two-minute fix.

“Invalid code” on every TOTP attempt. Ninety per cent of the time this is clock drift. TOTP depends on both ends agreeing on the current time; if your phone’s clock is thirty seconds out, every code is rejected. Enable automatic network time on the device generating the codes. If you’re running an authenticator on a machine you control, check the clock is actually syncing — I’ve seen a homelab VM drift minutes off because NTP was firewalled and nobody noticed until 2FA broke.

The code works, then instantly doesn’t. You typed it in the last second or two of its window and the round-trip pushed it past the boundary. Wait for the next code and type it early. Most servers accept the code from the immediately previous window to soften this, but not all do.

Wrong algorithm on import. If you migrated a seed by hand and the codes are consistently wrong (not drift, just plain wrong), you probably imported it as SHA1 when the service uses SHA256, or the digit count is off. Check the original otpauth:// URI’s parameters.

WebAuthn “no credentials found.” Usually the browser or platform doesn’t see your key. For a hardware token, re-seat it and make sure the browser has USB/NFC permission; on Linux you may need a udev rule granting your user access to the FIDO device. For a platform passkey, the credential is tied to that specific device or synced vault — logging in from a fresh machine where the passkey never synced simply won’t offer it.

Locked out entirely. This is what the recovery codes are for. Every serious site issues single-use backup codes at 2FA setup. If you have them printed offline, you’re never truly locked out. If you skipped that step, your only route is the provider’s account-recovery process — slow, invasive, and sometimes fatal to the account. Print the codes. This is the single highest-value five minutes in this whole article.

The verdict

Use WebAuthn or passkeys wherever a site offers them, especially for the accounts that can reset everything else: email, password manager, registrar, GitHub. They’re the only second factor that survives a convincing phishing page. Keep TOTP as the fallback for the long tail of sites that haven’t caught up — but hold the seeds yourself, in something you can export and back up, not a cloud silo that can disappear a client out from under you.

Who is this for? Anyone who’s ever felt that small cold dread realising their entire second factor lives in one app they don’t control. The fix isn’t another shiny app. It’s understanding that 2FA is a secret or a key pair — and making sure the one holding it is you.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.