Passkeys WebAuthn Authentication: Complete Guide to Replacing Passwords in 2026

Passkeys and WebAuthn: The End of Passwords

Passwords are the weakest link in web security. Users reuse them across sites, phishing attacks steal them daily, and even strong passwords end up in data breaches. Passkeys — built on the WebAuthn/FIDO2 standard — replace passwords with cryptographic key pairs stored on user devices. They’re phishing-resistant by design, faster than typing passwords, and supported by Apple, Google, and Microsoft across all major platforms. Therefore, this guide covers how passkeys work, how to implement them, and how to migrate your existing authentication system.

How Passkeys Work: The Cryptography Behind the UX

A passkey is a public-private key pair. The private key never leaves the user’s device — it’s stored in the platform authenticator (Touch ID, Face ID, Windows Hello) or a hardware security key. The server only stores the public key. During authentication, the server sends a challenge, the device signs it with the private key, and the server verifies the signature with the stored public key.

This architecture makes passkeys phishing-resistant because the private key is bound to the origin (domain). If a user visits a phishing site at evil-bank.com, the authenticator won’t find a passkey for that domain — the attack simply doesn’t work. Moreover, there’s no shared secret (password) that can be stolen from the server. Even if your database is compromised, attackers get only public keys — which are useless without the corresponding private keys locked in users’ devices.

Cross-device sync is handled by platform providers: Apple syncs passkeys via iCloud Keychain, Google via Google Password Manager, and Microsoft via Microsoft Account. A passkey created on your iPhone automatically appears on your Mac, iPad, and any browser signed into your Apple account. Additionally, cross-device authentication lets you scan a QR code on your phone to sign in on a computer that doesn’t have your passkey — the phone acts as the authenticator via Bluetooth proximity.

Passkeys WebAuthn passwordless authentication security
Passkeys replace passwords with cryptographic key pairs — phishing-resistant by design

Implementing WebAuthn Registration

WebAuthn registration (creating a passkey) involves three steps: the server generates a challenge, the browser calls navigator.credentials.create() which triggers the platform authenticator, and the server verifies and stores the credential.

// SERVER: Generate registration options
// Using @simplewebauthn/server (Node.js)
import {
  generateRegistrationOptions,
  verifyRegistrationResponse
} from '@simplewebauthn/server';

const rpName = 'My Application';
const rpID = 'myapp.com';
const origin = 'https://myapp.com';

// Step 1: Generate challenge
app.post('/api/auth/register/options', async (req, res) => {
  const user = await getUser(req.session.userId);

  const options = await generateRegistrationOptions({
    rpName,
    rpID,
    userID: user.id,
    userName: user.email,
    userDisplayName: user.name,
    attestationType: 'none',  // Don't need device attestation
    authenticatorSelection: {
      residentKey: 'preferred',     // Discoverable credential
      userVerification: 'preferred', // Biometric if available
      authenticatorAttachment: 'platform'  // Built-in authenticator
    },
    excludeCredentials: user.passkeys.map(pk => ({
      id: pk.credentialID,
      type: 'public-key'
    }))
  });

  // Store challenge for verification
  req.session.currentChallenge = options.challenge;
  res.json(options);
});

// Step 3: Verify and store credential
app.post('/api/auth/register/verify', async (req, res) => {
  const verification = await verifyRegistrationResponse({
    response: req.body,
    expectedChallenge: req.session.currentChallenge,
    expectedOrigin: origin,
    expectedRPID: rpID
  });

  if (verification.verified) {
    // Store the public key credential
    await savePasskey(req.session.userId, {
      credentialID: verification.registrationInfo.credentialID,
      publicKey: verification.registrationInfo.credentialPublicKey,
      counter: verification.registrationInfo.counter,
      deviceType: verification.registrationInfo.credentialDeviceType,
      backedUp: verification.registrationInfo.credentialBackedUp,
      createdAt: new Date()
    });
    res.json({ success: true });
  }
});
// CLIENT: Browser-side registration
import { startRegistration } from '@simplewebauthn/browser';

async function registerPasskey() {
  // Get options from server
  const optionsRes = await fetch('/api/auth/register/options',
    { method: 'POST' });
  const options = await optionsRes.json();

  // Trigger platform authenticator (Touch ID, Face ID, etc.)
  const credential = await startRegistration(options);

  // Send credential to server for verification
  const verifyRes = await fetch('/api/auth/register/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credential)
  });

  const result = await verifyRes.json();
  if (result.success) {
    console.log('Passkey registered successfully!');
  }
}

Implementing WebAuthn Authentication

Authentication follows a similar flow: the server generates a challenge, the browser triggers the authenticator, and the server verifies the signed challenge against the stored public key.

// SERVER: Authentication flow
import {
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';

// Step 1: Generate authentication challenge
app.post('/api/auth/login/options', async (req, res) => {
  const options = await generateAuthenticationOptions({
    rpID,
    // Empty allowCredentials = discoverable credential
    // The browser will show all passkeys for this domain
    allowCredentials: [],
    userVerification: 'preferred'
  });

  req.session.currentChallenge = options.challenge;
  res.json(options);
});

// Step 3: Verify authentication
app.post('/api/auth/login/verify', async (req, res) => {
  const passkey = await findPasskeyByCredentialID(
    req.body.id
  );

  const verification = await verifyAuthenticationResponse({
    response: req.body,
    expectedChallenge: req.session.currentChallenge,
    expectedOrigin: origin,
    expectedRPID: rpID,
    authenticator: {
      credentialID: passkey.credentialID,
      credentialPublicKey: passkey.publicKey,
      counter: passkey.counter
    }
  });

  if (verification.verified) {
    // Update counter (replay protection)
    await updatePasskeyCounter(
      passkey.id,
      verification.authenticationInfo.newCounter
    );

    // Create session
    req.session.userId = passkey.userId;
    res.json({ success: true });
  }
});

The user experience is dramatically better than passwords. Instead of typing an email and password, the user clicks “Sign in” and uses Face ID, Touch ID, or Windows Hello. The entire flow takes 2-3 seconds. Furthermore, there’s no “forgot password” flow, no password reset emails, and no credential stuffing attacks to worry about.

Biometric authentication and passwordless login
Users authenticate with Face ID or Touch ID — the entire flow takes 2-3 seconds

Migration Strategy: Passwords to Passkeys

You can’t switch to passkey-only authentication overnight — not all users have compatible devices, and some environments (shared computers, older devices) don’t support passkeys. The practical migration path is progressive.

Phase 1: Offer passkeys alongside passwords. After a user logs in with their password, prompt them to create a passkey. Make it a simple, optional flow: “Sign in faster with Face ID?” Users who create passkeys get a better experience immediately.

Phase 2: Make passkeys the default. New account registration defaults to passkey creation. The login page shows a “Sign in with passkey” button prominently, with “Sign in with password” as a secondary option. Track adoption metrics — when 80%+ of logins use passkeys, move to phase 3.

Phase 3: Password-optional accounts. Allow users to remove their password entirely, going passkey-only. This eliminates the password as an attack vector for those users. However, always maintain a recovery mechanism (backup codes, email-based recovery) for users who lose all their devices.

Security Considerations and Edge Cases

Passkeys solve phishing and credential stuffing but introduce new considerations. Device loss is the primary concern: if a user loses all devices and hasn’t synced passkeys to the cloud, they lose access. Implement account recovery through verified email, SMS backup codes, or identity verification. Additionally, passkey sync through iCloud Keychain or Google Password Manager means that compromising someone’s Apple/Google account could compromise their passkeys — though this is still far more secure than password reuse across dozens of sites.

For enterprise deployments, consider attestation — verifying that passkeys were created on approved devices (company-issued hardware keys). The WebAuthn spec supports attestation, though most consumer applications should set attestation to “none” for maximum compatibility.

Security infrastructure and authentication architecture
Progressive migration from passwords to passkeys — offer, default, then make passwords optional

Related Reading:

Resources:

In conclusion, passkeys and WebAuthn represent the most significant authentication improvement in decades. They eliminate phishing, credential stuffing, and password reuse — the three biggest authentication threats. Start by offering passkeys alongside passwords today, track adoption, and progressively make passwords optional. The user experience is better, the security is stronger, and the ecosystem support from Apple, Google, and Microsoft guarantees long-term viability.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top