With Authsignal rules you can require MFA only when certain conditions are met - for example, if a user is authenticating on a new device. Rules can also be used to target users and you can pass your own custom data points as input.

This guide will cover how to modify your AWS Cognito integration code to use rules. It builds on previous guides which show how to integrate AWS Cognito with Authsignal when using Amplify or when using the AWS SDK.

Conditional MFA on login

A common scenario for rules is to control when a secondary MFA step is required after completing a primary authentication step such as username and password.

Define Auth Challenge lambda

In this case we assume the Define Auth Challenge lambda is implemented to require multiple authentication steps.

import { DefineAuthChallengeTriggerHandler } from "aws-lambda";

export const handler: DefineAuthChallengeTriggerHandler = async (event) => {
  const { session } = event.request;

  if (session.length === 1 && session[0].challengeName === "SRP_A") {
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = "PASSWORD_VERIFIER";
  } else if (session.length === 2 && session[1].challengeName === "PASSWORD_VERIFIER") {
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = "CUSTOM_CHALLENGE";
  } else if (
    session.length === 3 &&
    session[2].challengeName === "CUSTOM_CHALLENGE" &&
    session[2].challengeResult === true
  ) {
    event.response.issueTokens = true;
    event.response.failAuthentication = false;
  } else {
    event.response.issueTokens = false;
    event.response.failAuthentication = true;
  }

  return event;
};

The “CUSTOM_CHALLENGE” step is delegated to Authsignal.

Create Auth Challenge lambda

When tracking our action in the Create Auth Challenge lambda we can check the state field in the response to see whether our rule has determined that a challenge is required for the action.

In addition to the url for the pre-built UI, we will also pass this state value along with a token back to our app as public challenge parameters.

import { Authsignal } from "@authsignal/node";
import { CreateAuthChallengeTriggerHandler } from "aws-lambda";

const authsignal = new Authsignal({
  apiSecretKey: process.env.AUTHSIGNAL_SECRET,
  apiUrl: process.env.AUTHSIGNAL_URL,
});

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

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

  event.response.publicChallengeParameters = { state, token, url };

  return event;
};

The app code

Now we can adapt our app code to use the state param to determine whether MFA is required.

// Sign in with username and password using Amplify
const { nextStep } = await signIn({
  username: email,
  password,
  options: {
    authFlowType: "CUSTOM_WITH_SRP",
  },
});

const { state, token, url } = nextStep.additionalInfo;

if (state === "ALLOW") {
  // No MFA is required
  // Pass the initial token to confirm sign-in
  await confirmSignIn({ challengeResponse: token });
} else {
  // MFA is required
  // Launch the pre-built UI to obtain a validation token for an Authsignal challenge
  const response = await authsignal.launch(url, { mode: "popup" });

  await confirmSignIn({ challengeResponse: response.token });
}

Verify Auth Challenge Response lambda

Finally, we need to update our Verify Auth Challenge Response lambda to handle if the user is allowed to bypass MFA.

The only change here is to set event.response.answerCorrect to true if the state of the action is either “CHALLENGE_SUCCEEDED” (because the user successfully completed an MFA step) or “ALLOW” (because the user wasn’t required to complete MFA).

import { Authsignal, UserActionState } from "@authsignal/node";
import { VerifyAuthChallengeResponseTriggerHandler } from "aws-lambda";

const authsignal = new Authsignal({
  apiSecretKey: process.env.AUTHSIGNAL_SECRET,
  apiUrl: process.env.AUTHSIGNAL_URL,
});

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

  const { state } = await authsignal.validateChallenge({
    action: "cognitoAuth",
    userId,
    token,
  });

  event.response.answerCorrect =
    state === UserActionState.CHALLENGE_SUCCEEDED || state === UserActionState.ALLOW;

  return event;
};

Next steps