X

How to Implement Passkeys with WebAuthn in a Web Application

Passkeys let people sign in with a device or password manager instead of typing a password. For a web application, the browser talks to an authenticator through WebAuthn, while your server creates challenges, verifies responses, and stores each account's public key.

That server-side verification is essential: WebAuthn is not a front-end-only login button. This guide explains the registration and sign-in ceremonies, how passkey autofill fits into an existing form, and the recovery and compatibility decisions to make before rollout. It focuses on the web standard rather than a particular framework or authentication provider.

How passkeys and WebAuthn fit together

WebAuthn is the browser API for creating and using public-key credentials. A passkey is a discoverable WebAuthn credential: an authenticator can identify the credential for a site without the site first providing a credential ID. Depending on the provider, a passkey may be available on one device or across a user's devices.

During registration, the authenticator creates a key pair. The private key stays with the authenticator; the application stores the credential ID and public key. During sign-in, the server sends a fresh challenge, and the authenticator signs it. The server checks the signature and the request context before creating a normal application session.

The browser and authenticator bind credentials to the relying party (RP), which is your application. This origin-bound design helps prevent a credential from being used by a lookalike site. It does not replace safe session handling, account recovery, or protection against malicious code running on your own site.

The two WebAuthn ceremonies

Both registration and sign-in are exchanges between the browser and your backend. The frontend requests options from the server, passes them to the WebAuthn API, then sends the resulting credential response back for server-side verification.

Ceremony Browser API What the server must do
Register a passkey navigator.credentials.create() Create and track a challenge, verify the response, then store the credential ID and public key for the account
Sign in with a passkey navigator.credentials.get() Create and track a new challenge, verify the signed assertion against the stored public key, then issue the application's session

Use a maintained WebAuthn library on the server. The protocol includes binary values and strict origin, challenge, and credential checks; implementing its cryptography or verification rules by hand is an avoidable security risk.

Register a passkey

Start only after the user has securely established which account they are adding a credential to. If a signed-in user adds a passkey from account settings, require recent reauthentication or another appropriate step-up check. A stolen password should not be enough to silently add an attacker's passkey.

The registration flow is:

  1. Create registration options on the server. Generate a cryptographically random, one-time challenge with at least 16 bytes of random data. Keep it short-lived and associate it with the account, session, and registration operation. Include the RP name and ID, a stable user handle, and the public-key algorithms your server supports.
  2. Ask the browser to create the credential. Send the options to the page and call navigator.credentials.create({ publicKey: options }). Request a discoverable credential when you are implementing passkeys; the WebAuthn option is authenticatorSelection.residentKey: "required". The authenticator prompts the person to approve creation.
  3. Verify the response on the server. Check that the challenge is the outstanding registration challenge, and verify the expected origin, RP ID, credential data, and user-verification policy with your WebAuthn library. Reject expired, reused, or mismatched challenges.
  4. Save the credential. Store the credential ID, public key, account association, and the metadata your library needs for later assertions. Do not store biometric information or treat the public key as a password.

Choose your RP ID deliberately. It is tied to the domain, and changing it can make credentials created for the old ID unusable. Use the production domain you intend to keep, and test any subdomain arrangement before enabling registration for customers.

Registration also needs a duplicate policy. Your backend can send the user's existing credential IDs in excludeCredentials so an authenticator can avoid offering a duplicate. Let the server library handle credential ID encoding and option serialization; WebAuthn messages contain binary values that should not be passed through ordinary JSON serialization unchanged.

Sign in with a passkey

For each sign-in attempt, the server creates a new random challenge and records it for a short time. It then returns request options to the browser. Never reuse a registration challenge for login or accept a challenge supplied by the client.

If the user has already entered a username, the server can provide the credential IDs registered to that account in allowCredentials. For a username-less flow, leave allowCredentials out (or empty) so the authenticator can discover an eligible credential for the RP.

The frontend calls navigator.credentials.get({ publicKey: options }) and sends the assertion response to the backend. The server must verify the challenge, origin, RP ID hash, credential, and signature against the stored public key. It should also enforce the configured user-presence and user-verification requirements, consume the challenge, and only then issue the same kind of secure session used by other login methods.

Do not decide that authentication succeeded just because the browser returned a credential. The backend's verified result is what authorizes a session.

Add passkey autofill to a sign-in form

You can offer a dedicated Sign in with a passkey button, or let a passkey appear alongside saved passwords in a username field. The latter is called conditional mediation or passkey autofill.

For a form with a username field, use the WebAuthn autocomplete token:

<input name="username" autocomplete="username webauthn">

When conditional mediation is available, the page can request an assertion with mediation: "conditional" and without restricting the request to a known credential ID. Check support with PublicKeyCredential.isConditionalMediationAvailable() before relying on this interface. Its appearance and support vary, so keep a working button or other sign-in path as a fallback.

The conditional request still needs fresh options from your backend and the same server-side assertion verification as a button-based flow. Use your WebAuthn library's documented helpers to convert options and responses between JSON and the binary values expected by the browser API.

Secure deployment and browser support

WebAuthn requires a secure context. Deploy the real sign-in flow over HTTPS; browsers generally allow localhost during development, but that is not a substitute for testing your deployed origin and RP ID.

The core Web Authentication API is broadly available in modern browsers, but passkey-provider availability and optional interfaces such as conditional mediation can differ by browser, operating system, and authenticator. Feature-detect the capability you need rather than relying on browser names. Test the actual device and browser combinations your users rely on, and keep another login method available while rolling out passkeys.

The RP ID and origin checks are security controls, not configuration details to skip when testing. Make sure local, staging, and production environments use the intended values, and do not accept arbitrary origins to make a test pass.

Plan for recovery and credential management

Passkeys change how people prove account control, but they do not remove the need for account recovery. Decide what happens when a user loses a device, cannot access a credential provider, or needs to remove a compromised credential.

  • Let users register more than one passkey where appropriate, and show which credentials are associated with their account.
  • Require a secure, recent account check before adding or removing credentials. Notify the account owner about credential changes.
  • Provide a recovery route that is not weaker than the sign-in flow it replaces. Protect recovery email, phone, support, or backup-code processes against account takeover.
  • Let a user revoke a lost credential, and invalidate related sessions when your incident policy calls for it.
  • Keep passwords or another supported sign-in method during migration if users need it, but give each method a clear security policy.

Treat WebAuthn verification and session management as separate parts of the system. A valid assertion proves control of a credential for the RP; your application still needs secure cookies, CSRF protections where applicable, session expiry, and authorization checks.

Test the failure paths, not only a successful login

Before launch, test registration and sign-in with the browsers and authenticators you support. Include cases where the user cancels, has no eligible credential, switches devices, or needs account recovery. Also confirm that the server rejects an expired or replayed challenge, a response for a different origin or RP ID, and a signature that does not match the stored credential.

Test the account lifecycle too: adding a second passkey, removing one, replacing a lost device, and signing in through any retained alternative. A passkey rollout is ready when users have a usable path through both normal login and recovery—not just when the browser prompt appears.

Official references

Categories: Development
Related Post