Every day, millions of passwords leak in data breaches. Users reuse the same password across twenty sites. Phishing pages trick people into typing credentials into fake login forms. Password managers help, but they still rely on a shared secret that can be stolen server-side.
The core flaw is structural: a password is a shared secret. Both you and the server know it. If the server gets breached, your secret is exposed. If you type it into the wrong site, the attacker has it. The secret exists in two places, and either can be compromised.
We need a model where the server never holds a secret that can authenticate you. A model where leaking the server database reveals nothing useful to an attacker. That model is public key cryptography applied to authentication, standardized by the FIDO2 alliance and exposed in browsers as the WebAuthn API.
FIDO stands for Fast IDentity Online. It is an industry consortium formed in 2012 by companies including PayPal, Lenovo, Nok Nok Labs, and Validity Sensors, later joined by Google, Microsoft, Apple, and most major technology vendors. The goal: create open, interoperable standards for passwordless authentication.
FIDO2, finalized in 2018, is the current generation of the standard. It comprises two sub-specifications:
Together they let any website request a cryptographic authentication from a user’s device without ever asking for a password.
These terms are often used interchangeably but refer to different layers of the stack:
| Component | Standard Body | Role |
|---|---|---|
| WebAuthn | W3C | Browser API: navigator.credentials.create() and navigator.credentials.get() |
| CTAP | FIDO Alliance | Transport protocol: how browser talks to USB/NFC/BLE security keys |
| FIDO2 | FIDO Alliance | Umbrella: WebAuthn + CTAP + authenticator requirements |
A user visiting a WebAuthn-enabled site triggers the browser API, which may route the request to either a platform authenticator (built into the device) or a cross-platform authenticator (external security key via CTAP). The server never sees a password.
Imagine a wax seal. You press your unique ring into hot wax, creating an imprint that anyone can recognize as yours. The ring creates the seal, but the seal alone cannot create another ring. This asymmetric relationship is the heart of public key cryptography.
In WebAuthn:
This is the opposite of a password. With a password, the server stores a hash of a secret you both know. If the server is breached, the attacker can try to crack the hash. With WebAuthn, the server stores only the public key. A leaked database of public keys is worthless — public keys are designed to be public. The attacker learns nothing.
The cryptographic scheme used by most authenticators is ECDSA over the P-256 curve (algorithm ID -7 in the WebAuthn spec). This gives 128-bit security with small key sizes, making it ideal for constrained authenticator hardware.
A public key alone does not authenticate you. You must prove possession of the corresponding private key. This happens through a challenge-response protocol:
The challenge ensures freshness. Even if an attacker records a signed response, they cannot replay it because the next challenge will be different. This is what makes WebAuthn phishing-resistant: a signed response for “example.com” is cryptographically bound to that origin and cannot be used against “attacker.com”.
We can represent the signature verification mathematically:
The server computes this verification. If it returns valid, you are authenticated. No password was typed, no shared secret ever existed.
Authenticators come in two broad categories: platform and cross-platform.
Platform authenticators are built into the device. Examples include Touch ID on MacBooks, Windows Hello with IR cameras, Face ID on iPhones, and Android biometric authentication. The private key lives in the device’s secure hardware. These are convenient but tied to a specific device — unless passkey sync is used.
Cross-platform authenticators are external devices. Examples include YubiKeys, SoloKeys, and Google Titan Keys connected via USB, NFC, or BLE. A phone can also act as a cross-platform authenticator over BLE. These are portable across devices but require carrying an extra token.
The choice between platform and cross-platform depends on your threat model and convenience needs. For most users, platform authenticators with passkey sync provide the best balance.
Registration — called the creation ceremony in WebAuthn — is the process of generating a new key pair and registering the public key with a server. The flow has three participants: the client (browser), the authenticator (the device creating the key), and the relying party (the server).
const credential = await navigator.credentials.create({
publicKey: {
challenge: new Uint8Array(32),
rp: { name: "Example Corp", id: "example.com" },
user: {
id: new Uint8Array(16),
name: "user@example.com",
displayName: "User"
},
pubKeyCredParams: [
{ type: "public-key", alg: -7 }
],
authenticatorSelection: {
userVerification: "required"
},
attestation: "direct"
}
});
Key parameters:
On success, the browser returns a PublicKeyCredential object containing the credential ID and an attestation response. The client sends this to the server, which stores the credential ID and public key for future authentication.
On the server side, validation involves checking the challenge, origin, relying party ID, and the attestation signature:
# Server-side: validate challenge matches one we generated
# Verify origin matches our site
# Verify rpId matches our domain
# Store credentialId + publicKey for this user
A common mistake is reusing the same challenge for registration and authentication. Challenges must be single-use and tied to a specific ceremony. The server should track pending challenges and expire them after a short window (typically 5 minutes).
Authentication — called the assertion ceremony — proves possession of the private key by signing a fresh challenge.
const assertion = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array(32),
allowCredentials: [{
type: "public-key",
id: credentialId
}],
userVerification: "required"
}
});
The authenticator looks up the credential by ID, prompts the user for verification (biometric or PIN), and signs the challenge with the stored private key. The resulting assertion contains:
authenticatorData || SHA256(clientDataJSON)The server verifies the signature against the stored public key:
# Server-side verification:
# 1. Look up credentialId in database
# 2. Retrieve stored public key
# 3. Hash clientDataJSON and concatenate with authenticatorData
# 4. Verify signature using stored public key
# 5. Check that challenge matches an active, unused challenge
# 6. Check that origin matches our site
# 7. Check that rpIdHash matches our domain hash
# 8. (optional) Check that counter > last known counter
The signature counter is an optional feature that detects cloned authenticators. If the counter does not increase monotonically, the authenticator may have been cloned. In practice, passkeys on synced platforms do not support counters because multiple devices may hold copies.
When an authenticator creates a new key pair, it can optionally sign the credential with a manufacturer key to prove it is a genuine device. This is called attestation.
Attestation formats include:
Attestation is valuable for enterprise deployments where policy dictates that only specific hardware models are allowed. For most consumer websites, attestation is set to none or indirect to avoid privacy concerns. The attestation certificate can identify the model of authenticator, which some users may consider fingerprinting.
The biggest friction point for WebAuthn adoption was device lock-in. If you registered a credential on your iPhone, you could not use it on your MacBook — unless the website re-enrolled you. Passkeys solve this by syncing the credential across devices through the platform vendor’s cloud.
For Apple devices, passkeys sync via iCloud Keychain, backed by end-to-end encryption. A passkey created on an iPhone appears on all the user’s Apple devices signed into the same iCloud account.
For Google, passkeys sync via Google Password Manager, available on Android and Chrome on any operating system.
The sync mechanism works by wrapping the private key in an encrypted blob that is stored in the cloud and decrypted only on the user’s trusted devices. The encryption key is derived from the user’s cloud account password and device-specific keys. The cloud provider never has access to the raw private key.
This means passkeys are both more secure and more convenient than passwords:
WebAuthn distinguishes between two concepts:
The userVerification parameter in the API accepts three values:
"required" — the ceremony fails if the authenticator cannot verify the user"preferred" — UV if available, otherwise fall back to UP"discouraged" — do not prompt for UV, use UP onlyFor most applications, "preferred" is the right choice. It provides biometric verification on devices that support it while gracefully degrading on older hardware.
Authenticators that store credentials internally (not in cloud sync) are called resident keys or discoverable credentials. These store the credential ID and private key on the authenticator itself, allowing the browser to discover available credentials without the server specifying which one to use. Non-resident credentials require the server to provide the credential ID in allowCredentials.
Conditional UI, specified in the WebAuthn Level 3 draft, lets the browser show passkey autofill suggestions in the username field. When a user focuses the username input, the browser displays available passkeys for that domain, allowing one-tap login without visiting a separate login page.
const assertion = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array(32),
allowCredentials: [],
userVerification: "preferred"
},
mediation: "conditional"
});
The mediation: "conditional" parameter tells the browser to show a passive autofill suggestion rather than an immediate modal. The user can click the passkey to log in, or ignore it and type credentials manually.
Recovery from authenticator loss depends on the method:
Best practice: always allow multiple credentials per account. Let users register at least one backup method (a second passkey, a recovery code, or a fallback TOTP).
No single authentication method fits every scenario. Passkeys excel for consumer web applications where the user has an ecosystem account (Apple, Google). TOTP remains useful for backup and enterprise scenarios. SMS OTP is better than nothing but should be phased out. Passwords are the baseline we are trying to replace.
| Factor | WebAuthn | Passkeys | TOTP | SMS OTP | Passwords |
|---|---|---|---|---|---|
Phishing Resistance Prevents fake login page attacks | High Challenge-response, bound to origin | High Same as WebAuthn + sync | Low Codes can be phished via fake sites | Low SMS interception, SIM swap | None Trivially phished |
Usability Ease of daily use | Medium Requires authenticator per device | High Biometric + sync, seamless | Medium Type 6-digit code | Low Wait for SMS, type code | High Type password (if remembered) |
Setup Cost Effort to enable | Low Built into browsers and devices | Low Auto-synced after creation | Medium Install app, scan QR, enter seed | Low Phone number required | None No extra setup needed |
Recovery Restoring access after loss | Complex Need backup authenticator or codes | Simple Cloud account recovery (iCloud/Google) | Complex Must re-enroll all apps | Complex Carrier-dependent, slow | Simple Reset password via email |
Sync Available across devices | No Tied to one device | Yes iCloud Keychain, Google PM | No Per-device TOTP seeds | No Tied to phone number | Manual Password manager or memory |
Security Level Overall protection | Very High Public key crypto, no shared secrets | Very High Same crypto + cloud backup | Medium Shared secret, time-based | Low SS7 attacks, SIM swap | Varies Depends on strength & hygiene |
The industry trajectory is clear: passkeys are the endgame. Apple, Google, and Microsoft have all committed to the standard. The FIDO2 specification is a W3C recommendation. Browser support is universal across Chrome, Safari, Firefox, and Edge. The only remaining gap is backward compatibility — and that is closing fast.
Every website should support WebAuthn today. Start with passwordless as an option alongside existing methods, measure adoption, and gradually make it the primary flow. The cryptographic foundation is sound, the UX is improving rapidly, and the security benefits over passwords are transformative.
Before deploying WebAuthn:
none)?