Passkeys and WebAuthn: The Era of Passwordless Authentication
Half of my customer support tickets were "I forgot my password." At first, I thought this was normal. Everyone forgets passwords. But the problem didn't stop there.
Users reused the same password across multiple sites, and when one site had a data breach, our service became vulnerable too. When we enforced stronger password complexity policies, users forgot their passwords even more frequently. When we introduced 2FA, complaints about "login is too cumbersome" flooded in.
This was the fundamental limitation of passwords. If they're complex enough to be secure, they're hard to remember. If they're easy to remember, they're vulnerable. While searching for a solution to this dilemma, I discovered Passkeys.
At first, I thought it was "just another authentication method." But once I understood it, it was completely different. This wasn't about improving passwords—it was about eliminating passwords entirely.
The Key Must Be Physically Held
The core idea of Passkeys was so simple it was surprising: "Don't store secrets on the server."
Traditional password systems work like this: users enter a password, and it's compared against a hash stored on the server. The server must store something in the database. This is where the problem begins. If the database gets hacked, everything is compromised.
Passkeys work in the opposite way. The secret key is stored only on the user's device (smartphone, laptop), and the server only holds the public key. This is like the relationship between a lock and a key.
Imagine creating a safe deposit box at a bank. The traditional password approach is like telling the bank employee your safe's password. Every time you visit, you tell them the password, they verify it, and open the safe. The problem is that the bank employee also knows your password. If the employee is malicious or their notes get stolen, your safe is at risk.
The Passkey approach is like you holding the key while the bank only has the lock. Every time you visit, you unlock it with your key. The bank doesn't know your key. The lock alone can't open the safe. This is the essence of public key cryptography.
WebAuthn Standard Changed Everything
Passkeys didn't appear suddenly. They're built on top of WebAuthn, a standard created by W3C and the FIDO Alliance. Without this standard, each company would have created their own authentication system, resulting in compatibility hell.
The genius of WebAuthn is that it's designed as a browser API. Developers just need to call navigator.credentials.create() and navigator.credentials.get(). The browser handles the rest. Whether to use biometrics, security keys, or PIN is decided by the operating system and device.
Think of it like a postal system. Imagine if every neighborhood used a different mail system—red envelopes in neighborhood A, blue envelopes in neighborhood B, and addresses written only in numbers in neighborhood C. Sending mail nationwide would be nearly impossible.
WebAuthn is a nationwide common postal system. All neighborhoods use the same address format and the same way of attaching stamps. That's why you can send mail anywhere. With Apple, Google, and Microsoft all supporting the WebAuthn standard, a Passkey created once works across all platforms.
Registration and Authentication: Two Different Dances
The Passkey system operates in two stages: Registration and Authentication. When I first implemented it, I confused these two and struggled for quite a while.
Registration Flow: Creating the Key
When a user first registers a Passkey:
- Server generates challenge: Random byte string. This is one-time use.
- Client generates key pair: Public and private keys are created on the user's device.
- Sign challenge with private key: Proof that "I received this challenge and have this private key."
- Server stores public key: After verifying the public key and signature, only the public key is stored in the database.
Here's what it looks like in code:
// Server: Generate registration challenge
import { generateRegistrationOptions } from '@simplewebauthn/server';
const options = await generateRegistrationOptions({
rpName: 'My App',
rpID: 'example.com',
userID: user.id,
userName: user.email,
challenge: randomBytes(32),
authenticatorSelection: {
residentKey: 'required', // Discoverable credential
userVerification: 'preferred',
},
});
// Client: Generate key pair
const credential = await navigator.credentials.create({
publicKey: options
});
// Server: Verify and store public key
const verification = await verifyRegistrationResponse({
response: credential,
expectedChallenge: storedChallenge,
expectedOrigin: 'https://example.com',
});
if (verification.verified) {
await savePublicKey(user.id, verification.registrationInfo);
}
Authentication Flow: Unlocking with the Key
When a user logs in:
- Server generates new challenge: Another random string.
- Client signs with private key: Finds the stored private key and signs the challenge.
- Server verifies with public key: Uses the database's public key to verify if the signature is valid.
- On verification success, login: Issue session token.
// Server: Generate authentication challenge
const options = await generateAuthenticationOptions({
rpID: 'example.com',
challenge: randomBytes(32),
allowCredentials: userCredentials.map(cred => ({
id: cred.credentialID,
type: 'public-key',
})),
});
// Client: Sign with private key
const assertion = await navigator.credentials.get({
publicKey: options
});
// Server: Verify signature
const verification = await verifyAuthenticationResponse({
response: assertion,
expectedChallenge: storedChallenge,
expectedOrigin: 'https://example.com',
authenticator: storedPublicKey,
});
if (verification.verified) {
return createSession(user.id);
}
Discoverable vs Non-Discoverable: Subtle But Important Difference
The most confusing part during implementation was Discoverable Credentials. This is about whether the Passkey includes user information.
Non-Discoverable: The server must tell the client "This user's credential ID is this." The user enters their email first, then the server finds the user's credential list and sends it to the client.
Discoverable (Resident Key): The Passkey itself includes user information. The user doesn't need to enter anything. The device's authenticator (Face ID, Touch ID) shows a list saying "Available Passkeys for this site are these."
With Discoverable, passwords are completely unnecessary. Visit site → Face ID authentication → Done. You don't even enter an email. This is the real future of Passkeys.
Reality: Not All Devices Support It
The theory was perfect. But when I actually deployed it, problems emerged. Old Android phones, enterprise users insisting on Internet Explorer, Windows 7 laptops—WebAuthn didn't work in these environments.
When I checked browser compatibility:
- Chrome/Edge: 67+ (2018)
- Safari: 13+ (2019)
- Firefox: 60+ (2018)
- Mobile: iOS 14+, Android 9+
More widespread than expected, but still, 5-10% of users were on unsupported environments. So I adopted a hybrid strategy.
// Feature detection
async function checkPasskeySupport() {
if (!window.PublicKeyCredential) {
return false;
}
const available = await PublicKeyCredential
.isConditionalMediationAvailable();
return available;
}
// Hybrid authentication UI
function LoginForm() {
const [passkeySupported, setPasskeySupported] = useState(false);
useEffect(() => {
checkPasskeySupport().then(setPasskeySupported);
}, []);
return (
<div>
{passkeySupported ? (
<button onClick={loginWithPasskey}>
Sign in with Passkey
</button>
) : null}
<form onSubmit={loginWithPassword}>
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit">Sign in with Password</button>
</form>
</div>
);
}
Migrating Existing Users: Gradual Transition
The trickiest part was how to transition existing users. Thousands of users were already logging in with passwords. Suddenly requiring Passkey-only would obviously create backlash.
Optional migration strategy:
- Post-login suggestion: Show a "Would you like to add a Passkey?" banner to users who logged in with password.
- Incentive: Small reward for Passkey registration (like 1 week free premium features).
- Gradual enforcement: After 6 months, new accounts require Passkey, existing accounts remain optional.
- Password backup: Provide recovery codes in case users lose their Passkey.
SimpleWebAuthn Library Made Implementation 10x Easier
I initially tried to implement the WebAuthn spec directly. CBOR encoding, attestation verification, ECDSA signature verification... After 2 weeks of struggling, I gave up.
Discovering the SimpleWebAuthn library was lucky. It abstracted complex cryptographic operations, allowing registration and authentication in just a few lines of code.
The library handles:
- Base64URL encoding/decoding
- CBOR parsing
- Challenge verification
- Origin verification
- Attestation statement parsing
- Public key extraction and storage format conversion
Doing these low-level operations manually would have taken a month.
Phishing Became Fundamentally Impossible
The biggest advantage of Passkeys wasn't security—it was phishing prevention. This was a surprising side effect I discovered accidentally.
Traditional passwords are vulnerable to phishing. Create a fake site (examp1e.com), get users to enter their password, and you're done. Even 2FA can be bypassed with real-time relay attacks.
Passkeys are different. Origin verification is built into the WebAuthn spec. If a user created a Passkey on example.com, that Passkey only works on example.com. It won't work at all on examp1e.com. Because the browser checks the origin, it's technically impossible even if the user is fooled.
There was a moment when I realized how powerful this was. A phishing site impersonating our service was discovered, but not a single user using Passkeys was affected. Login attempts from the fake site were simply impossible.
Cross-Platform Sync: Apple vs Google vs Microsoft
The real revolution of Passkeys was synchronization. Old security keys (like YubiKey) were physical devices. If you lost it, that was it. Passkeys are stored encrypted in the cloud and synced to all your devices.
- Apple: Synced via iCloud Keychain. Available on iPhone, iPad, Mac.
- Google: Synced via Google Password Manager. Available on Android, Chrome.
- Microsoft: Stored locally with Windows Hello, cloud sync coming soon.
What's amazing is that cross-platform works too. You can use a Passkey created on iPhone on Android. Scan a QR code to create a temporary Bluetooth connection for authentication.
I actually tested this. When logging in on my MacBook, I authenticated with my iPhone's Face ID. A QR code appeared, I scanned it with my iPhone camera, authenticated with Face ID, and I was logged in on my MacBook. It felt like magic.
Summary: Passwords Are Now Legacy, Not Optional
Three months after introducing Passkeys, "password reset" tickets almost disappeared from customer support. Users using Passkeys have a 95% lower login failure rate. No security incidents.
Key realizations:
- Passwords are fundamentally flawed: Secrets that humans must remember are always vulnerable.
- Public key cryptography is simple: The lock (public key) and key (private key) analogy is everything.
- Standards create innovation: Without the WebAuthn common specification, this would have been impossible.
- Gradual transition is realistic: You can't convert all users overnight. Hybrid approach is the answer.
- Phishing prevention is a bonus: Built-in origin verification makes phishing technically impossible.
Implementation considerations:
- Use proven libraries like SimpleWebAuthn. DIY implementation creates security vulnerabilities.
- Discoverable credentials eliminate passwords completely.
- Feature detection identifies unsupported environments and provides fallback.
- Offer gradual migration paths for existing users.
- Always provide recovery codes or backup authentication methods.
Passwords are now legacy. In 10 years, we'll say "We used to use something called passwords." No need to wait that long. Implement Passkeys right now. Users get more convenience, developers get more security. There's no better deal than that.