Passkeys replace passwords with public-key cryptography. Your server (the relying party) never stores a shared secret; it stores a public key and verifies signatures produced by an authenticator the user controls. If the relying party skips origin verification, mishandles challenges, or stores credentials without the right metadata, you lose the phishing resistance that makes passkeys worth adopting. This post focuses on the RP-side architecture: ceremony verification, credential storage, and the trust boundaries between browser, authenticator, and your server.

System description

A relying party issues random challenges, verifies signed responses from authenticators, and stores credential public keys. Two ceremonies define the protocol: registration (create a credential) and authentication (prove you hold it). The browser mediates both ceremonies, enforcing origin binding before the authenticator ever sees the request.

Architecture choice

There are two models for authenticator attestation, and the choice affects what you can enforce about credential provenance.

Accept all authenticators (no attestation verification)

Accept attestation: "none" or "indirect". Register any credential the browser presents, regardless of which authenticator created it.

Use this when:

  • You're building a consumer-facing application

  • You want maximum compatibility across devices and passkey providers (iCloud Keychain, Google Password Manager, 1Password, hardware keys)

  • You don't need to enforce hardware-specific security policies

Main risks: You cannot distinguish a hardware-bound key from a software-synced key by attestation alone, though the BE (Backup Eligible) and BS (Backup State) flags still provide that signal. You cannot block known-vulnerable authenticator models.

Verify attestation against FIDO Metadata Service

Request attestation: "direct". Verify the attestation statement's certificate chain against root certificates from the FIDO Metadata Service (MDS). Enforce an allowlist of AAGUIDs (authenticator model identifiers).

Use this when:

  • Enterprise policy requires specific hardware authenticators (FIPS-certified, company-provisioned)

  • You need to block authenticator models with known vulnerabilities

  • Regulatory requirements mandate authenticator-level assurance

Trade-off: You must maintain a trust store of attestation root certificates, consume MDS updates, and implement format-specific verification logic for each attestation type (packed, tpm, android-key, apple, fido-u2f). Requiring attestation also excludes passkey providers that return none. Use a maintained WebAuthn server library for attestation verification. Do not implement format-specific parsing manually.

Common middle ground: Accept all authenticators by default. Check the BE flag to distinguish synced from device-bound credentials. If your policy requires device-bound keys for specific roles or actions, enforce that at registration time without requiring full attestation verification.

Golden path

Build this first. Then relax constraints only if you have a specific reason:

Generate session-bound challenge → browser mediates ceremony → authenticator signs response → RP verifies ceremony → store credential with metadata → issue session

Each step is a verification gate. A failure at any point rejects the ceremony.

  1. Generate session-bound challenge: 16+ bytes of randomness, stored server-side with a 60-120s TTL

  2. Browser mediates ceremony: The browser enforces RP ID matching against the page origin before the authenticator is involved; your server validates the result afterward

  3. Authenticator signs response: Signs over authenticatorData || SHA-256(clientDataJSON), binding challenge and origin into the signature

  4. RP verifies ceremony: Validates origin, challenge, rpIdHash, flags (UP = User Present, UV = User Verified), and signature. For discoverable flows, confirms credentialId is bound to the userHandle

  5. Store credential with metadata: Persist credentialId, public key, signCount, transports, BE, BS, and aaguid alongside the user record

  6. Issue session: The ceremony proves identity at a point in time; the session token carries it forward

Minimal system context

  • Challenge store (session state): Server-side storage (HTTP session, Redis, signed cookie) that maps a session to a pending challenge. Challenges are single-use; consumed after verification or expired after TTL

  • Ceremony verifier (control plane): Parses clientDataJSON and authenticatorData, enforces origin and RP ID checks, verifies cryptographic signatures

  • Credential store (data plane): Persistent storage for credential records. Each record maps a credentialId to a user, a public key, a sign count, transport hints, and BE / BS flags

  • Session manager (identity): Issues authenticated sessions after successful ceremonies. Separate from the challenge store

Security properties of WebAuthn ceremonies

The ceremony binds:

  • The credential to a specific RP ID (via rpIdHash in authenticatorData)

  • The ceremony to a specific origin (via origin in clientDataJSON)

  • The response to a specific challenge (via challenge in clientDataJSON, covered by the signature)

  • User presence and optionally user verification (via UP and UV flags in authenticatorData)

It does not bind:

  • The resulting session token to the ceremony (the session is a bearer token after issuance)

  • The credential to a specific device, if the credential is sync-eligible BE=1)

  • The RP ID to a single origin (multiple subdomains can share an RP ID if it's set to the registrable domain)

The origin check in clientDataJSON is the primary anti-phishing mechanism. The authenticator never sees the origin; it works with rpIdHash. The browser enforces origin-to-RP-ID matching before the ceremony begins, and the RP verifies the origin after. Both checks must pass.

Threat model

Baseline assumptions

  • Browsers are trusted to enforce origin and RP ID matching correctly. Browser-level compromises (extensions, malware) are out of scope

  • The authenticator protects private keys. Authenticator firmware vulnerabilities are out of scope for this model

  • TLS is in place. The RP is served over HTTPS. Network-level MITM is not modeled

  • Standard infra controls (database AuthN, input validation, rate limiting) are assumed. This model focuses on the WebAuthn ceremony and credential management logic

A note on risk: you won’t fix everything

This table isn’t a checklist where every row must be fully eliminated. Focus on preventing the worst failures and limiting blast radius. In practice: ship prevention for the High rows first, then add monitoring and response for what you can’t realistically prevent.

Phase 1: Ceremony integrity

Focus: Preventing replay, phishing, and verification bypasses

Asset

Threat

Baseline Controls

Mitigation Options

Risk

Ceremony

Challenge replay: Attacker captures a signed WebAuthn response and replays it against the RP. If challenges aren't session-bound and single-use, the replayed response passes verification

Server-generated challenge

1. Session binding: Store challenges server-side, tied to the authenticated or anonymous session that initiated the ceremony

2. Single-use: Delete the challenge after successful verification or TTL expiry

3. Short TTL: Expire unused challenges within 60-120 seconds

High

Origin trust

Sibling-domain phishing: RP sets rp.id = "example.com". Attacker compromises blog.example.com (XSS, subdomain takeover) and initiates a ceremony with the same RP ID. The authenticator produces a valid assertion because rpIdHash matches. If the RP doesn't verify origin in clientDataJSON, it accepts the assertion

RP ID binding

1. Strict origin check: Verify origin in clientDataJSON against an explicit allowlist using exact string equality (===). Do not use regex or suffix matching (endsWith), which allows bypasses like https://malicious-example.com

2. Subdomain hygiene: Audit all subdomains under your RP ID domain. Unused subdomains are takeover targets

3. Narrow RP ID: If your app runs on a single subdomain, set rp.id to that subdomain rather than the registrable domain

Medium

User verification

UV bypass: RP requests userVerification: "required" but doesn't check UV=1 in the response flags. A stolen hardware key without a PIN set would authenticate with UP only (someone touched it) without verifying the user's identity

Flag in authenticatorData

1. Flag enforcement: If you set userVerification: "required", verify UV=1 in authenticatorData flags. Reject if UV=0

2. Registration policy: Require UV at registration so the authenticator sets up a local verification method (PIN, biometric)

Medium

Phase 2: Credential management

Focus: Preventing enumeration, confusion, and stale credential risks

Asset

Threat

Baseline Controls

Mitigation Options

Risk

User privacy

Credential enumeration: RP returns allowCredentials (list of credential IDs) for a given username during authentication. Attacker probes usernames to determine which accounts exist and how many credentials they have

Auth required for lookup

1. Discoverable credentials: Use the passkey flow where allowCredentials is empty. The authenticator finds matching credentials internally by rpIdHash

2. Conditional UI: Use mediation: "conditional" so the browser populates the credential picker from its own store

3. Dummy responses: For non-discoverable fallback flows, return a plausible-looking dummy challenge for nonexistent users

Low

Credential integrity

Credential confusion: Attacker registers a credential ID that already belongs to another user. If the RP doesn't enforce uniqueness, authentication becomes ambiguous

Credential lookup by ID

1. Uniqueness check: Reject registration if the credentialId already exists for any user

2. User binding: Always verify that the credential's associated userHandle matches the authenticating user

Medium

Cloned authenticator

Sign count regression: For device-bound credentials (BE=0), an attacker who clones the authenticator's private key produces assertions with a stale or divergent sign count. Without verification, the RP cannot detect the clone

signCount stored per credential

1. Conditional enforcement: For BE=0 credentials, flag or reject if the new signCount is less than or equal to the stored value

2. Skip for synced: For BE=1 credentials, accept signCount=0 (synced passkeys don't reliably increment counters)

3. Alert: Log sign count anomalies for investigation

Low

Credential metadata

Missing transport hints: RP doesn't store transports from getTransports() at registration. Future authentication ceremonies can't hint the browser about which authenticator to prompt (USB, BLE, NFC, internal), degrading UX and causing fallback delays

None

1. Store transports: Call response.getTransports() during registration and persist the result alongside the credential

2. Pass hints: Include stored transports in allowCredentials descriptors during authentication

Low

Phase 3: Post-ceremony session

Focus: Preventing session hijacking after a successful passkey ceremony

Asset

Threat

Baseline Controls

Mitigation Options

Risk

Authenticated session

Session theft: The passkey ceremony proves identity at one moment. The resulting session token is a bearer token. If stolen (XSS, malware, network interception on a misconfigured setup), the attacker inherits the session without ever touching the authenticator

Standard session management

1. Short-lived sessions: Reduce the window during which a stolen token is useful

2. Step-up auth: Require a fresh passkey ceremony for sensitive operations (password change, payment, admin actions)

3. Binding signals: Tie the session to client properties (IP range, TLS fingerprint) where feasible, accepting the false-positive cost for mobile users

Medium

Account

Credential stuffing the ceremony endpoint: Attacker floods the authentication endpoint with ceremony initiation requests to exhaust server-side challenge storage or cause resource exhaustion

Rate limiting

1. Per-IP rate limits: Throttle ceremony initiation requests

2. Challenge storage limits: Cap the number of pending challenges per session or per IP

3. Proof of work: For anonymous ceremony initiation, consider lightweight client puzzles

Low

Verification checklist

  • Ceremony verification

    • origin in clientDataJSON is checked against an explicit allowlist of expected origins using exact string equality (not regex or suffix matching)

    • challenge in clientDataJSON matches the server-side challenge for this specific session

    • Challenges are single-use and expire within 120 seconds

    • rpIdHash in authenticatorData equals SHA-256 of the configured RP ID

    • type in clientDataJSON is "webauthn.create" for registration and "webauthn.get" for authentication

    • For discoverable credential flows, userHandle from the assertion identifies the user, and credentialId is confirmed to be bound to that user

    • If the challenge store is unreachable, ceremony initiation fails closed (returns error), not open (skips challenge binding)

  • Flag enforcement

    • UP=1 is verified on every ceremony

    • UV=1 is verified when userVerification: "required" was requested

    • BE and BS flags are stored per credential and available for policy decisions

  • Credential storage

    • Each credential record stores: credentialId, public key, signCount, transports, aaguid, BE, BS

    • credentialId is unique across all users (registration rejects duplicates)

    • Sign count is verified for BE=0 credentials; signCount=0 is accepted for BE=1 credentials

  • Enumeration resistance

    • Authentication endpoints return identical response timing and structure for existing and non-existing users

    • The primary authentication flow uses discoverable credentials (empty allowCredentials)

  • Session management

    • Sensitive operations require step-up authentication (fresh passkey ceremony), not just an existing session

    • Ceremony initiation endpoints are rate-limited per IP

  • Environment isolation

    • RP ID is environment-specific; credentials registered in dev/staging cannot authenticate in production

  • Attestation (if verifying)

    • Attestation root certificates are sourced from FIDO Metadata Service and updated regularly

    • Only AAGUIDs on the allowlist are accepted during registration

    • Credentials with unknown or revoked attestation are rejected

Implementation & Review

The full threat model matrix, architectural diagrams, and a printable verification checklist for this pattern are available in the Secure Patterns repository. Use these artifacts to guide your design reviews and internal audits.

Keep reading