Contact salesSign inSign up

Authsignal secures millions of passkey transactions out of our hosted Sydney region.

Authsignal secures millions of passkey transactions out of our hosted Sydney region.

Join us today!
Blog
/
Current article

How to integrate AWS Cognito with Authsignal to rapidly implement passwordless login and MFA.

Last Updated:
October 1, 2024
Chris Fisher
How to pair AWS Cognito with Authsignal to rapidly implement passwordless login and MFA

This blog post is part 1 in a series of blog posts.

AWS Cognito has a flexible and powerful integration model for implementing custom MFA and passwordless login flows using Lambda triggers. While the out-of-the-box options for MFA which Cognito offers may be limited - namely SMS and authenticator app - its custom authentication challenge model provides a great way to offer users a wider range of authentication options. By pairing Cognito with Authsignal, you can offer email OTP, email magic link, SMS OTP (including via WhatsApp), authenticator app, passkeys or security keys, push, and facial biometrics. Authsignal enable you to deploy step up authentication with Cognito.

Passwords vs passwordless

This blog post will focus on how to implement passwordless login using the Authsignal pre-built UI with email OTP as an authentication method.

However, if you still need to support passwords, you can use Authsignal to present a secondary MFA challenge after Cognito has validated the user’s password.

Full example code

You can find the full code example for passwordless login on Github and the changes required for password + MFA on this branch.

The custom authentication challenge lambdas

The example requires four lambdas, which can be deployed into your AWS environment and then connected to your user pool.

Create Auth Challenge lambda

This lambda uses the Authsignal Node.js SDK to return a url back to the app, which can be passed to the Authsignal Web SDK to launch the Authsignal pre-built UI.

export const handler: CreateAuthChallengeTriggerHandler = async (event) => {
  // This can be any value which defines your login action
  const action = "cognitoAuth";

  const { url } = await authsignal.track({
    action,
    userId: event.request.userAttributes.sub,
    email: event.request.userAttributes.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
) => {
  // This must be the same value used in the previous lambda
  const action = "cognitoAuth";
  
  const { state } = await authsignal.validateChallenge({
    action,
    userId: event.request.userAttributes.sub,
    token: event.request.challengeAnswer,
  });

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

  return event;
};

The app code

The Amplify SDK and the Authsignal SDK can be used together to implement passwordless login with a minimal amount of code.

Step 1 - Call the Amplify SDK signIn method

First, we need to initiate sign-in by calling the Amplify signIn method and passing the email which the user has entered. This will invoke the Create Auth Challenge lambda, which we implemented previously, and return an Authsignal URL, which we’ll use to present an email OTP challenge. We set the authFlowType here to “CUSTOM_WITHOUT_SRP” because we’re not including a password.

const { nextStep } = await signIn({
  username: email,
  options: {
    authFlowType: "CUSTOM_WITHOUT_SRP",
  },
});

const url = nextStep.additionalInfo.url;

Step 2 - Call the Authsignal SDK launch method

In this step, we pass the URL obtained from the previous step to the Authsignal SDK launch method. We set the mode to "popup," so the challenge is presented modally within an iframe on the same page. Once the user completes the challenge, the SDK asynchronously returns a token, which we’ll validate in the final step.

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

Step 3 - Call the Amplify SDK confirmSignIn method

Finally, we pass the token returned by the Authsignal SDK to the Amplify SDK confirmSignIn method. This will invoke the Verify Auth Challenge Response lambda and validate that the user has successfully completed the email OTP challenge in the previous step.

const {isSignedIn} = await confirmSignIn({ challengeResponse: token });

If the Authsignal token is valid, Amplify will create an authenticated session and issue an access token for the user.

Using the AWS SDK instead of Amplify

As the steps outlined above demonstrate, using Authsignal together with Amplify makes it easy to implement passwordless login in just a few steps. But it’s also simple to use the @aws-sdk/client-cognito-identity-provider library instead of Amplify. Because of the way that Amplify SDK maintains state between its signIn and confirmSignIn calls, it doesn’t work well if you need to redirect to another page in between these steps. This means that using the Authsignal pre-built UI in redirect mode isn’t possible. However, with the AWS SDK, you have the flexibility to launch the pre-built UI in either redirect mode or in popup mode. Below, we’ll cover how to use the AWS SDK to launch the pre-built UI in redirect mode.

Full example code

You can find a full example using Authsignal with the AWS SDK on Github.

Modifying the Create Auth Challenge lambda

Since we’re using redirect mode, we’ll need to specify the URL which the Authsignal pre-built UI should redirect back to once the user has completed the email OTP challenge. We do this by modifying the track call in the Create Auth Challenge lambda code.

export const handler: CreateAuthChallengeTriggerHandler = async (event) => {
  const { url } = await authsignal.track({
    action: "cognitoAuth",
    userId: event.request.userAttributes.sub,
    email: event.request.userAttributes.email,
    redirectUrl: "http://localhost:5173/callback"
  });

  event.response.publicChallengeParameters = { url };

  return event;
};

Modifying the app code

Step 1 - Use InitiateAuthCommand from the AWS SDK

This will invoke the Create Auth Challenge lambda, which we implemented above, and return an Authsignal URL, which we’ll use to present an email OTP challenge.

const input = {
  ClientId: "YOUR_COGNITO_CLIENT_ID",
  AuthFlow: AuthFlowType.CUSTOM_AUTH,
  AuthParameters: {
    USERNAME: email,
  },
};

const output = await cognitoClient.send(new InitiateAuthCommand(input));
 
const url = output.ChallengeParameters.url;

localStorage.setItem("username", email);
localStorage.setItem("session", output.Session);

In addition to obtaining the url, we also persist the Cognito username and session in local storage - this enables us to retrieve them again later after the user has been redirected back from the pre-built UI.

Step 2 - Call the Authsignal SDK launch method

In this step, we pass the URL obtained from the previous step to the Authsignal SDK launch method. If not specified, the SDK defaults to redirect mode, so we’ll use that.

authsignal.launch(url);

Unlike the Amplify example above, we can’t await the result of this launch call. Instead, we have to implement a callback route, which the Authsignal pre-built UI will redirect back to after the challenge. This route will handle the logic of validating the result of the challenge and finalizing sign-in.

Step 3 - Use the RespondToAuthChallengeCommand from the AWS SDK

Our callback route will grab the Authsignal validation token from URL search params and pass it to the Verify Auth Challenge Response lambda via the RespondToAuthChallengeCommand, along with the username and session we persisted in local storage in the first step.

const input = {
  ClientId: "YOUR_COGNITO_CLIENT_ID",
  ChallengeName: ChallengeNameType.CUSTOM_CHALLENGE,
  Session: localStorage.getItem("session"),
  ChallengeResponses: {
    USERNAME: localStorage.getItem("username"),
    ANSWER: JSON.stringify({ 
      token: (new URLSearchParams(window.location.search)).get('token'),
    }),
  },
};

const output = await cognitoClient.send(new RespondToAuthChallengeCommand(input));
 
const accessToken = output.AuthenticationResult?.AccessToken;
const refreshToken = output.AuthenticationResult?.RefreshToken;

// Persist the Cognito accessToken/refreshToken in local storage or as required

For more detail, you can follow our guide on integrating Authsignal with the AWS SDK from your app.

Using the AWS SDK from your backend

It’s also possible to use the AWS SDK from your backend in a server-side integration. In this case, you would use the AdminInitiateAuth command and the AdminRespondToAuthChallenge command. For more detail on this approach, you can refer to our guide on integrating Authsignal with the AWS SDK from your backend. Note that this approach requires configuring your user pool settings in a slightly different way as you need to use a client secret when authenticating to Cognito from your backend.

Summary

It can sometimes feel confusing to navigate all the different ways to integrate with AWS Cognito. In this blog post, we’ve outlined some of the key ways to integrate and how Authsignal can help significantly speed up the implementation process.

Try out our passkey demo
Passkey Demo
Subscribe to our monthly newsletter
Subscribe
You might also like
Add MFA to Keycloak using Authsignal: A Step-by-Step Guide
Authsignal offers an easy-to-integrate solution that simplifies the process of adding MFA to Keycloak.
Authsignal in partnership with MATTR claims authentication world first, binding Mobile Driver’s License (mDL) to Palm Biometrics
Authsignal has launched a world-first solution that binds a mobile driver's license (mDL) with Palm Biometrics.
Biometrics Passkey-Binding: Ensure Digital Credential Ownership and Real Human Presence
Learn about biometric passkey-binding pairs facial recognition with cryptographic passkeys for secure, seamless authentication, protecting against phishing, deepfakes, and fraud while improving user experience.
Secure your customers’ accounts today with Authsignal.