Why add passkeys to your sign-in form
Passkeys let users authenticate with a fingerprint, face, or device PIN. If every user had a passkey, sign-in could be a single button tap: the user verifies with their screen lock and is signed in. But during the transition from passwords, sites must serve both kinds of users. Forcing people to remember which sites use passkeys, or asking them to pick an authentication method up front, is poor UX.
Passkeys are also unfamiliar to many users. Surfacing them through the autofill interface they already know from password managers solves both problems at once: it bridges the password-to-passkey transition and makes the new mechanism feel natural.
Conditional UI merges passkeys into autofill
The mechanism that makes this work is conditional UI, a feature of the WebAuthn standard. With conditional UI, passkeys appear as suggestions in the same autofill dropdown that shows saved passwords.
When the user focuses the username field, the browser shows an autofill dialog with stored passkeys next to saved passwords. Selecting a passkey proceeds with sign-in and prompts for device screen lock; selecting a password works exactly as it does today. Users with a passkey get the stronger authentication without learning a new flow, and password-only users are unaffected.
How the authentication flow works
Passkey authentication uses the WebAuthn API and involves four components:
- Backend: Stores user account data, including the public key.
- Frontend: Talks to the browser and fetches data from the backend.
- Browser: Runs the JavaScript and mediates WebAuthn API calls.
- Passkey provider: Creates and stores the passkey, typically a password manager like Google Password Manager or a security key.
The flow proceeds as follows:
- The user loads the sign-in page; the frontend requests an authentication challenge from the backend.
- The backend generates a WebAuthn challenge tied to the user's account and returns it.
- The frontend calls
navigator.credentials.get()with the challenge, starting authentication in the browser. - The browser works with the passkey provider to prompt for passkey selection (often via the autofill dialog that appears when the user focuses the sign-in field) and verifies identity through device screen lock or biometrics.
- After verification, the passkey provider signs the challenge, and the browser returns the public key credential, signature included, to the frontend.
- The frontend forwards the credential to the backend.
- The backend checks the signature against the user's stored public key. If it matches, the user is signed in.
Starting sign-in with a conditional WebAuthn request
To authenticate a user with a passkey via form autofill, the page should issue a conditional WebAuthn get call on load. The request to navigator.credentials.get() passes the mediation: 'conditional' option.
A conditional request does not surface UI immediately. It remains pending until the user interacts with the username field’s autofill prompt. Selecting a passkey resolves the promise with a credential, letting the page complete sign-in without a traditional form submission. If the user picks a password instead, the promise stays unresolved and normal password handling proceeds; it becomes the page’s job to finish the password sign-in.
Marking the username field
Passkey autofill requires an autocomplete attribute on the username input element with both username and webauthn:
<input type="text" name="username" autocomplete="username webauthn" autofocus>
Adding autofocus to that field causes the autofill prompt to appear immediately on page load, listing available passwords and passkeys.
Confirming browser support
Before making a conditional WebAuthn call, confirm the browser supports each of the following:
- WebAuthn via the
PublicKeyCredentialinterface. - Capability detection through
PublicKeyCredential.getClientCapabilities(). - Conditional UI, indicated by the
conditionalGetcapability.
The snippet below shows how to check those prerequisites:
if (window.PublicKeyCredential && PublicKeyCredential.getClientCapabilities) {
const capabilities = await PublicKeyCredential.getClientCapabilities();
// Check if conditional mediation is available.
if (capabilities.conditionalGet === true) {
// The browser supports conditional mediation.
}
}
Preparing request options from the server
Your backend supplies several options for navigator.credentials.get(), typically delivered as a JSON payload from a dedicated endpoint. The key elements are:
challenge: A server-generated ArrayBuffer value, normally Base64URL-encoded for transport. It prevents replay attacks; issue a fresh challenge for each attempt and expire it quickly.allowCredentials: Pass an empty array so the browser can enumerate every credential for the givenrpId.userVerification: Your preference for local user checks such as a screen lock. The default and recommended value is"preferred". Other values:"required"fails the operation if verification cannot be done;"discouraged"asks the authenticator to avoid verification where possible.rpId: Your relying party ID, generally the site’s domain (for example,example.com). This must exactly match therp.idused at passkey creation.
Since ArrayBuffer fields such as challenge are Base64URL-encoded in JSON, parse the server response with PublicKeyCredential.parseRequestOptionsFromJSON() to decode them back into the structure navigator.credentials.get() expects.
// Fetch an encoded PubicKeyCredentialRequestOptions from the server.
const _options = await fetch('/webauthn/signinRequest');
// Deserialize and decode the PublicKeyCredentialRequestOptions.
const decoded_options = JSON.parse(_options);
const options = PublicKeyCredential.parseRequestOptionsFromJSON(decoded_options);
...
Initiating conditional authentication
With the processed publicKeyCredentialRequestOptions object — called options in the example — issue the conditional call:
// To abort a WebAuthn call, instantiate an AbortController.
const abortController = new AbortController();
// Invoke WebAuthn to authenticate with a passkey.
const credential = await navigator.credentials.get({
publicKey: options,
signal: abortController.signal,
// Specify 'conditional' to activate conditional UI
mediation: 'conditional'
});
Parameters worth noting:
publicKey: The options object from your server, decoded and ready.signal: AnAbortControllersignal gives you programmatic cancellation, useful if another WebAuthn request needs to take its place.mediation: 'conditional': The flag that turns this into a background request waiting on autofill, instead of an immediate modal.
Handling the returned credential
When the user selects a passkey and finishes verification, such as unlocking the device, the promise resolves with a PublicKeyCredential. Rejections need mapping by the error’s name property:
NotAllowedError: The user canceled or did not pick a passkey.AbortError: The request was aborted, probably through yourAbortController.- Other exceptions indicate unexpected failures; the browser generally displays an error dialog.
The resolved object holds the data your server must verify. The relevant properties are:
id: Base64URL-encoded credential ID.rawId: ArrayBuffer version of that ID.response.clientDataJSON: ArrayBuffer including the challenge and origin for server-side checks.response.authenticatorData: ArrayBuffer containing the RP ID and other authenticator output.response.signature: ArrayBuffer containing the core signature; verify it against the stored public key.response.userHandle: ArrayBuffer with the user ID from registration.authenticatorAttachment: Eitherplatformorcross-platform. The latter can happen after a phone-based sign-in; consider then offering to create a passkey on the current device.type: Always"public-key".
Before POSTing the credential, call .toJSON() to produce a JSON-safe form where every ArrayBuffer field is Base64URL strings. Then JSON.stringify() the result for the request body:
...
// Encode and serialize the PublicKeyCredential.
const _result = credential.toJSON();
const result = JSON.stringify(_result);
// Encode and send the credential to the server for verification.
const response = await fetch('/webauthn/signinResponse', {
method: 'post',
credentials: 'same-origin',
body: result
});
Verification and clean-up on the server
Server-side verification must cover these four tasks:
- Parse the incoming credential.
- Retrieve the stored public key for the credential
id. - Check the received
signatureagainst that public key. - Validate the challenge, origin, and other returned data.
Use a maintained FIDO/WebAuthn server library rather than writing the cryptographic checks by hand. Open-source options are collected in the awesome-webauthn GitHub repository. Once all checks pass, the server can complete the sign-in.
If your server cannot locate a passkey with the supplied ID, the user may have deleted it from your backend while it still lives with their passkey provider. That leftover often surfaces again in autofill prompts and leads to failed sign-in attempts. To remove the stale credential, call the static PublicKeyCredential.signalUnknownCredential() method from the client when the server signals an unknown ID (a 404, for instance). Pass the RP ID and the missing credential ID, and the provider should drop the orphan:
// Detect authentication failure due to lack of the credential
if (response.status === 404) {
// Feature detection
if (PublicKeyCredential.signalUnknownCredential) {
await PublicKeyCredential.signalUnknownCredential({
rpId: "example.com",
credentialId: "vI0qOggiE3OT01ZRWBYz5l4MEgU0c7PmAA" // base64url encoded credential ID
});
} else {
// Encourage the user to delete the passkey from the password manager nevertheless.
...
}
}
Post-sign-in flows
What happens after authentication depends on the method the user chose. You can use that moment to improve passkey adoption, keep credential data consistent, and avoid unnecessary friction.
Users who signed in without a passkey
A user who authenticates with a password may not have a passkey for the account, or may not have one on the current device. That makes the post-sign-in moment a natural opportunity to introduce passkeys.
- Upgrade passwords to passkeys: The conditional create WebAuthn feature lets the browser automatically generate a passkey for a user after successful password authentication. This lowers the barrier to adoption by removing manual steps. See the guide on helping users adopt passkeys more seamlessly for implementation details.
- Prompt manually for passkey creation: Asking explicitly can work well after a more involved sign-in, such as one requiring multi-factor authentication (MFA). Be mindful not to over-prompt, as repeated requests can feel intrusive.
For guidance on messaging and other adoption tactics, refer to the examples in Communicating passkeys to users.
Users who signed in with a passkey
Successful passkey authentication also opens several follow-up actions that can improve the user experience and keep account state accurate across devices and providers.
Offer a local passkey after cross-device authentication
When a user signs in via a cross-device mechanism (for instance, scanning a QR code), the passkey they used may not reside on the device they are now using. This can occur if their passkey provider is not supported by the signing-in OS or browser, or if the provider is no longer accessible on that device even though a passkey remains available elsewhere.
In these cases, invite the user to create a passkey on the current device so they can avoid repeating the cross-device flow. Check the authenticatorAttachment property of the credential; a value of "cross-platform" indicates a cross-device authentication. If so, explain the benefit and walk them through the local creation process.
Sync passkey metadata with the provider via Signals
Your Relying Party (RP) can use the WebAuthn Signals API to keep the passkey provider's data in line with your own records. This helps with consistency in UI elements like account selection dialogs.
For example, you can signal when a passkey no longer exists so providers can remove stale credentials from their lists. Similarly, you can notify the provider when a user changes their username or display name so the displayed information stays current.
More details on keeping things synchronized are available in the guide on keeping passkeys consistent with server credentials via the Signal API.
Skip the second factor
Passkeys already provide strong protection against phishing and other common threats, so adding a second factor yields little security gain while adding an extra step to the sign-in experience. Avoid requiring one after a passkey authentication.
Implementation checklist
- Support passkey sign-in via form autofill.
- Notify the backend when a matching credential is not found for a passkey.
- Prompt for manual passkey creation if the user has none after sign-in.
- Use conditional create to automatically register a passkey after password (plus second factor) sign-in.
- Offer local passkey creation when the sign-in used a cross-device passkey.
- Send the list of available passkeys and updated user details (username, display name) to the provider after sign-in or on change.



