system design

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

  • Server generates a random challenge and returns it along with rp (relying party) info.
  • Browser calls navigator.credentials.create() with the challenge.
  • Authenticator generates an asymmetric key pair; private key stored in secure enclave.
  • Authenticator returns an attestation object containing the public key, credential ID, and signed challenge.
  • 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

  • Server generates a fresh challenge and returns allowed credential IDs for the user.
  • Browser calls navigator.credentials.get().
  • Authenticator signs the challenge with the stored private key.
  • 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

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

1972 Soviet postage stamp commemorating the Mars 2 probe

worth a read

Mars For The Rest of Us — a weekly-or-more deep dive on the technical side of Mars exploration: rocket propulsion, microbiology, mission architecture, and everything in between. Written by Maciej Ceglowski.

Read it on Substack →
Scroll to Top