open atlas
↑ Back to track
Security Foundations SECF · 03 · 03

Passkeys and WebAuthn

Passkeys are origin-bound public-key credentials; the browser signs over the page's true origin so a phished or relayed login is rejected. But WebAuthn only secures the login event — session and recovery stay your problem.

SECF Middle ◷ 16 min
Level
FoundationsJuniorMiddleSenior

A polished proxy at accounts-secure-login.com mirrors your real login page pixel for pixel. The victim arrives via a convincing email, the page calls navigator.credentials.get, the victim does Face ID, and the attacker captures a genuine, cryptographically valid signature — relayed straight from the victim’s real authenticator. With a password they’d own the account. With a TOTP code they’d have a working 30-second token. With a passkey they have a signature that your server rejects on the first check, because the victim’s browser stamped the true origin — accounts-secure-login.com — into the signed bytes, and your server expects accounts.example.com. The attacker did everything right and still got nothing. That single, browser-controlled field is the entire reason passkeys are called phishing-resistant — and understanding exactly why it works, and exactly what it does not cover, is the difference between deploying WebAuthn and getting breached anyway.

By the end of this lesson you’ll know the mechanism that makes a passkey login unphishable, the tradeoffs you accept when you adopt it (recovery, synced-credential trust, device theft), and the failure modes that bite teams who think a passkey secures the whole account.

What a passkey actually is

A passkey is a WebAuthn credential: an asymmetric key pair where the private key never leaves the authenticator (a phone’s secure enclave, a laptop’s TPM, or a roaming hardware key like a YubiKey) and the server stores only the matching public key. There is no shared secret. The server has nothing an attacker can phish, no password database to leak, no seed an attacker can mirror. That alone is a category change from passwords and TOTP, where the server holds a verifiable secret and so does anyone who steals it.

Two ceremonies define the protocol. Registration (navigator.credentials.create) generates a fresh key pair scoped to your site’s rpId (the registrable domain, e.g. example.com) and ships the public key to your server, which stores it against the user. Authentication (navigator.credentials.get) proves possession of the private key: the server sends a one-time random challenge, the authenticator signs over it (plus context), and the server verifies the signature against the stored public key. Possession of the key, gated by a local user-verification gesture (biometric or PIN), is the proof of identity.

The crucial part is what gets signed. The authenticator does not sign the bare challenge. It signs a structure that includes the clientDataJSON — and the browser, not the page’s JavaScript, fills that object with the page’s real origin and the server’s challenge. The signature therefore binds the assertion to the exact origin the user’s browser was actually on. That binding is the whole game.

Why the relay attack fails: the origin is signed, by the browser

Walk the adversary-in-the-middle (AiTM) attack that defeats passwords and TOTP, and watch it die against a passkey. The attacker runs a reverse proxy on a look-alike domain that faithfully forwards your real pages. The victim lands there and authenticates. With a password, the proxy reads the typed secret and replays it to the real site — game over. With TOTP, the proxy reads the typed 6-digit code and replays it inside its validity window — game over. These work because the secret is a readable bearer value: whoever captures it can present it anywhere.

A passkey breaks the pattern at the byte level. When the victim’s browser builds clientDataJSON, it writes the page’s origin into it — and the page is accounts-secure-login.com. The authenticator signs over those bytes. The proxy relays the resulting assertion to your real server, which expects origin accounts.example.com. On verification the signature is cryptographically valid but bound to the wrong origin, so the server’s origin check rejects it. The proxy cannot fix this: it cannot make the browser lie about the origin (the browser fills the field, not the page’s JS), and it cannot re-sign for the correct origin because it never had the private key. In practice the authenticator usually won’t even surface the credential, because the look-alike domain’s rpId won’t match what the passkey was registered to — so the ceremony fails before a signature exists. The attacker is left holding a valid signature that is worthless everywhere except the phishing domain it was bound to.

lesson.inset.note

This is the precise sense of “phishing-resistant.” It is not that users are smarter or that the page is harder to clone — the clone can be perfect. It is that the proof is cryptographically scoped to an origin the attacker does not control, and that scope is enforced by the browser and re-checked by the server, two parties the attacker can’t impersonate. Contrast TOTP, which is “phishing-resistant in marketing, phishable in practice”: the code is a readable value the user hands over, and a relay forwards it just fine.

The tradeoffs you actually accept

Adopting passkeys is not free, and the costs are not where beginners look.

Recovery becomes the hardest part, not the login. A passkey lives on a device or in a sync fabric. Users lose phones, wipe laptops, and switch ecosystems. So you must offer recovery — and recovery is where the security of the whole account is silently decided. If your recovery path is “email me a reset link,” and that email account is protected only by a password, then the true strength of the account equals a phishable password on a third-party mailbox. You built an unphishable front door and left a phishable side door wide open. The rule is blunt: recovery must be at least as strong as the primary factor. The cleanest answer is to register two passkeys at enrollment (one platform, one roaming hardware key), so the second credential is the recovery method — no weaker channel needed.

Synced vs device-bound passkeys is a real trust decision. Modern consumer passkeys sync across a user’s devices through their platform account (iCloud Keychain, Google Password Manager). That is wonderful for adoption — lose a phone, your passkeys are on the new one — but it means the credential’s security now also depends on the platform account’s security, and it means the old assumption that “the private key lives in one tamper-resistant chip” is no longer true. Device-bound passkeys (a hardware security key, or platform keys with a no-sync policy) stay in one place and are stronger against account-level compromise, but they make loss catastrophic unless you’ve registered a backup. For high-assurance contexts (admin, finance, regulated systems) you often require device-bound keys and attestation; for consumer scale you accept synced keys and lean on platform-account security. Knowing which you have is part of your threat model.

signCount is not the tamper-evidence it once was. WebAuthn’s authenticatorData carries a signature counter meant to detect a cloned hardware authenticator: if the counter ever goes backwards, you’ve seen a clone. That signal is real for single-device hardware keys. But synced passkeys are copied across devices by design, so their counter is frequently 0 or meaningless across the fleet — a regression isn’t proof of compromise. Treat signCount as a soft signal, never a hard gate, and never as the thing standing between you and a cloned credential on a synced platform.

DimensionPassword / TOTP (shared secret)Passkey (WebAuthn)
What the server storesA verifiable secret (hash / shared seed) — leakableA public key only — nothing to phish or leak
Phishing / AiTM relayRelayed straight through — secret is a bearer valueRejected — signature is bound to the wrong origin
Replay of a captured proofWorks within validity window (TOTP) / forever (password)Blocked by the one-time server challenge
Weakest reachable pathThe secret itselfRecovery, session handling, or device theft — not the login
Clone detectionN/AsignCount — real for hardware keys, 0/meaningless when synced

Where it breaks: WebAuthn only secures the login event

The most expensive passkey mistake is believing the credential secures the account. It secures exactly one thing: the authentication event. Everything around it is still your responsibility, and that is where teams who “went passwordless” still get taken over.

The session is still a bearer token. A passkey login mints a session cookie — and that cookie is cash, exactly like before. If you never rotate it, give it a long lifetime, and let third-party script run on the authenticated page, a single XSS exfiltrates a live session and the attacker is in without ever touching the passkey. WebAuthn made the login unphishable; it said nothing about what happens after. You still need session rotation on login and privilege change, short lifetimes with re-auth, HttpOnly/Secure/SameSite cookies, and a CSP that stops injected script from reading the session.

Possession on an unlocked device defeats user verification. WebAuthn proves possession of the key, gated by a local UV gesture — but on an already-unlocked laptop the OS may have the gesture cached or auto-satisfy it. A thief with a stolen, unlocked device can use a synced passkey to sign in, and the cryptography is working exactly as designed. There is no crypto flaw to find. The defense is layered controls above the login: explicit step-up re-authentication (userVerification: 'required') for sensitive actions, risk signals (new device, new geo, impossible travel), short bound sessions, and — critically — keeping authentication separate from authorization, so a privileged action re-checks both “is this the right human right now” and “is this principal allowed,” instead of trusting a passive session minted hours ago.

Common mistake

The trap migration is keeping the password as a permanent parallel login “just in case.” Now the account has two front doors: an unphishable passkey and a phishable password — and attackers always take the weaker one, so your real security is the password you thought you’d upgraded away from. The fix is a migration with teeth: once a user has 2+ passkeys, downgrade or remove the password for that account, and never leave email-OTP recovery as a no-friction reset that quietly becomes the account’s true strength.

How a senior actually deploys passkeys

Treat the passkey as a strong authenticator, not a complete identity system, and harden the chain around it. Register two credentials at enrollment so recovery is a second passkey, not a phishable reset link. Migrate the password away once coverage is real, rather than keeping a permanent weak parallel path. Make recovery at least as strong as the login — second passkey or hardware key first, and where an out-of-band reset must exist, add friction (delay, notification to all devices, step-up, an enrollment-time recovery code). Add step-up for sensitive actions so a passive session can’t act, and rotate and bound sessions so a stolen one expires fast. Decide synced vs device-bound deliberately per assurance level. And write down the standing assumption — a passkey secures the login, not the session, the recovery, or the device — so nobody re-makes the “login is enough” mistake. The test for any passkey deployment is one walk: trace every path that can land an attacker in the account, and confirm none of them is more phishable than the passkey itself.

Pick the best fit

You're adding passkeys to an app that has password + email-OTP login today. You must keep the account at least as phishing-resistant via every path, including recovery. Pick the design.

Quiz

An AiTM proxy on a look-alike domain relays a victim's full WebAuthn ceremony and captures a cryptographically valid signature. Why is it useless against your real server?

Quiz

A team migrated to passkeys and still suffered account takeovers, with no passkey ever stolen. What's the most likely cause?

Order the steps

Order the server-side checks of a WebAuthn authentication assertion, from receiving it to issuing a session:

  1. 1 Look up the stored public key for the presented credential id
  2. 2 Verify the signature over authenticatorData + hash(clientDataJSON) with that public key
  3. 3 Check the signed origin equals your expected origin, and the challenge matches the one you issued (then consume it)
  4. 4 Enforce policy flags (user-verified bit if required), then issue the session
Recall before you leave
  1. 01
    Explain precisely why a passkey login survives an adversary-in-the-middle relay when a password and a TOTP code do not.
  2. 02
    A passkey login is unphishable. List what WebAuthn does NOT secure, and what you must add for each.
Recap

A passkey is a WebAuthn credential — an asymmetric key pair whose private half never leaves the authenticator, with the server holding only the public key, so there’s no shared secret to phish or leak. Authentication proves possession by signing a one-time server challenge, and the decisive detail is that the browser stamps the page’s true origin and the challenge into clientDataJSON before the authenticator signs. That binds the proof to a specific origin, so an adversary-in-the-middle proxy that relays a real, valid signature still fails the server’s origin check — the signature is bound to the attacker’s domain, and the proxy can’t re-sign for yours without the private key. That’s what “phishing-resistant” means, and it’s why a passkey survives a relay that a password or a TOTP code does not. But WebAuthn secures only the login event. The session is still a bearer cookie you must rotate, bound, and shorten; recovery becomes the hardest part and must be at least as strong as the login (prefer a second registered passkey, never a no-friction email reset on a password-only mailbox); a stolen unlocked device can use a valid synced passkey with no crypto flaw, which is why you add step-up, risk signals, and separated authorization; and signCount is a clone signal for hardware keys but 0/meaningless on synced passkeys, so it’s soft, never a gate. Deploy passkeys as a strong authenticator, register two at enrollment, migrate the password away, and remember the account is only as strong as its weakest reachable path.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.