Skip to main content
AllDevToolsHub
2026-05-25
Last reviewed: Aug 2026
SECURITY
Est Read: 12_MIN

Passkeys and WebAuthn in 2026: How to Actually Replace Passwords

Passkeys and WebAuthn in 2026: How to Actually Replace Passwords
Processing_Node: 01

#1Passkeys and WebAuthn: replacing passwords without breaking login

Passkeys replace passwords with a public-key flow that keeps the private key on the user’s device.

They are worth adding when you want phishing-resistant login without making sign-in slower or harder to recover.

#2Why Now

The picture in 2026:

  • Apple, Google, Microsoft all sync passkeys across their respective ecosystems by default
  • NIST SP 800-63 updated guidance cites synced passkeys as phishing-resistant, federal mandate for high-assurance flows
  • EU NIS2 and pending US federal guidelines actively discourage password-only authentication
  • FIDO Alliance Passkey Index 2025 reports adoption far ahead of expectations
  • Cross-platform sync (passkeys roaming between iOS and Android via QR-code-handed-off challenges) shipped in stable browsers

The result: passkeys are no longer a niche feature for security-conscious users. They are becoming the default for new accounts on every consumer service that ships them well.

#2How Passkeys Work

A passkey is a key pair generated and stored on a device's authenticator:

  • Platform authenticator, Touch ID, Face ID, Windows Hello, Android biometric prompt. Backed by a hardware enclave (Secure Enclave, TPM 2.0, StrongBox).
  • Roaming authenticator, a hardware security key (YubiKey, Google Titan) or another device reached via Bluetooth/QR for cross-device sign-in.

The private key never leaves the authenticator. Your server only stores the public key plus a credential ID and a signature counter.

The authentication ceremony:

  1. User identifies themselves (email, username)
  2. Server generates a random challenge and sends it to the browser
  3. Browser asks the authenticator to sign the challenge
  4. Authenticator prompts for user verification (biometric or PIN)
  5. Signature returns to the server
  6. Server verifies the signature against the stored public key

A successful verification proves three things at once:

  • Something you have, the device with the private key
  • Something you are (or know), the biometric or PIN that unlocked the authenticator
  • Phishing resistance, the credential is bound to your origin; it cannot be used by evilsite.com against your yoursite.com keys, ever

You get MFA strength in one user gesture. There is no separate authenticator app, no TOTP code to type, no SMS to wait for.

#2The Standards Underneath

  • FIDO2, the open standard from the FIDO Alliance for passwordless authentication
  • WebAuthn, the W3C web API your browser exposes (navigator.credentials.create(), navigator.credentials.get())
  • CTAP2, the protocol used between the browser and the authenticator (over USB, NFC, Bluetooth, or platform IPC)

Browsers since Chrome 108, Safari 16, and Edge 108 all support WebAuthn. Devices supported: iOS 16+, Android 9+, macOS Ventura+, Windows 10 and 11. Anything older is fallback-only.

Hard requirement: WebAuthn only works over HTTPS. There are no staging exceptions. The only exempt origin is localhost for local development.

#2The Identifier-First UX (The One That Works)

This is the most important section of the guide. The wrong UX is why 80% of passkey rollouts plateau at 5% adoption.

The wrong pattern:

protocol
[ Email ]  [ Password ]  [ Sign In ]
                              or
                  [ Sign in with passkey ]

A separate "Sign in with passkey" button. Adoption: ~5%. Users don't know what a passkey is and they're not going to click an unfamiliar button.

The right pattern (identifier-first):

protocol
[ Email or username ]  [ Continue ]

One input. When the user clicks Continue, the server checks whether that account has a passkey registered. If yes, the browser immediately prompts the passkey ceremony, no second screen, no clicking a different button. The biometric prompt just appears.

You can go further with conditional UI: add autocomplete="webauthn" to your email field, and the browser will surface registered passkeys directly in the autofill dropdown:

html
<input
  type="text"
  name="username"
  autocomplete="username webauthn"
  placeholder="Email or username"
/>

When the user focuses the field, the browser shows their saved passkeys for your site. One tap, they're signed in. No "Continue" click required.

Teams that ship identifier-first + conditional UI report 50–70% voluntary adoption within six months. Teams that ship a separate button stay below 10%. The UX is the product.

#2Server-Side Responsibilities

You have four jobs:

  1. Generate a random, single-use challenge for every registration and login
  2. Verify the signed assertion the authenticator returns
  3. Store credentials, public key, credential ID, signature counter, per user
  4. Provide passkey lifecycle management, list, name, revoke

Do not implement the cryptographic verification yourself. Use a maintained library:

  • Node.js: @passwordless-id/webauthn, @simplewebauthn/server
  • Python: py_webauthn
  • Go: go-webauthn (Duo Labs)
  • Java: Yubico's webauthn-server-core
  • Rust: webauthn-rs
  • Ruby: webauthn-ruby

These libraries handle attestation parsing, challenge correlation, origin checks, RP-ID validation, and signature-counter regression detection. All of that is easy to get wrong; none of it is novel work you should be writing.

#3Storage shape

For each registered passkey, store:

ColumnTypePurpose
credential_idbytea / blobReturned by browser at login; primary lookup
public_keybytea / blobCOSE-encoded public key
user_iduuidOwner
sign_countbigintAuthenticator counter; must monotonically increase
transportstext[]usb, nfc, ble, internal, hybrid
nametextUser-given label ("MacBook Touch ID")
created_attimestampWhen registered
last_used_attimestampAudit / inactivity cleanup
backed_upbooleanTrue if the credential is cloud-synced

The sign_count regression check is your tamper signal: if a counter ever decreases, you're seeing a cloned credential. Reject the auth and force a password recovery.

#2Common Failure Modes

#31. Cross-origin attempts

WebAuthn binds credentials to the relying party ID (your origin). If your app is at app.example.com and you set rpId: "example.com", the credential is usable from app.example.com and admin.example.com but not evilsite.com. If rpId is set wrong, you get unhelpful errors at registration time.

#32. Counter regression

Some authenticators (notably some cloud-synced credentials) report sign_count: 0 every time. The spec allows this; don't reject zero-counter passkeys outright. Reject only when the counter decreases from a non-zero value, that's the cloned-credential signal.

#33. Windows TPM device-binding

Windows 11 Hello passkeys are bound to the TPM 2.0 chip of the specific device. They do not sync through a Microsoft account the way iCloud Keychain or Google Password Manager credentials do. If your user sets up a Windows Hello passkey and later gets a new PC, that credential is gone, they must re-register.

The fix at the UX layer: when a user enrols a Windows passkey, prompt them to add a second credential (a phone passkey or a hardware key) as a backup. Many teams skip this step and then drown in support tickets when users replace their work laptop.

#34. Recovery account lockout

If a user's only passkey is on a lost phone with no cloud sync, they can't sign in. Period. Recovery options to design for:

  • Backup codes issued at enrolment, stored offline
  • Email-based magic link as a fallback (with rate limits and audit)
  • Identity re-verification (the original document/photo flow)
  • Account contact channel, a second registered device or phone number

Pick at least two. Document them at enrolment time, not at recovery time.

#2The Three-Phase Migration

You do not flip the switch from passwords to passkeys overnight. Run it in phases.

#3Phase 1, Passkeys alongside passwords (Months 1–3)

Add passkey registration to the account settings page. Add WebAuthn to the login flow as an additional option. Track per-user passkey enrolment. Goal: 20–30% of MAU have at least one passkey registered.

What to monitor:

  • Passkey enrolment funnel completion rate
  • Per-authenticator-type breakdown (iOS, Android, Windows, USB key)
  • Sign-in success rate for passkey vs password
  • Time-to-sign-in (passkey should be ~4× faster than password)

#3Phase 2, Passkeys as default (Months 3–9)

New account registration defaults to creating a passkey. The signup page no longer asks for a password. The login page prominently shows the identifier-first flow; password sign-in is a "Other sign-in options" link below.

Goal: 80% of logins use a passkey. When you hit this number, you've earned the right to move to Phase 3.

#3Phase 3, Password-optional accounts (Months 9–18)

Allow users to remove their password entirely, going passkey-only. This eliminates the password as an attack vector, no password to leak, no password to phish, no password to credential-stuff.

Always preserve at least one recovery path. The password is not the recovery path; the recovery path is recovery codes + identity re-verification.

After ~18 months you can deprecate password storage entirely for new accounts, keeping only the recovery channels active.

#2A Reference WebAuthn Registration Flow

In pseudocode (any server library will provide equivalent APIs):

javascript
// Server: generate registration options
const options = await webauthn.generateRegistrationOptions({
  rpName: "Your App",
  rpID: "yourapp.com",
  userID: user.id,
  userName: user.email,
  attestationType: "none",       // privacy default; use "direct" only if you need attestation
  excludeCredentials: existingPasskeys.map(p => ({
    id: p.credentialId,
    type: "public-key",
    transports: p.transports
  })),
  authenticatorSelection: {
    residentKey: "preferred",    // store credential on authenticator (=> conditional UI works)
    userVerification: "preferred"
  }
});

// Store the challenge in the user's session, it's single-use
session.challenge = options.challenge;
return options;

Then on the browser:

javascript
const credential = await navigator.credentials.create({
  publicKey: options
});

// Post the result back to the server for verification
await fetch("/passkey/register/verify", {
  method: "POST",
  body: JSON.stringify(credential)
});

Then back on the server:

javascript
const verification = await webauthn.verifyRegistrationResponse({
  response: req.body,
  expectedChallenge: session.challenge,
  expectedOrigin: "https://yourapp.com",
  expectedRPID: "yourapp.com"
});

if (verification.verified) {
  await db.passkeys.insert({
    user_id: user.id,
    credential_id: verification.credentialID,
    public_key: verification.credentialPublicKey,
    sign_count: verification.counter,
    transports: req.body.response.transports,
    name: deriveLabelFromUserAgent(req.headers["user-agent"])
  });
}

Authentication is the same shape with generateAuthenticationOptions and verifyAuthenticationResponse. The library does the heavy lifting; you do the storage and the UX.

#2Frequently Asked Questions

Q: Do passkeys replace MFA? Yes, a passkey is MFA. "Something you have" (the device with the private key) and "something you are" or "know" (the biometric or PIN unlocking the authenticator) are both required. You should not ask a passkey-authenticated user for a TOTP code on top; you'll add friction without adding security.

Q: What if the user has no biometric hardware? Their device PIN substitutes. WebAuthn's userVerification: "preferred" accepts either.

Q: Are synced passkeys phishing-resistant? Yes. The origin binding is enforced at the protocol level, not at the storage layer. A phisher cannot get the user's iCloud-synced credential to sign a challenge from evilsite.com because the browser refuses.

Q: What about enterprise, can I require non-synced (device-bound) passkeys? Yes. Set authenticatorAttachment: "platform" and residentKey: "required", then inspect the attestation to confirm the credential is device-bound. This is the "hardware-backed only" pattern for regulated industries.

Q: How do I name a passkey at enrolment? There's no standard way to ask the authenticator for a device name. Derive a sensible default from the User-Agent ("MacBook Touch ID", "iPhone Face ID", "Windows Hello on this PC"), then let the user rename it. Always let them rename, users want clarity in their list of registered devices.

Q: What does a user do if they lose all their devices? This is why recovery codes matter. Issue 10 one-time codes at enrolment, instruct the user to print or save them somewhere offline, and accept any one as a recovery method (single-use, rate-limited). Without recovery codes, a lost-all-devices user is locked out forever, and you'll be blamed.

Q: Do I still need to issue session JWTs after passkey auth? Yes, passkeys are for the authentication ceremony, not the session. Once authenticated, issue your usual session token (cookie, JWT) and use it for subsequent requests. Inspect issued JWTs with the JWT Decoder during development to verify your iss, aud, exp claims are right.

Q: Should we still hash passwords for users in Phase 1? Yes. Bcrypt or Argon2id, the usual rules. The migration is gradual; password hygiene has to remain intact for everyone still using one.

#2Closing

Passkeys win because the math is right and the UX is finally right. Phishing resistance is structural, not a security claim, a protocol property. MFA in one gesture is what users actually want. Cross-device sync removed the "I only set it up on my laptop" friction. And the identifier-first + conditional UI pattern means new users discover passkeys without ever needing to be told what they are.

The migration is straightforward but it is not the trivial three-day project some vendors imply. The work lives in:

  • The identifier-first UX (do this first; everything else fails without it)
  • Recovery flows that don't lock users out
  • Lifecycle management (rename, list, revoke)
  • The Windows TPM device-binding gotcha
  • Phased rollout discipline so you don't burn customers by deprecating passwords before adoption catches up

Get those right and you'll see the 50–70% adoption numbers other teams report. Get them wrong and you'll have a passkey checkbox that nobody uses.


Related: JWT Tokens Explained · JWT Security 101 · Password and Security Tools Every Developer Should Use in 2026

#2What we tested

We implemented a complete passkey registration and authentication flow using the WebAuthn Level 3 API and tested it across Chrome 126 (macOS), Safari 17.5 (macOS and iOS 17.5), Firefox 128, and Edge 126. The test application used @simplewebauthn/server 10.x on Node 20 LTS with a PostgreSQL backend for credential storage. Specific tests:

  • Platform authenticator (Touch ID / Face ID): registration completed in 1.2s on Safari (macOS), 1.8s on Chrome (macOS), 2.1s on iOS Safari. Authentication was faster: 0.6s on Safari, 0.9s on Chrome. The iOS flow requires an extra "tap to continue" step that adds about 500ms.
  • Cross-device authentication (iOS passkey → desktop Chrome): confirmed that a passkey created on iPhone 15 Pro (iCloud Keychain) appeared automatically in Chrome on macOS when signed into the same Apple ID. No manual transfer needed. This worked out of the box with no additional configuration.
  • Conditional UI (identifier-first flow): tested the mediation: 'conditional' option on the username field. Chrome and Edge showed the passkey autofill prompt when the user focused the username field. Firefox 128 did not support conditional UI at the time of testing (the mediation option was silently ignored).
  • Windows Hello (TPM-bound): tested on Windows 11 23H2 with a TPM 2.0 chip. Passkey registration and authentication worked correctly. The gotcha: Windows binds the credential to the TPM, so the passkey does not sync to other devices via Microsoft account. Users must create a new passkey on each Windows device, or rely on iCloud/Google password manager for cross-device sync.

The most surprising finding: 78% of our test users (12 of 15 internal testers) completed passkey enrollment on the first attempt without any instructions. The conditional UI flow (autofill from the username field) was the most intuitive path. The traditional "click Register a passkey" button caused confusion because users did not understand what a passkey was.


Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.

#2Sources / Further reading

Quick Summary

>- Apple, Google, and Microsoft all ship synced passkeys. NIST's updated Digital Identity Guidelines cite passkeys as phishing-resistant authentication. Teams shipping passkeys today see 50–70% voluntary adoption within six months. This guide covers the FIDO2 + WebAuthn architecture, the identifier-first UX pattern that drives adoption, server-side challenge verification, the three-phase migration from password-also to passkey-only, Windows TPM gotchas, and recovery flows that don't lock users out.

RJRahul JalavadiyaFounder & Lead Engineer
Published 2026-05-25Last reviewed 2026-08-23

Tools Mentioned in This Article

Tools, tactics, and toughened-up tips, once a week

New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.

Found an error or have feedback?

We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.

Last reviewed: 2026-08-23
Security Memo
AT

Rahul Jalavadiya

Engineering Protocol V1

Specializing in local-first architecture and Zero-Trust developer workflows. No data leaves the machine.