AWS Cognito has a flexible integration model with support for passwordless authentication flows via Lambda triggers.

For example this AWS blog post demonstrates how to use Lambda triggers with Amplify to build a passwordless authentication flow with OTP codes sent via email.

This guide will demonstrate how to use the Authsignal pre-built UI to present a challenge. This can be achieved in just a few lines of code by using our Node.js SDK in the lambdas as well as our Web SDK in the client.

The example uses Email OTP but the same approach can also be used for other authentication methods such as Authenticator App, SMS or WhatsApp OTP, or Email Magic Link.

Example repo

You can find the full code example on Github.

This example uses major version 6 of the aws-amplify js client library. If you are using version 5 then there are some minor differences which you can see here.

User flow

1

The user enters their email to sign in

Demo app sign-in page
2

The app launches the pre-built UI to present a challenge

Demo app launching the pre-built UI

Installation

yarn install

Configuration

Rename the example env config file from .env.example to .env then update it with values for your Authsignal tenant and Cognito user pool.

The AWS lambda triggers

The example repo contains four lambdas which can be deployed to your AWS environment.

Once deployed, these lambdas can be connected to your Cognito user pool:

iOS passkey prompt.

Create Auth Challenge lambda

This lambda uses the Authsignal Node.js SDK to return a short-lived token back to the app which can be passed to the Authsignal Web SDK to launch the Authsignal pre-built UI in a popup:

export const handler: CreateAuthChallengeTriggerHandler = async (event) => {
  const userId = event.request.userAttributes.sub;
  const email = event.request.userAttributes.email;

  const { url } = await authsignal.track({
    action: "cognitoAuth",
    userId,
    email,
  });

  event.response.publicChallengeParameters = { url };

  return event;
};

Verify Auth Challenge Response lambda

This lambda takes the result token returned by the Authsignal Web SDK and passes it to the Authsignal Node.js SDK to validate the result of the challenge:

export const handler: VerifyAuthChallengeResponseTriggerHandler = async (
  event
) => {
  const userId = event.request.userAttributes.sub;
  const token = event.request.challengeAnswer;

  const { state } = await authsignal.validateChallenge({ userId, token });

  event.response.answerCorrect = state === "CHALLENGE_SUCCEEDED";

  return event;
};

The React web app

Running the app

Run the app with the following command:

yarn dev

Sign up

The example app only has a “Sign in” page - as part of this flow we try to create the user in Cognito first and if they already exist we simply ignore the error and continue.

try {
  const signUpInput: SignUpInput = {
    username: email,
    password: Math.random().toString(36).slice(-16) + "X", // Dummy value - never used
    options: {
      userAttributes: {
        email,
      },
    },
  };

  await signUp(signUpInput);
} catch (ex) {
  if (ex instanceof Error && ex.name !== "UsernameExistsException") {
    throw ex;
  }
}

Similar to the example in this AWS blog post, a dummy password is randomly generated because Amplify requires one when signing up, but it won’t actually be used.

Sign in

We call the Amplify signIn method, which invokes the Create Auth Challenge lambda and returns a URL for the pre-built UI. We pass this URL to the Authsignal Web SDK, which opens it in a popup or modal to present the challenge. Once the user has completed the challenge, the Authsignal Web SDK returns a token. We pass this token back to the Amplify confirmSignIn method, which invokes the Verify Auth Challenge Response lambda.

const signInInput = {
  username: email,
  options: {
    authFlowType: "CUSTOM_WITHOUT_SRP",
  },
};

const { nextStep } = await signIn(signInInput);

const url = nextStep.additionalInfo.url;

const { token } = await authsignal.launch(url, { mode: "popup" });

await confirmSignIn({ challengeResponse: token });

Next steps