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