Page updated Nov 14, 2023

Sign-in with custom flow

The Auth category can be configured to perform a custom authentication flow defined by you. The following guide shows how to setup a simple passwordless authentication flow.

Prerequisites

  • An Android application targeting at least Android SDK API level 24 with Amplify libraries integrated

Configure Auth Category

In terminal, navigate to your project, run amplify add auth, and choose the following options:

1? Do you want to use the default authentication and security configuration? Manual configuration?
2 `Select the authentication/authorization services that you want to use: User Sign-Up, Sign-In, connected with AWS IAM controls (Enables per-user Storage features for images or other content, Analytics, and more)`
3? Please provide a friendly name for your resource that will be used to label this category in the project:
4 `<hit enter to take default or enter a custom label>`
5? Please enter a name for your identity pool.
6 `<hit enter to take default or enter a custom name>`
7? Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM)
8 `No`
9? Do you want to enable 3rd party authentication providers in your identity pool?
10 `No`
11? Please provide a name for your user pool:
12 `<hit enter to take default or enter a custom name>`
13? How do you want users to be able to sign in?
14 `Username`
15? Do you want to add User Pool Groups?
16 `No`
17? Do you want to add an admin queries API?
18 `No`
19? Multifactor authentication (MFA) user login options:
20 `OFF`
21? Email based user registration/forgot password:
22 `Enabled (Requires per-user email entry at registration)`
23? Please specify an email verification subject:
24 `Your verification code`
25? Please specify an email verification message:
26 `Your verification code is {####}`
27? Do you want to override the default password policy for this User Pool?
28 `No`
29? What attributes are required for signing up?
30 `Email`
31? Specify the app's refresh token expiration period (in days):
32 `30`
33? Do you want to specify the user attributes this app can read and write?
34 `No`
35? Do you want to enable any of the following capabilities?
36 `NA`
37? Do you want to use an OAuth flow?
38 `No`
39? Do you want to configure Lambda Triggers for Cognito?
40 `Yes`
41? Which triggers do you want to enable for Cognito?
42 `Create Auth Challenge, Define Auth Challenge, Verify Auth Challenge Response`
43? What functionality do you want to use for Create Auth Challenge?
44 `Custom Auth Challenge Scaffolding (Creation)`
45? What functionality do you want to use for Define Auth Challenge?
46 `Custom Auth Challenge Scaffolding (Definition)`
47? What functionality do you want to use for Verify Auth Challenge Response?
48 `Custom Auth Challenge Scaffolding (Verification)`
49
50? Do you want to edit your boilerplate-create-challenge function now?
51 `Yes`
52? Please edit the file in your editor: <local file path>/src/boilerplate-create-challenge.js

The boilerplate for Create Auth Challenge opens in your favorite code editor. Enter the following code in this file:

1//crypto-secure-random-digit is used here to get random challenge code - https://github.com/ottokruse/crypto-secure-random-digit
2const digitGenerator = require('crypto-secure-random-digit');
3
4function sendChallengeCode(emailAddress, secretCode) {
5 // Use SES or custom logic to send the secret code to the user.
6}
7
8function createAuthChallenge(event) {
9 if (event.request.challengeName === 'CUSTOM_CHALLENGE') {
10 // Generate a random code for the custom challenge
11 const challengeCode = digitGenerator.randomDigits(6).join('');
12
13 // Send the custom challenge to the user
14 sendChallengeCode(event.request.userAttributes.email, challengeCode);
15
16 event.response.privateChallengeParameters = {};
17 event.response.privateChallengeParameters.answer = challengeCode;
18 }
19}
20
21exports.handler = async (event) => {
22 createAuthChallenge(event);
23};

Note that the sendChallengeCode method is empty. You can use an AWS service like SES to setup email delivery and populate the function sendChallengeCode to send the challenge code to the user.

Amazon Cognito invokes the Create Auth Challenge trigger after Define Auth Challenge to create a custom challenge. In this lambda trigger, you define the challenge to present to the user. privateChallengeParameters contains all the information to validate the response from the user. Save and close the file. Now open the file under "<your_android_project>/amplify/backend/function/<project_code>CreateAuthChallenge/src/package.json" and add the following:

1"dependencies": {
2 "crypto-secure-random-digit": "^1.0.9"
3}

Save and close the file, then switch back to the terminal and follow the instructions:

1? Press enter to continue
2 `Hit Enter`
3
4? Do you want to edit your boilerplate-define-challenge function now?
5 `Yes`
6? Please edit the file in your editor: <local file path>/src/boilerplate-define-challenge.js

The boilerplate for Define Auth Challenge opens in your favorite code editor. Enter the following code in this file:

1exports.handler = async function (event) {
2 if (event.request.session.length == 0) {
3 event.response.issueTokens = false;
4 event.response.failAuthentication = false;
5 event.response.challengeName = 'CUSTOM_CHALLENGE';
6 } else if (
7 event.request.session.length == 1 &&
8 event.request.session[0].challengeName == 'CUSTOM_CHALLENGE' &&
9 event.request.session[0].challengeResult == true
10 ) {
11 event.response.issueTokens = true;
12 event.response.failAuthentication = false;
13 event.response.challengeName = 'CUSTOM_CHALLENGE';
14 } else {
15 event.response.issueTokens = false;
16 event.response.failAuthentication = true;
17 }
18};

Amazon Cognito invokes the Define Auth Challenge trigger to initiate the custom authentication flow. In the first step you define the auth flow to go through Custom Challenge. In the second if block, if CUSTOM_CHALLENGE returns with challengeResult == true you recognize the custom auth challenge is successful, and tell Cognito to issue tokens. In the last else block you tell Cognito to fail the authentication flow.

Save and close the file, then switch back to the terminal and follow the instructions:

1? Press enter to continue
2 `Hit Enter`
3
4? Do you want to edit your boilerplate-verify function now?
5 `Yes`
6? Please edit the file in your editor: <local file path>/src/boilerplate-verify.js

The boilerplate for Verify Auth Challenge opens in your favorite code editor. Enter the following code to this file:

1function verifyAuthChallengeResponse(event) {
2 if (
3 event.request.privateChallengeParameters.answer ===
4 event.request.challengeAnswer
5 ) {
6 event.response.answerCorrect = true;
7 } else {
8 event.response.answerCorrect = false;
9 }
10}
11
12exports.handler = async (event) => {
13 verifyAuthChallengeResponse(event);
14};

Amazon Cognito invokes the Verify Auth Challenge trigger to verify if the response from the end user for a custom challenge is valid or not. The response from the user will be available in event.request.challengeAnswer. The code above compares that with the privateChallengeParameters value set in the Create Auth Challenge trigger. Save and close the file, then switch back to the terminal and follow the instructions:

1? Press enter to continue
2 `Hit Enter`

Once finished, run amplify push to publish your changes.

Register a user

The CLI flow as mentioned above requires a username and a valid email id as parameters to register a user. Invoke the following api to initiate a sign up flow.

1AuthSignUpOptions options = AuthSignUpOptions.builder()
2 .userAttribute(AuthUserAttributeKey.email(), "my@email.com")
3 .build();
4Amplify.Auth.signUp("username", "Password123", options,
5 result -> Log.i("AuthQuickStart", "Result: " + result.toString()),
6 error -> Log.e("AuthQuickStart", "Sign up failed", error)
7);
1val options = AuthSignUpOptions.builder()
2 .userAttribute(AuthUserAttributeKey.email(), "my@email.com")
3 .build()
4Amplify.Auth.signUp("username", "Password123", options,
5 { Log.i("AuthQuickStart", "Sign up succeeded: $it") },
6 { Log.e ("AuthQuickStart", "Sign up failed", it) }
7)
1val options = AuthSignUpOptions.builder()
2 .userAttribute(AuthUserAttributeKey.email(), "my@email.com")
3 .build()
4try {
5 val result = Amplify.Auth.signUp("username", "Password123", options)
6 Log.i("AuthQuickStart", "Result: $result")
7} catch (error: AuthException) {
8 Log.e("AuthQuickStart", "Sign up failed", error)
9}
1RxAmplify.Auth.signUp(
2 "username",
3 "Password123",
4 AuthSignUpOptions.builder().userAttribute(AuthUserAttributeKey.email(), "my@email.com").build())
5 .subscribe(
6 result -> Log.i("AuthQuickStart", "Result: " + result.toString()),
7 error -> Log.e("AuthQuickStart", "Sign up failed", error)
8 );

The next step in the sign up flow is to confirm the user. A confirmation code will be sent to the email id provided during sign up. Enter the confirmation code received via email in the confirmSignUp call.

1Amplify.Auth.confirmSignUp(
2 "username",
3 "the code you received via email",
4 result -> Log.i("AuthQuickstart", result.isSignUpComplete() ? "Confirm signUp succeeded" : "Confirm sign up not complete"),
5 error -> Log.e("AuthQuickstart", error.toString())
6);
1Amplify.Auth.confirmSignUp(
2 "username", "the code you received via email",
3 { result ->
4 if (result.isSignUpComplete) {
5 Log.i("AuthQuickstart", "Confirm signUp succeeded")
6 } else {
7 Log.i("AuthQuickstart","Confirm sign up not complete")
8 }
9 },
10 { Log.e("AuthQuickstart", "Failed to confirm sign up", it) }
11)
1try {
2 val code = "code you received via email"
3 val result = Amplify.Auth.confirmSignUp("username", code)
4 if (result.isSignUpComplete) {
5 Log.i("AuthQuickstart", "Signup confirmed")
6 } else {
7 Log.i("AuthQuickstart", "Signup confirmation not yet complete")
8 }
9} catch (error: AuthException) {
10 Log.e("AuthQuickstart", "Failed to confirm signup", error)
11}
1RxAmplify.Auth.confirmSignUp("username", "the code you received via email")
2 .subscribe(
3 result -> Log.i("AuthQuickstart", result.isSignUpComplete() ? "Confirm signUp succeeded" : "Confirm sign up not complete"),
4 error -> Log.e("AuthQuickstart", error.toString())
5 );

You will know the sign up flow is complete if you see the following in your console window:

1Confirm signUp succeeded

You will know the sign up flow is complete if you see the following in your console window:

1Confirm signUp succeeded

Sign in a user

Implement a UI to get the username from the user. After the user enters the username you can start the sign in flow by calling the following method:

1AWSCognitoAuthSignInOptions options = AWSCognitoAuthSignInOptions.builder()
2 .authFlowType(AuthFlowType.CUSTOM_AUTH_WITHOUT_SRP)
3 .build();
4Amplify.Auth.signIn(
5 "username",
6 "password",
7 options,
8 result -> Log.i("AuthQuickstart", result.isSignedIn() ? "Sign in succeeded" : "Sign in not complete"),
9 error -> Log.e("AuthQuickstart", error.toString())
10);
1val options = AWSCognitoAuthSignInOptions.builder()
2 .authFlowType(AuthFlowType.CUSTOM_AUTH_WITHOUT_SRP)
3 .build()
4Amplify.Auth.signIn(
5 "username",
6 "password",
7 options,
8 { result ->
9 if (result.isSignedIn) {
10 Log.i("AuthQuickstart", "Sign in succeeded")
11 } else {
12 Log.i("AuthQuickstart", "Sign in not complete")
13 }
14 },
15 { Log.e("AuthQuickstart", "Failed to sign in", it) }
16)
1val options = AWSCognitoAuthSignInOptions.builder()
2 .authFlowType(AuthFlowType.CUSTOM_AUTH_WITHOUT_SRP)
3 .build()
4try {
5 val result = Amplify.Auth.signIn("username", "password", options)
6 if (result.isSignedIn) {
7 Log.i("AuthQuickstart", "Sign in succeeded")
8 } else {
9 Log.e("AuthQuickstart", "Sign in not complete")
10 }
11} catch (error: AuthException) {
12 Log.e("AuthQuickstart", "Sign in failed", error)
13}
1AWSCognitoAuthSignInOptions options = AWSCognitoAuthSignInOptions.builder()
2 .authFlowType(AuthFlowType.CUSTOM_AUTH_WITHOUT_SRP)
3 .build();
4RxAmplify.Auth.signIn("username", "password", options)
5 .subscribe(
6 result -> Log.i("AuthQuickstart", result.isSignedIn() ? "Sign in succeeded" : "Sign in not complete"),
7 error -> Log.e("AuthQuickstart", error.toString())
8 );

Since this is a custom authentication flow with a challenge, the result of the signin process has a next step .confirmSignInWithCustomChallenge. Implement a UI to allow the user to enter the custom challenge.

Confirm sign in with custom challenge

Get the custom challenge (1234 in this case) from the user and pass it to the confirmSignin() api.

1Amplify.Auth.confirmSignIn(
2 "confirmation",
3 result -> Log.i("AuthQuickstart", "Confirm sign in succeeded: " + result.toString()),
4 error -> Log.e("AuthQuickstart", "Failed to confirm sign in", error)
5);
1Amplify.Auth.confirmSignIn("confirmation",
2 { Log.i("AuthQuickstart", "Confirm sign in succeeded: $it") },
3 { Log.e("AuthQuickstart", "Failed to confirm sign in", it) }
4)
1try {
2 val result = Amplify.Auth.confirmSignIn("confirmation")
3 Log.i("AuthQuickstart", "Confirm sign in succeeded: $result")
4} catch (error: AuthException) {
5 Log.e("AuthQuickstart", "Failed to confirm signin", error)
6}
1RxAmplify.Auth.confirmSignIn("confirmation")
2 .subscribe(
3 result -> Log.i("AuthQuickstart", result.toString()),
4 error -> Log.e("AuthQuickstart", error.toString())
5 );

You will know the sign in flow is complete if you see the following in your console window:

1Confirm sign in succeeded

Lambda Trigger Setup

The Amplify CLI can be used to generate triggers required by a custom authentication flow. See the CLI Documentation for details. The CLI will create a custom auth flow skeleton that you can manually edit.

More information on available triggers can be found in the Cognito documentation.

Custom Auth Flow with SRP

Cognito User Pool allows to start the custom authentication flow with SRP as the first step. If you would like to use this flow, setup Define Auth Lambda trigger to handle SRP_A as the first challenge as shown below:

1exports.handler = (event, context) => {
2 if (event.request.session.length == 1 &&
3 event.request.session[0].challengeName == 'SRP_A') {
4 event.response.issueTokens = false;
5 event.response.failAuthentication = false;
6 event.response.challengeName = 'PASSWORD_VERIFIER';
7 } else if (event.request.session.length == 2 &&
8 event.request.session[1].challengeName == 'PASSWORD_VERIFIER' &&
9 event.request.session[1].challengeResult == true) {
10 event.response.issueTokens = false;
11 event.response.failAuthentication = false;
12 event.response.challengeName = 'CUSTOM_CHALLENGE';
13 } else if (event.request.session.length == 3 &&
14 event.request.session[2].challengeName == 'CUSTOM_CHALLENGE' &&
15 event.request.session[2].challengeResult == true) {
16 event.response.issueTokens = true;
17 event.response.failAuthentication = false;
18 } else {
19 event.response.issueTokens = false;
20 event.response.failAuthentication = true;
21 }
22 context.done(null, event);
23};

If your lambda is setup to start with SRP as the first step, make sure to initiate the signIn process with customWithSRP as the authentication flow:

1AWSCognitoAuthSignInOptions options = AWSCognitoAuthSignInOptions.builder()
2 .authFlowType(AuthFlowType.CUSTOM_AUTH_WITH_SRP)
3 .build();
4Amplify.Auth.signIn(
5 "username",
6 "password",
7 options,
8 result -> Log.i("AuthQuickstart", result.isSignedIn() ? "Sign in succeeded" : "Sign in not complete"),
9 error -> Log.e("AuthQuickstart", error.toString())
10);
1val options = AWSCognitoAuthSignInOptions.builder()
2 .authFlowType(AuthFlowType.CUSTOM_AUTH_WITH_SRP)
3 .build()
4Amplify.Auth.signIn(
5 "username",
6 "password",
7 options,
8 { result ->
9 if (result.isSignedIn) {
10 Log.i("AuthQuickstart", "Sign in succeeded")
11 } else {
12 Log.i("AuthQuickstart", "Sign in not complete")
13 }
14 },
15 { Log.e("AuthQuickstart", "Failed to sign in", it) }
16)
1val options = AWSCognitoAuthSignInOptions.builder()
2 .authFlowType(AuthFlowType.CUSTOM_AUTH_WITH_SRP)
3 .build()
4try {
5 val result = Amplify.Auth.signIn("username", "password", options)
6 if (result.isSignedIn) {
7 Log.i("AuthQuickstart", "Sign in succeeded")
8 } else {
9 Log.e("AuthQuickstart", "Sign in not complete")
10 }
11} catch (error: AuthException) {
12 Log.e("AuthQuickstart", "Sign in failed", error)
13}
1AWSCognitoAuthSignInOptions options = AWSCognitoAuthSignInOptions.builder()
2 .authFlowType(AuthFlowType.CUSTOM_AUTH_WITH_SRP)
3 .build();
4RxAmplify.Auth.signIn("username", "password", options)
5 .subscribe(
6 result -> Log.i("AuthQuickstart", result.isSignedIn() ? "Sign in succeeded" : "Sign in not complete"),
7 error -> Log.e("AuthQuickstart", error.toString())
8 );

CAPTCHA-based authentication

Here is the sample for creating a CAPTCHA challenge with a Lambda Trigger.

The Create Auth Challenge Lambda Trigger creates a CAPTCHA as a challenge to the user. The URL for the CAPTCHA image and the expected answer are added to the private challenge parameters:

1export const handler = async (event) => {
2 if (!event.request.session || event.request.session.length === 0) {
3 event.response.publicChallengeParameters = {
4 captchaUrl: <captcha url>,
5 };
6 event.response.privateChallengeParameters = {
7 answer: <expected answer>,
8 };
9 event.response.challengeMetadata = "CAPTCHA_CHALLENGE";
10 }
11 return event;
12};

This Define Auth Challenge Lambda Trigger defines a custom challenge:

1export const handler = async (event) => {
2 if (!event.request.session || event.request.session.length === 0) {
3 // If we don't have a session or it is empty then send a CUSTOM_CHALLENGE
4 event.response.challengeName = "CUSTOM_CHALLENGE";
5 event.response.failAuthentication = false;
6 event.response.issueTokens = false;
7 } else if (event.request.session.length === 1 && event.request.session[0].challengeResult === true) {
8 // If we passed the CUSTOM_CHALLENGE then issue token
9 event.response.failAuthentication = false;
10 event.response.issueTokens = true;
11 } else {
12 // Something is wrong. Fail authentication
13 event.response.failAuthentication = true;
14 event.response.issueTokens = false;
15 }
16
17 return event;
18};

The Verify Auth Challenge Response Lambda Trigger is used to verify a challenge answer:

1export const handler = async (event, context) => {
2 if (event.request.privateChallengeParameters.answer === event.request.challengeAnswer) {
3 event.response.answerCorrect = true;
4 } else {
5 event.response.answerCorrect = false;
6 }
7
8 return event;
9};