Amplify has re-imagined the way frontend developers build fullstack applications. Develop and deploy without the hassle.

Page updated Sep 12, 2024

Custom Auth Challenge

Secure Remote Password (SRP) is a cryptographic protocol enabling password-based authentication without transmitting the password over the network. In Amazon Cognito custom authentication flows, CUSTOM_WITH_SRP incorporates SRP steps for enhanced security, while CUSTOM_WITHOUT_SRP bypasses these for a simpler process. The choice between them depends on your application's security needs and performance requirements. This guide demonstrates how to implement both types of custom authentication flows using AWS Amplify with Lambda triggers.

You can use defineAuth and defineFunction to create an auth experience that uses CUSTOM_WITH_SRP and CUSTOM_WITHOUT_SRP. This can be accomplished by leveraging Amazon Cognito's feature to define a custom auth challenge and 3 triggers:

  1. Create auth challenge
  2. Define auth challenge
  3. Verify auth challenge response

To get started, install the aws-lambda package, which is used to define the handler type.

Terminal
npm add --save-dev @types/aws-lambda

Create auth challenge trigger

To get started, create the first of the three triggers, create-auth-challenge. This is the trigger responsible for creating the reCAPTCHA challenge after a password is verified.

amplify/auth/create-auth-challenge/resource.ts
import { defineFunction } from "@aws-amplify/backend"
export const createAuthChallenge = defineFunction({
name: "create-auth-challenge",
})

After creating the resource file, create the handler with the following contents:

amplify/auth/create-auth-challenge/handler.ts
import type { CreateAuthChallengeTriggerHandler } from "aws-lambda";
export const handler: CreateAuthChallengeTriggerHandler = async (event) => {
if (event.request.challengeName === "CUSTOM_CHALLENGE") {
// Generate a random code for the custom challenge
const challengeCode = "123456";
event.response.challengeMetadata = "TOKEN_CHECK";
event.response.publicChallengeParameters = {
trigger: "true",
code: challengeCode,
};
event.response.privateChallengeParameters = { trigger: "true" };
event.response.privateChallengeParameters.answer = challengeCode;
}
return event;
};

Define auth challenge trigger

Next, you will want to create the trigger responsible for defining the auth challenge flow, define-auth-challenge.

amplify/auth/define-auth-challenge/resource.ts
import { defineFunction } from "@aws-amplify/backend"
export const defineAuthChallenge = defineFunction({
name: "define-auth-challenge",
})

After creating the resource file, create the handler with the following contents if you are using CUSTOM_WITHOUT_SRP:

amplify/auth/define-auth-challenge/handler.ts
import type { DefineAuthChallengeTriggerHandler } from "aws-lambda"
export const handler: DefineAuthChallengeTriggerHandler = async (event) => {
// Check if this is the first authentication attempt
if (event.request.session.length === 0) {
// For the first attempt, we start with the custom challenge
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = "CUSTOM_CHALLENGE";
} else if (
event.request.session.length === 1 &&
event.request.session[0].challengeName === "CUSTOM_CHALLENGE" &&
event.request.session[0].challengeResult === true
) {
// If this is the second attempt (session length 1),
// it was a CUSTOM_CHALLENGE, and the result was successful
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
// If we reach here, it means either:
// 1. The custom challenge failed
// 2. We've gone through more attempts than expected
// In either case, we fail the authentication
event.response.issueTokens = false;
event.response.failAuthentication = true;
}
return event;
};

Or if you are using CUSTOM_WITH_SRP:

amplify/auth/define-auth-challenge/handler.ts
import type { DefineAuthChallengeTriggerHandler } from "aws-lambda"
export const handler: DefineAuthChallengeTriggerHandler = async (event) => {
// First attempt: Start with SRP_A (Secure Remote Password protocol, step A)
if (event.request.session.length === 0) {
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = "SRP_A";
} else if (
event.request.session.length === 1 &&
event.request.session[0].challengeName === "SRP_A" &&
event.request.session[0].challengeResult === true
) {
// Second attempt: SRP_A was successful, move to PASSWORD_VERIFIER
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = "PASSWORD_VERIFIER";
} else if (
event.request.session.length === 2 &&
event.request.session[1].challengeName === "PASSWORD_VERIFIER" &&
event.request.session[1].challengeResult === true
) {
// Third attempt: PASSWORD_VERIFIER was successful, move to CUSTOM_CHALLENGE
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = "CUSTOM_CHALLENGE";
} else if (
event.request.session.length === 3 &&
event.request.session[2].challengeName === "CUSTOM_CHALLENGE" &&
event.request.session[2].challengeResult === true
) {
// Fourth attempt: CUSTOM_CHALLENGE was successful, authentication complete
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
// If we reach here, it means one of the challenges failed or
// we've gone through more attempts than expected
event.response.issueTokens = false;
event.response.failAuthentication = true;
}
return event;
};

Verify auth challenge response trigger

Lastly, create the trigger responsible for verifying the challenge response. For the purpose of this example, the verification check will always return true.

amplify/auth/verify-auth-challenge-response/resource.ts
import { defineFunction, secret } from "@aws-amplify/backend"
export const verifyAuthChallengeResponse = defineFunction({
name: "verify-auth-challenge-response",
})

After creating the resource file, create the handler with the following contents:

amplify/auth/verify-auth-challenge-response/handler.ts
import type { VerifyAuthChallengeResponseTriggerHandler } from "aws-lambda"
export const handler: VerifyAuthChallengeResponseTriggerHandler = async (
event
) => {
event.response.answerCorrect = true;
return event;
};

Configure auth resource

Finally, import and set the three triggers on your auth resource:

amplify/auth/resource.ts
import { defineAuth } from "@aws-amplify/backend"
import { createAuthChallenge } from "./create-auth-challenge/resource"
import { defineAuthChallenge } from "./define-auth-challenge/resource"
import { verifyAuthChallengeResponse } from "./verify-auth-challenge-response/resource"
/**
* Define and configure your auth resource
* @see https://docs.amplify.aws/gen2/build-a-backend/auth
*/
export const auth = defineAuth({
loginWith: {
email: true,
},
triggers: {
createAuthChallenge,
defineAuthChallenge,
verifyAuthChallengeResponse,
},
})

After deploying the changes, whenever a user attempts to sign in with CUSTOM_WITH_SRP or CUSTOM_WITHOUT_SRP, the Lambda challenges will be triggered.