Passkeys for Web Developers: WebAuthn, RP IDs, Server Verification and Fallbacks

Passwords remain a familiar way to log into a website, but they create predictable problems: reused secrets, phishing pages, credential stuffing and support requests when somebody forgets a password. Passkeys offer a different model. A browser works with an authenticator to prove that a user has a cryptographic credential for the site. The server checks a signature rather than receiving a reusable password.

The concept sounds simple, but production implementations have details that matter: which domain is authorized to use a passkey, how a server binds each challenge to a login attempt, how a new device recovers access, and how to test flows that cross multiple sites. This guide explains the moving parts without pretending that a few lines of browser JavaScript are a complete authentication system.

A passkey is not a password stored in the browser

A passkey uses public-key cryptography. The authenticator creates a key pair: private key material stays within the authenticator or its protected credential-management ecosystem, and the website stores a public key linked to the account. During sign-in, the authenticator signs a server-provided challenge and contextual data; the server checks the result.

This design avoids giving a phishing site a reusable password. The browser and authenticator enforce relying-party scope, so the lookalike domain examp1e.com cannot simply request credentials for example.com. Passkeys can be synchronized by some credential providers or remain on a specific security key or device. Do not assume every authenticator offers the same recovery or synchronization behavior.

A passkey proves control of a credential, not that every session or account action is automatically safe. The application still needs session management, authorization checks, anti-CSRF measures where relevant, rate limits, and secure account recovery. The localStorage, sessionStorage and cookies guide provides a useful overview of why server-managed sessions are separate from credential creation.

The two ceremonies: registration and authentication

WebAuthn defines two main flows. Registration creates a new credential. Authentication uses an existing credential to sign a challenge. Both require cooperation between the browser and a backend service.

A simplified registration sequence:

  1. The user starts registration from an authenticated account settings page, or from a carefully designed new-account flow.
  2. The server creates a fresh, unpredictable challenge and registration options for the account, with an expiration time.
  3. The browser calls navigator.credentials.create() and the authenticator produces a credential.
  4. The browser returns the credential response to the server.
  5. The server validates the response against the saved request, checks origin and RP ID expectations, enforces policy, and stores the credential’s public key and metadata.

Authentication is similar, but the browser calls navigator.credentials.get(). The server verifies the signed assertion, challenge, origin, RP ID hash, user presence and user verification flags as required by policy, then issues an application session.

These flows are good candidates for a sequence diagram. The Mermaid live editor can help describe the user, browser, authenticator and server without burying security responsibilities in a single controller function. The site’s API diagram tutorial shows how to represent both successful and failed paths.

RP ID and origin are not interchangeable

WebAuthn security is anchored in website identity. The origin includes scheme, host and port, such as https://login.example.com. The relying party ID, or RP ID, is a domain identifier and does not include a scheme or port.

By default, a page’s domain is the RP ID. A subdomain may use a parent domain as its RP ID when that domain is a registrable-domain suffix of its own host; a public suffix such as com is not allowed. For example, registration on register.example.com can use example.com so a page on login.example.com can also use that credential. An unrelated domain cannot claim example.com as its RP ID simply because an application shares user accounts across two brands.

For deployments across genuinely different registrable domains, the Related Origin Requests mechanism can allow specific relationships when supported and correctly configured. It involves a JSON document at /.well-known/webauthn on the RP ID domain and browser compatibility checks; it is not the same as a shared cookie or a CORS header. Consult passkeys.dev’s related origins documentation for its deployment and capability requirements.

Browser code: request registration options from the server

Modern browsers expose WebAuthn through the Credentials API. This example assumes your server supplies WebAuthn’s JSON serialization format and that the modern conversion helpers are available. It is a client-side sketch, not a complete login system:

async function beginPasskeyRegistration() {
  if (!window.isSecureContext || !window.PublicKeyCredential ||
      !navigator.credentials ||
      typeof PublicKeyCredential.parseCreationOptionsFromJSON !== "function" ||
      typeof PublicKeyCredential.prototype.toJSON !== "function") {
    throw new Error("Passkey registration is unavailable here");
  }

  const optionsResponse = await fetch("/api/passkeys/register/options", {
    credentials: "same-origin",
    cache: "no-store"
  });
  if (!optionsResponse.ok) throw new Error("Cannot start registration");

  const optionsJSON = await optionsResponse.json();
  const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJSON);
  const credential = await navigator.credentials.create({ publicKey });
  if (!credential) throw new Error("No passkey was created");

  const verifyResponse = await fetch("/api/passkeys/register/verify", {
    method: "POST",
    credentials: "same-origin",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(credential.toJSON())
  });
  if (!verifyResponse.ok) throw new Error("Registration was not accepted");
}

For a production implementation, the server must associate the registration request with an already authenticated and authorized account, protect against CSRF when applicable, limit the lifetime of the challenge, and validate the full WebAuthn response using a maintained library. Modern API features, including parseCreationOptionsFromJSON() and toJSON(), should be checked against the browsers you support. For older clients, use a well-tested encoding adapter rather than converting arbitrary strings into binary data ad hoc.

A button to start this process should show a clear pending state, handle cancellation, and never report success until the server accepts the credential. A user closing the platform dialog is not a security event and should not produce a scary error message.

What the server must verify

For passwordless sign-in, request discoverable credentials and require user verification according to your policy; enforce that requirement on the server as well as requesting it in the browser. Merely touching a security key (user presence) is different from unlocking it with a PIN or biometric (user verification).

A request reaching /verify is not proof of successful registration or login. The server must validate the cryptographic results and its original expectations. In broad terms, verify:

  • The challenge is fresh, belongs to this pending operation and has not already been consumed.
  • The returned origin matches an origin your application allows for the request.
  • The RP ID hash corresponds to the intended relying party.
  • The expected WebAuthn ceremony and credential type are correct.
  • The signature validates against the stored public key for authentication, and required user presence or verification flags match policy.
  • Registration validates the returned algorithm and attestation data according to policy before storing the credential ID and public key.
  • Credential records are tied to the correct account, with duplicate registration, counters and backup-state handling according to your library and authenticator behavior. A counter that stays at zero is not by itself proof of failure or cloning.

These are review points, not a complete verification algorithm: use the WebAuthn specification’s registration and authentication procedures through maintained WebAuthn server software. Implementing your own CBOR parser, authenticator-data handling and signature verification is a poor shortcut for ordinary applications. Follow the MDN passkeys overview to understand the protocol and then select a mature server library for your language.

For debugging the HTTP exchanges, Web2Generators’ cURL Command Builder helps reproduce ordinary JSON endpoints. It cannot generate a real signed authenticator assertion: WebAuthn cryptographic ceremonies require an authenticator and correct browser context. Never confuse a successful fake API call with a verified passkey login.

What about sign-in and autofill?

On sign-in, the server creates authentication request options with a new challenge and an appropriate credential policy. The browser uses navigator.credentials.get() and returns an assertion. Some sites provide a “Sign in with a passkey” button; others offer passkeys through an account field using conditional UI where supported.

Either way, keep a visible, usable alternative path for people whose device, browser or credential manager cannot satisfy the request. Do not automatically infer that a canceled prompt means the account lacks a passkey. A person may be using a different device or choosing not to authenticate at that moment.

Recovery and migration are product decisions

A passkey launch should answer practical questions before enabling a “remove password” button. Can users register more than one authenticator? Can they recover an account if they lose all passkeys? What happens when an employee loses access to an enterprise device? Who can add or remove a credential, and which actions require recent authentication?

Strong account recovery is part of the security model. Email-only recovery, weak help-desk checks or reusable emergency codes can undermine otherwise phishing-resistant sign-in. Offer additional passkeys where feasible, carefully protect recovery changes, and alert users when credentials are added or removed. If users still need passwords during migration, encourage unique high-entropy secrets. The Password and Passphrase Generator is useful for disposable test accounts and for generating fallback credentials that will be kept securely.

Test the failure paths as carefully as the success path

A minimum cross-browser plan should include:

  1. Registration and login with a platform authenticator and, where supported, an external security key.
  2. An expired or reused challenge, an unexpected origin and an unknown credential. The server must reject each securely.
  3. Cancellation of the authentication prompt without losing the user’s form state.
  4. Login from a second device, including cross-device authentication if your intended browser and device combination supports it.
  5. Shared-subdomain or related-origin behavior, if your deployment relies on it.
  6. A recovery flow and credential removal, including replay prevention and session invalidation.

Use the site’s Network inspection guide to observe your application’s options and verification endpoints. Be careful when exporting logs: even when private keys never reach the server, request and response captures can reveal account identifiers, challenges, session cookies and other sensitive context. The Network panel is only one part of testing; browser prompts, authenticator state and server validation all matter.

Premium is coming soon

An optional premium subscription to remove ads is planned. Subscriptions are not available yet.

You can keep using our free tools without an account.