Designing a Passwordless Authentication System with WebAuthn and Passkeys
How we architect passwordless login: WebAuthn/FIDO2 credential flows, passkey sync trust boundaries, recovery paths, and the failure modes teams underestimate.
The problem passwords actually create
Password-based authentication fails in a specific, well-understood way: it asks a human to remember and re-transmit a shared secret across every session, over a channel an attacker can trivially intercept, relay, or phish. Credential stuffing, phishing kits, and password-reset abuse are not exotic attacks; they are the dominant initial-access vector in most breach reports precisely because the underlying primitive — a shared secret typed into a form — has no cryptographic binding to the site requesting it. Multi-factor authentication bolted onto a password (SMS OTP, TOTP codes) narrows the window but does not close it: OTP codes are themselves phishable via real-time relay proxies, and SMS is subject to SIM-swap.
When we design a passwordless system, the requirement is not 'remove the password field' — that's a UX change, not a security one. The actual requirement is phishing resistance: the credential must be cryptographically bound to the origin that issued it, so that even a perfect visual clone of the login page cannot obtain something replayable elsewhere. WebAuthn (the browser API) built on the FIDO2/CTAP2 protocol is the only widely deployed primitive that satisfies this, because the browser — not the user — enforces the origin check as part of the credential ceremony. Everything else in the architecture exists to make that primitive usable at consumer or workforce scale without reintroducing a fallback that is weaker than the thing it replaced.
Core credential model: relying party, authenticator, and the key pair
WebAuthn is a public-key ceremony. During registration, the relying party (the application backend) sends a challenge and a small policy document to the browser. The browser hands it to an authenticator — a platform authenticator (a phone's secure enclave, a laptop's TPM) or a roaming authenticator (a hardware security key over USB/NFC/BLE) — which generates a fresh asymmetric key pair scoped to that origin. The private key never leaves the authenticator's secure boundary; the public key, a credential ID, and optionally an attestation statement are returned to the relying party and stored against the user's account. No secret material worth stealing is ever held server-side — a full database dump of the credentials table gives an attacker nothing usable, which changes the entire risk calculus of storing authentication state.
A critical design decision is discoverable vs. non-discoverable credentials. Discoverable ('resident') credentials are stored on the authenticator itself and let a user sign in by presenting the authenticator alone, with the OS prompting for account selection — this is what makes a true 'usernameless' flow possible. Non-discoverable credentials require the relying party to already know the username so it can send back the matching credential ID list; they're cheaper to store on constrained hardware keys but reintroduce a username-enumeration step. For a consumer product we default to discoverable credentials because the UX win (tap to sign in, no username field) is large and the storage cost lives on the user's own device. For workforce systems bound to a directory, non-discoverable credentials paired with the known corporate identity are often the better trade, since username is never actually secret in that context.
Attestation — a cryptographic statement from the authenticator vendor about what kind of device generated the key — is optional and easy to over-use. We only require it in contexts where device provenance is a compliance control (e.g., mandating FIDO-certified hardware keys for privileged administrative accounts). For general consumer passkeys we deliberately request 'none' attestation: verifying device make and model doesn't materially improve security for a normal login and adds a privacy cost (attestation can fingerprint hardware), so pulling it into the trust decision without a concrete policy reason is a net negative.
The two ceremonies: registration and authentication
Registration is a three-hop exchange: the server issues a random challenge and options (relying party ID, user handle, allowed algorithms, resident-key requirement) over TLS; the browser's WebAuthn API invokes the authenticator, which performs local user verification (biometric, PIN, or presence) before signing the challenge with the newly generated private key; the server then verifies the signature against the returned public key, checks the origin and RP ID embedded in the client-data JSON match what it expects, and persists the credential. That origin check is the whole point — it is enforced by the browser, not by application code, which is why WebAuthn resists phishing in a way OTP delivery never can: a look-alike domain simply cannot produce a signature that validates against the real origin's stored public key.
Authentication mirrors this: a fresh challenge, a signature over that challenge plus the authenticator's signature counter (or a newer 'backup state' flag for synced credentials), and a server-side verification pass that also checks the counter has increased since last use. A stalled or decreasing signature counter used to be the canonical signal of a cloned hardware authenticator; with cloud-synced passkeys that invariant weakens, because a passkey can legitimately exist on more than one enrolled device simultaneously. We treat counter regressions as a risk signal to correlate in the SIEM rather than a hard block, since a false positive there just locks out a legitimate user on a second device — a worse outcome than a slightly delayed detection.
Sync fabric, cross-device flows, and account recovery
The biggest architectural shift from FIDO2 hardware keys to consumer 'passkeys' is that the private key material is now frequently synchronized across a user's devices by a platform provider's encrypted keychain, rather than living in one physical token. That's a genuine usability win — losing your phone no longer means losing your only credential — but it moves a piece of the trust boundary outside the relying party's control and into the sync provider's account security. We treat the sync fabric as a dependency to be reasoned about explicitly, not ignored: a relying party's threat model now implicitly includes 'how strong is the security around the user's platform account,' because compromising that account can propagate a usable credential to an attacker's device.
Cross-device authentication for a new or unenrolled device is handled by the 'hybrid transport' flow: the desktop browser shows a QR code, the phone scans it, and a CTAP2 exchange happens over Bluetooth proximity plus a server-mediated tunnel — Bluetooth proves physical proximity so the flow can't be relayed to a remote attacker, while the tunnel carries the actual protocol messages. This is the piece we spend the most design attention on, because it's also the piece most exposed to real-time phishing relay if implemented carelessly (an attacker's fake login page proxying the QR code to a victim's real session).
Recovery is the honest weak point of any passwordless design, and we treat it as a first-class part of the architecture rather than an afterthought. If every account has a fallback path — email-link reset, SMS OTP, support-desk override — that fallback is by definition the weakest authenticator in the system and becomes the attacker's target of choice, exactly as it is with passwords today. Our default posture is to require enrollment of at least two independent authenticators (e.g., a synced passkey plus a hardware key held in reserve) before disabling the legacy password/OTP fallback entirely, and to gate any account-recovery flow behind step-up verification that is at least as strong as the factor it's replacing, with the event routed through the same SIEM pipeline as a privileged-access change.
Trade-offs and what we would not do
We would not attest every credential by default — it buys little for consumer flows and creates a privacy and device-fingerprinting liability that has to be justified against a specific compliance requirement, not applied as a blanket policy. We would not treat 'passwordless' as synonymous with 'phishing-resistant' without checking which authenticator classes are actually permitted; a deployment that still allows SMS OTP as an equal-weight second factor has not raised its ceiling, it has just added a second door with the same lock. We would not silently downgrade a user to a weaker fallback on authenticator failure without an explicit, logged step-up event — silent downgrade paths are exactly what attackers probe for, and they're invisible unless the SIEM is watching for the transition itself, not just the eventual login success. And we would not roll a bespoke WebAuthn client-side implementation; the browser and platform APIs exist precisely because origin-binding correctness in a hand-rolled ceremony is easy to get subtly wrong, and the whole security property collapses if that binding is loose.
The honest limitation to communicate to any client considering this: passwordless does not eliminate account-takeover risk, it relocates it — from a guessable or phishable shared secret to the security of the device, the platform sync account, and the recovery path. A well-architected system makes each of those relocated risks explicit, monitored, and deliberately weighted, rather than assuming the WebAuthn ceremony alone has solved authentication.
Building something like this?
We engineer secure, regulated, and AI-driven systems at this depth. Tell us what you are building and we will help you architect it.
Start Your Project