Forgot Password in ASP.NET Core Identity

Forgot Password in ASP.NET Core Identity

In this article, I will discuss How to Implement the Forgot Password Functionality in ASP.NET Core Identity. Please read our previous article discussing How to Block Login if Email is not Confirmed in ASP.NET Core Identity.

Forgot Password in ASP.NET Core Identity

The “Forget Password” functionality in ASP.NET Core Identity is one of the important features for handling situations where users forget their passwords. Here’s why it’s important and how it works:

  • User Experience: Users often forget their passwords, especially if they have accounts on multiple platforms. Providing a ‘Forget Password’ option improves the user experience by allowing users to reset their password without assistance easily.
  • Security: This feature enhances security. Users can request a password reset link sent to their registered email address when they forget their password. This process ensures that only the legitimate account owner can reset the password.
Workflow of Forget Password in ASP.NET Core Identity:
  • User Requests Password Reset: The user clicks on the “Forget Password” link and enters their email address.
  • Generate Token: ASP.NET Core Identity generates a secure token that is unique to the user and the current time. This token is often sent via email.
  • Send Email: The system sends an email to the user’s registered email containing the password reset token and a link to reset the password.
  • User Resets Password: The user clicks on the link, which redirects them to a page where they can set a new password. The page typically asks for the token (either automatically included in the URL or entered manually) and the new password.
  • Validate Token: The system validates the token to ensure it is valid and corresponds to the user. This validation is crucial for security.
  • Update Password: If the token is valid, the system updates the user’s password with the new one.
Reset User Password in ASP.NET Core Identity using Forget Password:

Resetting a user’s password in ASP.NET Core Identity using the “Forgot Password” feature involves several steps. Let us see the step-by-step process to implement Reset Password in ASP.NET Core Identity using Forget Password. The following is the workflow of Forgot Password.

We need to provide one link with the name Forgot Password in the Login Page, as shown in the below image.

Reset User Password in ASP.NET Core Identity using Forget Password

When the user clicks on the Forgot Password link, the Forgot Password page asks the user to enter the Email Address and then click on the Submit button, as shown in the image below.

Reset User Password in ASP.NET Core Identity using Forget Password

Once the User Enter the Email address and clicks on the Submit button, an email is sent to the above Email ID (if the Email ID exists and the Email ID is Confirmed), using which the user can set his new password. After sending the email, we need to give one message, something like the one below, saying the password reset link is sent to your email address.

How to Implement the Forgot Password Functionality in ASP.NET Core Identity

Then, the user will receive the Password Reset Email, something like the one below, in his email address.

How to Implement the Forgot Password Functionality in ASP.NET Core Identity

Once the user clicks on the above link, it will open the following page and ask the user to enter the Password, Confirm Password, and click the Reset button, as shown in the image below.

Forgot Password Functionality in ASP.NET Core Identity

Once the user provides the Password and Confirm Password and clicks the Reset button, the password will reset, and you will get the following message.

Forgot Password Functionality in ASP.NET Core Identity

Let us proceed and see how we can implement this using ASP.NET Core Identity.

Modify the Login View:

Please modify the Login View as follows. Here, we are adding the Forgot Password link.

@model LoginViewModel

<div class="row">
    <div class="col-md-6">
        <h1>Local Account Login</h1>
        <hr />
        <form method="post" asp-action="Login" asp-controller="Account">
            <input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" />
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Email"></label>
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Password"></label>
                <input asp-for="Password" class="form-control" />
                <span asp-validation-for="Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <div class="checkbox">
                    <label asp-for="RememberMe">
                        <input asp-for="RememberMe" />
                        @Html.DisplayNameFor(m => m.RememberMe)
                    </label>
                </div>
            </div>
            <div class="form-group">
                <button type="submit" class="btn btn-primary">Login</button>
            </div>
            <br />
            <div class="form-group">
                <a class="btn btn-primary" asp-controller="Account" asp-route-IsResend="false"
                   asp-action="ResendConfirmationEmail">Confirm Email</a>
            </div>
            <div>
                <a asp-controller="Account" asp-action="ForgotPassword">Forgot Password?</a>
            </div>
        </form>
    </div>
    <div class="col-md-6">
        <h1>External Login</h1>
        <hr />
        @{
            if (Model.ExternalLogins == null || Model.ExternalLogins?.Count == 0)
            {
                <div>No external logins configured</div>
            }
            else
            {
                <form method="post" asp-action="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl">
                    <div>
                        @foreach (var provider in Model.ExternalLogins)
                        {
                            <button onclick="externalLogin(@provider.Name, @Model.ReturnUrl)" type="submit" class="btn btn-primary"
                                    name="provider" value="@provider.Name"
                                    title="Log in using your @provider.DisplayName account">
                                @provider.DisplayName
                            </button>
                        }
                    </div>
                </form>
            }
        }
    </div>
</div>
Forgot Password View Model

Create a class file named ForgotPasswordViewModel.cs and then copy and paste the following code. This is going to be the Model for our ForgotPassword view. We need the user’s Email address to send the password reset link. So, this class contains just one property, i.e., Email.

using System.ComponentModel.DataAnnotations;
namespace ASPNETCoreIdentityDemo.Models
{
    public class ForgotPasswordViewModel
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; }
    }
}
Forgot Password Get Action Methods

Next, add the following ForgotPassword HttpGet Action Method within the Account Controller. This action method will display the forward password screen to the user.

[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPassword()
{
    return View();
}
ForgotPassword View:

Next, add a view named ForgotPassword.cshtml within the Views/Account directory and copy and paste the following code. This is the forgot password view.

@model ForgotPasswordViewModel
@{
    ViewData["Title"] = "ForgotPassword";
}

<h2>Forgot Password</h2>
<hr />
<div class="row">
    <div class="col-md-12">
        <form method="post" asp-action="ForgotPassword" asp-controller="Account">
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Email"></label>
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
    </div>
</div>
Creating a Private Method to Send Password Reset Email:

Please add the following SendForgotPasswordEmail private method to the Account Controller, which will send the Password Reset Email to the user.

private async Task SendForgotPasswordEmail(string? email, ApplicationUser? user)
{
    // Generate the reset password token
    var token = await userManager.GeneratePasswordResetTokenAsync(user);

    // Build the password reset link which must include the Callback URL
    // Build the password reset link
    var passwordResetLink = Url.Action("ResetPassword", "Account",
            new { Email = email, Token = token }, protocol: HttpContext.Request.Scheme);

    //Send the Confirmation Email to the User Email Id
    await emailSender.SendEmailAsync(email, "Reset Your Password", $"Please reset your password by <a href='{HtmlEncoder.Default.Encode(passwordResetLink)}'>clicking here</a>.", true);
}

In the above code, first, we are generating the reset password token using the GeneratePasswordResetTokenAsync method. Then we create the Password Reset link, which includes the Email and the unique token, and then we send that callback which we expect to be executed when the user clicks on the link.

Creating ForgotPassword HttpPost Method:

Next, add the following ForgotPassword Post action method in the Account Controller. Here, we are finding the user by email, which we receive as the input value from the Model, and then checking whether his email address is confirmed or not. If the user exists and his email address is confirmed, we send the Password Reset Link to his Email ID and then redirect him to the ForgotPasswordConfirmation page.

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
    if (ModelState.IsValid)
    {
        // Find the user by email
        var user = await userManager.FindByEmailAsync(model.Email);

        // If the user is found AND Email is confirmed
        if (user != null && await userManager.IsEmailConfirmedAsync(user))
        {
            await SendForgotPasswordEmail(user.Email, user);

            // Send the user to Forgot Password Confirmation view
            return RedirectToAction("ForgotPasswordConfirmation", "Account");
        }

        // To avoid account enumeration and brute force attacks, don't
        // reveal that the user does not exist or is not confirmed
        return RedirectToAction("ForgotPasswordConfirmation", "Account");
    }

    return View(model);
}
ForgotPasswordConfirmation Action Method:

Once the Email is sent, we redirect the user to the password confirmation page and say that the password reset link has been sent to your email address. So, for this, please add the following action method in the Account Controller:

[AllowAnonymous]
public ActionResult ForgotPasswordConfirmation()
{
    return View();
}
ForgotPasswordConfirmation View:

Next, add a view named ForgotPasswordConfirmation.cshtml within the Views/Account directory and copy and paste the following code. This view will render once we send the Forgot Password email link to the user.

@{
    ViewBag.Title = "Forgot Password Confirmation";
}

<hgroup class="title">
    <h1>Forgot Password Confirmation</h1>
</hgroup>
<div>
    <p>
        If you have an account with us, we have sent an email
        with the instructions to reset your password.
    </p>
</div>
Reset Password View Model:

To be able to reset the user password, we need the following

  • Email,
  • Password Reset Token,
  • New Password and
  • Confirm Password

Here, the user needs to provide the new password and confirmation password. Email and reset token are in the password reset link. So, create a class file named ResetPasswordViewModel.cs and copy and paste the following code. This is the model which we will use in our Password Reset View.

using System.ComponentModel.DataAnnotations;
namespace ASPNETCoreIdentityDemo.Models
{
    public class ResetPasswordViewModel
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "Password and Confirm Password must match")]
        public string ConfirmPassword { get; set; }

        public string Token { get; set; }
    }
}
ResetPassword Get Action Method

Next, add the following ResetPassword Get Action Method in the Account Controller. This action method will be invoked when the user clicks on the Password Reset Link in his/her email.

[HttpGet]
[AllowAnonymous]
public IActionResult ResetPassword(string Token, string Email)
{
    // If password reset token or email is null, most likely the
    // user tried to tamper the password reset link
    if (Token == null || Email == null)
    {
        ViewBag.ErrorTitle = "Invalid Password Reset Token";
        ViewBag.ErrorMessage = "The Link is Expired or Invalid";
        return View("Error");
    }
    else
    {
        ResetPasswordViewModel model = new ResetPasswordViewModel();
        model.Token = Token;
        model.Email = Email;
        return View(model);
    }
}
Reset Password View

Next, add a view named ResetPassword.cshtml view within the Views/Account directory and copy and paste the following code. We are using 2 hidden input fields to store email and password reset tokens, as we need them on postback.

@model ResetPasswordViewModel

@{
    ViewData["Title"] = "Reset Password";
}

<h2>Reset Password</h2>
<hr />
<div class="row">
    <div class="col-md-12">
        <form method="post" asp-action="ResetPassword" asp-controller="Account">
            <div asp-validation-summary="All" class="text-danger"></div>
            <input asp-for="Token" type="hidden" />
            <input asp-for="Email" type="hidden" />
            <div class="form-group">
                <label asp-for="Password"></label>
                <input asp-for="Password" class="form-control" />
                <span asp-validation-for="Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ConfirmPassword"></label>
                <input asp-for="ConfirmPassword" class="form-control" />
                <span asp-validation-for="ConfirmPassword" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-primary">Reset</button>
        </form>
    </div>
</div>
ResetPassword Post Action Method

Next, add the following ResetPassword Post Action Method in the Account Controller. When the user provides the necessary details and clicks the Submit button, the following HTTP Post action method will handle the request.

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{
    if (ModelState.IsValid)
    {
        // Find the user by email
        var user = await userManager.FindByEmailAsync(model.Email);

        if (user != null)
        {
            // reset the user password
            var result = await userManager.ResetPasswordAsync(user, model.Token, model.Password);

            if (result.Succeeded)
            {
                return RedirectToAction("ResetPasswordConfirmation", "Account");
            }

            // Display validation errors. For example, password reset token already
            // used to change the password or password complexity rules not met
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError("", error.Description);
            }
            return View(model);
        }

        // To avoid account enumeration and brute force attacks, don't
        // reveal that the user does not exist
        return RedirectToAction("ResetPasswordConfirmation", "Account");
    }
    // Display validation errors if model state is not valid
    return View(model);
}

ResetPasswordAsync: The Identity System first validates the token to ensure it is valid and hasn’t expired. This validation process checks whether the token matches the one generated for the user and whether it’s within the allowed time frame for use. The Identity System allows users to set a new password if the token is valid. Once the password is reset, the token is invalidated to prevent reuse.

ResetPasswordConfirmation Action Method:

Once the Password is reset, we redirect the user to the password reset confirmation page and say that the password has been reset. So, for this, please add the following action method in the Account Controller:

[AllowAnonymous]
public ActionResult ResetPasswordConfirmation()
{
    return View();
}
ResetPasswordConfirmation View:

Next, add a view named ResetPasswordConfirmation.cshtml within the Views/Account directory and copy and paste the following code. This view is going to render once the user Reset the Password.

@{
    ViewData["Title"] = "ResetPasswordConfirmation";
}

<h4>
    Your password is reset. Please click <a asp-action="Login" asp-controller="Account">here to login</a>
</h4>

So, this is how we can implement Forgot Password to Reset User Password in ASP.NET Identity. Now, run the application and test the functionality, and it should work as expected.

Note: If you observe the tokens are not stored in the AspNetUserTokens table. So, in the next article, I will discuss why the Forgot Password and Email Confirmation tokens are not stored in the database table. Then, we will see how to store those tokens in the AspNetUserTokens table.

In the next article, I will discuss How to Store Tokens in ASP.NET Core Identity. In this article, I explain How to Implement the Forgot Password Functionality in ASP.NET Core Identity. I hope you enjoy this article, Forgot Password in ASP.NET Core Identity.

Leave a Reply

Your email address will not be published. Required fields are marked *