Sign in with a passkey through form autofill

Create a sign in experience that leverages passkeys while still accommodating existing password users.

This guide explains how to use form autofill to allow users to sign in with passkeys alongside passwords. Using form autofill creates a unified sign-in experience, simplifying the transition from passwords to the more secure and user-friendly passkey authentication method.

Learn how to implement WebAuthn's conditional UI to support both passkey and password users with minimal friction in your existing sign-in forms.

Why use form autofill to sign-in with a passkey?

Passkeys let users sign in to websites using their fingerprint, face, device PIN.

If all users had passkeys, the authentication flow could be a single sign-in button. Tapping the button would let the user directly verify the account with screen lock, and sign in.

However, transitioning from passwords to passkeys presents challenges. Websites need to support both password and passkey users during this period. Expecting users to remember which sites use passkeys and asking them to choose a sign-in method upfront creates a poor user experience.

Passkeys are also a new technology, and explaining them clearly can be difficult. Using the familiar autofill interface helps address both the transition challenge and the need for user familiarity.

Use conditional UI

To support both passkey and password users effectively, include passkeys in your form's autofill suggestions. This approach uses conditional UI, a feature of the WebAuthn standard.

Example of passkey selection through form autofill.

When the user focuses on the username input field, an autofill dialog appears, suggesting stored passkeys alongside saved passwords. The user can select either a passkey or a password and proceed to sign in, using the device screen lock if they choose a passkey.

This lets users sign in to your website with the existing sign-in form, but with the added security benefit of passkeys if they have one.

How passkey authentication works

To authenticate with a passkey, you use the WebAuthn API.

The four components in a passkey authentication flow are:

  • Backend: Stores user account details, including the public key.
  • Frontend: Communicates with the browser and fetches necessary data from the backend.
  • Browser: Runs your JavaScript and interacts with the WebAuthn API.
  • Passkey provider: Creates and stores the passkey. This is typically a password manager such as Google Password Manager, or a security key.
Passkey authentication flow, showing the interaction between frontend, backend, browser and passkey provider.
Full passkey authentication flow.

The passkeys authentication process follows this flow:

  1. The user visits the sign-in page, and the frontend requests an authentication challenge from the backend.
  2. The backend generates and returns a WebAuthn challenge associated with the user's account.
  3. The frontend calls navigator.credentials.get() with the challenge to initiate authentication using the browser.
  4. The browser, interacting with the passkey provider, prompts the user to select a passkey (often using an autofill dialog triggered by focusing the sign-in field) and verify their identity using the device screen lock or biometrics.
  5. After successful user verification, the passkey provider signs the challenge, and the browser returns the resulting public key credential (including the signature) to the frontend.
  6. The frontend sends this credential to the backend.
  7. The backend verifies the credential's signature against the user's stored public key. If verification succeeds, the backend signs the user in.

Authenticate with a passkey through form autofill

To initiate passkey authentication using form autofill, make a conditional WebAuthn get call when the sign-in page loads. This call to navigator.credentials.get() includes the mediation: 'conditional' option.
A conditional request to WebAuthn's navigator.credentials.get() API does not show UI immediately. Instead, it waits in a pending state until the user interacts with the username field's autofill prompt. If the user selects a passkey, the browser resolves the pending promise with a credential to sign the user in, bypassing the traditional form submission. If the user chooses a password instead, the promise is not resolved, and the standard password sign-in flow continues.rm. It's then the page's responsibility to sign the user in.

Annotate form input field

To enable passkey autofill, add the autocomplete attribute to your form's username input field. Include both username and webauthn as space-separated values.

<input type="text" name="username" autocomplete="username webauthn" autofocus>

Adding autofocus to this field automatically triggers the autofill prompt on page load, showing available passwords and passkeys immediately.

Feature detection

Before invoking a conditional WebAuthn API call, check if:

  • The browser supports WebAuthn with PublicKeyCredential.

Browser Support

  • Chrome: 67.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

Browser Support

  • Chrome: 108.
  • Edge: 108.
  • Firefox: 119.
  • Safari: 16.

Source

The following snippet shows how you can check if the browser supports these features:

// Availability of window.PublicKeyCredential means WebAuthn is usable.  
if (window.PublicKeyCredential &&  
    PublicKeyCredential.isConditionalMediationAvailable) {  
  // Check if conditional mediation is available.  
  const isCMA = await PublicKeyCredential.isConditionalMediationAvailable();  
  if (isCMA) {  
    // Call WebAuthn authentication  
  }  
}  

Fetch information from the backend

Your backend needs to provide several options to the frontend to initiate the navigator.credentials.get() call. These options are typically fetched as a JSON object from an endpoint on your server.

Key properties in the options object include:

  • challenge: A server-generated challenge in an ArrayBuffer (typically Base64URL encoded for JSON transport). This is essential to prevent replay attacks. Your server must generate a fresh challenge for every sign-in attempt and should invalidate it after a short time or if an attempt fails.
  • allowCredentials: An array of credential descriptors. Pass an empty array. This prompts the browser to list all credentials for the specified rpId.
  • userVerification: Specifies your preference for user verification, like requiring a device screen lock. The default and recommended value is "preferred". Possible values are:

    • "required": User verification must be performed by the authenticator (such as PIN or biometrics). The operation fails if verification cannot be performed.
    • "preferred": The authenticator attempts user verification, but the operation can succeed without it.
    • "discouraged": The authenticator should avoid user verification if possible.
  • rpId: Your relying party ID, typically your website's domain (such as example.com). This value must exactly match the rp.id used when the passkey credential was created.

Your server should construct this options object. ArrayBuffer values (like the challenge) must be Base64URL encoded for JSON transport. On the frontend, after parsing the JSON, use PublicKeyCredential.parseRequestOptionsFromJSON() to convert the object (including decoding Base64URL strings) into the format navigator.credentials.get() expects.

The following code snippet shows how you can fetch and decode the information needed to authenticate with a passkey.

// 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);
...

Call WebAuthn API with the conditional flag to authenticate the user

Once you have the publicKeyCredentialRequestOptions object (referred to as options in the example code below) prepared, call navigator.credentials.get() to initiate the conditional passkey authentication.

// 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'
});

Key parameters for this call:

  • publicKey: This must be the publicKeyCredentialRequestOptions object (named options in the example) that you fetched from your server and processed in the previous step.
  • signal: Passing an AbortController's signal (like abortController.signal) lets you programmatically cancel the get() request. This is useful when you want to invoke another WebAuthn call.
  • mediation: 'conditional': This is the crucial flag that makes the WebAuthn call conditional. It tells the browser to wait for user interaction with an autofill prompt rather than immediately showing a modal dialog.

Send the returned public key credential to the RP server

AIf the user selects a passkey and successfully verifies their identity (for instance, using their device screen lock), the navigator.credentials.get() promise resolves. This returns a PublicKeyCredential object to your frontend.

The promise can be rejected for several reasons. You should handle these errors in your code by checking the name property of the Error object:

  • NotAllowedError: The user canceled the operation, or no passkey was selected.
  • AbortError: The operation was aborted, possibly by your code using an AbortController.
  • Other exceptions: An unexpected error occurred. The browser typically shows an error dialog to the user.

The PublicKeyCredential object contains several properties. Key properties relevant for authentication include:

  • id: The base64url encoded ID of the authenticated passkey credential.
  • rawId: An ArrayBuffer version of the credential ID.
  • response.clientDataJSON: An ArrayBuffer of client data. This field contains information such as the challenge and the origin your server must verify.
  • response.authenticatorData: An ArrayBuffer of authenticator data. This field includes information such as the RP ID.
  • response.signature: An ArrayBuffer containing the signature. This value is the core of the credential and your server must verify this signature using the stored public key for the credential .
  • response.userHandle: An ArrayBuffer that contains the user ID provided during passkey registration.
  • authenticatorAttachment: Indicates if the authenticator is part of the client device (platform) or external (cross-platform). A cross-platform attachment can occur if the user signed in with a phone. In such cases, consider prompting them to create apasskey on the current device for future convenience.
  • type: This field is always set to "public-key".

To send this PublicKeyCredential object to your backend, first call the .toJSON() method. This method creates a JSON-serializable version of the credential, which correctly handles the conversion of ArrayBuffer properties (like rawId, clientDataJSON, authenticatorData, signature, and userHandle) to Base64URL encoded strings. Then, use JSON.stringify() to convert this object into a string and send it in the body of your request to the server.

...
// 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
});

Verify the signature

When your backend server receives the public key credential, it must verify its authenticity. This involves:

  1. Parsing the credential data.
  2. Looking up the stored public key associated with the credential's id.
  3. Verifying the received signature against the stored public key.
  4. Validating other data, such as the challenge and origin.

We recommend using a server-side FIDO/WebAuthn library to handle these cryptographic operations securely. You can find open source libraries in the awesome-webauthn GitHub repo.

If the signature and all other assertions are valid, the server can sign the user in. For detailed server-side validation steps, see Server-side passkey authentication

Signal if the matching credentials are not found on the backend

If your backend server cannot find a credential with a matching ID during sign-in, the user might have previously deleted this passkey from your server but not from their passkey provider. This mismatch can lead to a confusing user experience if the passkey provider continues to suggest a passkey that no longer works with your site. To improve this, you should signal the passkey provider to remove the orphaned passkey.

You can use the PublicKeyCredential.signalUnknownCredential() method, part of the Webauthn Signal API, to inform the passkey provider that the specified credential has been removed or does not exist. Call this static method on the client-side if your server indicates (for example, with a specific HTTP status code like 404) that a presented credential ID is unknown. Provide the RP ID and the unknown credential ID to this method. The passkey provider, if it supports the signal, should remove the passkey.

// 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.
    ...
  }
}

After authentication

Depending on how the user signed in, we suggest different flows to follow.

If the user has signed in without a passkey

If the user has signed in to your website without a passkey, they might not have a passkey registered for that account or on their current device. This is an opportune moment to encourage passkey creation. Consider the following approaches:

  • Upgrade passwords to passkeys: Use conditional create, a WebAuthn feature that allows the browser to automatically create a passkey for the user after a successful password sign-in. This can significantly improve passkey adoption by simplifying the creation process. Learn how it works and how to implement it at Help users adopt passkeys more seamlessly
  • Manually prompt for passkey creation: Encourage users to create a passkey. This can be effective after a user completes a more involved sign-in process, such as multi-factor authentication (MFA). However, avoid excessive prompts, which can be intrusive to the user experience."

To see how you can encourage users to create a passkey and learn other good practices, see the examples on Communicating passkeys to users.

If the user has signed in with a passkey

After a user successfully signs in with a passkey, you have several opportunities to further enhance their experience and maintain account consistency.

Encourage creating a new passkey after a cross-device authentication

If a user signs in with a passkey using a cross-device mechanism (for example, scanning a QR code with their phone), the passkey they used might not be stored locally on the device they are signing into. This can happen when:

  • They have a passkey but on a passkey provider that doesn't support the signing-in operating system or browser.
  • They have lost access to the passkey provider on the signing-in device, but a passkey is still available on another.

In this situation, consider prompting the user to create a new passkey on the current device. This can save them from repeating the cross-device sign-in process in the future. To determine whether the user has signed in using a cross-device passkey, check the authenticatorAttachment property of the credential. If its value is "cross-platform" it indicates a cross-device authentication. If so, explain the convenience of creating a new passkey and guide them through the creation process.

Synchronize passkey details with the provider using signals

To ensure consistency and a better user experience, your Relying Party (RP) can use WebAuthn Signals API to communicate updates about credentials and user information to the passkey provider.

For example, to keep the passkey provider's list of a user's passkeys accurate, keep the credentials on the backend in-sync. You can signal that a passkey does not exist anymore so that the passkey providers can remove unnecessary passkeys.

Similarly, you can signal if a user updates their username or display name on your service, to help keep the user information displayed by the passkey provider (for example, in account selection dialogs) up to date.

To learn more about the good practices to keep passkeys consistent, see Keep passkeys consistent with credentials on your server with the Signal API.

Don't ask for a second factor

Passkeys offer robust, built-in protection against common threats like phishing. Therefore, a second authentication factor does not add significant security value. Instead, it creates an unnecessary step for users during sign-in.

Checklist

  • Allow users to sign in with a passkey through form autofill.
  • Signal when a passkey's matching credential is not found on the backend.
  • Prompt users to manually create a passkey if the user hasn't created one yet after a sign-in.
  • Automatically create a passkey (conditional create) after the user signs in with a password (and a second factor).
  • Prompt for local passkey creation if the user has signed in with a cross-device passkey.
  • Signal the list of available passkeys and updated user details (username, display name) to the provider after sign-in or when changes occur.

Resources