Passkeys and WebAuthn: The End of Passwords
Passkeys WebAuthn authentication represents the most significant advancement in user authentication since the introduction of two-factor authentication. Built on the FIDO2 standard, passkeys replace passwords with cryptographic key pairs stored on user devices. Therefore, phishing attacks become impossible because there’s no shared secret to steal, and users enjoy a frictionless login experience using biometrics or device PINs.
Major platforms including Apple, Google, and Microsoft now support passkeys natively, enabling cross-device synchronization through iCloud Keychain, Google Password Manager, and Windows Hello. Moreover, passkeys work across browsers and operating systems through the WebAuthn API, making them a universal authentication solution. Consequently, organizations adopting them see dramatic reductions in account takeovers and support costs related to password resets. Industry reports from the FIDO Alliance suggest that password-reset tickets routinely account for a large share of help-desk volume, so removing them has a direct operational payoff.
Understanding the Cryptographic Model
To reason about passkeys correctly, it helps to understand what actually moves across the wire. During registration, the authenticator generates an asymmetric key pair using an algorithm such as ES256 (ECDSA over the P-256 curve) or RS256. The private key never leaves the secure enclave or TPM; only the public key and a credential identifier are transmitted to the server. As a result, a database breach exposes nothing usable — public keys cannot be used to forge logins.
Each authentication is a fresh challenge-response. The server sends a random challenge, the authenticator signs it together with the relying party ID and the client data, and the server verifies that signature against the stored public key. Because the signature is bound to the origin, a credential minted for myapp.com simply will not produce a valid assertion for myapp-phishing.com. This origin binding is the structural reason phishing fails, and it is enforced by the browser rather than by user vigilance.
Passkeys WebAuthn Authentication: Registration Flow
The registration process creates a unique cryptographic key pair for each user-site combination. The private key remains on the user’s device (protected by biometrics), while the public key is stored on the server. Furthermore, the entire process happens without any shared secrets crossing the network, making it inherently resistant to phishing and credential stuffing attacks.
// Server: Generate registration options
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';
app.post('/api/auth/register/options', async (req, res) => {
const user = await db.users.findById(req.body.userId);
const options = await generateRegistrationOptions({
rpName: 'My Application',
rpID: 'myapp.com',
userID: user.id,
userName: user.email,
userDisplayName: user.name,
attestationType: 'none',
authenticatorSelection: {
residentKey: 'required', // Discoverable credential (passkey)
userVerification: 'preferred', // Biometric or PIN
authenticatorAttachment: 'platform', // Device authenticator
},
excludeCredentials: user.credentials.map(cred => ({
id: cred.credentialID,
type: 'public-key',
})),
});
// Store challenge for verification
await db.challenges.set(user.id, options.challenge);
res.json(options);
});
// Client: Create passkey
async function registerPasskey() {
const optionsRes = await fetch('/api/auth/register/options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: currentUser.id }),
});
const options = await optionsRes.json();
// Browser prompts user for biometric/PIN
const credential = await navigator.credentials.create({
publicKey: options,
});
// Send response to server for verification
await fetch('/api/auth/register/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential),
});
}Notice the excludeCredentials list. Without it, a user who already registered a passkey on a device could accidentally create a second credential for the same authenticator, leaving orphaned keys. The residentKey: 'required' setting is what makes the credential discoverable — the authenticator stores enough metadata to present the account at login time, which is what enables the username-less “just tap” experience users expect.
Authentication Flow
Login with passkeys is remarkably simple from the user’s perspective — they tap a button, verify with biometrics, and they’re in. Behind the scenes, the browser creates a cryptographic assertion signed by the private key, which the server verifies against the stored public key. Additionally, the assertion includes the origin URL, preventing phishing sites from replaying credentials.
// Server: Generate authentication options
app.post('/api/auth/login/options', async (req, res) => {
const options = await generateAuthenticationOptions({
rpID: 'myapp.com',
userVerification: 'preferred',
// Empty allowCredentials enables discoverable credentials
// The authenticator shows all passkeys for this domain
allowCredentials: [],
});
await db.challenges.set('login', options.challenge);
res.json(options);
});
// Server: Verify authentication response
app.post('/api/auth/login/verify', async (req, res) => {
const { body } = req;
// Find user by credential ID
const credential = await db.credentials.findByCredentialID(body.id);
if (!credential) return res.status(401).json({ error: 'Unknown credential' });
const verification = await verifyAuthenticationResponse({
response: body,
expectedChallenge: await db.challenges.get('login'),
expectedOrigin: 'https://myapp.com',
expectedRPID: 'myapp.com',
authenticator: {
credentialPublicKey: credential.publicKey,
credentialID: credential.credentialID,
counter: credential.counter,
},
});
if (verification.verified) {
// Update counter to prevent replay attacks
await db.credentials.updateCounter(credential.id, verification.authenticationInfo.newCounter);
// Issue session token
const token = await createSession(credential.userId);
res.json({ token });
}
});Edge Cases the Happy Path Hides
The signature counter deserves attention. The WebAuthn spec lets authenticators increment a counter on each use so that a cloned credential can be detected when its counter regresses. In practice, however, synced passkeys (the kind that roam through iCloud Keychain or Google Password Manager) frequently report a counter of zero and never increment it, because the credential genuinely lives in multiple places. Therefore, do not hard-fail when newCounter equals the stored value for synced credentials — reserve strict counter enforcement for hardware security keys, where regression is a real signal.
Another common pitfall is the relying party ID. The rpID must be a registrable suffix of the origin’s domain. A credential created with rpID: 'myapp.com' works on www.myapp.com and app.myapp.com, but a credential scoped to app.myapp.com will not work on the apex domain. Plan this hierarchy before your first user enrolls, because changing the rpID later invalidates every existing passkey. Similarly, localhost is treated as a secure context for development, but everything in production requires HTTPS.
Account recovery is the genuinely hard problem. A purely device-bound passkey that is lost with a broken phone leaves the user locked out, which is why most teams register at least two authenticators or keep a recovery channel such as a verified email magic link or a printed recovery code. The trade-off is real: every fallback you add is also a smaller phishing-resistant surface, so design recovery to require step-up verification rather than reopening the password door you just closed.
Cross-Device Synchronization
Modern passkey implementations synchronize credentials across devices through platform-specific cloud services. Apple uses iCloud Keychain, Google uses Google Password Manager, and Microsoft uses Windows Hello. Furthermore, cross-device authentication is supported through QR codes and Bluetooth, allowing users to authenticate on one device using a passkey stored on another. This hybrid transport — sometimes called caBLE — is why a user can sign in on a friend’s desktop by scanning a QR code with their own phone without the credential ever leaving that phone.
When NOT to Use Passkeys (and the Trade-offs)
Passkeys are not a universal answer for every account in every context. Shared kiosk or shop-floor machines, where many users sign in to the same device, fit the platform-authenticator model poorly because the credential is tied to a person’s biometrics, not to the workstation. In those environments a roaming hardware key or a managed credential often works better. Likewise, regulated workflows that mandate a specific second factor may still require a layered approach rather than a single tap.
There is also an ecosystem cost. Attestation — proving exactly which authenticator model created a key — is powerful for high-assurance enterprise use, but it leaks device fingerprinting signals and many synced passkey providers strip it, so do not build a security model that depends on attestation from consumer devices. Finally, support burden shifts rather than disappearing: instead of “I forgot my password” tickets you will field “my passkey isn’t showing up on my new laptop” questions, which require staff who understand the sync model. Compared to a one-time SMS code, passkeys win decisively on phishing resistance but demand more upfront UX investment. For a broader view of credential hardening, see our companion guide on API security with mTLS and certificate pinning.
Production Deployment Considerations
When deploying passkeys, maintain password login as a fallback during the transition period. Additionally, provide clear UI guidance explaining what passkeys are and how to set them up. Monitor adoption metrics and gradually encourage users to switch. A pattern that works well in production teams is to offer enrollment immediately after a successful password login, when the user has already proven who they are, rather than interrupting them at an inconvenient moment. See the passkeys.dev developer resource for implementation best practices and browser compatibility tables, and review our overview of modern OAuth 2.1 with PKCE and DPoP for how passkeys slot into a token-issuing flow.
Key Takeaways
- Start with a solid foundation and build incrementally based on your requirements
- Test thoroughly in staging before deploying to production environments
- Monitor performance metrics and iterate based on real-world data
- Follow security best practices and keep dependencies up to date
- Document architectural decisions for future team members
In conclusion, Passkeys WebAuthn authentication eliminates the weakest link in application security — passwords. By implementing them carefully, you protect users from phishing, credential stuffing, and password reuse attacks while delivering a faster, more convenient login experience. Plan your relying party hierarchy, handle the synced-counter and recovery edge cases honestly, and you can begin a measured rollout today with libraries like SimpleWebAuthn while keeping a graceful fallback in place.