Post Cover

Implementing Cognito Forgot Password and Update Profile in ASP.NET Core using AWS SDK

ASP.NET Core AWS Cognito Posted Apr 27, 2021

In the previous article, we looked at implementing user Login and Signup flows over Cognito using AWS SDK via ASP.NET Core. In this article, let's continue and implement other important user journeys once logged into any application - Updating Password, Updating Profile and Forgot Password.

For starters, Cognito is a User Identity Management service provided by AWS in its Cloud suite. Cognito provides user identity and authorization management for applications, while applications can use the solutions in granting access to their functionalities. AWS Cognito provides OAuth2 auth flows such as Authorization Code where the application can redirect to AWS Cognito hosted login screens where the user credentials are validated against Cognito data store and is redirected to the configured landing pages on the application side with an idToken which represents user authorization.

We're skipping on the installation and project setup, since we've discussed in detail about the libraries to install, the project structure and the DI designing. I'd highly recommend you to go through the part one, which deals with Implementing Cognito User Login and Signup in ASP.NET Core using AWS SDK.

Reset a user's forgotten password based on Email address aka Forgot Password Flow:

In the case a user forgets the password to login, we can let user reset password based on a confirmation code. To do this, we should first create a forgot password request and let cognito send a confirmation code for us. We do it in two steps:

  1. Check if the emailAddress exists in UserPool or not
  2. If exists, initiate a forgot password request for the Cognito to send a confirmation code
private async Task<ListUsersResponse> FindUsersByEmailAddress(string emailAddress)
{
    ListUsersRequest listUsersRequest = new ListUsersRequest
    {
        UserPoolId = _cloudConfig.UserPoolId,
        Filter = $"email=\"{emailAddress}\""
    };
    return await _provider.ListUsersAsync(listUsersRequest);
}

public async Task<InitForgotPwdResponse> TryInitForgotPasswordAsync(InitForgotPwdModel model)
{
    var listUsersResponse = await FindUsersByEmailAddress(model.EmailAddress);

    if (listUsersResponse.HttpStatusCode == HttpStatusCode.OK)
    {
        var users = listUsersResponse.Users;
        var filtered_user = users.FirstOrDefault();

        if (filtered_user != null)
        {
            var forgotPasswordResponse = await _provider.ForgotPasswordAsync(new ForgotPasswordRequest
            {
                ClientId = _cloudConfig.AppClientId,
                Username = filtered_user.Username
            });

            if (forgotPasswordResponse.HttpStatusCode == HttpStatusCode.OK)
            {
                return new InitForgotPwdResponse
                {
                    IsSuccess = true,
                    Message = $"Confirmation Code sent to {forgotPasswordResponse.CodeDeliveryDetails.Destination} via {forgotPasswordResponse.CodeDeliveryDetails.DeliveryMedium.Value}",
                    UserId = filtered_user.Username,
                    EmailAddress = model.EmailAddress
                };
            }
            else
            {
                return new InitForgotPwdResponse
                {
                    IsSuccess = false,
                    Message = $"ForgotPassword Response: {forgotPasswordResponse.HttpStatusCode.ToString()}"
                };
            }
        }
        else
        {
            return new InitForgotPwdResponse
            {
                IsSuccess = false,
                Message = $"No users with the given emailAddress found."
            };
        }
    }
    else
    {
        return new InitForgotPwdResponse
        {
            IsSuccess = false,
            Message = $"ListUsers Response: {listUsersResponse.HttpStatusCode.ToString()}"
        };
    }
}

To find the Users with the given EmailAddress we use the ListUsersAsync() method on the provider, which returns an Array of matched users. If there are users we go ahead and initiate forgot password which leads to the confirmation code. We'd also pass the Cognito user objectId which uniquely represents a user inside the Cognito along with our response because this is required while resetting the password which would be our next implementation.

Reset Password with Confirmation Code:

Resetting password is a straight forward affair - we require the emailAddress, new password, configuration code and the cognito user objectId (the one sent in the initiation response). We pass these values along with the AppClientId to the ConfirmForgotPasswordAsync() method on the provider. A successful response means the password is updated and the user can now proceed to login.

public async Task<ResetPasswordResponse> TryResetPasswordWithConfirmationCodeAsync(ResetPasswordModel model)
{
    try
    {
        var response = await _provider.ConfirmForgotPasswordAsync(new ConfirmForgotPasswordRequest
        {
            ClientId = _cloudConfig.AppClientId,
            Username = model.UserId,
            Password = model.NewPassword,
            ConfirmationCode = model.ConfirmationCode
        });

        if (response.HttpStatusCode == HttpStatusCode.OK)
        {
            return new ResetPasswordResponse
            {
                IsSuccess = true,
                Message = "Password Updated. Please Login."
            };
        }
        else
        {
            return new ResetPasswordResponse
            {
                IsSuccess = false,
                Message = $"ResetPassword Response: {response.HttpStatusCode.ToString()}"
            };
        }
    }
    catch (CodeMismatchException)
    {
        return new UserSignUpResponse
        {
            IsSuccess = false,
            Message = "Invalid Confirmation Code",
            EmailAddress = model.EmailAddress
        };
    }
}

Update a logged in user's password aka Change Password Flow:

To change an already authenticated user, we require the user's Emailaddress, current password and the new password. We follow the below steps:

  1. First check if the current password is valid - repeat the Login flow mechanism
  2. If the password is valid - initiate the change password with the new password and the details obtained from login response
public async Task<BaseResponseModel> TryChangePasswordAsync(ChangePwdModel model)
{
    try
    {
        // Check if the current Password is correct
        var user = new CognitoUser(emailAddress, _cloudConfig.AppClientId, _userPool, _provider);
        var authRequest = new InitiateSrpAuthRequest()
        {
            Password = password
        };

        var authResponse = await user.StartWithSrpAuthAsync(authRequest);
        var result = authResponse.AuthenticationResult;

        var request = new ChangePasswordRequest
        {
            AccessToken = result.AccessToken,
            PreviousPassword = model.CurrentPassword,
            ProposedPassword = model.NewPassword
        };

        var response = await _provider.ChangePasswordAsync(request);
        return new ChangePwdResponse { 
            UserId = user.Username, 
            Message = "Password Changed", 
            IsSuccess = true 
        };
    }
    catch (UserNotFoundException)
    {
        // occurs when the provided emailAddress doesn't exist
        return new AuthResponseModel
        {
            IsSuccess = false,
            Message = "EmailAddress not found."
        };
    }
    catch (NotAuthorizedException)
    {
        // occurs when the provided current password is invalid
        return new AuthResponseModel
        {
            IsSuccess = false,
            Message = "Incorrect password"
        };
    }
}

Update a logged in user's profile information aka Update Profile Flow:

Update Profile is similar to a new User Signup flow, except that here we need to first get the user information which is already available in Cognito and PATCH some new changes into it. When you look at the User Interaction, you'd need one GET API for fetching the information and one POST call to put the changes.

public async Task<UserProfileResponse> GetUserAsync(string emailAddress)
{
    // Find for users by emailAddress
    var users = await FindUsersByEmailAddress(emailAddress);

    // extract the first matching user
    // which technically is the current user
    var userResponse = users.Users.FirstOrDefault();

    // attributes property
    // contains all the values
    // that are associated with the user
    var attributes = userResponse.Attributes;

    // extract each property from the list
    // and assign them to the response model
    var response = new UserProfileResponse
    {
        EmailAddress = attributes.GetValueOrDefault("email", string.Empty),
        GivenName = attributes.GetValueOrDefault("given_name", string.Empty),
        PhoneNumber = attributes.GetValueOrDefault("phone_number", string.Empty),
        Gender = attributes.GetValueOrDefault("gender", string.Empty),
        UserId = attributes.GetValueOrDefault("sub", string.Empty)
    };

    // address is generally maintained
    // as a JSON of components
    // such as street_address, country, region, pincode and so on
    // based on the OpenID design guidelines
    
    var address = attributes.GetValueOrDefault("address", string.Empty);
    if (!string.IsNullOrEmpty(address))
    {
        response.Address = JsonConvert.DeserializeObject<Dictionary<string, string>>(address);
    }

    return response;
}

Once we have the Profile sent to the client, the client POSTs changes back to the POST endpoint which calls the UpdateProfile method with the new values. But we have another required value to be passed to the UpdateUserAttributesAsync() method we'd call on the provider - the accessToken. This token is something Cognito returns during TryLoginAsync() call so the client needs to pass this token as well during POST.

This construction is similar to the CreateUserAsync(), wherein we'd create an UpdateUserAttributesRequest and within the Attributes we add all the attributes that we'd like to update. Since Address is a JSON object, we'd add all our address components in a Dictionary and then add the serialized string as the value to the address field. Then we pass this object as a parameter to the UpdateUserAttributesAsync() method. Once the call is successful, we're good to go.

public async Task<UpdateProfileResponse> UpdateUserAttributesAsync(UpdateProfileModel model)
{
    var userAttributesRequest = new UpdateUserAttributesRequest
    {
        AccessToken = model.AccessToken
    };

    userAttributesRequest.UserAttributes.Add(new AttributeType
    {
        Value = model.GivenName,
        Name = "given_name"
    });

    userAttributesRequest.UserAttributes.Add(new AttributeType
    {
        Value = model.PhoneNumber,
        Name = "phone_number"
    });

    // upload the incoming profile photo to user's S3 folder
    // and get the s3 url
    // add the s3 url to the profile_photo attribute of the userCognito
    // if (model.ProfilePhoto != null)
    // {
    //     var picUrl = await _storage.AddItem(model.ProfilePhoto, "profile");
    //     userAttributesRequest.UserAttributes.Add(new AttributeType
    //     {
    //         Value = picUrl,
    //         Name = "picture"
    //     });
    // }

    if (model.Gender != null)
    {
        userAttributesRequest.UserAttributes.Add(new AttributeType
        {
            Value = model.Gender,
            Name = "gender"
        });
    }

    if (!string.IsNullOrEmpty(model.Address) ||
        string.IsNullOrEmpty(model.State) ||
        string.IsNullOrEmpty(model.Country) ||
        string.IsNullOrEmpty(model.Pincode))
    {
        var dictionary = new Dictionary<string, string>();

        dictionary.Add("street_address", model.Address);
        dictionary.Add("region", model.State);
        dictionary.Add("country", model.Country);
        dictionary.Add("postal_code", model.Pincode);

        userAttributesRequest.UserAttributes.Add(new AttributeType
        {
            Value = JsonConvert.SerializeObject(dictionary),
            Name = "address"
        });
    }

    var response = await _provider.UpdateUserAttributesAsync(userAttributesRequest);
    return new UpdateProfileResponse { 
        UserId = model.UserId, 
        Message = "Profile Updated", 
        IsSuccess = true 
    };
}

Logout Flow:

This flow is to invalidate all the active user tokens (idToken and accessToken) when the user decides to logout of the client. The API needs to call GlobalSignOutAsync() method on the provider with a GlobalSignOutRequest object which requires the accessToken, similar to the Update Profile flow.

public async Task<UserSignOutResponse> TryLogOutAsync(UserSignOutModel model)
{
    var request = new GlobalSignOutRequest { 
        AccessToken = model.AccessToken 
    };
    
    var response = await _provider.GlobalSignOutAsync(request);
    return new UserSignOutResponse { 
        UserId = model.UserId, 
        Message = "User Signed Out" 
    };
}
Author-Image

Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity.

You can now show your support. 😊

We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept