IBM Developer

Tutorial

Implement advanced authentication and identity management with Amazon Cognito

Enable custom enrollment, forgot-password implementation, and real-time user migration

By Prasad Reddy

In this tutorial, you’ll learn about how to implement advanced authentication and identity management using Amazon Cognito. You’ll learn how to enable:

  • Custom enrollment: Implementing a flexible user onboarding experience beyond the standard Cognito registration process.
  • Custom forgot-password implementation: Enhancing security and providing a seamless password recovery experience.
  • Real-time user migration: Enabling a smooth transition for users from existing authentication systems without requiring password resets.

By the end of this tutorial, you will have a comprehensive understanding of custom authentication workflows, enabling you to build flexible and secure identity solutions with Amazon Cognito.

Prerequisites

To get started, you should have:

  • A basic understanding of AWS services, including Cognito, Lambda, IAM, and API Gateway
  • Familiarity with identity providers and access to an AWS account

Estimated time

This tutorial should take you about 2 hours to complete. (Note: You may require at least 24 hours to implement all processes for your project.)

How to complete this tutorial

To implement the steps that follow, you will need an AWS account.

Note: The code provided should be included in Lambda functions with Node.js as the runtime. These Lambda functions should then be exposed through an API Gateway, enabling your application to consume these services.

Steps

Custom enrolment in Amazon Cognito

Custom enrolment consists of a two-step process:

  1. Partial registration: Creating a user with minimal details (for example, phone number or email).
  2. Confirmation of registration: Collecting user credentials to finalize the registration process.

This approach is useful when user records need to be created first (for tracking or transaction purposes) before full registration is completed.

This structured guide provides step-by-step instructions with code snippets to help you implement custom enrolment in Amazon Cognito, ensuring a smooth and secure authentication flow for your users.

Step 1: Partial registration

Partial registration involves creating a user with limited information and later allowing them to complete their registration.

  1. Before creating a new user, you must ensure that the user does not already exist to avoid duplicate records. To do this, you’ll use the listUsers method:

     const AWS = require('aws-sdk');
     const cognito = new AWS.CognitoIdentityServiceProvider();
    
     async function checkIfUserExists(username, userPoolId) {
         const params = {
             UserPoolId: userPoolId,
             Filter: `username = \"${username}\"`
         };
    
         try {
             const data = await cognito.listUsers(params).promise();
             return data.Users.length > 0; // Returns true if the user exists
         } catch (error) {
             console.error('Error checking user:', error);
             return false;
         }
     }
    
  2. Once we confirm that the user does not exist, we create a partial user using the adminCreateUser method:

     var params = {
         UserPoolId: UserPoolId, 
         Username: ccode + phone_number + Date.now(),
         MessageAction: 'SUPPRESS', // Prevents welcome email from being sent
         TemporaryPassword: phone_number,
         UserAttributes: [
             { Name: 'family_name', Value: 'tempfamilyname' },
             { Name: 'gender', Value: 'notknown' },
             { Name: 'name', Value: 'tempname' },
             { Name: 'phone_number', Value: "+" + ccode + phone_number },
             { Name: 'preferred_username', Value: phone_number }
         ]
     };
    
     cognitoidentityserviceprovider.adminCreateUser(params, function(err, data) {
         if (err) {
             let res = { message: err.message, statusCode: err.statusCode };
             callback(null, res);
             return;
         } else {
             var subObject = search('sub', data.User.Attributes);
             var sub = subObject ? subObject.Value : null;
    
             let res = { Username: data.User.Username, sub: sub, statusCode: 200 };
             callback(null, res);
             return;
         }
     });
    

At this stage, the user is created in Cognito but has not set a password or confirmed their registration.

Step 2: Confirming or registering the user

To complete registration, you need to authenticate the user and set a permanent password. You’ll do this using two AWS Cognito methods:

  • adminInitiateAuth: Initiates authentication using the ADMIN_NO_SRP_AUTH flow, passing the username and password from the partial user creation.
  • adminRespondToAuthChallenge: Handles the NEW_PASSWORD_REQUIRED challenge and sets a final password.

To confirm and register the user, complete the following steps:

  1. To start authentication, use the adminInitiateAuth method with the ADMIN_NO_SRP_AUTH flow:

     async function initiateAuth(username, password, userPoolId, clientId) {
         const params = {
             UserPoolId: userPoolId,
             ClientId: clientId,
             AuthFlow: 'ADMIN_NO_SRP_AUTH',
             AuthParameters: {
                 USERNAME: username,
                 PASSWORD: password
             }
         };
    
         try {
             const data = await cognito.adminInitiateAuth(params).promise();
             console.log('Authentication initiated:', data);
             return data; // May return a challenge if required
         } catch (error) {
             console.error('Error initiating authentication:', error);
         }
     }
    
  2. If Cognito responds with a NEW_PASSWORD_REQUIRED challenge, you handle it using adminRespondToAuthChallenge:

     async function respondToAuthChallenge(session, username, newPassword, userPoolId, clientId) {
         const params = {
             UserPoolId: userPoolId,
             ClientId: clientId,
             ChallengeName: 'NEW_PASSWORD_REQUIRED',
             Session: session,
             ChallengeResponses: {
                 USERNAME: username,
                 NEW_PASSWORD: newPassword
             }
         };
    
         try {
             const data = await cognito.adminRespondToAuthChallenge(params).promise();
             console.log('User successfully confirmed:', data);
             return data;
         } catch (error) {
             console.error('Error confirming user:', error);
         }
     }
    

You have now implemented the registration process. You have:

  • Initiated authentication using adminInitiateAuth with ADMIN_NO_SRP_AUTH, passing the username and temporary password.
  • If Cognito responds with a NEW_PASSWORD_REQUIRED challenge, you have handled it using adminRespondToAuthChallenge.
  • The user provides their preferred username and a new password, completing registration.

Complete example code

    cognitoidentityserviceprovider.listUsers(delcreparams, function(err, data)  {
        if (err) {
            let res = { message: err.message, statusCode: err.statusCode };
            callback(null, res);
            return;
        } else {
            if (data && data.Users[0]) {
                let delClientId;
                var delcusername = data.Users[0].Username;

                var delinAuthparams = {
                    UserPoolId: UserPoolId,
                    ClientId: delClientId,
                    AuthFlow: 'ADMIN_NO_SRP_AUTH',
                    AuthParameters: { "USERNAME": delcusername, "PASSWORD": phone_number }
                };    

                cognitoidentityserviceprovider.adminInitiateAuth(delinAuthparams, function(err, data) {
                    if (err) {
                        callback(null, { message: err.message, statusCode: err.statusCode });
                        return;
                    } else {
                        let delsession = data.Session;
                        var delrespondToAuthparams = {
                            Session: delsession,
                            UserPoolId: UserPoolId,
                            ClientId: delClientId,
                            ChallengeName: 'NEW_PASSWORD_REQUIRED',
                            ChallengeResponses: { 
                                "NEW_PASSWORD": password,
                                "USERNAME": delcusername,
                                "userAttributes.preferred_username": username
                            }
                        };

                        cognitoidentityserviceprovider.adminRespondToAuthChallenge(delrespondToAuthparams, function(err, data) {
                            if (err) {
                                callback(null, { message: err.message, statusCode: err.statusCode });
                                return;
                            } else {
                                callback(null, { AuthenticationResult: data.AuthenticationResult, statusCode: 200 });
                                return;
                            }
                        });
                    }
                });
            }
        }
    });

Implement a custom forgot-password process

Instead of using the default Cognito feature where a one-time password (OTP) is sent to either an email or phone number with a predefined body, this implementation customizes the forgot-password process by ensuring email and phone number verification before initiating the process. The custom solution utilizes local SMS providers to send OTPs and allows password resetting based on OTP validation.

Step 1: Store the OTP and send SMS to the user’s mobile

The following AWS Lambda function generates a OTP, updates the user attributes in Cognito, and sends the OTP to the user using a local SMS provider.

```
const AWS = require('aws-sdk');

exports.handler = (event, context, callback) => {
    let phone_number;
    let ccode;
    let cusername;
    let currentStage = 'uat';
    let request;
    let UserPoolId;
    let infobipurl = '';


        phone_number = request.phone_number;
        ccode = request.ccode; 
        UserPoolId = process.env.ProdUserPoolId;


    var cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18' });
    var filter = "phone_number = \"" + "+" + ccode + phone_number + "\"";
    var params = {
        "Filter": filter,
        "UserPoolId": UserPoolId
    };
    cognitoidentityserviceprovider.listUsers(params, function(err, data) {
            if (err) {
                console.log(err);
                let res = {
                    message: err.message,
                    statusCode: err.statusCode
                };
                console.info("forgotpassword response\n" + JSON.stringify(res, null, 2));
                callback(null, res);
                return;
            }
            else {

                if (data && data.Users[0]) {
                    let status;
                    cusername = data.Users[0].Username;

                    if (data.Users[0].UserStatus === "CONFIRMED" ) {
                        let iscognito = '';

                            let answer = Math.floor(1000 + Math.random() * 9000).toString();
                            let UserAttributes = [];
                            let otp = {
                                "Name": 'custom:OTP',
                                "Value": answer
                            };
                            let OTPupdatedTime = {
                                "Name": 'custom:OTPupdatedTime',
                                "Value": Date.now().toString()
                            };

                            UserAttributes.push(otp);
                            UserAttributes.push(OTPupdatedTime);

                            let updateparams = {
                                UserPoolId: UserPoolId,
                                Username: cusername,
                                UserAttributes: UserAttributes
                            };

                            cognitoidentityserviceprovider.adminUpdateUserAttributes(updateparams, function(err, data) {
                                if (err) {
                                    let res1 = {
                                        message: err.message,
                                        statusCode: err.statusCode
                                    };

                                    callback(null, res1);
                                }
                                else{
                                    //Write code to send SMS
                                }
                            });
                        }

                    }
                    else {

                        let fpsres = {
                            message: "User status is not confirmed",
                            status: data.Users[0].UserStatus,
                            statusCode: 412
                        };

                        callback(null, fpsres);
                        return;
                    }
                }

            }
    )    };
```

Step 2: Validate OTP and set password

Once the OTP is validated, the following Lambda function sets a new password for the user.

```
function search(nameKey, myArray) {
    for (var i = 0; i < myArray.length; i++) {
        if (myArray[i].Name === nameKey) {
            return myArray[i];
        }
    }
}
const AWS = require('aws-sdk');
const axios = require("axios");
var ssm = new AWS.SSM();
exports.handler = (event, context, callback) => {
    let phone_number;
    let ccode;
    let otp;
    let password;
    let cusername;
    let currentStage = 'uat';
    let request;
    let UserPoolId;
    let curstatus;
    phone_number = request.phone_number;
    ccode = request.ccode;
    otp = request.otp;
    password = request.password;
    UserPoolId = process.env.UATUserPoolId;

    var cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18' });
    var filter = "phone_number = \"" + "+" + ccode + phone_number + "\"";
    var params = {
        "Filter": filter,
        "UserPoolId": UserPoolId
    };
    cognitoidentityserviceprovider.listUsers(params, function (err, data) {
        if (err) {
            let res = {
                message: err.message,
                statusCode: err.statusCode
            };

            callback(null, res);
            return;
        }
        else {
            if (data && data.Users[0]) {
                let status;
                cusername = data.Users[0].Username;

                if (data.Users[0].UserStatus === "CONFIRMED") {
                    let lotp;
                    var otpObject = search('custom:OTP', data.Users[0].Attributes);
                    if (otpObject) {
                        lotp = otpObject.Value;
                    }
                    if (lotp && otp === lotp) {
                        var spparams = {
                            UserPoolId: UserPoolId,
                            Username: cusername,
                            Password: password,
                            Permanent: true
                        };
                        cognitoidentityserviceprovider.adminSetUserPassword(spparams, function (err, data) {
                            if (err) {
                                let speres = {
                                    message: err.message,
                                    statusCode: err.statusCode
                                };

                                callback(null, speres);
                                return;
                            }
                            else {
                                let spres = {
                                    message: "Password update sucessfull",
                                    statusCode: 200
                                };

                                callback(null, spres);
                                return;
                            }
                        });
                    }
                    else {
                        //if the otp not matched
                        let otpres = {
                            message: "otp not matched or not available",
                            statusCode: 417
                        };

                        callback(null, otpres);
                        return;
                    }

                }

                else {
                    let usersres = {
                        message: "User status is not confirmed",
                        statusCode: 412
                    };

                    callback(null, usersres);
                    return;
                }
            }

            else {
                let res = {
                    message: "UserNotFound",
                    statusCode: 415
                };

                callback(null, res);
                return;
            }
        }
    });

};
```

Implement real-time user migration

There are two primary methods for migrating users to Amazon Cognito if user data exists in another database:

  • Bulk import: Users can be imported using a CSV file, requiring them to reset their passwords.
  • Real-time migration: Using Cognito Lambda triggers, users can be migrated dynamically when they attempt to log in.

To implement real-time user migration, complete the following steps:

  1. Enable ALLOW_CUSTOM_AUTH for the app client.
  2. Add and configure Lambda triggers in your user pool.

    CreateAuthchallangeLambda:

     exports.handler = (event, context, callback) => {
         if (event.request.challengeName == 'CUSTOM_CHALLENGE') {
             event.response.publicChallengeParameters = {};
             event.response.publicChallengeParameters.test = 'test';
             event.response.privateChallengeParameters = {};
             event.response.privateChallengeParameters.answer = 'test';
             event.response.challengeMetadata = 'CAPTCHA_CHALLENGE';
         }
         callback(null, event);
     };
    

    DefineAuthchallange Lambda:

     exports.handler = (event, context, callback) => {
     if (event.request.session.length == 1 && event.request.session[0].challengeName == 'CUSTOM_CHALLENGE' && event.request.session[0].challengeResult == true) {
             event.response.issueTokens = true;
             event.response.failAuthentication = false;
         } else {
             event.response.issueTokens = false;
             event.response.failAuthentication = false;
             event.response.challengeName = 'CUSTOM_CHALLENGE';
         }
         callback(null, event);
     };
    

    Migrate Lambda code:

    
     const AWS = require('aws-sdk');
     const axios = require('axios');
     var ssm = new AWS.SSM();
     exports.handler = (event, context, callback) => {
    
         let status;
         if ( event.triggerSource === "UserMigration_Authentication" ) {
    
             const AWS = require('aws-sdk');
             const axios = require('axios');
    
             exports.handler = (event, context, callback) => {
                 // TODO implement
    
                 let status;
                 if ( event.triggerSource === "UserMigration_Authentication" ) {
    
                     //write code to get the data from Old database                
    
                                                 if (User && User.data && User.data.member) {
                                                     const cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18' });
                                                     const filter = "phone_number = \"" + "+" + User.data.member.mobilenumber + "\"";
                                                     const params = {
                                                         "Filter": filter,
                                                         "UserPoolId": UserPoolId
                                                     };
    
                                                     cognitoidentityserviceprovider.listUsers(params, function(err, data) {
                                                         if (err) {
    
                                                             let res = {
                                                                 message: err.message,
                                                                 statusCode: err.statusCode
                                                             };
    
                                                             callback(null, res);
                                                             return;
                                                         } else {
    
                                                             if (data && data.Users[0]) {
    
                                                                 const delparams = {
                                                                     UserPoolId: UserPoolId,
                                                                     Username: data.Users[0].Username
                                                                 };
    
                                                                 cognitoidentityserviceprovider.adminDeleteUser(delparams, function(err, data) {
                                                                     if (err) {
    
                                                                         let res = {
                                                                             message: err.message,
                                                                             statusCode: err.statusCode
                                                                         };
    
                                                                         callback(null, res);
                                                                         return;
                                                                     } else {
    
                                                                         let userAttributes = {
                                                                             "custom:lastname": User.data.member.lastname ? User.data.member.lastname : "",
                                                                             "gender": User.data.member.gender ? User.data.member.gender : "",
                                                                             "name": User.data.member.firstname ? User.data.member.firstname : "",
                                                                             "phone_number": "+" + User.data.member.mobilenumber,
                                                                             "preferred_username": event.userName,
                                                                             "family_name": User.data.member.facebookId ? User.data.member.facebookId : "",
                                                                             "birthdate": User.data.member.birthday ? User.data.member.birthday : "",
                                                                             "email": User.data.member.email ? User.data.member.email : "",
                                                                             "custom:status": status,
                                                                             "custom:membertype": User.data.member.memberType ? User.data.member.memberType : "",
                                                                             "custom:city": User.data.member.city ? User.data.member.city : "",
                                                                             "custom:iscognito": "1",
                                                                         };
    
                                                                         event.response.userAttributes = userAttributes;
                                                                         event.response.finalUserStatus = "CONFIRMED";
                                                                         event.response.messageAction = "SUPPRESS";
    
                                                                     }
                                                                 });
                                                             } else {
    
                                                                 let userAttributes = {
                                                                     "custom:lastname": User.data.member.lastname ? User.data.member.lastname : "",
                                                                     "gender": User.data.member.gender ? User.data.member.gender : "",
                                                                     "name": User.data.member.firstname ? User.data.member.firstname : "",
                                                                     "phone_number": "+" + User.data.member.mobilenumber,
                                                                     "preferred_username": event.userName,
                                                                     "family_name": User.data.member.facebookId ? User.data.member.facebookId : "",
                                                                     "birthdate": User.data.member.birthday ? User.data.member.birthday : "",
                                                                     "email": User.data.member.email ? User.data.member.email : "",
    
                                                                 };
    
                                                                 event.response.userAttributes = userAttributes;
                                                                 event.response.finalUserStatus = "CONFIRMED";
                                                                 event.response.messageAction = "SUPPRESS";
                                                                 context.succeed(event);
    
                                                             }
                                                         }
                                                     });
                                                 } else {
                                                     let res1 = {
                                                         message: "User does not exist",
                                                         statusCode: 415
                                                     };
                                                     console.info("migrateuser response\n" + JSON.stringify(res1, null, 2));
                                                     callback("User does not exist", res1);
                                                     return;
                                                 }
                                             });
    
         }
     };
    

This structured approach ensures secure and customized password recovery while facilitating real-time user migration to Amazon Cognito.

Summary and next steps

In this tutorial, you’ve learned how to use Amazon Cognito as an identity provider. Many developers hesitate to use Cognito because they find custom authentication and migration scenarios challenging to implement. You’ve now learned how to simplify the process and have seen code examples that will make your implementation process easier.

Now that you've set up custom registration with AWS Cognito, try integrating it with your application! Experiment with custom attributes, validation logic, and user flows