WritingHow to Implement Passkeys and Passwordless Auth with WebAuthn — Clixo
6 min readpasskeys, webauthn, passwordless, authentication, security

How to Implement Passkeys and Passwordless Auth with WebAuthn

A practical developer guide to implementing passkeys using the WebAuthn API — covering registration, authentication, fallbacks, and account recovery.

Passwords are the leading root cause of credential-based attacks — phishing, stuffing, and breach reuse all exploit the same fundamental weakness. Passkeys, built on the WebAuthn standard, eliminate that surface area entirely. They are phishing-resistant by cryptographic design, and major platform support (Apple, Google, Microsoft) has crossed the threshold where adoption is finally practical for production applications.

This guide covers the real implementation path: what the API looks like, what you need on the server, and what to handle for account recovery.

How Passkeys Actually Work

When a user creates a passkey, the device generates a public-private key pair. The public key is stored on your server. The private key never leaves the device — it lives in the secure enclave, protected by biometrics or a PIN.

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-webauthn for Go, py_webauthn for Python, @simplewebauthn/server for Node.js, webauthn for 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:

  1. Generate a random challenge (at least 16 bytes, cryptographically random).
  2. Return the challenge along with a PublicKeyCredentialCreationOptions object including your RP ID (your domain), a user handle, and the supported algorithms (prefer ES256 and RS256).
  3. 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:

  1. Decode the clientDataJSON and attestationObject from the response.
  2. Verify the challenge matches what you issued and that the origin matches your RP ID.
  3. Verify the rpIdHash in the authenticator data.
  4. Check the flags: user presence must be set; user verification should be set if you required it.
  5. Store the credential ID, public key (in COSE format), sign counter, and user ID.

Your WebAuthn library handles steps 1-5 with a single function call. Let it.

The Authentication Flow

Server side — generate an authentication challenge:

  1. Generate a new random challenge.
  2. Return it in an PublicKeyCredentialRequestOptions object, optionally including the allowCredentials list for the user.
  3. Store the challenge with a short TTL.

Browser side:

const assertion = await navigator.credentials.get({
  publicKey: requestOptions
});

Server side — verify:

  1. Find the credential by its ID.
  2. Verify the signature over the authenticatorData + clientDataJSON hash using the stored public key.
  3. Verify the sign counter is greater than the stored value. A counter that regresses is a signal of cloned credentials.
  4. Update the stored sign counter.
  5. Issue a session.

Fallback and Account Recovery

Passkeys are device-bound (unless synced via iCloud Keychain or Google Password Manager). A user who loses their device needs a way back in. Plan for this before launch, not after a support ticket.

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.

  1. Pilot with staff accounts. Find and fix implementation issues before they affect customers.
  2. Offer as an option. Let users add a passkey alongside their existing password. Most early adopters will self-select.
  3. Make it the default for new signups. New accounts enroll with a passkey, password becomes optional.
  4. Enforce for privileged roles. Admin accounts and service accounts should require passkeys or hardware keys before general users.

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

NIST SP 800-63-4, finalized in 2025, formally recognizes passkeys as AAL2-compliant authenticators. For applications subject to federal standards or those seeking FedRAMP equivalents, passkeys satisfy phishing-resistant authentication requirements that SMS OTP does not.

Passkeys require deliberate implementation work upfront, but they are the clearest path to eliminating credential phishing at the application layer. The cryptographic guarantees are strong; the implementation complexity is manageable with the right libraries.

If you are building passwordless authentication into a new product or retrofitting it onto an existing one, Clixo can architect and ship the full auth layer.