Pavan Rangani

HomeBlogPasskeys WebAuthn Authentication: Complete Guide to Replacing Passwords in 2026

Passkeys WebAuthn Authentication: Complete Guide to Replacing Passwords in 2026

By Pavan Rangani · February 23, 2026 · Security

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 they work, how to implement them, and how to migrate your existing authentication system without breaking users who are mid-flight.

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 is 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 credential for that domain — the attack simply doesn’t work. Moreover, there’s no shared secret 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 via iCloud Keychain, Google via Google Password Manager, and Microsoft via Microsoft Account. A credential 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 credential — the phone acts as the authenticator via Bluetooth proximity, which prevents remote attackers from completing the flow.

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

Understanding the Ceremony: Attestation, Assertion, and the Relying Party

WebAuthn defines two “ceremonies.” Registration is an attestation ceremony where a new credential is created and bound to your relying party ID (RP ID, essentially your domain). Authentication is an assertion ceremony where an existing credential signs a fresh challenge. Both are mediated by the browser, which enforces origin checks the server cannot bypass — this is precisely why the model is so resistant to credential theft.

The RP ID deserves careful attention because it is a frequent source of bugs. The RP ID must be a registrable suffix of the page’s origin. For a site served at app.example.com, a valid RP ID is either app.example.com or example.com, but never example.org or a bare localhost:3000 with a port. Consequently, if you set the RP ID to the parent domain, credentials work across all subdomains; if you set it to the subdomain, they’re scoped tightly. Choose deliberately, because changing it later invalidates every credential your users already registered.

Two more concepts matter in practice. User verification (“preferred”, “required”, or “discouraged”) controls whether a biometric or PIN check happens — required gives you two factors (possession of the device plus the biometric) in a single tap. Resident keys, also called discoverable credentials, store the user handle on the authenticator itself, which is what enables a true usernameless login where the user clicks one button and the browser lists their accounts. Without discoverable credentials, you must first identify the user and pass an allowCredentials list.

Implementing WebAuthn Registration

WebAuthn registration 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. The challenge must be single-use and tied to the session to prevent replay.

// 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 });
  }
});

The excludeCredentials list is easy to overlook but important: it prevents a user from accidentally registering two credentials for the same authenticator, which would clutter their account picker. Notice also that you persist backedUp and credentialDeviceType. Those flags tell you whether the credential is synced to a cloud (multi-device) or bound to a single device — useful later when you decide whether a user is safe to make password-optional.

// CLIENT: Browser-side registration
import { startRegistration } from '@simplewebauthn/browser';

async function registerPasskey() {
  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);

  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. With an empty allowCredentials, the browser surfaces every discoverable credential for the domain, enabling a usernameless login.

// 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,
    allowCredentials: [],  // discoverable credential, usernameless
    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
    );
    req.session.userId = passkey.userId;
    res.json({ success: true });
  }
});

The signature counter deserves a note. Single-device hardware keys increment a counter on every use, and the spec lets you reject an assertion whose counter is lower than the stored value, which flags a cloned authenticator. However, synced platform credentials from Apple and Google generally report a counter of zero and never increment, so you must not treat a non-increasing counter as a failure for those. A common pattern is to enforce the check only when both the stored and incoming counters are non-zero.

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, and the entire flow takes two to three seconds. Furthermore, there’s no “forgot password” flow, no reset emails, and no credential-stuffing attacks to absorb.

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 overnight — not all users have compatible devices, and some environments such as shared computers or older browsers lack support. The practical migration path is progressive, layered on top of your existing login rather than replacing it wholesale.

Phase 1: Offer alongside passwords. After a user logs in with their password, prompt them to enroll: “Sign in faster with Face ID?” Users who opt in get a better experience immediately, and you start building a population that no longer depends on shared secrets.

Phase 2: Make it the default. New account registration defaults to credential creation. The login page shows a prominent passwordless button with the password option secondary. Track adoption metrics — in production teams typically wait until a large majority of logins are passwordless before tightening further.

Phase 3: Password-optional accounts. Allow users to remove their password entirely. This eliminates the shared secret as an attack vector for those accounts. However, always keep a recovery mechanism — backup codes or verified email-based recovery — for anyone who loses every enrolled device.

Security Considerations, Edge Cases, and When NOT to Go All-In

The model solves phishing and credential stuffing but introduces new trade-offs you must plan for. Device loss is the primary concern: a user who loses every device and never synced to the cloud is locked out, so recovery flows are mandatory, not optional. Account recovery itself becomes the new weakest link — if your “forgot device” path falls back to an emailed magic link, an attacker who controls the inbox bypasses everything, so harden recovery as carefully as the happy path.

Synced credentials inherit the security of the cloud account that holds them. Compromising someone’s Apple or Google account can compromise their synced credentials, although this is still far stronger than reusing one password across dozens of sites. For regulated or high-assurance environments, this is exactly where you should reconsider a blanket rollout: synced credentials are convenient but device-bound hardware keys with attestation give you provable control over where private keys live.

Consider avoiding a passwordless-only approach when your audience skews toward shared kiosks, locked-down corporate desktops without platform authenticators, or regions where biometric hardware is uncommon — in those cases keep a robust fallback. For enterprise deployments that do need strong guarantees, use attestation to verify credentials were created on approved devices; the spec supports it, though most consumer applications should set attestation to “none” for maximum compatibility and to avoid leaking device fingerprints.

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 them alongside passwords today, track adoption, and progressively make passwords optional while keeping recovery airtight. The user experience is better, the security is stronger, and the ecosystem support from Apple, Google, and Microsoft guarantees long-term viability.

← Back to all articles