WebAuthn in 2026: the production explainer for engineers who missed the passkey shift
Passkeys are now standard on every major platform. Here is what a backend engineer actually needs to understand before touching the implementation.
Auth is a solved problem, right? Add Google sign-in, wire the redirect dance, store a user ID. That was the working consensus for a few years. Then phishing got good, password spraying got cheap, and engineers started reading breach reports and finding their own hashes in the dataset. The OAuth shortcut did not fix the underlying problem. WebAuthn did.
The FIDO Alliance standardised WebAuthn as a W3C spec in 2019. For its first two years it was an enterprise concern: hardware security keys, corporate IT mandates, FIDO2 as a checkbox next to SOC 2. Then Apple shipped passkeys in iOS 16 and macOS Ventura in September 2022. Google followed on Android and Chrome in 2023. By 2026, 1Password, Bitwarden, and Dashlane all store passkeys natively, and every major browser supports the full API. The user-side infrastructure is already on your customers' devices. The question shifted from 'is WebAuthn mature enough?' to 'why haven't you shipped it yet?'
Most WebAuthn explainers were written in 2019 or 2020, when browser support was patchy and the passkey sync layer did not exist. This one covers the 2026 picture: what the spec actually does, the three concepts that consistently trip engineering teams, and the production decisions that no guide written before 2023 could have made for you.
What WebAuthn actually is (and what it isn't)
WebAuthn is a browser API spec. Specifically: navigator.credentials.create() for registration and navigator.credentials.get() for authentication. It sits between your web page and the authenticator, which is whatever holds the key pair: a platform component (the Secure Enclave on an iPhone, Windows Hello on a laptop), an external hardware key, or a cross-device companion phone.
During registration, the authenticator:
- Generates a fresh public/private key pair scoped to your origin (your domain, not your app or your server).
- Stores the private key in secure hardware where it cannot be exported or read directly.
- Returns the public key and a credential ID to your server to store alongside the user record.
During authentication:
- Your server sends a challenge: a random byte string, single-use, server-generated.
- The authenticator signs the challenge with the private key.
- Your server verifies the signature against the stored public key.
The private key never leaves the device. The challenge prevents replay attacks. The origin binding prevents phishing: a credential created for example.com cannot be used on examp1e.com — the domain check is enforced by the browser before the authenticator is asked to sign. This is the property that makes WebAuthn categorically different from passwords.
What WebAuthn does not cover: session management, access tokens, account recovery, multi-factor orchestration, or anything that happens after authentication succeeds. Those are still your responsibility.
| Type | What it is | Syncs? | When to use |
|---|---|---|---|
| Platform authenticator | Built into the device (Secure Enclave, Windows Hello, Android biometric subsystem) | Yes — via iCloud Keychain or Google Password Manager | Default for most users; zero extra hardware required |
| Roaming authenticator | External hardware key (YubiKey, Google Titan Key) | No — credential is bound to the physical key | High-assurance or regulated environments requiring device-bound credentials |
| Cross-device / hybrid | Phone authenticates on behalf of a laptop via QR code and Bluetooth proximity | Credential stays on the phone; no sync to laptop | Bootstrapping passkeys on shared or managed devices; cross-platform flows |
The three concepts engineers consistently trip over
Credential discovery. A non-discoverable credential requires the user to enter a username first, so your server can look up the relevant credential ID and include it in the authentication options it sends back. A discoverable credential (also called a resident key) is stored on the authenticator indexed by your relying-party ID, so the authenticator can enumerate credentials without a username prompt. Passkeys are discoverable by definition. This is what lets the browser surface a 'sign in as [name]' picker before the user has typed anything. The choice between discoverable and non-discoverable credentials affects your backend credential storage, your frontend login flow, and whether you can offer conditional UI. Decide before you build.
Attestation. During registration, an authenticator can supply an attestation statement: a signed assertion about its own make and model. This is useful if your security policy requires users to authenticate with a FIDO2 hardware key from an approved list of vendors. For a consumer-facing app or standard B2B SaaS, skip it. Verifying attestation certificate chains is complex, the root certificates change over time, the benefit is marginal for most threat models, and adding the check introduces friction during registration. Set attestation to 'none' in your registration options and revisit only if a specific compliance requirement demands otherwise.
User verification vs. user presence. Setting userVerification to 'required' means the authenticator must confirm the user's identity via biometric (fingerprint or face scan) or a PIN. Setting it to 'preferred' means you'd like verification but will accept a tap confirming mere presence. Presence proves the user has the device. Verification proves the user is the device owner. For most web applications, 'preferred' is the right default. For high-value operations — admin escalation, financial transactions, access to sensitive records — require verification per-request rather than relying on what happened at login time.
Passkeys: WebAuthn with sync layered on top
A passkey is a WebAuthn credential that syncs across a user's devices. That is the complete definition.
The sync happens outside the WebAuthn spec itself. iCloud Keychain syncs passkeys across all of a user's Apple devices logged into the same Apple ID. Google Password Manager does the same across Android and Chrome. Third-party password managers that support passkeys handle their own encrypted sync. The private key is wrapped by the user's account credential and synced the same way any other keychain entry would be.
Three implications for your implementation:
- Multi-device login works without extra ceremony, provided the user has the same platform account on multiple devices. A user who registers a passkey on their iPhone can authenticate on their Mac without scanning anything or registering a second credential.
- Device-bound assumptions no longer hold. A passkey is closer to a password-manager entry than to a hardware token. If your mental model is 'one credential equals one device equals one session source', synced passkeys break it.
- Revocation is not instantaneous. Invalidating a credential ID on your server blocks new authentications using that credential, but if the passkey has synced to multiple devices and the user authenticates from another device before revocation propagates through your system, there is a window. For most applications this is acceptable. For high-assurance flows, device-bound hardware keys remain the right choice.
The production decisions the spec doesn't make for you
Options, roughly ordered by how well they hold up when an attacker has compromised the user's email:
- One-time codes sent to a verified phone number or email address. Still the most common fallback. You are back to depending on email or SMS security, but it is what users understand. Pair with rate limiting and anomaly detection.
- Backup passkeys. Encourage users to register a second passkey on a different device or on a hardware key. The challenge is getting users to do this before they need it. Some teams surface it as part of onboarding; others prompt after the first successful passkey login.
- Admin-initiated recovery for B2B SaaS with an operations team. A verified human checks identity and issues a recovery code. Audit-trail the action.
- Magic links should not be your only recovery path. If an attacker has the user's email, magic links grant the same access as no auth. Use them as one step in a multi-factor recovery, not as a standalone.
A common rollout sequence: passwords remain active as a fallback; users are offered passkey enrolment at login or in account settings; after enrolment the password continues to exist for recovery; several months later passwords require active opt-in to use, with a step-up verification prompt. This gives users time to set up a backup passkey before the password path degrades.
Conditional UI and the login form that stops being a form
Conditional UI is what makes passkeys feel native. When the user focuses the username field, the browser checks whether there are discoverable credentials for your origin and surfaces a passkey picker in the autofill menu, before the user has typed anything. The user taps their name, confirms with biometric, and they are in.
The browser-side change is small. You start a WebAuthn authentication with mediation set to 'conditional', which tells the browser to attach the result to the autofill flow rather than triggering an immediate modal:
const abortController = new AbortController();
// mediation: 'conditional' attaches to the autofill picker rather than showing a modal.
// This call will not resolve until the user selects a passkey from the menu.
const credential = await navigator.credentials.get({
publicKey: {
challenge: fromBase64Url(serverChallenge),
rpId: 'example.com',
userVerification: 'preferred',
},
signal: abortController.signal,
mediation: 'conditional',
});
if (credential) {
await submitPasskeyAuthentication(credential as PublicKeyCredential);
}
// If the user types a username instead, abort the conditional request
// and fall through to your regular password or non-conditional flow.
One constraint that catches most teams: conditional UI requires the username input element to carry autocomplete="username webauthn". Without that attribute, the browser will not trigger the passkey picker. The bug when it is missing is 'conditional UI works on my test page but not on the real login page' — because the test page happened to have the attribute and the real one does not. Two words in your HTML template.
Where to actually start
Do not implement the CBOR encoding, attestation parsing, or signature verification yourself. The binary structures in WebAuthn are specified tightly enough that small errors produce code that appears correct and fails on authenticators you have not tested against. Use a maintained library:
- Node.js and TypeScript: @simplewebauthn/server paired with @simplewebauthn/browser.
- Java and Spring: webauthn4j-spring-security.
- Python: py_webauthn (maintained by Duo Labs).
- Go: go-webauthn/webauthn.
What to store per credential:
CREATE TABLE webauthn_credentials (
credential_id TEXT PRIMARY KEY, -- base64url-encoded
user_id UUID NOT NULL REFERENCES users(id),
public_key BYTEA NOT NULL, -- COSE-encoded public key
sign_count BIGINT NOT NULL DEFAULT 0,
device_type TEXT, -- 'platform' | 'roaming'
transports TEXT[], -- e.g. {'internal', 'hybrid'}
backed_up BOOLEAN, -- true for synced passkeys
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_used_at TIMESTAMPTZ
);The sign_count field: hardware tokens increment it on every use, and your server should verify it only moves forward, because a counter going backwards signals a cloned credential. For synced passkeys, the picture is different — the WebAuthn spec explicitly permits a sign count of 0 for multi-device credentials, because the counter cannot be kept consistent across synced copies. In practice: enforce the sign count for roaming authenticators; treat 0 as valid for platform/synced credentials. The backed_up column from the authenticator data flags which type you have.
Start with registration only, on a test account you control. Verify the public key round-trips correctly through your storage layer, that the library parses the authenticator data without error, and that a second authentication with the same credential succeeds. Ship registration and authentication together only after you have verified the full ceremony end to end in your own environment. The implementation is smaller than it looks. The thinking about account recovery and the password transition is where the actual time goes.
Frequently asked questions
Related reading
LLM database access: the RBAC gap most teams don't see
Giving an LLM access to your database is easy. The problem is that your application-layer RBAC is invisible when the model generates SQL. Here's where it goes wrong and how to fix it at the layer that enforces.
The minimum viable security posture for a 10-person SaaS
Seven controls that prevent 90% of real breaches at a 10-person SaaS, ranked in the order that actually matters—not the order that looks good on a compliance questionnaire.
Non-human identity: the security problem every 10-person SaaS team ignores
Every SaaS security guide converges on MFA and password managers. The real attack surface at a small SaaS team is 150+ non-human identities with no rotation, no scoping, and no audit.