Email confirmation cancel link issue

Marcelo Batemarchi asked on July 27, 2022 19:08

When a user access the page to request to reset the password, they will add their email and we will send a reset password link to that email. We do that using the code below:

        public ResetPasswordRequestController(ApplicationUserManager<ApplicationUser> userManager,
                     IMessageService messageService,
                     IEmailTemplateInfoProvider emailTemplateProvider)
    {
        this.userManager = userManager;
        this.messageService = messageService;
        this.emailTemplateProvider = emailTemplateProvider;
    }

     [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> RequestReset(ResetPasswordRequestViewModel model)
    {

        if (!ModelState.IsValid)
        {
            // Displays the sign-in form if the user credentials are invalid
            return View("~/Components/ResetPasswordRequest/_ResetPasswordRequest.cshtml", model);
        }

        // Gets the user entity for the specified email address
        ApplicationUser user = await userManager.FindByEmailAsync(model.Email);

        if (user != null)
        {
            UserInfo ui = UserInfo.Provider.Get(user.Id);

            //Ignore if user has requested a reset in the previous seconds to avoid multiple requests
            if (DateTime.Now.AddSeconds(-30) > ui.UserLastModified)
            {
                // Generates a password reset token for the user
                string token = await userManager.GeneratePasswordResetTokenAsync(user);
                string encodedToken = HttpUtility.UrlEncode(token);
                // Prepares the URL of the password reset link (targets the "ResetPassword" action)

                string resetUrl = $"{model.ResetPasswordFormUrl}?userId={user.Id}&token={encodedToken}";

                string cancelUrl = $"{model.ResetPasswordFormUrl}?userId={user.Id}&cancel=1";

                await SendEmail(messageService, user, encodedToken, resetUrl, cancelUrl);

                UserSettingsInfo userSettings = UserSettingsInfoProvider.GetUserSettingsInfoByUser(user.Id);
                userSettings.UserPasswordRequestHash = MD5HashHelper.GetHashString(token);
                userSettings.Update();

            }
        }
        // Displays a view asking the visitor to check their email and click the password reset link
        return View("~/Components/ResetPasswordRequest/_ResetPasswordRequest.Success.cshtml", model);

    }

As you can see we use the userManager.GeneratePasswordResetTokenAsync(user); to generate a token that is going to be used in the reset password link and we use another simple link for the cancel with just the userID and property indicating that cancel=1

This other controller receives the request when the user sets the new password and submit it (This part is all working fine)

        [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> PasswordResetConfirmation(ResetPasswordFormViewModel model)
    {
        // Validates the received password data based on the view model
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        ApplicationUser user = await userManager.FindByIdAsync(model.UserId.ToString());
        if (user != null)
        {
            var result = await userManager.ResetPasswordAsync(user, model.Token, model.NewPassword);
            // Changes the user's password if the provided reset token is valid
            if (result.Succeeded)
            {
                UserSettingsInfo userSettings = UserSettingsInfoProvider.GetUserSettingsInfoByUser(model.UserId);
                userSettings.UserPasswordRequestHash = null;
                userSettings.Update();

                return View("~/Components/ResetPasswordForm/_ResetPasswordForm.Success.cshtml", model);
            }
            else
            {
                // Occurs if the reset token is invalid or password invalid
                // Returns a view informing the user that the password reset failed
                ModelState.AddModelError(nameof(ChangePasswordFormViewModel.CurrentPassword), result.Errors.FirstOrDefault().Description);
                return View("~/Components/ResetPasswordForm/_ResetPasswordForm.cshtml", model);
            }
        }
        else
        {
            ModelState.AddModelError(nameof(ChangePasswordFormViewModel.CurrentPassword), ResHelper.GetString("Polestar.Component.ResetPasswordForm.UserNotFound"));
            return View("~/Components/ResetPasswordForm/_ResetPasswordForm.cshtml", model);
        }
    }

The problem is with the part below. I identify that the request is asking for a cancelation of the link and I clear the UserPasswordRequestHash. In my understanding, when I do that, the reset password link became invalid because we no longer have the hash, but if I do the cancellation, if I try to click on the other link to reset the password, it will reset anyways.

using CMS.Helpers;
using CMS.Membership;
using Kentico.Membership;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;

namespace Polestar.Components
{
    public class ResetPasswordFormViewComponent : ViewComponent
    {
        private readonly ApplicationUserManager<ApplicationUser> userManager;

        public ResetPasswordFormViewComponent(ApplicationUserManager<ApplicationUser> userManager,
                 IMessageService messageService)
        {
            this.userManager = userManager;
        }

        public IViewComponentResult Invoke(string loginPageUrl = "", int userID = 0, string token = "", string cancel = "")
        {

            ResetPasswordFormViewModel model = new ResetPasswordFormViewModel();

            model.LoginPageUrl = loginPageUrl;
            model.UserId = userID;
            model.Token = token;
            model.Sucess = true;

            if (ValidationHelper.GetBoolean(cancel, false))
            {
                try
                {
                    ApplicationUser user = userManager.FindByIdAsync(model.UserId.ToString()).Result;
                    if (user != null)
                    {
                        user.PasswordHash = null;
                        userManager.UpdateAsync(user);

                        UserSettingsInfo userSettings = UserSettingsInfoProvider.GetUserSettingsInfoByUser(userID);
                        userSettings.UserPasswordRequestHash = null;
                        userSettings.Update();

                        return View("~/Components/ResetPasswordForm/_ResetPasswordForm.Cancel.cshtml", model);
                    }
                    else
                    {
                        model.Sucess = false;
                        return View("~/Components/ResetPasswordForm/_ResetPasswordForm.Cancel.cshtml", model);
                    }
                }
                catch (Exception)
                {
                    // An InvalidOperationException occurs if a user with the given ID is not found
                    // Returns a view informing the user that the password reset request is not valid
                    model.Sucess = false;
                    return View("~/Components/ResetPasswordForm/_ResetPasswordForm.Cancel.cshtml", model);
                }
            }
            else
            {
                return View("~/Components/ResetPasswordForm/_ResetPasswordForm.cshtml", model);
            }
        }
    }
}

If I just click to reset the password, when I try the second time the link is no longer valid (which is correct). So my understanding is that inside of ApplicationUserManager

   Please, sign in to be able to submit a new answer.