How to Implement Passkeys and Passwordless Auth with WebAuthn
Implement passkey registration and authentication with WebAuthn, including server validation, synced credentials, recovery, and rollout.
Implementing passkeys involves browser APIs, server-side verification, credential storage, and account recovery. Plan registration and authentication together with the fallback methods users will need if they lose access to a credential.
How Passkeys Work
A passkey uses a public-private key pair. Your server stores the public key. Credentials may be device-bound or synced between devices by a credential provider; do not assume every passkey’s private key remains on one device. NIST guidance on syncable authenticators.
To authenticate, the server sends a challenge. The device signs it with the private key. The server verifies the signature against the stored public key. There is no password to steal, no shared secret to phish.
Because passkeys are bound to the origin (your domain), a phishing site cannot use a passkey registered for your domain. The browser enforces this binding at the API level.
Prerequisites Before You Write Code
- HTTPS is mandatory. WebAuthn only works on secure origins. Localhost is exempted for development, but every other environment needs a valid TLS certificate — no exceptions.
- A WebAuthn server library. Do not implement the cryptographic verification yourself. Use a maintained library for your stack:
go-webauthnfor Go,py_webauthnfor Python,@simplewebauthn/serverfor Node.js,webauthnfor Ruby. - A credential storage table. You need to persist the public key, credential ID, sign counter, and associated user ID.
The Registration Flow
Registration happens in two phases: browser and server.
Server side — generate a registration challenge:
- Generate a random challenge (at least 16 bytes, cryptographically random).
- Return the challenge along with a
PublicKeyCredentialCreationOptionsobject including your RP ID (your domain), a user handle, and the supported algorithms (preferES256andRS256). - Store the challenge server-side with a short TTL (60–90 seconds). Tie it to the user's session.
Browser side — call the API:
const credential = await navigator.credentials.create({
publicKey: creationOptions
});The browser prompts the user to authenticate with biometrics or a PIN, then returns a PublicKeyCredential object.
Server side — verify and store:
- Decode the
clientDataJSONandattestationObjectfrom the response. - Verify the challenge matches what you issued and the origin is one your application explicitly expects. The origin and RP ID are different values.
- Verify the
rpIdHashin the authenticator data. - Check the flags: user presence must be set; user verification should be set if you required it.
- Store the credential ID, public key (in COSE format), sign counter, and user ID.
Use your WebAuthn library’s verification routine and follow its requirements for storing the result.
The Authentication Flow
Server side — generate an authentication challenge:
- Generate a new random challenge.
- Return it in an
PublicKeyCredentialRequestOptionsobject, optionally including theallowCredentialslist for the user. - Store the challenge with a short TTL.
Browser side:
const assertion = await navigator.credentials.get({
publicKey: requestOptions
});Server side — verify:
- Find the credential by its ID.
- Verify the signature over the
authenticatorData+clientDataJSONhash using the stored public key. - Apply the library’s signature-counter checks. Authenticators may report zero when they do not support a counter; a non-increasing supported counter needs risk evaluation, not an assumption that every credential must increment.
- Update the stored sign counter.
- Verify the expected challenge, origin, RP ID hash, and required presence/verification flags before issuing a session. Follow the complete WebAuthn verification procedure.
Fallback and Account Recovery
A user may lose access to a device-bound credential or to the account that syncs their credentials. Provide an appropriate recovery path. Include recovery in the launch scope.
Practical recovery options:
- Magic link to verified email. Simple, widely understood, works for most consumer apps.
- Recovery codes. Generate 8-10 single-use codes at passkey enrollment time. Present them once and ask the user to save them.
- Multiple passkeys. Encourage users to register a passkey on more than one device. Make it a clear UI step during onboarding.
- SMS as a last resort. Works for consumer apps, not recommended for high-security contexts where SIM swapping is a threat.
For B2B applications, keep verified fallback methods available during any transition period. Enforcing passkeys before users have registered a second device creates unnecessary lockout risk.
Handling the Progressive Rollout
Do not enable passkeys for all users on day one. A phased rollout is lower-risk.
- Pilot with staff accounts. Find and fix implementation issues before they affect customers.
- Offer as an option. Let users add a passkey alongside their existing password.
- Make it the default for new signups. New accounts enroll with a passkey, password becomes optional.
- Enforce for privileged roles. Prioritize interactive administrator accounts according to your access policy. Workload and service identities need authentication designed for non-interactive use.
Testing Without Real Biometrics
Chrome DevTools has a WebAuthn emulator under the Security panel that lets you create virtual authenticators and simulate passkey registration and authentication without real hardware. Use it during development to avoid friction in your automated test suite.
Compliance Note
A passkey deployment does not automatically satisfy an assurance level. NIST permits syncable authenticators at AAL2 when the applicable requirements are met, including user verification and protections for synced keys. Synced keys do not meet AAL3’s non-exportability requirement. Review the complete deployment against NIST SP 800-63B-4.