Skip to main content
← All Articles Β· Β· 9 min read

Passkeys and WebAuthn Implementation Guide 2026: Browser Support & UX Patterns

Implement passkeys and WebAuthn in 2026: browser support matrix, registration and authentication flows, server-side verification, UX best practices, and library recommendations.

passkeyswebauthnauthenticationsecuritypasswordlessfido2

Passwords are the single largest source of account breaches. Passkeys solve this at the protocol level: they’re phishing-resistant, can’t be reused across sites, and authenticate via biometrics or device PIN β€” nothing the user needs to remember or protect. In 2026, passkey support is built into every major browser and OS. This guide covers the implementation from first registration to production-ready authentication flow.

What Are Passkeys?

A passkey is a FIDO2 credential β€” a cryptographic key pair generated on your device. The private key never leaves the device (stored in the secure enclave or TPM). The public key is stored on your server. Authentication works by challenging the device to sign a random nonce with the private key. No password is transmitted. No password database to breach.

Under the hood, passkeys use the WebAuthn API (navigator.credentials), the W3C standard for authenticating with cryptographic credentials from the browser.

Why passkeys beat passwords:

  • Phishing-resistant: bound to your domain, can’t be used on fake sites
  • No shared secrets: the server never sees a password to store or leak
  • No reuse: each site gets a unique key pair
  • Faster: authenticate with FaceID/TouchID/Windows Hello β€” no typing

Browser and Platform Support Matrix (2026)

PlatformStatusNotes
Chrome 108+FullAndroid, Windows, macOS, Linux
Safari 16+FullmacOS, iOS β€” iCloud Keychain sync
Firefox 122+FullDesktop; Android still limited
Edge 108+FullSame as Chrome (Chromium)
iOS 16+FullSafari + passkey sync via iCloud
Android 9+FullGoogle Password Manager sync
Windows 11FullWindows Hello (biometric + PIN)
macOS Ventura+FullTouch ID + iCloud Keychain

Conditional UI (passkey suggestions in autofill) is supported in Chrome, Safari, and Edge. Firefox is catching up.

Registration Flow

Registration creates a new credential and stores the public key on your server.

// 1. Request challenge from your server
const response = await fetch('/api/auth/register/begin', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ username: '[email protected]' })
});
const options = await response.json();

// 2. Create the credential on the device
const credential = await navigator.credentials.create({
  publicKey: {
    challenge: base64urlDecode(options.challenge),
    rp: {
      name: "My App",
      id: "myapp.com"  // Must match your domain
    },
    user: {
      id: base64urlDecode(options.userId),
      name: "[email protected]",
      displayName: "Alice"
    },
    pubKeyCredParams: [
      { alg: -7, type: "public-key" },   // ES256
      { alg: -257, type: "public-key" }  // RS256
    ],
    authenticatorSelection: {
      authenticatorAttachment: "platform", // Use device authenticator
      residentKey: "required",            // Enable discoverable credentials
      userVerification: "required"        // Require biometric/PIN
    },
    timeout: 60000,
    attestation: "none"  // "direct" if you need attestation
  }
});

// 3. Send credential to server for verification and storage
const verifyResponse = await fetch('/api/auth/register/complete', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    id: credential.id,
    rawId: base64urlEncode(credential.rawId),
    response: {
      clientDataJSON: base64urlEncode(credential.response.clientDataJSON),
      attestationObject: base64urlEncode(credential.response.attestationObject)
    },
    type: credential.type
  })
});

Authentication Flow

Authentication challenges the stored credential to sign a nonce.

// 1. Request challenge and allowed credentials from server
const response = await fetch('/api/auth/login/begin', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ username: '[email protected]' })
});
const options = await response.json();

// 2. Authenticate with the device passkey
const assertion = await navigator.credentials.get({
  publicKey: {
    challenge: base64urlDecode(options.challenge),
    rpId: "myapp.com",
    allowCredentials: options.allowCredentials.map(cred => ({
      id: base64urlDecode(cred.id),
      type: "public-key"
    })),
    userVerification: "required",
    timeout: 60000
  }
});

// 3. Send assertion to server for verification
const verifyResponse = await fetch('/api/auth/login/complete', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    id: assertion.id,
    rawId: base64urlEncode(assertion.rawId),
    response: {
      clientDataJSON: base64urlEncode(assertion.response.clientDataJSON),
      authenticatorData: base64urlEncode(assertion.response.authenticatorData),
      signature: base64urlEncode(assertion.response.signature),
      userHandle: assertion.response.userHandle
        ? base64urlEncode(assertion.response.userHandle) : null
    },
    type: assertion.type
  })
});

Server-Side Verification

Server verification must:

  1. Validate the challenge matches what was issued
  2. Verify the signature using the stored public key
  3. Check the rpIdHash matches your domain
  4. Check the userPresent and userVerified flags

Use a library rather than implementing this yourself:

// Node.js with SimpleWebAuthn
import { verifyAuthenticationResponse } from '@simplewebauthn/server';

const verification = await verifyAuthenticationResponse({
  response: assertionFromClient,
  expectedChallenge: storedChallenge,
  expectedOrigin: 'https://myapp.com',
  expectedRPID: 'myapp.com',
  authenticator: {
    credentialID: storedCredential.credentialID,
    credentialPublicKey: storedCredential.publicKey,
    counter: storedCredential.counter,
    transports: storedCredential.transports
  },
  requireUserVerification: true
});

if (verification.verified) {
  // Update counter to prevent replay attacks
  await db.updateCredentialCounter(
    storedCredential.id,
    verification.authenticationInfo.newCounter
  );
  // Issue session token
}
LibraryLanguageStarsNotes
SimpleWebAuthnTypeScript/Node2.8kMost ergonomic, well-maintained
go-webauthnGo1.9kProduction-grade, used by GitHub
py_webauthnPython1.2kDjango/Flask friendly
webauthn4jJava/Kotlin890Spring Boot integration
HankoTypeScript7.2kFull auth service + passkeys UI components

Hanko is worth highlighting separately β€” it’s an open-source auth platform that handles passkeys, email magic links, and OAuth, with pre-built React/Vue components that cover the full UX. Use it when you want passkeys without building the flow from scratch.

UX Patterns

Always provide a password fallback. Even in 2026, some devices and browsers have edge cases. A passkey-first flow with a β€œsign in another way” option is the right pattern:

[Sign in with Passkey]         ← primary CTA
[Continue with Google]         ← OAuth fallback
[Use password instead] (link)  ← password fallback

Conditional UI (autofill suggestions). Add autocomplete="username webauthn" to your username input. Browsers that support conditional UI will show passkey suggestions in the autofill dropdown automatically:

<input
  type="email"
  name="username"
  autocomplete="username webauthn"
  placeholder="Enter your email"
/>

Then trigger conditional mediation on page load:

if (PublicKeyCredential.isConditionalMediationAvailable) {
  const available = await PublicKeyCredential.isConditionalMediationAvailable();
  if (available) {
    // Trigger without UI blocking β€” autofill will show passkey suggestions
    navigator.credentials.get({
      mediation: 'conditional',
      publicKey: { challenge: ..., rpId: 'myapp.com' }
    }).then(handleAuthentication);
  }
}

Registration timing. Prompt users to add a passkey after a successful password login, not during signup. Users are in the app, already trust it, and are more likely to complete the flow.

Security Checklist

  • Generate challenges server-side (minimum 16 random bytes)
  • Validate origin matches your domain exactly
  • Validate rpIdHash in every assertion
  • Store and check the credential counter (detects cloned authenticators)
  • Set challenge TTL (60 seconds is standard)
  • Use HTTPS everywhere β€” WebAuthn requires a secure context
  • Implement account recovery flow (email OTP or backup codes) for lost devices

Passkeys won’t eliminate every auth problem, but they eliminate password breaches, credential stuffing, and phishing attacks at the protocol level. For any application handling sensitive user data, the implementation investment pays for itself quickly.

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.