Web Authentication API: Passwordless Login with Passkeys

Passwords are the weakest link in security. Passkeys replace them with biometrics and device-bound credentials. Learn WebAuthn, FIDO2, and how to implement passwordless login in your web app with real code examples.

Web Authentication API: Passwordless Login with Passkeys illustration
On this page7 sections

Passwords are fundamentally broken. Users reuse them across sites, phishing steals them daily, and even hashed databases get breached. Passkeys replace passwords entirely with cryptographic key pairs stored on the user’s device, authenticated with biometrics (fingerprint, face) or a device PIN. The encryption, hashing, and cryptography guide explains the primitives behind that key-pair model, while the API security cheatsheet covers the server-side controls around authentication endpoints. No password to remember, no password to steal.

How Passkeys Work

  1. Registration: The user’s device generates a public/private key pair. The public key is sent to your server. The private key never leaves the device.
  2. Authentication: Your server sends a random challenge. The device signs it with the private key (after biometric verification). Your server verifies the signature with the stored public key.

The private key is protected by the device’s secure enclave (TPM, Secure Enclave, Android Keystore). Even if your server is breached, attackers get only public keys - which are useless without the device. Passkeys authenticate an existing account; SCIM provisioning and deprovisioning handles the separate enterprise account-lifecycle problem.

WebAuthn API: Registration

// Client-side: create a new passkey
async function registerPasskey() {
  // 1. Get registration options from your server
  const response = await fetch('/api/auth/register/options', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username: 'alice@example.com' }),
  });
  const options = await response.json();

  // 2. Browser prompts user for biometric verification
  const credential = await navigator.credentials.create({
    publicKey: {
      challenge: base64ToBuffer(options.challenge),
      rp: {
        name: 'My App',
        id: 'example.com',    // Must match your domain
      },
      user: {
        id: base64ToBuffer(options.userId),
        name: 'alice@example.com',
        displayName: 'Alice Smith',
      },
      pubKeyCredParams: [
        { alg: -7, type: 'public-key' },    // ES256
        { alg: -257, type: 'public-key' },  // RS256
      ],
      authenticatorSelection: {
        authenticatorAttachment: 'platform',  // Device biometrics
        residentKey: 'required',              // Discoverable credential
        userVerification: 'required',         // Biometric required
      },
      timeout: 60000,
    },
  });

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

WebAuthn API: Authentication

// Client-side: authenticate with existing passkey
async function loginWithPasskey() {
  // 1. Get authentication options from server
  const response = await fetch('/api/auth/login/options', {
    method: 'POST',
  });
  const options = await response.json();

  // 2. Browser prompts for biometric verification
  const assertion = await navigator.credentials.get({
    publicKey: {
      challenge: base64ToBuffer(options.challenge),
      rpId: 'example.com',
      allowCredentials: [],  // Empty = discoverable (shows all passkeys)
      userVerification: 'required',
      timeout: 60000,
    },
  });

  // 3. Send signed assertion to server
  const result = await fetch('/api/auth/login/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      id: assertion.id,
      rawId: bufferToBase64(assertion.rawId),
      response: {
        authenticatorData: bufferToBase64(
          assertion.response.authenticatorData
        ),
        clientDataJSON: bufferToBase64(
          assertion.response.clientDataJSON
        ),
        signature: bufferToBase64(
          assertion.response.signature
        ),
      },
      type: assertion.type,
    }),
  });

  const session = await result.json();
  // User is now authenticated!
}

Server-Side: Python Verification

# Using the py_webauthn library
from webauthn import (
    generate_registration_options,
    verify_registration_response,
    generate_authentication_options,
    verify_authentication_response,
)
from webauthn.helpers.structs import (
    AuthenticatorSelectionCriteria,
    ResidentKeyRequirement,
    UserVerificationRequirement,
)

# Registration options
def get_registration_options(user):
    options = generate_registration_options(
        rp_id="example.com",
        rp_name="My App",
        user_id=user.id.encode(),
        user_name=user.email,
        user_display_name=user.name,
        authenticator_selection=AuthenticatorSelectionCriteria(
            resident_key=ResidentKeyRequirement.REQUIRED,
            user_verification=UserVerificationRequirement.REQUIRED,
        ),
    )
    # Store challenge in session for verification
    session['challenge'] = options.challenge
    return options

# Verify registration
def verify_registration(credential_data):
    verification = verify_registration_response(
        credential=credential_data,
        expected_challenge=session['challenge'],
        expected_origin="https://example.com",
        expected_rp_id="example.com",
    )

    # Store credential for future authentication
    store_credential(
        user_id=current_user.id,
        credential_id=verification.credential_id,
        public_key=verification.credential_public_key,
        sign_count=verification.sign_count,
    )

Passkeys vs Passwords Comparison

Aspect Passwords Passkeys
Phishing Vulnerable (users type on fake sites) Immune (domain-bound, cannot be phished)
Breach impact Hashed passwords can be cracked Public keys are useless to attackers
Reuse across sites Common (80% of users) Impossible (unique key per site)
User experience Remember, type, reset Touch fingerprint sensor
MFA needed Yes (passwords alone are weak) No (biometric + device is inherently 2FA)
Cross-device Works everywhere Synced via iCloud/Google (or use QR code)

Progressive Enhancement Strategy

// Check if the browser supports WebAuthn
function supportsPasskeys(): boolean {
  return window.PublicKeyCredential !== undefined &&
    typeof window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function';
}

// Check if a platform authenticator is available
async function hasPlatformAuth(): Promise<boolean> {
  if (!supportsPasskeys()) return false;
  return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
}

// Progressive enhancement:
// 1. Everyone gets username/password as baseline
// 2. If passkeys supported: show "Add Passkey" option in settings
// 3. On next login: offer "Sign in with Passkey" as primary option
// 4. Keep password as fallback for unsupported devices

Key Takeaways

  • Passkeys are phishing-proof - the private key is domain-bound and never transmitted
  • Passkeys are inherently two-factor: something you have (device) + something you are (biometric)
  • Use progressive enhancement - offer passkeys alongside passwords, do not force them
  • Store only public keys on your server - a breach exposes nothing usable
  • Discoverable credentials (resident keys) enable username-less login - the user just touches their fingerprint sensor
  • Major platforms support passkey sync: iCloud Keychain (Apple), Google Password Manager, Windows Hello
  • Libraries handle the crypto: py_webauthn (Python), SimpleWebAuthn (Node.js), webauthn4j (Java)

Passkeys are the future of authentication, and the future is already here. Apple, Google, and Microsoft have committed to universal passkey support. Start with progressive enhancement - add passkey registration alongside existing password auth - and give your users the option to never type a password again.

Share this article

Stuck on implementation?

Get private, 1-on-1 help with system design, performance, scaling, or any technical challenge.

Book a Session

Related Production Resources

Course

Free learning tracks

Turn this guide into a structured production engineering path.

Lab

Interactive engineering labs

Practice the same ideas through scenario-based simulators.

Reference

Production cheatsheets

Keep the operational commands and checks nearby.

Glossary

Key terms

Review the vocabulary behind the architecture.

Discussion

Questions, corrections, or production notes? Add them here so other learners can benefit.

Comments load on demand

To keep this article fast and private by default, the GitHub-powered discussion loads only when you reach this section.

Prefer GitHub? Open the project discussions directly .

Continue Reading

Related practical guides from the same production engineering path.

Frontend 12 min read

State Management Showdown: NgRx vs Signals vs Services in Angular

Angular has three state management approaches and choosing wrong means either over-engineering a todo app or under-engineering an enterprise dashboard. Compare NgRx, Signals, and plain services with real examples and a decision framework.

Angular NgRx
Frontend 12 min read

Micro-Frontends: Splitting a Monolith UI Without the Pain

Your frontend monolith has 200 components, 15 teams, and deploys take 45 minutes. Micro-frontends let teams ship independently. Learn Module Federation, shared dependencies, routing strategies, and when NOT to use them.

Micro-Frontends Module Federation
Backend 13 min read

OAuth 2.0 and OpenID Connect: The Developer's No-BS Guide

OAuth 2.0 is not authentication. OpenID Connect is. This guide cuts through the confusion - authorization code flow, PKCE, refresh tokens, JWTs, and the security mistakes that get apps hacked.

OAuth OpenID Connect
Backend 11 min read

M2M Authentication: Securing Service-to-Service Communication

A comprehensive guide to Machine-to-Machine authentication - from OAuth 2.0 Client Credentials to mTLS, JWTs, and API keys. Learn how to secure your microservices.

Authentication Security