Low Level Design: Passkey Authentication

WebAuthn / FIDO2 Overview

Passkeys use public-key cryptography. The private key never leaves the device; the server stores only the public key.

Registration Flow

  1. Server generates a random challenge and returns it along with rp (relying party) info.
  2. Browser calls navigator.credentials.create() with the challenge.
  3. Authenticator generates an asymmetric key pair; private key stored in secure enclave.
  4. Authenticator returns an attestation object containing the public key, credential ID, and signed challenge.
  5. Server validates attestation and stores the credential.
// Client-side (simplified)
const credential = await navigator.credentials.create({
  publicKey: {
    challenge: base64ToArrayBuffer(serverChallenge),
    rp: { name: "Example", id: "example.com" },
    user: { id: userId, name: userEmail, displayName: userName },
    pubKeyCredParams: [{ type: "public-key", alg: -7 }] // ES256
  }
});
// POST credential.response to /webauthn/register

Authentication (Assertion) Flow

  1. Server generates a fresh challenge and returns allowed credential IDs for the user.
  2. Browser calls navigator.credentials.get().
  3. Authenticator signs the challenge with the stored private key.
  4. Server verifies the signature against the stored public key and checks the sign count.
// Client-side (simplified)
const assertion = await navigator.credentials.get({
  publicKey: {
    challenge: base64ToArrayBuffer(serverChallenge),
    allowCredentials: [{ type: "public-key", id: credentialId }]
  }
});
// POST assertion.response to /webauthn/authenticate

Data Model

Credential Table

Credential (
  id              BIGSERIAL PRIMARY KEY,
  user_id         BIGINT,
  credential_id   BYTEA UNIQUE,        -- opaque handle from authenticator
  public_key      BYTEA,               -- COSE-encoded public key
  aaguid          UUID,                -- authenticator model identifier
  sign_count      INT DEFAULT 0,       -- replay protection
  device_name     TEXT,                -- user-provided label
  created_at      TIMESTAMP,
  last_used_at    TIMESTAMP
)

Challenge Store (Redis)

SET challenge:{session_id} <base64_challenge> EX 300   -- 5-minute TTL

Registration Validation

  • Verify attestation statement format and signature.
  • Check origin matches expected RP origin.
  • Verify rpIdHash in authenticatorData matches SHA-256 of the RP ID.
  • Confirm the challenge in clientDataJSON matches the stored challenge.

Assertion Validation

  • Re-verify rpIdHash and origin.
  • Verify signature over authenticatorData || SHA-256(clientDataJSON) using stored public key.
  • Assert sign_count > stored_sign_count to detect cloned authenticators; update stored value.

Multiple Passkeys and Sync

Users may register multiple credentials (phone + laptop). Synced passkeys are backed up via iCloud Keychain or Google Password Manager, identified by aaguid. Platform authenticators set the BS (backup state) flag in authenticatorData.

Fallback Flow

Users without a registered passkey fall back to password + 2FA. The registration prompt is shown post-login to encourage passkey adoption.

See also: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

See also: Apple Interview Guide 2026: iOS Systems, Hardware-Software Integration, and iCloud Architecture

See also: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering

Scroll to Top