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 web applications is one of the essential features for handling situations where users forget their passwords. Let us understand why it’s important and how it works in Web Applications:

  • User Experience: User often forgets their passwords, especially if they have accounts on multiple platforms. Providing a ‘Forget Password’ option improves the user experience by allowing users to easily reset their password without assistance.
  • Security: This feature enhances security. When a user forgets their password, they can request a password reset link sent to their registered email address. This process ensures that only the legitimate account owner can reset the password.
How to 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 proceed and see the step-by-step process to implement Reset Password in ASP.NET Core Identity.

Let us first understand the work flow and then we will implement the same. We need to provide one link with the name Forgot Password on the Login Page, as shown in the image below

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

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

Forgot Password in ASP.NET Core Identity

Once the User Enters the Email address and clicks the Submit button, an email is sent to the above Email ID (if the Email ID exists), which the user can use to set his new password. After sending the email, we need to give one message, something like the one below, saying a password reset link was 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 below, in his email address.

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

The user needs to click the above link, which will open the following page and ask the user to enter the Password, Confirm Password. Once you enter Password and Confirm Password, then click on the Reset button, as shown in the below image.

Forgot Password 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.

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

How to Implement Forget Password in ASP.NET Core Identity:

To Implement Forget Password in ASP.NET Core Identity, we need to follow the below steps:

  • 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 address containing the password reset token and a link to reset the password.
  • User Resets Password: The user clicks on the link, redirecting 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.

Let us proceed and see how we can implement this step by step using ASP.NET Core Identity in our ASP.NET Core MVC Application.

Modify the Login View:

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

@model LoginViewModel

<div class="container my-5">
    <div class="row">
        <!-- Local Account Login -->
        <div class="col-md-6">
            <div class="card shadow-sm">
                <div class="card-header bg-primary text-white">
                    <h4 class="mb-0">Local Account Login</h4>
                </div>
                <div class="card-body">
                    <form method="post" asp-action="Login" asp-controller="Account" novalidate>
                        <input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" />

                        <!-- Validation Summary -->
                        <div asp-validation-summary="All" class="text-danger mb-3"></div>

                        <!-- Email Field -->
                        <div class="mb-3">
                            <label asp-for="Email" class="form-label"></label>
                            <input asp-for="Email" class="form-control" placeholder="Enter your email" />
                            <span asp-validation-for="Email" class="text-danger"></span>
                        </div>

                        <!-- Password Field -->
                        <div class="mb-3">
                            <label asp-for="Password" class="form-label"></label>
                            <input asp-for="Password" class="form-control" placeholder="Enter your password" />
                            <span asp-validation-for="Password" class="text-danger"></span>
                        </div>

                        <!-- Remember Me -->
                        <div class="form-check mb-3">
                            <input asp-for="RememberMe" class="form-check-input" />
                            <label asp-for="RememberMe" class="form-check-label">
                                @Html.DisplayNameFor(m => m.RememberMe)
                            </label>
                        </div>

                        <!-- Login Button & Confirm Email Button -->
                        <div class="d-flex justify-content-between align-items-center">
                            <button type="submit" class="btn btn-success">Log In</button>
                            <a class="btn btn-warning"
                               asp-controller="Account"
                               asp-action="ResendConfirmationEmail"
                               asp-route-IsResend="false">Confirm Email</a>
                        </div>
                    </form>

                    <!-- Register & Forgot Password Options -->
                    <div class="mt-3 d-flex justify-content-end">
                        <a asp-controller="Account" asp-action="Register" class="btn btn-link">
                            Register
                        </a>
                        <a asp-controller="Account" asp-action="ForgotPassword" class="btn btn-link">
                            Forgot Password?
                        </a>
                    </div>
                </div>
            </div>
        </div>

        <!-- External Login -->
        <div class="col-md-6 mt-4 mt-md-0">
            <div class="card shadow-sm">
                <div class="card-header bg-primary text-white">
                    <h4 class="mb-0">External Login</h4>
                </div>
                <div class="card-body">
                    @if (Model.ExternalLogins == null || Model.ExternalLogins.Count == 0)
                    {
                        <div class="alert alert-info mb-0">
                            No external logins configured
                        </div>
                    }
                    else
                    {
                        <p class="mb-3">You can log in using one of the following providers:</p>
                        <div class="d-grid gap-2">
                            @foreach (var provider in Model.ExternalLogins)
                            {
                                <button type="button"
                                        class="btn btn-outline-primary external-login-btn"
                                        data-provider="@provider.Name"
                                        data-url="@Url.Action("ExternalLogin", "Account", new { provider = provider.Name, returnUrl = Model.ReturnUrl })"
                                        title="Log in using your @provider.DisplayName account">
                                    @provider.DisplayName
                                </button>
                            }
                        </div>
                    }
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <script type="text/javascript">
        document.querySelectorAll('.external-login-btn').forEach(button => {
            button.addEventListener('click', function () {
                var provider = this.getAttribute('data-provider');
                var url = this.getAttribute('data-url');

                // Define popup window size
                var width = 500;
                var height = 600;
                var left = (window.innerWidth / 2) - (width / 2);
                var top = (window.innerHeight / 2) - (height / 2);

                // Open the popup window
                var popup = window.open(url, provider, `width=${width},height=${height},top=${top},left=${left},resizable=yes`);

                // Monitor the popup to check if it closes
                var checkPopup = setInterval(function () {
                    if (popup.closed) {
                        clearInterval(checkPopup);
                        // Optionally, reload or fetch updated user state
                        location.reload(); // Refresh the parent window to reflect login state
                    }
                }, 500); // Check every 500ms if the popup is closed
            });
        });
    </script>
}
Forgot Password View Model

Create a class file named ForgotPasswordViewModel.cs within the Models folder, and then copy and paste the following code. This is going to be the Model for our ForgotPassword view. We just 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(ErrorMessage = "Please enter your email address.")]
        [EmailAddress(ErrorMessage = "The email address is not valid.")]
        public string Email { get; set; }
    }
}
Forgot Password Get Action Method

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"] = "Forgot Password";
}

<div class="container my-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card shadow-sm">
                <div class="card-header bg-primary text-white">
                    <h4 class="mb-0 text-center">Reset Your Password</h4>
                </div>
                <div class="card-body">
                    <p class="text-muted text-center mb-4">
                        Enter your registered email address below, and we will send you instructions to reset your password.
                    </p>

                    <!-- Validation Summary -->
                    <form method="post" asp-action="ForgotPassword" asp-controller="Account" novalidate>
                        <div asp-validation-summary="All" class="alert alert-danger" role="alert" style="display:none;"></div>

                        <!-- Email Input -->
                        <div class="form-group mb-3">
                            <label asp-for="Email" class="form-label fw-bold">Email Address</label>
                            <input asp-for="Email" class="form-control" placeholder="Enter your email address" />
                            <span asp-validation-for="Email" class="text-danger"></span>
                        </div>

                        <!-- Submit Button -->
                        <div class="d-grid">
                            <button type="submit" class="btn btn-primary btn-lg">
                                Send Reset Link
                            </button>
                        </div>
                    </form>
                </div>
                <div class="card-footer text-center">
                    <p class="mb-0">
                        <a asp-controller="Account" asp-action="Login" class="text-primary text-decoration-none">
                            Back to Login
                        </a>
                    </p>
                </div>
            </div>
        </div>
    </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. In the code below, we first generate the reset password token using the GeneratePasswordResetTokenAsync Identity method. Then we create the Password Reset link, which includes the Email and the generated unique token, and then we send that password reset link (this is a callback method), which we expect to be executed when the user clicks on the link.

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

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

    // Encode the link to prevent XSS attacks
    var safeLink = HtmlEncoder.Default.Encode(passwordResetLink);

    // Create the email subject
    var subject = "Reset Your Password";

    // Craft HTML message body
    var messageBody = $@"
    <div style=""font-family: Arial, Helvetica, sans-serif; font-size: 16px; color: #333; line-height: 1.5; padding: 20px;"">
        <h2 style=""color: #007bff; text-align: center;"">Password Reset Request</h2>
        <p style=""margin-bottom: 20px;"">Hi {user.FirstName} {user.LastName},</p>
        
        <p>We received a request to reset your password for your <strong>Dot Net Tutorials</strong> account. If you made this request, please click the button below to reset your password:</p>
        
        <div style=""text-align: center; margin: 20px 0;"">
            <a href=""{safeLink}"" 
               style=""background-color: #007bff; color: #fff; padding: 10px 20px; text-decoration: none; font-weight: bold; border-radius: 5px; display: inline-block;"">
                Reset Password
            </a>
        </div>
        
        <p>If the button above doesn’t work, copy and paste the following URL into your browser:</p>
        <p style=""background-color: #f8f9fa; padding: 10px; border: 1px solid #ddd; border-radius: 5px;"">
            <a href=""{safeLink}"" style=""color: #007bff; text-decoration: none;"">{safeLink}</a>
        </p>
        
        <p>If you did not request to reset your password, please ignore this email or contact support if you have concerns.</p>
        
        <p style=""margin-top: 30px;"">Thank you,<br />The Dot Net Tutorials Team</p>
    </div>";

    // Send the email
    await emailSender.SendEmailAsync(email, subject, messageBody, IsBodyHtml: true);
}
Creating ForgotPassword HttpPost Method:

Next, add the following ForgotPassword Post action method in the Account Controller. Here, we find the user by email, which we receive as the input value from the view. If the user exists, we send the Password Reset Link to his email ID and then redirect to the ForgotPasswordConfirmation page.

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
    // Check if the model passes all validation rules (e.g., [Required], [EmailAddress]).
    // If these rules fail, ModelState will contain errors.
    if (ModelState.IsValid)
    {
        // Attempt to find a user in the database whose email address matches the one entered by the user.
        var user = await userManager.FindByEmailAsync(model.Email);

        // If a user is found
        if (user != null)
        {
            // Send the user an email containing a unique, secure link to reset their password.
            // This is done by generating a password reset token and building the appropriate URL.
            await SendForgotPasswordEmail(user.Email, user);

            // Redirect the user to the "ForgotPasswordConfirmation" page,
            // which typically displays a message that an email has been sent.
            return RedirectToAction("ForgotPasswordConfirmation", "Account");
        }

        // If the user was not found, we still redirect to "ForgotPasswordConfirmation" without revealing that the email does not exist.
        // This approach helps prevent account enumeration and brute force attacks.
        return RedirectToAction("ForgotPasswordConfirmation", "Account");
    }

    // If ModelState is not valid (e.g., the email field is empty or invalid),
    // re-display the form so the user can correct the errors.
    return View(model);
}
ForgotPasswordConfirmation Action Method:

Once the email is sent, we need to redirect the user to the password confirmation page and inform them 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";
}

<div class="container my-5">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card shadow-sm">
                <div class="card-header bg-primary text-white text-center">
                    <h4 class="mb-0">Forgot Password Confirmation</h4>
                </div>
                <div class="card-body text-center">
                    <p class="text-muted mb-4">
                        If the email address you provided is associated with an account,
                        we have sent an email with instructions to reset your password.
                    </p>
                    <p class="mb-4">
                        Please check your inbox for a password reset email. If you don’t see it,
                        check your spam or junk folder, or <strong><a asp-controller="Account" asp-action="ForgotPassword" class="text-primary">try again</a></strong>.
                    </p>
                    <p>
                        <strong>Need further assistance?</strong> Contact our
                        <a href="mailto:info@dotnettutorials.net" class="text-primary">support team</a>.
                    </p>
                </div>
                <div class="card-footer text-center">
                    <a asp-controller="Account" asp-action="Login" class="btn btn-primary">Return to Login</a>
                </div>
            </div>
        </div>
    </div>
</div>
Password Reset View Model:

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 and confirmed passwords. 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(ErrorMessage = "Email address is required.")]
        [EmailAddress(ErrorMessage = "Please enter a valid email address.")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Password is required.")]
        [DataType(DataType.Password, ErrorMessage = "Invalid password format.")]
        public string Password { get; set; }

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

        [Required(ErrorMessage = "The password reset token is required.")]
        public string Token { get; set; }
    }
}
ResetPassword Get Action Method

Next, add the following ResetPassword Get Action Method to 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 use two hidden input fields to store the email address and password reset token, as we need them when we post the form back to the server.

@model ResetPasswordViewModel

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

<div class="container my-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card shadow-sm">
                <div class="card-header bg-primary text-white text-center">
                    <h4 class="mb-0">Reset Your Password</h4>
                </div>
                <div class="card-body">
                    <p class="text-muted text-center mb-4">
                        Enter your new password below. Make sure to use a strong password for better security.
                    </p>

                    <!-- Validation Summary -->
                    <form method="post" asp-action="ResetPassword" asp-controller="Account" novalidate>
                        <div asp-validation-summary="All" class="alert alert-danger" role="alert" style="display:none;"></div>

                        <!-- Hidden Fields for Token and Email -->
                        <input asp-for="Token" type="hidden" />
                        <input asp-for="Email" type="hidden" />

                        <!-- New Password Field -->
                        <div class="form-group mb-3">
                            <label asp-for="Password" class="form-label fw-bold">New Password</label>
                            <input asp-for="Password" class="form-control" placeholder="Enter your new password" />
                            <span asp-validation-for="Password" class="text-danger"></span>
                        </div>

                        <!-- Confirm Password Field -->
                        <div class="form-group mb-3">
                            <label asp-for="ConfirmPassword" class="form-label fw-bold">Confirm Password</label>
                            <input asp-for="ConfirmPassword" class="form-control" placeholder="Re-enter your new password" />
                            <span asp-validation-for="ConfirmPassword" class="text-danger"></span>
                        </div>

                        <!-- Reset Button -->
                        <div class="d-grid">
                            <button type="submit" class="btn btn-primary btn-lg">Reset Password</button>
                        </div>
                    </form>
                </div>
                <div class="card-footer text-center">
                    <p class="mb-0">
                        <a asp-controller="Account" asp-action="Login" class="text-primary text-decoration-none">
                            Back to Login
                        </a>
                    </p>
                </div>
            </div>
        </div>
    </div>
</div>
ResetPassword Post Action Method

Next, add the following ResetPassword Post Action Method to 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)
{
    // Check if the incoming model passes all validation rules specified in the data annotations.
    if (ModelState.IsValid)
    {
        // Find the user in the database using the provided email address.
        var user = await userManager.FindByEmailAsync(model.Email);

        // Proceed only if the user exists in the database.
        if (user != null)
        {
            // Attempt to reset the user's password using the token and the new password provided in the model.
            var result = await userManager.ResetPasswordAsync(user, model.Token, model.Password);

            // If the password reset operation is successful, redirect the user to the Reset Password Confirmation page.
            if (result.Succeeded)
            {
                return RedirectToAction("ResetPasswordConfirmation", "Account");
            }

            // If the password reset fails, loop through the list of errors returned by Identity
            // and add them to the ModelState to display on the view.
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError("", error.Description); // Add each error description to ModelState.
            }

            // Return the model back to the view so the user can fix any validation errors.
            return View(model);
        }

        // If the user is not found, redirect to the Reset Password Confirmation page to avoid
        // revealing whether an account exists for the provided email.
        // This approach prevents account enumeration attacks.
        return RedirectToAction("ResetPasswordConfirmation", "Account");
    }

    // If the model state is invalid (e.g., missing or incorrect data), return the same view
    // and display validation errors to the user.
    return View(model);
}
What Does the ResetPasswordAsync Method Do Internally?

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

ResetPasswordConfirmation Action Method:

Once the password is reset, we will 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"] = "Reset Password Confirmation";
}

<div class="container my-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card shadow-sm">
                <div class="card-header bg-primary text-white text-center">
                    <h4 class="mb-0">Password Reset Successful</h4>
                </div>
                <div class="card-body text-center">
                    <p class="text-muted mb-4">
                        Your password has been reset successfully. You can now log in to your account with your new password.
                    </p>
                    <div class="d-grid">
                        <a asp-action="Login" asp-controller="Account" class="btn btn-primary btn-lg">
                            Go to Login
                        </a>
                    </div>
                </div>
                <div class="card-footer text-center">
                    <p class="mb-0">
                        <strong>Need further assistance?</strong> Contact our
                        <a href="mailto:info@dotnettutorials.net" class="text-primary">support team</a>.
                    </p>
                </div>
            </div>
        </div>
    </div>
</div>

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: You can 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 *