WebAuthn and Passkeys: Passwordless Authentication
Passwords are the largest security vulnerability: weak credentials, reuse, phishing, and leaks. Passkeys use WebAuthn and device biometrics to replace passwords securely and seamlessly. This article explains the public-key model, registration and authentication flows, platform support, and a practical rollout path.
Key Points
- Topic Focus: React、DevOps
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
If you have ever built a login system, you have probably been woken in the middle of the night by an alert: an account was accessed from an unusual location, a credential-stuffing attack was hitting your /login endpoint, or a user was reporting that their account had been stolen. When you inspect the logs, the root cause of most incidents points to the same thing: passwords.
Passwords were designed in the 1960s, yet they are expected to carry the entire identity-security burden of the Internet in 2026. They can no longer handle it. This article explains why passwords are the largest security vulnerability and how passkeys, built on WebAuthn, use devices and public-key cryptography to remove passwords from the login flow entirely.
1. The Systemic Problems with Passwords
First, acknowledge one fact: passwords, as an authentication factor, are fundamentally broken.
The problem is not simply that “users choose weak passwords.” It is a chain of intertwined systemic flaws:
- Cognitive burden: An ordinary person has dozens of accounts and cannot remember a different random 16-character string for each one. The result is either weak passwords such as
abc123456or password reuse. - Reuse → credential stuffing: A user chooses the same password for a small forum and their bank. When the forum’s credential database is breached, as happens every week, the bank account is immediately exposed.
- Phishing: A password is a “transferable credential.” The password a user enters on
g00gle.comis exactly the same string that the real Google receives. The attacker only needs to trick the user into typing it once on a fake page, and the attack is complete. - Server-side leaks: Even if you use bcrypt, scrypt, or Argon2, password hashes remain high-value targets. GPU compute keeps getting cheaper, lowering the barrier to offline cracking.
- Poor experience: Mandatory uppercase and lowercase letters, numbers, symbols, periodic password changes, and SMS verification codes are all patches for a system that should already have been retired.
MFA can reduce the risk, but SMS codes can be intercepted through SIM hijacking, while TOTP still requires users to transcribe six digits manually. The experience remains poor. The real problem is that passwords should not exist at all.
Passkeys are the answer.
2. How WebAuthn Works: Public-Key Cryptography + Device Authentication
WebAuthn (Web Authentication) is a W3C standard. Its core idea can be summarized in one sentence: replace shared secrets with asymmetric cryptography.
The entire system rests on two facts:
- Every account has an independent public-private key pair for each website;
- The private key never leaves the user’s device, while the server stores only the public key.
This eliminates all the problems above at their source. The private key is not on the server, so a server breach reveals nothing that can be used to log in. It never leaves the device, so a phishing site cannot obtain it. Every site uses an independent key, so credential stuffing has no foothold.
Where is the private key stored? In the hardware security area of a modern device: Secure Enclave on Mac and iOS, a TPM chip on Windows, and Titan M or TrustZone on Android. Hardware protects the private key so thoroughly that even the operating system cannot read it. The OS can only ask the hardware to sign data on its behalf.
The “user verification” step that authorizes the hardware to sign, such as a fingerprint, Face ID, or a device PIN, happens locally and is never uploaded to the server. Your fingerprint data never leaves your phone. WebAuthn merely allows the device to tell the server, “I just verified the user locally; this request is trustworthy.”
The essence of the authentication flow:
- During registration, the device generates a key pair and sends the public key plus a
credential IDto the server for storage; - During login, the server sends a random
challenge. The device signs the challenge with the private key, and the server verifies the signature using the previously stored public key.
A valid signature means a successful login. Not a single password character is involved.
3. Passkey = The “Syncable” Form of WebAuthn
One point is easy to confuse: WebAuthn is the protocol; a passkey is the product form.
Early WebAuthn credentials were device-bound credentials. The private key was permanently tied to one device. If the device was lost, so was the credential, and every new device had to be registered again. Enterprises value this strong binding, but ordinary users cannot tolerate it.
Passkey is a concept jointly promoted by Apple, Google, and Microsoft since 2022. It is essentially a WebAuthn credential that can synchronize across devices:
- Apple devices synchronize through iCloud Keychain;
- Android devices synchronize through Google Password Manager;
- Windows synchronizes through a Microsoft account.
A passkey registered on an iPhone can therefore be used directly on a Mac or iPad. When the user replaces their phone, the passkey follows the account without requiring registration again. That is the experience passwordless authentication should provide.
For developers, the good news is: passkeys still use the WebAuthn API. Your code does not need to distinguish between a passkey and a device-bound WebAuthn credential. Whether synchronization is available is decided by the platform and the user’s system settings. Your server receives the same public-key format either way.
WebAuthn also supports hybrid transport: a website on a computer can use the phone in your pocket to complete authentication. Face ID appears on the phone, and the website on the computer signs you in. This is true cross-device passwordless authentication.
4. Registration Flow: Obtain the Public Key with create()
To register a passkey, the frontend calls navigator.credentials.create(), while the backend supplies a set of parameters. Below is a minimal runnable example. SimpleWebAuthn is recommended for production and is covered later.
Backend: Generate registration parameters
// server.js —— 生成 registration challenge
import { randomBytes } from 'node:crypto';
export function generateRegistrationOptions(user) {
const challenge = randomBytes(32);
// challenge 要存 session,验证时对照,且一次性、60s 过期
return {
challenge: challenge.toString('base64url'),
rp: { name: 'CodeNodeX', id: 'codenodex.dev' }, // rp.id 绑定域名,防钓鱼
user: {
id: Buffer.from(user.id).toString('base64url'),
name: user.email,
displayName: user.name,
},
pubKeyCredParams: [
{ type: 'public-key', alg: -7 }, // ES256
{ type: 'public-key', alg: -257 }, // RS256
],
authenticatorSelection: {
authenticatorAttachment: 'platform', // 只要平台认证器(指纹/Face ID),不要 USB key
userVerification: 'required', // 必须本地生物识别
residentKey: 'preferred', // 可同步 → 即 Passkey
},
timeout: 60000,
attestation: 'none', // 生产环境一般不用 attestation,降低隐私泄漏与实现复杂度
};
}
Frontend: Invoke biometric authentication and obtain the public key
// client.js —— 触发 Face ID / 指纹,拿到公钥
async function registerPasskey(optionsFromServer) {
// 注意:challenge / user.id 必须转成 BufferSource
const publicKey = {
...optionsFromServer,
challenge: base64urlToBuffer(optionsFromServer.challenge),
user: {
...optionsFromServer.user,
id: base64urlToBuffer(optionsFromServer.user.id),
},
};
// 这一行会唤起系统的 Face ID / 指纹 / PIN 弹窗
const credential = await navigator.credentials.create({ publicKey });
// 拿到的 credential 包含公钥 + credential ID,发给后端存起来
const payload = {
id: credential.id,
rawId: bufferToBase64url(credential.rawId),
type: credential.type,
response: {
attestationObject: bufferToBase64url(credential.response.attestationObject),
clientDataJSON: bufferToBase64url(credential.response.clientDataJSON),
},
};
await fetch('/api/passkey/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
}
Backend: Verify and persist the credential
The server must verify several things: whether the challenge is the one it just issued, whether the origin belongs to the application, whether the rpId matches, and whether userVerification was actually completed. After validation succeeds, it extracts the public key from the attestationObject and stores it together with the credential ID. This becomes the user’s credential on that device.
The difficulty in this step is converting repeatedly between base64url and ArrayBuffer while handling numerous fields. Handwritten implementations are easy to get wrong. That is why I strongly recommend using SimpleWebAuthn directly.
5. Authentication Flow: Sign and Verify with get()
The login flow reverses the process: the server issues a challenge, the device signs it with the private key, and the server verifies the signature.
Frontend: Open the passkey selector and sign the challenge
async function loginWithPasskey() {
// 让服务端发一个 challenge
const { challenge } = await fetch('/api/passkey/login-options').then(r => r.json());
const publicKey = {
challenge: base64urlToBuffer(challenge),
timeout: 60000,
userVerification: 'required',
// 不指定 allowCredentials → 让浏览器自动选出本站可用的 Passkey,真正的无密码体验
};
// 这一行在手机上弹 Face ID,在电脑上弹 Touch ID / Windows Hello
const assertion = await navigator.credentials.get({ publicKey });
const payload = {
id: assertion.id,
rawId: bufferToBase64url(assertion.rawId),
type: assertion.type,
response: {
authenticatorData: bufferToBase64url(assertion.response.authenticatorData),
clientDataJSON: bufferToBase64url(assertion.response.clientDataJSON),
signature: bufferToBase64url(assertion.response.signature),
},
};
// 发给后端验签,通过 → 颁发 session
const res = await fetch('/api/passkey/login-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (res.ok) {
console.log('登录成功,全程没输入一个字符');
}
}
Notice the comment above: do not specify allowCredentials. This is central to the passkey experience. The browser automatically finds passkeys for the site based on rp.id, which is your domain, so the user does not even need to enter a username first. They click “Sign in with a passkey,” complete one Face ID scan, and they are signed in.
For backend signature verification, use the previously stored public key to validate the signature. Also verify that the challenge has not been replayed, that the origin is trusted, and that the signCount is not abnormal, which helps detect cloned credentials. Once the signature is valid, issue a session to the user and complete the login. The entire process uses no password, verification code, or SMS message, yet it is more secure than all of them.
6. Current Browser and Platform Support
By 2026, passkeys can be considered fully available:
- Browsers: Chrome, Safari, Edge, and Firefox all support WebAuthn Level 3. Mobile Safari and Chrome on Android both support creating and using passkeys.
- Platforms:
- iOS / iPadOS 16+ and macOS Ventura+: cross-device synchronization through iCloud Keychain;
- Android 9+: synchronization through Google Password Manager, shared with Chrome OS;
- Windows 10/11: synchronization through Windows Hello and a Microsoft account.
- Cross-device authentication (hybrid): A website on a computer can display a QR code and invoke a passkey on the user’s phone. This capability is already implemented in mainstream browsers.
Put simply, more than 95% of users with modern devices can use passkeys. There is no longer any reason to worry that the technology is too early. It is not. Google, TikTok, GitHub, Best Buy, and others have already deployed it at scale.
7. Rollout Path: Adding Passkeys to an Existing System
Do not attempt a “big bang” migration. The right approach is coexistence + gradual rollout:
- Start with coexistence, not replacement. Add a “Sign in with a passkey” button to the existing password login page. In account settings, show signed-in users an “Add a passkey to this account” option. Let willing early adopters try it first; they will become advocates during your private rollout.
- When to register a passkey: The most natural time to suggest adding one is immediately after a successful login. You have already confirmed the user’s identity, so you can safely associate the public key with the account. Do not let an unauthenticated user create a passkey for an account without first proving their identity.
- Redesign the login UI: Make the passkey button the recommended default. “Sign in with Face ID” should be more prominent than “Sign in with a password.” Behavioral economics shows that defaults are far more powerful than persuasion.
- Library selection: For Node.js backends, I strongly recommend SimpleWebAuthn (
@simplewebauthn/server+@simplewebauthn/browser). It handles all the base64url conversion, challenge validation, and signature verification described above, with an API clean enough to make you want to cry. Go hasgo-webauthn, Python has thefido2library, and PHP hasweb-auth/webauthn-framework. - Database changes: Add a
passkeystable containinguser_id,credential_id,public_key,sign_count,created_at, anddevice_label. One user can have multiple records for multiple devices. Public keys are public and do not need encrypted storage. That is the beauty of asymmetric cryptography. - Transition strategy: Monitor usage for a while. After a user signs in successfully with a passkey N times in a row, you can gently ask, “Would you like to disable password login?” But always retain a password or another backup channel as a fallback.
For a consumer product with an existing login system and a mostly mobile user base, passkeys offer extremely high ROI. They significantly reduce account takeovers while improving login conversion because users no longer need to remember passwords. Security improvements with this kind of usability benefit are rare.
8. Recovery Strategy: What If the Device Is Lost?
This is the most frequently asked question when deploying passkeys, and the easiest one to overlook.
Passkey synchronization already mitigates most of the problem. Your passkey does not exist on only one device; it resides in your iCloud or Google account, so replacing a device does not lose it. Extreme cases still need to be considered:
- Keep at least one backup authentication channel. Being permanently locked out of an account is disastrous. The backup can be an email reset link, TOTP, or even a retained password. Do not make a passkey the only factor.
- Encourage users to add passkeys on multiple devices. Make “Add Face ID for this device” easy to use so users can register both a phone and a computer, creating natural redundancy.
- Redesign the account recovery flow. If every device is lost, the server needs a recovery process based on email verification, device fingerprinting, and behavioral risk scoring. Passkeys cannot simply replace every recovery mechanism.
- Consider device-bound WebAuthn for enterprise use. If compliance requirements dictate that private keys must never leave company devices, disable
residentKey: 'preferred'and use device-bound WebAuthn plus company-issued hardware keys such as YubiKey. This sacrifices convenience in exchange for stronger physical control.
Remember: an authentication system is only as secure as its recovery process is resilient. Passkeys raise everyday login security to the ceiling, but the recovery path must improve at the same time. Otherwise, you are installing an iron door beside a paper window.
Summary
- Passwords are a design from the last century. Credential stuffing, phishing, and leaks are structural failures that patches can no longer fix.
- WebAuthn uses public-private keys plus device hardware to lock private keys inside Secure Enclave or TPM. The server stores only public keys, eliminating password leaks and replay attacks at the protocol level.
- Passkey = syncable WebAuthn. Cross-device synchronization through iCloud or Google makes the passwordless experience practical.
- Use SimpleWebAuthn for implementation. A Node backend can support the flow in a few dozen lines, while the database needs only a
passkeystable for public keys. - Always retain a backup authentication channel. Recovery after device loss matters more than login itself.
Key takeaway: the fundamental flaw of a password is that it is a secret that can be copied. The elegance of a passkey is that the secret never lives anywhere except your device.
Related
Related Content
Article
Is Next.js App Router a Good Fit for a Technical Community?
Technical trends, engineering practices, and independent development perspectives centered on Next.js and React.
Article
SEO Design for a Content Community with Next.js
Technical trends, engineering practices, and an indie developer's perspective on Next.js and TypeScript.
Article
The Engineering Value of TypeScript in AI Application Frontends
Technical trends, engineering practices, and an independent developer perspective on TypeScript and React.
Article
What Value Do Server Components Offer Content Sites?
Technical trends and engineering practices around Next.js and React, from an independent developer's perspective.