Back to blog
Security
IntermediateForSecurity EngineersPlatform EngineersFull-Stack Developers
12 min

Passkeys vs Passwords: How WebAuthn Actually Works, Who Builds What, and How to Roll It Out

A password is a secret both sides have to keep. A passkey is a key pair only one side ever holds. This is what that actually means at the protocol level — with three diagrams for registration, login and the sync fabrics — plus who ships passkeys today and how to roll them out without breaking account recovery.

passkeyswebauthnfido2passwordless-authenticationpasskeys-vs-passwordsauthentication
Contents

A password is a secret two parties have to keep at the same time: you, and every server you ever typed it into. That is the entire design flaw. It does not matter how long or random the string is — if it can be typed, it can be phished, and if it is stored anywhere else, it can be breached and reused.

A passkey is not a stronger secret. It is not a secret at all, on the server side. It is one half of a key pair, and the half the server gets is mathematically useless to an attacker.

That distinction gets lost in most “turn on passkeys” marketing copy, which is a shame, because the protocol underneath is genuinely elegant and worth understanding at the message level — especially if you are the one who has to implement it. This article covers how the two core WebAuthn ceremonies actually work, what “synced” versus “device-bound” really means, who ships passkeys today and what they each hold, how to implement support without breaking account recovery, and an honest read on where this is still rough.

Passwords are a secret you keep sharing. Passkeys replace it with a key pair only your device holds — comparison of password risks vs passkey properties

What a Passkey Actually Is

Strip away the product framing and a passkey is a WebAuthn public-key credential, and WebAuthn is a W3C standard that, paired with the FIDO Alliance’s CTAP2 protocol, forms FIDO2. Three parties are involved in every ceremony, and it is worth naming them precisely because the vendor documentation rarely does:

  • The relying party (RP) — the website or app that wants to authenticate you. This is your server. It never sees, stores, or transmits a private key at any point.
  • The client — the browser or OS, which mediates between the RP’s JavaScript (navigator.credentials) and the authenticator, and — this part matters — independently verifies the origin before anything is signed.
  • The authenticator — the thing that actually holds the private key: a phone’s secure enclave, a laptop’s TPM, or a roaming hardware key (YubiKey, Titan). It never talks to the network directly; it only talks to the client over CTAP2.

A password login has two parties and one shared secret. A passkey login has three parties and zero shared secrets — the public key that sits on your server is, by definition, safe to leak. That is the whole point, and everything else in this article is detail on how that property is achieved and where it leaks back into “well, actually, it’s complicated.”

How Registration Works

“Adding a passkey” to an account is a WebAuthn registration ceremony (navigator.credentials.create()). Six messages, three parties, and the private key touches exactly one of them.

WebAuthn registration ceremony diagram: relying party sends a challenge, the browser calls navigator.credentials.create, the authenticator generates a key pair on-device and returns the public key, and the server stores only the public key
The private key is generated in step 3 and never appears in any of the other five messages.

A few details that matter once you actually implement this:

  • The challenge is single-use and server-generated. It exists purely to prevent replay — the signature the authenticator eventually produces is only valid for that exact challenge, so a captured registration response cannot be replayed against a different session.
  • Attestation is optional and mostly not worth requiring. The authenticator can cryptographically prove what make/model it is (attestation: "direct"), but doing so leaks device fingerprinting information, requires you to maintain a metadata trust store, and most consumer flows use attestation: "none". Reach for direct attestation only in regulated/enterprise contexts where you must enforce a specific hardware allow-list.
  • Discoverable (resident) credentials are what make “passkey” different from old-style WebAuthn 2FA. Setting residentKey: "required" tells the authenticator to store the credential such that it can be found without the RP sending a username first — that is what powers the “sign in with a passkey” account picker instead of “type your username, then use your security key.”
  • The credential ID and public key are what you store — never anything else. A minimal schema is (user_id, credential_id, public_key_cose, sign_count, transports, created_at). sign_count is a legacy anti-cloning counter from single-device authenticators; most synced passkeys report it as a flat 0, so treat it as advisory, not a security boundary.

How Login Works

Logging in is the authentication ceremony (navigator.credentials.get()), and it is the mirror image of registration — a challenge goes out, a signature comes back, and this time the server has something to check the signature against.

Diagram comparing the same passkey login attempt on the real site yourapp.com versus a look-alike phishing domain: on the real origin the authenticator signs the challenge and a session is created, on the phishing origin the authenticator refuses to sign and there is nothing to steal
Same user, same tap, two different origins — the browser decides the outcome before a signature is ever produced.

The detail that actually delivers the anti-phishing property is easy to skim past: the client — not the user — checks that the origin requesting the assertion matches the origin the credential was registered to. A pixel-perfect clone of your login page hosted on a look-alike domain can render an identical navigator.credentials.get() call, and the browser will simply refuse to produce a valid signature for it. There is no “type the password into the wrong box” failure mode, because there is no box.

Two implementation notes:

  • Conditional UI (autofill) is what makes this feel like a password field. Calling get() with mediation: "conditional" on page load lets the browser surface saved passkeys in the normal username autofill dropdown, so the login form still looks like a plain input — it just also offers a one-tap passkey option. Skipping this is the single biggest reason “we added passkeys” rollouts get low adoption: users never see the option because it is hidden behind a separate button.
  • User verification (userVerification: "preferred" vs "required") decides whether a bare “is a device present” check is enough, or whether the local biometric/PIN unlock is mandatory. For anything beyond a trivial account, require it — otherwise an unlocked, unattended device becomes a full authentication bypass.

Who Actually Holds the Private Key

Here is where the “just use passkeys” pitch gets less simple, and it is worth being precise about it because it changes your account-recovery design.

Diagram of passkey sync fabrics in 2026: Apple iCloud Keychain, Google Password Manager, Microsoft Windows Hello and Entra ID, and cross-platform password managers like 1Password and Bitwarden, bridged only by FIDO2 CTAP2 hybrid transport via QR code
Four sync fabrics, one cross-ecosystem fallback — and it is a transport bridge, not a credential-migration path.

Synced passkeys live inside a platform vendor’s end-to-end-encrypted cloud and follow the user’s account, not their device:

  • Apple syncs through iCloud Keychain across macOS, iOS, iPadOS and visionOS, gated behind the Apple ID and (for viewing) device passcode.
  • Google syncs through Google Password Manager across Android, ChromeOS and Chrome on any OS, tied to the Google account.
  • Microsoft anchors platform passkeys to Windows Hello and the Microsoft account; on the workforce side, Entra ID adds policy control over which authenticators are acceptable.
  • 1Password, Bitwarden, Dashlane and other password managers sync passkeys through their own vault, cross-platform by design — the whole point of a password manager is not being locked to one OS vendor.

Device-bound passkeys are the other model: the private key is generated inside one specific piece of hardware and is never exported, backed up, or synced anywhere, by design. This is the default (and often the only option) for FIDO2 hardware security keys, and it is a mode some enterprise MDM policies force even for platform authenticators, trading recoverability for a much narrower attack surface — the key can only ever be exfiltrated by physically compromising that one device.

What does not yet work cleanly: moving a synced passkey from one vendor’s fabric to another. A passkey created in iCloud Keychain does not appear in Google Password Manager. The practical bridge across ecosystems is FIDO2 CTAP2 hybrid transport — scan a QR code on a laptop, approve on a phone over Bluetooth, and that phone acts as a one-time roaming authenticator for that browser session. It is a genuinely useful fallback for “I’m on a friend’s computer” or a platform gap, but the private key still never leaves the phone, and it is not a migration tool. The FIDO Alliance’s Credential Exchange Protocol (CXP) is the standardization effort aimed at proper cross-vendor export/import; as of 2026 it is still early.

Who Offers What in 2026

For most teams the real decision is not “should we implement raw WebAuthn,” it is “which layer do we implement it at.” Here is the landscape, roughly ordered from consumer platform down to identity infrastructure:

Who Offers What in 2026
Provider What it is Passkey role
Apple, Google, Microsoft OS / browser platforms Ship the authenticator + sync fabric (iCloud Keychain, Google Password Manager, Windows Hello) that end users already have on their device
1Password, Bitwarden, Dashlane Password managers Store and sync passkeys across any OS/browser as a vault item, independent of any single platform vendor
Okta, Auth0, Microsoft Entra ID, Ping Identity Enterprise IdPs Let you turn on passkey sign-in for your workforce or customer app without touching WebAuthn code directly — the IdP is the relying party
Keycloak, Authentik Self-hosted IdPs Same idea, open source — see Keycloak vs Authentik if you are choosing between them; both support WebAuthn as a self-service credential
AWS Cognito, Clerk Managed app-auth platforms Ship WebAuthn/passkey sign-in as a built-in configuration option — no custom ceremony code required
SimpleWebAuthn, webauthn4j, go-webauthn/webauthn, py_webauthn Open-source relying-party libraries What you reach for if you are building the identity layer yourself — parse/verify the ceremony without hand-rolling CBOR/COSE
Yubico, Google (Titan), SoloKeys Hardware security keys Device-bound roaming authenticators for workforce/high-assurance accounts, independent of any OS sync fabric

The honest read: if your product already sits behind Okta, Auth0, Entra ID, or a self-hosted Keycloak/Authentik instance, turning on passkeys is a configuration change, not an engineering project. The IdP is already the relying party; you are enabling a credential type it already knows how to handle. The engineering project described below is for the case where you are the relying party — you run your own auth stack and want to add passkeys as a first-class credential.

How to Actually Implement It

Assume you are the relying party. The shape of the work:

1. Pick a server library — do not parse CBOR/COSE by hand. The WebAuthn wire format (attestation objects, COSE public keys, CBOR-encoded authenticator data) is exactly the kind of thing that is easy to get subtly wrong in a way that only shows up as a security bug later. Use a maintained library for your stack: SimpleWebAuthn (TypeScript/Node, also ships a browser helper), webauthn4j (Java), Fido2NetLib (.NET), go-webauthn/webauthn (Go, the actively maintained fork of the original Duo Labs library), py_webauthn (Python), webauthn-rs (Rust). All of them expose roughly the same two functions you need: “generate registration/authentication options” and “verify registration/authentication response.”

2. Get the Relying Party ID right before anything else. The rpID is the domain the credential is bound to (no scheme, no port) — example.com, not https://example.com or app.example.com if you also need it valid on www.example.com. Get this wrong and passkeys registered in one environment silently fail to authenticate in another (a very common “works locally, breaks in staging” bug, because localhost and your staging subdomain are different RP IDs).

3. Require resident keys and conditional UI from day one, not as a later enhancement — a passkey rollout without the autofill-picker UX gets a fraction of the adoption, because users never discover it is an option.

4. Design account recovery before you ship, not after the first support ticket. This is the part every “add passkeys” tutorial skips. At minimum:

  • Let users register more than one authenticator (phone + a backup security key, or two synced devices) — a single point of failure with an unrecoverable credential is worse than a password.
  • Keep a fallback path (email magic link, backup codes generated at enrollment, or a human-verified support flow) for the case where every registered authenticator is lost.
  • Decide explicitly whether you allow downgrading to a password/OTP fallback per-account, and if so, gate it behind extra friction — it is the path an attacker will try first once passkeys make direct credential theft useless.

5. Migrate gradually, not as a hard cutover. Add passkeys as an additional credential option alongside existing password/MFA, prompt existing users to enroll one after a successful login (when you already have a trusted session to authorize the enrollment), and only consider removing password login once enrollment is high and recovery paths are proven. If your session/token layer needs work in parallel, see Stop Storing JWTs in localStorage — the two problems (how you authenticate, how you keep someone authenticated) are separate and often get conflated.

6. Test the failure paths, not just the happy path: a user who cancels the OS biometric prompt, a browser with no platform authenticator (older desktop Safari/Firefox configurations), a user switching from a lost Android phone to a new iPhone mid-session. Passkeys degrade differently than passwords do, and “no fallback UI for a cancelled ceremony” is the most common bug in first passkey implementations.

What Still Breaks in Practice

  • Cross-ecosystem migration is genuinely unfinished. A user moving from Android to iPhone today re-enrolls passkeys per-site rather than carrying them over, unless they were using a cross-platform manager like 1Password or Bitwarden from the start.
  • Enterprise device loss is an operational problem, not just a cryptographic one. Someone’s laptop with device-bound corporate passkeys is stolen — who revokes it, how fast, and does your IdP even expose per-credential revocation cleanly? Test this before you need it.
  • Not every user has a passkey-capable device. Older phones, shared/kiosk devices, and some enterprise-locked-down machines cannot register a platform authenticator; you need a real fallback, not a workaround bolted on.
  • “Passkey” as a support term is confusing precisely because it hides the sync-fabric question. Support staff need to know to ask “which device and which account” was used to create it, because the recovery path is completely different for a synced iCloud Keychain passkey versus a device-bound YubiKey.

The Bottom Line

The cryptography is not the hard part — WebAuthn is a well-specified, narrow protocol, and the registration/login ceremonies above are genuinely all there is to the core exchange. The hard part is everything the protocol deliberately leaves to you: account recovery, fallback UX, and the fact that “passkey” quietly means four different sync fabrics with different failure modes depending on which vendor’s cloud a user happens to be in.

If you are not already an identity provider, the pragmatic move is to let one absorb this problem for you — Okta, Auth0, Entra ID, or a self-hosted Keycloak/Authentik — and spend your own engineering time on account recovery and rollout sequencing instead of CBOR parsing. If you are the relying party, the protocol rewards careful reading far more than most auth features do, and the payoff is real: there is no password database left to breach, and no login page left to phish in the way that has worked for thirty years.

Frequently asked questions

What is a passkey, exactly?

A passkey is a FIDO2/WebAuthn public-key credential pair. During registration, an authenticator (your phone, laptop, or a hardware security key) generates a private key that never leaves it and a public key that is sent to the website. To log in, the site sends a random challenge, the authenticator signs it with the private key after you unlock locally (biometric, PIN or hardware button), and the site verifies the signature with the public key it already has on file. Nothing that could be replayed as a credential ever crosses the network — that is the entire difference from a password.

Are passkeys just WebAuthn with a marketing name?

Mostly, yes, with one important addition. WebAuthn (a W3C standard) plus CTAP2 (the authenticator protocol from the FIDO Alliance) together make up FIDO2, which is the technical foundation passkeys run on. What 'passkey' adds on top is a UX and product convention: discoverable (resident) credentials that show up in an account picker without a username, and — critically — the expectation that the credential syncs across a user's devices via a platform vendor's cloud, rather than being lost the moment one device breaks. The cryptography did not change; the deployment model did.

What happens if I lose my phone?

It depends entirely on where the private key lived. If it was a synced passkey (iCloud Keychain, Google Password Manager, a password manager vault), you sign in to a new device with that same account and the passkey reappears — recovery rides on the vendor's account-recovery flow, not on the website's. If it was a device-bound passkey (most hardware security keys, and platform passkeys on some enterprise-managed devices with sync disabled), that specific credential is gone permanently and you need a second registered authenticator or a fallback recovery path. This is why every relying party still needs an account-recovery story — passkeys move the hard problem, they do not remove it.

Can passkeys be phished?

Not the way a password or a one-time code can. WebAuthn binds every credential to the exact origin (scheme + hostname) it was registered against, and that check happens inside the browser/OS ceremony — the user never gets a chance to accidentally hand a signature to the wrong site the way they might type a password into a look-alike domain. A phishing page at a different origin simply cannot obtain a valid assertion. The remaining risks move up a level: session-token theft after a legitimate login, social engineering into approving an attacker's *own* device-linking or cross-device sign-in request (a relay technique security researchers demonstrated against FIDO2 hybrid transport in 2024–2025), and compromise of the authenticator itself.

Do I need to support physical FIDO2 security keys (YubiKeys) as well as platform passkeys?

For most consumer and B2B SaaS products, no — supporting platform passkeys (Face ID, Windows Hello, Android biometric) covers the overwhelming majority of users, and WebAuthn does not distinguish between authenticator types at the protocol level, so the same relying-party code handles both. Physical security keys remain the right choice for a smaller set of cases: workforce accounts with elevated privilege, environments where phones are banned (secure facilities, some regulated industries), and organizations that want credentials that are provably device-bound rather than synced to a consumer cloud account they do not control.

From the community

Discussion on the Fediverse

Replies from Mastodon and Bluesky — straight from the open web, no tracking.

Loading replies …

ENDE