WebAuthn and Passkeys: A Complete Hands-On Guide to Passwordless Authentication in the Post-Password Era

前端工程(Updated Jun 2, 2026)

Passwords Are Dead, Long Live Passkeys

The fundamental problem with passwords: passwords are shared secrets — both the server and the user know them. If either side is compromised, security fails.

Problem Impact
Weak passwords 81% of data breaches stem from weak passwords
Password reuse One leak compromises all accounts
Phishing attacks Users cannot distinguish real from fake sites
Database breaches Stored password hashes can be cracked
MFA SMS hijacking SIM swap attacks

Passkeys (WebAuthn/FIDO2) replace passwords with public-key cryptography:

Password model: User → [password] → Server (stores password hash)
Passkey model:  User → [signature] → Server (stores only public key)

A public key cannot derive the private key. Even if the server is breached, attackers cannot impersonate users.


WebAuthn Protocol Flow

Registration Flow

1. User clicks "Register Passkey"
2. Server generates challenge → sends to frontend
3. Browser invokes authenticator → user verifies (fingerprint/Face ID/PIN)
4. Authenticator generates key pair → returns public key + signature
5. Frontend sends public key + signature to server
6. Server verifies signature → stores public key (credential)

Authentication Flow

1. User clicks "Sign in with Passkey"
2. Server generates challenge → sends to frontend
3. Browser invokes authenticator → user verifies
4. Authenticator signs challenge with private key → returns signature
5. Frontend sends signature to server
6. Server verifies signature with public key → authentication successful

Complete Code Implementation

Server: Registration Start

// POST /api/webauthn/register/start
import { generateRegistrationOptions } from '@simplewebauthn/server';

export async function registerStart(userId: string, username: string) {
  const options = await generateRegistrationOptions({
    rpID: 'example.com',
    rpName: 'ToolsKu',
    userID: userId,
    userName: username,
    attestationType: 'none',
    authenticatorSelection: {
      authenticatorAttachment: 'platform',  // Prefer platform authenticator
      residentKey: 'required',              // Discoverable credential (Passkey)
      userVerification: 'preferred',
    },
  });

  // Store challenge for later verification
  await storeChallenge(userId, options.challenge);

  return options;
}

Frontend: Registration

import { startRegistration } from '@simplewebauthn/browser';

async function registerPasskey() {
  try {
    // 1. Get registration options from server
    const options = await fetch('/api/webauthn/register/start', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ userId: user.id, username: user.name }),
    }).then(r => r.json());

    // 2. Call browser API to register
    const credential = await startRegistration({ optionsJSON: options });

    // 3. Send credential to server for verification
    const result = await fetch('/api/webauthn/register/finish', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ userId: user.id, credential }),
    }).then(r => r.json());

    if (result.verified) {
      showToast('Passkey registered successfully!');
    }
  } catch (error) {
    if (error.name === 'NotAllowedError') {
      showToast('User cancelled the operation');
    }
  }
}

Server: Registration Completion

import { verifyRegistrationResponse } from '@simplewebauthn/server';

export async function registerFinish(userId: string, credential: any) {
  const challenge = await getChallenge(userId);

  const verification = await verifyRegistrationResponse({
    response: credential,
    expectedChallenge: challenge,
    expectedOrigin: 'https://example.com',
    expectedRPID: 'example.com',
  });

  if (verification.verified) {
    // Store credential (public key)
    await storeCredential(userId, {
      id: verification.registrationInfo?.credentialID,
      publicKey: verification.registrationInfo?.credentialPublicKey,
      counter: verification.registrationInfo?.counter,
      deviceType: verification.registrationInfo?.credentialDeviceType,
    });
  }

  return { verified: verification.verified };
}

Frontend: Authentication

import { startAuthentication } from '@simplewebauthn/browser';

async function authenticateWithPasskey() {
  try {
    // 1. Get authentication options
    const options = await fetch('/api/webauthn/auth/start', {
      method: 'POST',
    }).then(r => r.json());

    // 2. Call browser API to authenticate
    const credential = await startAuthentication({ optionsJSON: options });

    // 3. Verify signature
    const result = await fetch('/api/webauthn/auth/finish', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ credential }),
    }).then(r => r.json());

    if (result.verified) {
      redirect('/dashboard');
    }
  } catch (error) {
    if (error.name === 'NotAllowedError') {
      showToast('Authentication cancelled');
    }
  }
}

Platform Authenticators vs Roaming Authenticators

Dimension Platform Authenticator Roaming Authenticator
Examples Touch ID, Face ID, Windows Hello YubiKey, Titan Key
Storage In-device TPM/Secure Enclave Hardware security chip
Cross-device iCloud/Google sync Must carry physically
Cost Free $25-70
User Experience Fingerprint/face, seamless Insert/touch
Loss Risk Device loss Key loss

Passkey Sync Mechanisms

Apple iCloud Keychain:
  iPhone registers Passkey → iCloud sync → Mac/iPad auto-available

Google Password Manager:
  Android registers Passkey → Google sync → Chrome all-platform available

Windows Hello:
  Local TPM storage → No cross-device sync
  → Recommend registering both platform + roaming Passkey

Fallback Strategy: Progressive Deployment

function AuthForm() {
  const [supportsPasskey, setSupportsPasskey] = useState(false);

  useEffect(() => {
    // Detect WebAuthn support
    setSupportsPasskey(
      window.PublicKeyCredential !== undefined &&
      typeof window.PublicKeyCredential === 'function'
    );
  }, []);

  return (
    <form>
      {supportsPasskey && (
        <Button onClick={authenticateWithPasskey}>
          Sign in with Passkey
        </Button>
      )}
      {/* Traditional password as fallback */}
      <Input type="email" placeholder="Email" />
      <Input type="password" placeholder="Password" />
      <Button type="submit">Sign in</Button>
    </form>
  );
}

Conditional UI: Auto-Detect Passkey

// Check if user has a registered Passkey
const isConditionalAvailable = await PublicKeyCredential.isConditionalMediationAvailable();

if (isConditionalAvailable) {
  // Browser auto-shows Passkey option in password field
  const credential = await startAuthentication({
    optionsJSON: options,
    useAutocomplete: true, // Enable conditional UI
  });
}
<!-- Password field auto-shows Passkey option -->
<input
  type="password"
  autocomplete="webauthn"
  placeholder="Password or Passkey"
/>

Security Considerations

1. Challenge Security

// ✅ Challenge must be a server-generated random number
const challenge = crypto.randomUUID();

// ❌ Never use fixed values or timestamps
const badChallenge = Date.now().toString();

2. Origin Verification

// ✅ Strictly verify Origin to prevent phishing
const expectedOrigin = 'https://example.com';

// ❌ Don't use wildcards
const badOrigin = '*';

3. Credential Counter

// Detect cloned credentials
if (newCounter <= storedCounter) {
  // Credential may have been cloned!
  throw new Error('Possible cloned authenticator');
}

4. Multi-Credential Strategy

Recommendation: Register at least 2 Passkeys per user
  - 1 platform Passkey (daily use)
  - 1 roaming Passkey or backup Passkey (for recovery)

Browser and Platform Support

Platform Passkey Support Sync Conditional UI
Chrome (Windows/macOS) Google
Safari (macOS/iOS) iCloud
Firefox None
Edge Google
Android Chrome Google
iOS Safari iCloud

Summary

Passkeys are the terminator of passwords — based on public-key cryptography, phishing-resistant, leak-resistant, and replay-resistant. By 2026, all mainstream browsers and operating systems support them. It's time to deploy in production. Use the @simplewebauthn library for quick integration, and combine with conditional UI for seamless user switching. Remember the fallback strategy: Passkey first, password as fallback, progressive enhancement.

Try these browser-local tools — no sign-up required →

#WebAuthn#Passkey#无密码认证#FIDO2#生物识别