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.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does passkey registration work with WebAuthn?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The server generates a random challenge and returns it with relying party info. The browser calls navigator.credentials.create(), which triggers the authenticator to generate a public/private key pair. The private key stays on the device in a secure enclave; the public key and credential ID are sent to the server, which validates the attestation and stores the credential.”
}
},
{
“@type”: “Question”,
“name”: “What is sign_count and why is it important in passkey authentication?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The authenticator increments a sign_count with each assertion. The server checks that the received sign_count is strictly greater than the stored value. If it's equal or lower, the server can detect a cloned authenticator and reject or flag the authentication attempt, providing replay attack protection.”
}
},
{
“@type”: “Question”,
“name”: “How are passkeys synced across a user's devices?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Platform authenticators can back up credentials to cloud keychain services such as iCloud Keychain (Apple) or Google Password Manager. The authenticatorData backup state (BS) flag indicates a credential is backed up. Synced passkeys allow seamless use across a user's devices without requiring re-registration on each one.”
}
},
{
“@type”: “Question”,
“name”: “What challenge storage strategy is recommended for WebAuthn?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Challenges should be stored server-side in Redis keyed by session ID with a short TTL (5 minutes). This prevents challenge reuse and replay attacks. The challenge must be at least 16 random bytes, base64url-encoded, and verified consumed (deleted from Redis) immediately after a successful or failed authentication attempt.”
}
}
]
}

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