So far we’ve shown how to integrate the Authsignal pre-built UI into your AWS Cognito login flow to rapidly implement passwordless login using authentication methods like email OTP.

Now we’ll look at how to further enhance this login flow with passkey autofill. Authsignal supports multiple ways of integrating passkeys - by using our pre-built UI or by using our Client SDKs.

In this case we’re going to use Client SDKs because:

  1. We can add passkey autofill directly to an existing app’s login page
  2. We’re using the pre-built UI in popup mode and browser support for passkey registration in cross-origin iframes is inconsistent

Example code

You can find the code changes for adding passkey autofill to the AWS Cognito Amplify example in this branch.

User flow

1

A saved passkey is presented as an option when the user clicks the email input.

2

The user signs in with their passkey.

Prerequisites

You’ll need to enable passkeys as an authenticator in the Authsignal Portal. If you’re running the example app on localhost you can follow these steps to set your Relying Party to localhost and expected origins to http://localhost:5173.

App integration code

The webauthn attribute value

First we need to tell the browser that an input element is available on the page which supports passkey autofill. This can be achieved by setting the autocomplete attribute to webauthn:

<input ... autocomplete="webauthn" />

Initializing passkey autofill

Now we can initialize passkey autofill by calling the Authsignal Web SDK’s passkey.signIn method and setting autofill: true:

authsignal.passkey
  .signIn({ action: "cognitoAuth", autofill: true })
  .then(handlePasskeySignIn);

This code should be run after the page containing the input element has loaded (e.g. in a useEffect hook if using React).

Calling the Amplify SDK

Finally, we need to handle implement the Amplify SDK sign-in methods, which will get called if a user selects a passkey via autofill. We do this by implementing an async function which the Authsignal Client SDK will call after the user has successfully authenticated with their passkey.

async function handlePasskeySignIn(response) {
  // 1. Call the Amplify signIn method passing the username
  await signIn({
    username: response.userName,
    options: {
      authFlowType: "CUSTOM_WITHOUT_SRP",
    },
  });

  // 2. Call the Amplify confirmSignIn method passing the token
  await confirmSignIn({
    challengeResponse: response.token,
  });
}

In the first step, we are initializing the Cognito sign-in flow, passing the Cognito username which has been returned by the Authsignal Client SDK. Then in the second step, we are finalizing the Cognito sign-in flow, passing the Authsignal token for the passkey authentication attempt. This token is validated in the backend by our Verify Auth Challenge Response lambda, using the exact same logic that we previously implemented for email OTP via the pre-built UI.

Creating passkeys

To test using an existing passkey via autofill we first need to be able to create a passkey. This example app prompts the user to create a passkey immediately after they login via the pre-built UI.

const result = await authsignal.passkey.signUp({ token });

if (result) {
  alert("Passkey created!");
}

The Authsignal SDK’s passkey signUp method requires an authenticated user token. To keep the demo simple, we use the token returned from the successful pre-built UI login attempt, which is only valid for 10 minutes. You can also generate a new token in your backend.

Lambda code

Create Auth Challenge

Finally we need to make a minor change to the Create Auth Challenge lambda code. Since the passkey autofill authentication step is performed before the Amplify signIn method is called, we need to look for an existing challenge and pass a challengeId into our track call.

// 1. Check if a challenge has already been initiated via passkey SDK
const { challengeId } = await authsignal.getChallenge({
  action: "cognitoAuth",
  userId,
  verificationMethod: "PASSKEY",
});

// 2. Pass the challengeId as an additional param when tracking the action
const { url } = await authsignal.track({
  action: "cognitoAuth",
  userId,
  email,
  challengeId,
});

If no passkey challenge has been initiated, challengeId will be undefined. This means the Create Auth Challenge lambda will be able to dynamically handle sign-in attempts via passkey autofill as well as via the pre-built UI.

Next steps