Page updated Nov 14, 2023

Sign-in with custom flow

Amplify Flutter v0 is now in Maintenance Mode until July 19th, 2024. This means that we will continue to include updates to ensure compatibility with backend services and security. No new features will be introduced in v0.

Please use the latest version (v1) of Amplify Flutter to get started.

If you are currently using v0, follow these instructions to upgrade to v1.

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

A Flutter application targeting Flutter SDK >= 2.10.0 with Amplify libraries integrated.

The following are also required, depending on which platforms you are targeting:

  • An iOS configuration targeting at least iOS 11.0
  • An Android configuration targeting at least Android API level 21 (Android 5.0) or above

For a full example please follow the project setup walkthrough

Configure Auth Category

How Custom Auth Works

The custom authentication flow supported by Amazon Cognito uses a series of AWS Lambda triggers, which are serverless functions invoked when particular events occur in Cognito. Together, these triggers allow you to establish a series of 'challenges' to which your users must successfully respond in order to authenticate.

The custom authentication flow consists of the following triggers, invoked in order:

  • Define Auth Challenge: This trigger defines the sequential series of challenges that the user will need to complete in the flow. These challenges can be custom challenges, as well as traditional SRP_A ] (with username/password verification).

  • Create Auth Challenge: This trigger defines the expected response for custom challenges, as well as key/value pairs that can be sent back to the client application to help guide your end users.

  • Verify Auth Challenge: This trigger is used to verify if the provided response for a given challenge is correct.

Setting-up custom auth flow with the Amplify CLI

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

Note that the sendChallengeCode method is empty, you can use 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_flutter_project>/amplify/backend/function/<project_code>CreateAuthChallenge/src/package.json" and add the following:

1{
2 "name": "<AUTH_CHALLENGE_NAME>",
3 "version": "2.0.0",
4 "description": "Lambda function generated by Amplify",
5 "main": "index.js",
6 "license": "Apache-2.0",
7 "devDependencies": {
8 "@types/aws-lambda": "^8.10.92"
9 }, // <- Include comma
10
11 // Add the following lines
12 "dependencies": {
13 "crypto-secure-random-digit": "^1.0.9"
14 }
15}

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 to this file:

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

Note that each of these if/else blocks are referencing event.request.session. The session is a JS array in which each index represents a step in the custom authentication flow.

The Amplify Auth library always starts with an SRP_A flow, so in the code above, you bypass SRP_A and return CUSTOM_CHALLENGE in the first step. In the second step, 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 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.

Setting-up custom auth flow manually

The custom auth flow can be configured manually.

If you have already configured custom auth without the aid of the Amplify CLI, you can use the custom auth flow by changing the authenticationFlowType value in your Amplify configuration to CUSTOM_AUTH.

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.

Because authentication flows in Cognito can be switched via your configuration, it is still required that users register with a password.

1// Create a boolean for checking the sign up status
2bool isSignUpComplete = false;
3
4...
5
6Future<void> signUpCustomFlow() async {
7 try {
8 final userAttributes = <CognitoUserAttributeKey, String>{
9 CognitoUserAttributeKey.email: 'email@domain.com',
10 CognitoUserAttributeKey.phoneNumber: '+15559101234',
11 // additional attributes as needed
12 };
13 final result = await Amplify.Auth.signUp(
14 username: 'myusername',
15 password: 'mysupersecurepassword',
16 options: CognitoSignUpOptions(userAttributes: userAttributes),
17 );
18 setState(() {
19 isSignUpComplete = result.isSignUpComplete;
20 });
21 } on AuthException catch (e) {
22 safePrint(e.message);
23 }
24}

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.

1// Use the boolean created before
2bool isSignUpComplete = false;
3
4...
5
6Future<void> confirmUser() async {
7 try {
8 final result = await Amplify.Auth.confirmSignUp(
9 username: 'myusername',
10 confirmationCode: '123456'
11 );
12
13 setState(() {
14 isSignUpComplete = result.isSignUpComplete;
15 });
16
17 } on AuthException catch (e) {
18 safePrint(e.message);
19 }
20}

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:

1// Create a boolean for checking the sign in status and keep the status
2bool isSignedIn = false;
3String? challengeHint;
4
5...
6
7Future<void> signInCustomFlow(String username) async {
8 try {
9 final result = await Amplify.Auth.signIn(username: username);
10 setState(() {
11 isSignedIn = result.isSignedIn;
12 // Get the publicChallengeParameters from your Create Auth Challenge Lambda
13 challengeHint = result.nextStep?.additionalInfo?['hint'];
14 });
15 } on AuthException catch (e) {
16 safePrint(e.message);
17 }
18}

Please note that you will be prevented from successfully calling signIn if a user has already signed in and a valid session is active. You must first call signOut to remove the original session. When running on the iOS platform, you will be able to call signIn if the session has expired, while on Android you must first call signOut regardless.

Confirm sign in with custom challenge

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

1Future<void> confirmSignIn(String generatedNumber) async {
2 try {
3 final result = await Amplify.Auth.confirmSignIn(
4 /// Enter the random number generated by your Create Auth Challenge trigger
5 confirmationValue: generatedNumber,
6 );
7 print('Result: $result');
8 } on AuthException catch (e) {
9 print(e.message);
10 }
11}

Once the user provides the correct response, they should be authenticated in your application.

Custom authentication flow with password verification

The example in this documentation demonstrates the passwordless custom authentication flow. However, it is also possible to require that users supply a valid password as part of the custom authentication flow.

To require a valid password, you can alter the DefineAuthChallenge code to handle a PASSWORD_VERIFIER step:

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