Implementing a combination autofill / conditional UI + modal flow

In this section we are going to explore how to create an authentication flow that can leverage both the modal and autofill login experiences. There are a variety of considerations that need to be taken into account, due to behaviors specified by the WebAuthn methods. This section will provide an example of the flow, code samples, and helper methods to help create your authentication flow.

Application demo

Before we proceed to the implementation guidance, let’s first take a look at what will be created. The example below will demonstrate a login page that is capable of using:

  • Autofill passkey credentials

  • Modal passkey credentials

  • Modal with a non-empty allowCredentials list

Figure 1 will walk through the three flows mentioned above, all leveraged from the same logic page.

Figure 1

Prerequisites

In order to leverage autofill and passkeys in your application, you need to ensure that your browser supports autofill and that your operating system supports passkeys.

To see if your browser supports autofill you can:

You will also need a WebAuthn Relying Party to provide the PublicKeyCredentialRequestOptions to trigger and verify the authentication ceremony. Please see our WebAuthn primer for developers for options to deploy a WebAuthn RelyingParty.

Enabling autofill on the UI

The first step will be to introduce an input tag on your page for the user to enter in their username, that also supports autofill for WebAuthn. This step is as straightforward as creating an input tag and adding the autocomplete attribute with the value of username webauthn.

Figure 2 shows sample code on how to enable WebAuthn autofill in your username input field

<input type="text" id="username-field" autoComplete="username webauthn" />

Figure 2

Note

The casing for autoComplete is typically just autocomplete, but our example has the capital C due to behaviors from React.

This will prompt the user with a credential if one is discovered on the device that matches the current origin of the webpage.

Buttons to trigger WebAuthn ceremonies

You may have noticed in the previous article, Simple Autofill Flow, that when a user opts to use autofill, they don’t enter anything, or press any buttons other than the username in the drop down list.

But in our demo video above, we have two other options that a user can leverage, both centered around the modal WebAuthn experience.

The first flow is to use a user identifier to ask the relying party to send a PublicKeyCredentialsRequestOptions with an allow credentials list containing the IDs of the user’s registered credentials.

The second flow is the discoverable credentials flow. This is similar to the autofill flow as it also leverages passkeys on a device. While this flow acts similar to the autofill flow, you should still include it in the case that a user’s browser doesn’t support autofill, or the OS doesn’t support passkeys.

To accommodate both flows above, we will add two different buttons - One for login using a username, and one for discoverable credentials.

Figure 3 includes the first button used to trigger a non-discoverable credential modal flow

<Button
  type="submit"
  onClick={signIn}>
  Continue
</Button>

Figure 3

Figure 4 includes the second button used to trigger a discoverable credential flow

<Button
  type="submit"
  onClick={usernamelessLogin}>
  Continue with Trusted Device or Security Key
</Button>

Figure 4

Initiating the authentication ceremony

As noted before there are three different flows that a user can take when they visit this page. In this section we are going to highlight how each of these different flows can be triggered, and the nuanced differences between them.

Before we begin let’s take a look at how this page will flow through the different paths that a user may take.

Figure 5 demonstrates a sample flow for a page that leverages both modal and autofill options.

../Images/autofill_modal_1.png

Figure 5

Aborting autofill requests

One of the major things to note above is the need to abort a conditional mediation request that is active. Unlike the modal experience, the autofill menus do not include a button to cancel a request.

From a specification standpoint, the intended behavior is for only ONE active credential request to be active at a time. This issue can be viewed here, at the WebAuthn GitHub page.

The WebAuthn specification does note that an AbortController can be used to trigger the cancellation of a WebAuthn ceremony. This can be used by both the modal and autofill flows. More information can be found in the WebAuthn specification at this link.

Keep the information above in mind, as we will demonstrate its use in the following sections.

Triggering the autofill flow

We’ll begin by implementing the autofill flow. We are starting with autofill as it is the first step in the sequence shown in Figure 5. This flow will be immediately triggered whenever a user loads into this page.

The first thing you want to do is to create an AbortController object that is global in scope. This will be used as a signal by your client application to end the autofill request if it needs to make room for a modal request.

Ensure that the AbortController exists in a scope where it can be triggered by other methods, and reinitialized if the autofill flow needs to be reenabled.

Figure 6 demonstrates sample code that can be used to initialize an AbortController

const [authAbortController, setAuthAbortController] = useState(
  new AbortController()
);

Figure 6

Next we will trigger the autofill request. We will do this by calling the method passkeySignIn() whenever the user navigates to the page.

Figure 7 demonstrates the method passkeySignIn(). This is the method that will handle authentication, if a user selects one of their passkeys.

async function passkeySignIn() {
  try {
    setAuthAbortController(new AbortController());
    // Reaching out to Cognito for auth challenge
    let requestOptions = await WebAuthnClient.getPublicKeyRequestOptions();

    const credential = await get({
      publicKey: requestOptions.publicKeyCredentialRequestOptions,
      mediation: "conditional",
      signal: authAbortController.signal
    });

    const userData = await WebAuthnClient.sendChallengeAnswer(credential);
    navigation.go("InitUserStep");
  } catch (error) {
    console.log(error);
  }
};

Figure 7

You may remember this method if you viewed the previous article Implementing a simple autofill / conditional UI flow for passkeys. There is one notable difference between our example here, and the example shown in the previous article.

This will be in the use of the signal property in the get() request. You will add your AbortController signal as the value to new property., This notes to the WebAuthn method that the request should be canceled if the signal has been aborted.

Also ensure that a new AbortController is created whenever autofill is re-invoked. If you attempt to use an AbortController that has already been aborted, then the WebAuthn method will be immediately canceled.

The rest of the method will remain standard in terms of a WebAuthn request. In this example assume that WebAuthnClient is a set of methods used to communicate with your RP. getPublicKeyRequestOptions() will be used to get the authentication challenge, while sendChallengeAnswer() will pass your credential to your relying party. In our example we also opt to use the @github/webauthn-json get() method, rather than the traditional navigator.credentials.get() call.

As with any WebAuthn authentication request your first step will be to call out to the relying party for a challenge to be signed by your credential.

Here is where the primary deviation occurs in relation to the modal flow. Instead of directly passing in an object that contains the publicKey property, you will add a new field to the object. This field is named mediation. You will attach the value conditional to the mediation property. This configuration will trigger the conditional mediation WebAuthn flow.

Note

Removing the mediation property will trigger the modal experience

You will pass the object that contains the publicKey property into the WebAuthn get() method. If successful then you will send your assertion to the relying party.

Triggering the modal flow

Next we will learn how to trigger the modal flow from a button click. Below you will find sample code that can be used by either button to trigger a non-autofill flow. Figure 8 demonstrates sample code that can be used to trigger a modal request

async function signIn() {
  try {
    // Reaching out to Cognito for auth challenge
    let requestOptions = await WebAuthnClient.getPublicKeyRequestOptions();

    const credential = await get({
      publicKey: requestOptions.publicKeyCredentialRequestOptions,
    });

    const userData = await WebAuthnClient.sendChallengeAnswer(credential);
    navigation.go("InitUserStep");
  } catch (error) {
    console.log(error);
    passkeySignIn();
  }
};

Figure 8

Notice how it is extremely similar to the method used for autofill, all that’s missing are the mediation and signal properties.

Another thing to note is in the method’s catch statement. You will notice that if the signIn() method fails, then the passkeySignIn() flow will be re-triggered, allowing for the use of autofill. If this is not done, then no authentication ceremony will take place if the user attempts to select an autofill option.

In the example in Figure 8, the method WebAuthnClient.getPublicKeyRequestOptions() will need variations or behaviors to be able to invoke discoverable and non-discoverable credential flows. The same logic will be used for both modal flows, but the behavior of the modal flow will change depending on if the PublicKeyCredentialRequestOptions includes an allowCredentials list.

You should now be able to use the autofill and modal flow in concert with each other to perform a variety of authentication ceremonies. Stay tuned for more passkey related material to help guide your passwordless implementation strategy.