Front-End Web & Mobile

Customizing Amazon Cognito User Pool Authentication Flow

Introduction

Modern authentication flows incorporate new challenge types, in addition to a password, to verify the identity of users. For example, these challenge types include CAPTCHAs or dynamic challenge questions. With Amazon Cognito Your User Pools, we now have a flexible authentication flow that you can customize to incorporate additional authentication methods and support dynamic authentication flows that are server driven. First, we generalize authentication into two common steps, which are implemented through two APIs (InitiateAuth and RespondToAuthChallenge). In this flow, a user authenticates by answering successive challenges until authentication either fails or the user is issued tokens. With these two steps, which can be repeated to include different challenges, we support any custom authentication flow. Second, we provide the ability to customize your authentication flow with AWS Lambda triggers. These triggers issue and verify their own challenges as part of the authentication flow.

In this post, we provide detail on the two APIs and their inputs and outputs as we demonstrate how you can customize Your User Pools authentication flow to contain additional challenges such as CAPTCHAs.

New APIs

InitiateAuth

This API kicks off the authentication flow. It explicitly indicates to Amazon Cognito how you are trying to authenticate, along with initial authentication parameters that are passed to the pre-authentication Lambda trigger. With a successful call, the response provides either tokens (for an authenticated user) or a challenge.

The InitiateAuth API has the following inputs:

AuthFlow String

The name of the auth flow is determined by the service. The following are supported: USER_SRP_AUTH, REFRESH_TOKEN_AUTH, CUSTOM_AUTH, ADMIN_NO_SRP_AUTH. USER_SRP_AUTH and REFRESH_TOKEN_AUTH were previously available through other APIs but they are easier to use with the new APIs. For a custom authentication flow, the CUSTOM_AUTH value is provided. For more information on the flows, see Custom Authentication Flow in the Amazon Cognito Developer Guide.

AuthParameters Map of String, String

Key/value pairs containing all of the inputs necessary to initiate this authentication method (e.g., USERNAME=johndoe, SRP_A=AB009809). Based on authentication flow and a combination of some parameters missing, authentication can be failed.

[ClientMetadata] Map of String, String

Optional field. Key/value pairs containing inputs that aren’t authentication parameters but are inputs to the pre-authentication Lambda trigger. Using this Lambda trigger, you can implement custom validations to accept/deny the request, based on, for example, the user context.

ClientId String

The app clientId for the app attempting to authenticate.

RespondToAuthChallenge

Used to respond to challenges, which can include multiple rounds until the user successfully authenticates (and Amazon Cognito issues tokens) or fails. You have control over how many challenge rounds are presented to the user. This can be decided based on the challenges already answered. With each call, the response is either a successful authentication when tokens are issued, a new challenge, or failure of the authentication.

The RespondToAuthChallenge API has the following inputs:

ChallengeName String

Name of the challenge being responded to.

Session String

An encrypted session received by the client in the previous step that the client must pass back as-is. The session contains state information about the current authentication. It cannot be replayed and it expires after 3 minutes.

ChallengeResponses Map of String, String

Key/value pairs containing all of the parameters necessary to respond to the challenge (e.g., captchaAnswer=AZ235F).

ClientId String

ClientId trying to authenticate.

InitiateAuth and RespondToAuthChallenge outputs

A successful call to either of the APIs results in tokens that indicate that the authentication flow is complete, or a challenge with a session and parameters.

AuthenticationResult containing Tokens

If this is the last step in the authentication flow, the result contains ID, access and refresh tokens. For more information, see Using Tokens with User Pools in the Amazon Cognito Developer Guide.

ChallengeName String

Name of the next challenge. For example, possible values can be CUSTOM_CHALLENGE, if a custom challenge needs to be answered, or PASSWORD_VERIFIER, if password verification is required.

Session String

An encrypted session received by the client in the previous step that the client must pass back as-is. The session contains state information about the current authentication. It cannot be replayed and it expires after 3 minutes.

ChallengeParameters Map of String, String

Key/value pairs containing all of the parameters necessary to prompt the user for the returned challenge (e.g., captchaUrl=https://xyz.com/captcha/123415).

Diving into the custom authentication flow

To provide you control over the authentication flow, we introduced a flow of CUSTOM_AUTH type and provided you with Lambda triggers you can use to implement this custom authentication flow. The flow can be broken down into the following decisions that you can customize through Lambda triggers:

  • Analyze the challenges a user has answered so far (successfully or unsuccessfully) and then succeed the authentication (and generate tokens), fail authentication, or prompt the user with a new challenge. This is called the Define Auth Challenge Lambda trigger.
  • Generate a challenge that consists of parameters used to challenge the user and valid answers that can be used when the challenge is answered. This is called the Create Auth Challenge Lambda trigger.
  • Verify if the answer provided by the user is valid. This is called the Verify Auth Challenge Lambda trigger.

Lambda triggers can be entered as code in the AWS Lambda console as shown.

lambda_console

You can configure Lambda triggers in the Amazon Cognito console on the User Pools Triggers page as shown next. When a Lambda trigger is selected in the Amazon Cognito console, the necessary execution rights are created automatically by the console.

configure_lambda

When an app initiates a flow of CUSTOM_AUTH type, Amazon Cognito executes the Define Auth Challenge Lambda trigger to issue a challenge type. This can be a standard (built-in) challenge, such as SRP, that can be handled by the Amazon Cognito service. Or it can be a custom challenge handled by a Lambda trigger that you supply (CUSTOM_CHALLENGE type). In general, this process takes as input the past challenges answered by the user and their result. If the challenge issued is of custom type, Amazon Cognito calls a Lambda trigger to create and issue the challenge. The Lambda trigger passes back the challenge parameters and valid answers. This process is shown in the following two diagrams.

diagram_1

diagram_2

Amazon Cognito User Pools passes the challenge and an encrypted piece of session information back to the client SDK. The client SDK gathers the answers to the challenge and passes back the answers and the encrypted session data as it received it. Amazon Cognito User Pools decrypts the data and calls a Lambda trigger to check if the answers match. Amazon Cognito User Pools then calls the Lambda trigger with the challenges answered so far (adding the challenge just answered). The Lambda trigger can issue a new challenge, issue tokens, or fail authentication, and the process can repeat accordingly. We dive deep into the Lambda contract specifications next.

Lambda Trigger contract specifications

There are three Lambda triggers mentioned previously. We provide detail on the different parameters the Lambda triggers take as inputs and the valid outputs. All three of these Lambda triggers also receive all of the user’s attributes as inputs.

DefineAuthChallenge: The challenges (state machine) Lambda trigger

This Lambda trigger has as input the unencrypted session challenge answering information (it was decrypted by the service after it was passed to and from the client). The trigger then returns a challenge name or a Boolean value that indicates whether Amazon Cognito should issue tokens or not.

Inputs

session List of ChallengeResult objects

This is a list containing ChallengeResult objects that indicate whether the challenge was successfully answered. A ChallengeResult object contains a challenge name (challengeName), a Boolean value with the challenge answering result (challengeResult), and challenge metadata that you populate when you generate a challenge (challengeMetadata).

Outputs

challengeName String

Amazon Cognito returns a new challenge name that the client must answer. Alternatively, Amazon Cognito can issue tokens or fail authentication using the following parameters. If you want to generate a challenge with the Create Auth Challenge Lambda trigger, your trigger must output a challengeName of CUSTOM_CHALLENGE.

issueTokens Boolean

Indicates whether tokens should be issued to the user, which means the user was authenticated.

failAuthentication Boolean

Indicates whether the user’s authentication attempt should be terminated because of a failure to authenticate.

Example Lambda Trigger

This example Lambda trigger issues tokens if SRP_A, PASSWORD_VERIFIER, and CUSTOM_CHALLENGE challenges were passed; otherwise it fails the authentication.

exports.handler = function(event, context) {
    if (event.request.session.length == 1 && event.request.session[0].challengeName == 'SRP_A') {
        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) {
        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) {
        event.response.issueTokens = true;
        event.response.failAuthentication = false;
    } else {
        event.response.issueTokens = false;
        event.response.failAuthentication = true;
    }
    context.done(null, event);
}

CreateAuthChallenge: The challenge generator Lambda trigger

This Lambda trigger takes a challenge name as input and then generates challenge parameters based on that challenge.

Inputs

challengeName String

The name of the challenge the Lambda trigger has been requested to issue.

session List of ChallengeResult objects

This is a list containing ChallengeResult objects that indicate whether the challenge was successfully answered. A ChallengeResult object contains a challenge name (challengeName), a Boolean value with the challenge answering result (challengeResult), and challenge metadata that you populate when you generate a challenge (challengeMetadata).

Outputs

publicChallengeParameters Map of String, String

The Lambda trigger returns a map of public challenge parameters. This map is sent to the client unencrypted so that the client can present the challenge to the user; for example, a CAPTCHA URL.

privateChallengeParameters Map of String, String

The Lambda trigger returns a map of private challenge parameters that it generates. This output is labeled “private” and the client cannot see it. For example, it might be used to hold CAPTCHA answer or other valid answers for the challenge.

challengeMetadata String

Metadata about the challenge. This data is put in the session of challenge results when this challenge is answered.

Example Lambda trigger

The following Lambda trigger generates the publicChallengeParameters and privateChallengeParameters maps, with the URL to a CAPTCHA in the public map and the valid answer to the CAPTCHA in the private map. The two pieces usually come from an external service.

exports.handler = function(event, context) {
    if (event.request.session.length == 2 && event.request.challengeName == 'CUSTOM_CHALLENGE') {
        event.response.publicChallengeParameters = {};
        event.response.publicChallengeParameters.captchaUrl = 'url/123.jpg'
        event.response.privateChallengeParameters = {};
        event.response.privateChallengeParameters.answer = '5';
        event.response.challengeMetadata = 'CAPTCHA_CHALLENGE';
    }
    context.done(null, event);
}

Verify Auth Challenge: The response verifying Lambda Trigger

This Lambda trigger verifies the answers to the challenges. It gets acceptable answers to the challenge, provided by the previous Create Auth Challenge Lambda trigger, and compares them to the client provided answer.

Inputs

privateChallengeParameters – Map of String, String

This input is the same as described previously for the Create Auth Challenge Lambda trigger. Amazon Cognito forwards the map of private challenge parameters from the Create Auth Challenge Lambda trigger to the Verify Auth Challenge Lambda trigger. These parameters include valid answers for the challenge.

challengeAnswer String

The answer provided by the user to the challenge.

Outputs

answerCorrect Boolean

Indicates whether the answer provided to the challenge was correct.

Example Lambda trigger

The following Lambda trigger checks if the challengeAnswer value is equal to an answer entry in the privateChallengeParameters map to determine if the challenge was answered correctly.

exports.handler = function(event, context) {
    if (event.request.privateChallengeParameters.answer == event.request.challengeAnswer) {
        event.response.answerCorrect = true;
    } else {
        event.response.answerCorrect = false;
    }
    context.done(null, event);
}

Client SDK considerations

The client SDKs expose a new callback that requests input from users in the case of custom challenges. The challengeParameters parameter is used to present the challenge to the user and the user’s response is passed back in the challengeResponses parameter to the RespondToAuthChallenge API call. Here is an example in JavaScript to complement the example Lambda triggers described previously. As can be seen in the following, the JavaScript SDK provides a customChallenge callback while the Android and iOS SDKs provide similar callbacks.

var authenticationData = {
    Username : 'username',
    Password : 'password',
};
var authenticationDetails = new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);
var poolData = { 
    UserPoolId : '...', // Your user pool id here
    ClientId : '...' // Your client id here
};
var userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(poolData);
var userData = {
    Username : 'username',
    Pool : userPool
};
var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);
cognitoUser.setAuthenticationFlowType(‘CUSTOM_AUTH’);
cognitoUser.authenticateUser(authenticationDetails, {
    onSuccess: function (result) {
      console.log('access token + ' + result.getAccessToken().getJwtToken());
    },

    customChallenge: function (challengeParameters) {
      //gather user responses in challengeResponses based on challengeParameters
      cognitoUser.sendCustomChallengeAnswer(challengeResponses, this);
    },

    onFailure: function(err) {
      alert(err);
    },
});
 
       

Security considerations

The custom user pool authentication flow works in parallel with all other user pool authentication flows, unless you specify otherwise. If you want to implement a custom flow that presents more challenges than, for example, USER_SRP_AUTH, you should choose Only allow Custom Authentication in the Amazon Cognito console, or include the CUSTOM_AUTH_FLOW_ONLY enum in the ExplicitAuthFlows user pool client property.

flag_custom_auth

Conclusions

As you can see, you can customize your authentication flow using the AWS Mobile SDKs. This enables you to create an experience that is designed to provide security and a user-friendly experience for your users. We’d like to hear how you plan to use this feature in your applications, so feel free to leave a comment to share other uses for this feature.

If you have comments or questions, please leave a comment, visit our forums, or post on Stack Overflow.