Your login form stores a secret on your server and asks the user to reproduce it from memory. Every famous password failure descends from that design: phishing works because the user types the secret into the wrong site, reuse works because the same secret opens many sites, and breaches work because the server holds a list of secrets worth stealing. Passkeys do not patch those failures. They remove the shared secret, and the failures go with it.
The constraint
An auth credential has to prove the same user came back, resist replay and phishing, and survive the user losing a phone. Passwords technically do the first and fail the rest because they deputize a human as the security mechanism. The fix has to move proof-of-identity into cryptography while keeping sign-in easier than typing a password, or users route around it.
The options
Password plus OTP. The shared secret stays on your server, phishable and breachable, and the second factor adds friction plus SIM-swap surface on SMS. A real improvement over a password alone, and still a shared-secret system at its core.
A password manager. Fixes reuse and weak passwords, but the credential is still a string that gets typed somewhere, and your server still stores the verifier table attackers want.
A hardware key as a second factor. Strong cryptography, wrong job: it protects a password, it does not replace one, and a bound-to-one-device credential has no recovery story when the key is in a lake.
A passkey. The passkeys.dev definition is exact: passkeys are "a replacement for passwords," "a secret stored on one's devices," where "Relying Party servers store public keys." In spec terms a passkey is a discoverable WebAuthn credential (WebAuthn Level 3's glossary lists the terms as aliases, with "resident key" deprecated), meaning the authenticator stores everything needed to sign in without the server first identifying the user.
The mechanism
WebAuthn defines two ceremonies and nothing else. Per the spec: "the first is Registration, where a public key credential is created on an authenticator, and scoped to a Relying Party with the present user's account... The second is Authentication, where the Relying Party is presented with an Authentication Assertion proving the presence and consent of the user who registered the public key credential."
server sends challenge + rp.id + user handle
authenticator mints a keypair scoped to that rp.id
server stores credential id + public key only
server sends a fresh random challenge
authenticator signs challenge + origin data
server verifies with the stored public key
https origin or registrable domain suffix
(localhost exempt for dev), signature covers
the origin, so a phished one fails verification
The phishing resistance is not policy, it is scope. The spec says "a public key credential can only be used for authentication with the same entity (as identified by RP ID) it was registered with," a scoping enforced jointly by user agents and authenticators. Apple's version of the same guarantee: passkeys "are intrinsically linked with the app or website they were created for, so people can never be tricked into using their passkey to sign in to a fraudulent app or website." The challenge the server sends, per webauthn.guide, exists "to prevent replay attacks."
Underneath the ceremonies is ordinary public-key crypto. This runs anywhere Node runs and is the entire trust model:
import { generateKeyPairSync, sign, verify, randomBytes } from "node:crypto";
// registration: the private key stays in the authenticator;
// the relying party stores only the public key + credential id
const { publicKey, privateKey } = generateKeyPairSync("ec", {
namedCurve: "P-256", // es256, webauthn's baseline algorithm
});
// sign-in: server issues a fresh challenge, authenticator signs it
const challenge = randomBytes(32);
const signature = sign("sha256", challenge, privateKey);
// verification uses only what the server stores
console.log(verify("sha256", challenge, publicKey, signature));
// true
// a signature over different data fails: replay protection
console.log(verify("sha256", randomBytes(32), publicKey, signature));
// false
The spec backs the key custody claim in one sentence: the credential private key "is expected to never be exposed to any other party, not even to the owner of the authenticator." Your database holds public keys, which are worthless to steal.
Where it bites
Trust moved to the sync fabric. Synced passkeys mean losing a phone no longer loses the credential: Apple's passkeys "are synced with iCloud Keychain" and "end-to-end encrypted, so even Apple can't read them," and Google Password Manager does the equivalent on Android. The trade is that the cloud account is now the anchor: compromise the Apple or Google account and the recovery factor, and the passkeys follow. For high-assurance cohorts the answer is device-bound passkeys (security keys, Windows Hello), which per the same spec model never leave the device and need their own backup story.
Recovery is out of scope, so it is your problem. The spec defines two ceremonies and stops. What happens when a user loses every device and their sync account is a policy you write, and passkeys.dev's own bootstrapping guidance falls back to "legacy" login challenges and account recovery steps. That recovery path is now the weakest link in your auth: attackers who cannot phish a passkey will phish your reset flow instead. A passkey-first login with a lazy email reset is a strong door behind a screen window.
The credential is welded to the domain. RP ID scoping means a passkey registered under login.example.com rules cannot be exercised from a phishing lookalike, and it also cannot be exercised from your next domain. Rebrands, domain migrations, and regional splits mean re-registration for every user. Pick the registrable-domain RP ID deliberately on day one.
The support matrix has edges. Apple's floor is iOS 16 on iPhone 8 and later, iPadOS 16 on iPad 5th generation and later, macOS Ventura, and Safari 16 on older macOS per Apple's passkeys Q&A; Google documents Android 9 (API 28) and above, with third-party providers needing Android 14; passkeys.dev's support matrix lists Windows Hello as device-bound with OS-level synced passkeys on Windows still marked planned. Consumer apps can ship passkey-first. Apps serving old Android fleets or Windows-Hello-only enterprises need the fallback designed, not bolted on.
The numbers
- Minimum platforms, per the vendors: iOS 16 on iPhone 8+, iPadOS 16 on iPad 5th gen+, macOS 13, tvOS 16; Android 9 (API level 28) via Google Password Manager; Windows Hello device-bound on Windows 10 and 11.
- The API only exists in secure contexts: the origin scheme must be
https, withlocalhostoverhttpexempted for development, per the spec's origin rules. - WebAuthn Level 2 has been a W3C Recommendation since April 2021; Level 3 formally equates "passkey" with the older "discoverable credential" and "resident key" terms.
Our position
For consumer-facing apps we build passkey-first: the passkey replaces the password as the daily credential, which deletes the phishing and credential-breach columns at once. It does not replace recovery, so we keep exactly one non-passkey path, treat it as the security boundary it is, and instrument it like one. For enterprise and regulated work we scope device-bound credentials and attestation separately, because the sync fabric is a trust decision a compliance team should make explicitly.
The takeaway
kills phishing, reuse, server secret breaches
reset flow is the new attack surface, design it
account linking: rp.id is welded to the domain
sign-in = get() + verify signed challenge
challenge exists to kill replay
device loss ok; cloud account is the anchor
device-bound keys for high-assurance cohorts
third-party providers need android 14+
windows hello: device-bound; os-level sync
still marked planned
Passkey or password-plus-fallback is a data-model decision we make on day one of a custom software build, because retrofitting auth means touching every session your users hold. If you want passkey sign-in scoped for your app and its recovery path designed properly, send us the current auth flow.
// sources
// next step
We work this way on client projects too.
Tell us what you are making. We reply with a plan, a timeline, and a price.
