ASP.NET vNext Identity Part 5 – Forgotten Passwords

Before I start this article, I promised that I would be styling the login box and associated registration screens so that they look good. Four hours later and I’ve pretty much done what I wanted. I’m not a CSS whizz-kid, so I had to use css-tricks several times and go back and forth on registrations. Finally I accomplished what I set out to do – make the login screen look nicer. I also added a Disabled field into the Email section of config.json so that I can disable the email part of the registration completely during development. You can check out the code at my GitHub Repository.

If you take a look at the new code you will see that I’ve added a couple of things that were not there before. First off, there is a Social link with an ominous Coming Soon notice. That section is where I’ll be adding social logins and is the subject for a different article. The second item is that there is a Forgot Password? link at the bottom of the sign-in form, right next to the register button:

blog-code-0406-1

That link goes to the /Account/Forgot action and is for when the user forgets the password. I want the user to be able to enter their username and – if they are registered – they will be sent a link to reset their password. Once the user clicks on the link, he/she get a password change prompt and then get redirected back to the login screen. It’s a fairly common workflow for this subject.

Since I’ve got a link to the /Account/Forgot action, let’s start there. I need a controller action, which is, as is normal for the start of a workflow, very simple. This is in the AccountController.cs controller:

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

Incidentally, I put my workflows within the AccountController inside a #region – this allows me to group and collapse the actions related to a workflow and means they don’t interfere with one another when I’m developing. Pro Tip: Use #region.

@model AspNetIdentity.ViewModels.ForgotPasswordViewModel

@{
    Layout = "~/Views/Layout/LoginPage.cshtml";
    ViewBag.Title = "Forgot Password";
}

<div class="flex-container">
    <div class="login-form-outer" id="register">
        <h4>Forgot Password</h4>
        <p>Enter your email below and we will send a link via email so you can reset your password.</p>
        @using (Html.BeginForm("Forgot", "Account", FormMethod.Post, new { id = "forgot-form", role = "form" }))
        {
            @Html.AntiForgeryToken()
            @Html.ValidationSummary(true)
            <div class="form-group">
                <div class="control-label"><span class="fa fa-envelope"></span></div>
                <div class="form-control">
                    @Html.TextBoxFor(m => m.Email)
                    @Html.ValidationMessageFor(m => m.Email)
                </div>
            </div>
            <div class="submit-btn" id="forgot-btn">Email Link</div>
        }
    </div>
</div>

@section scripts
{
    <script src="~/lib/jquery/dist/jquery.min.js"></script>

    <script>
        $(document).ready(function () {
            $("#forgot-btn").click(function () {
                $("#forgot-form").submit();
            });
        });
    </script>
}

This is the same sort of form that I used in the Registration workflow. I ask for some information and then submit the information (via the form and a ViewModel) back to the controller for processing. I do need a ViewModel to receive the data. This is as easy as the action, really:

using System.ComponentModel.DataAnnotations;

namespace AspNetIdentity.ViewModels
{
    public class ForgotPasswordViewModel
    {
        [Required]
        [Display(Name = "Email")]
        public string Email { get; set; }
    }
}

So much for the easy stuff. Now comes the hard stuff, but it isn’t really that much harder. When the form gets posted back to the server, it will call the /Account/Forgot action as a POST (as opposed to a GET), so I need to handle the form in a new method:

         [HttpPost]
         [AllowAnonymous]
         [ValidateAntiForgeryToken]
         public async Task<IActionResult> Forgot(ForgotPasswordViewModel model)
        {
            if (ModelState.IsValid)
            {
                Debug.WriteLine("Forgot: Checking for user ID = " + model.Email);
                var user = await UserManager.FindByNameAsync(model.Email);
                // If the user does not exist or the user has not confirmed their email,
                // then say we confirmed, but don't actually do anything.
                if (user == null || !(await UserManager.IsEmailConfirmedAsync(user)))
                {
                    Debug.WriteLine("Forgot: User does not exist - lying to the user");
                    return View("ForgotConfirmation");
                }

                // If we found a user and it's valid, then work out the code and send
                // it via email.
                var code = await UserManager.GeneratePasswordResetTokenAsync(user);
                Debug.WriteLine("Forgot: Code = " + code);
                var callBackUrl = Url.Action("ResetPassword", "Account",
                    new { userId = user.Id, code = code },
                    protocol: Context.Request.Scheme);
                Debug.WriteLine("Forgot: Link = " + callBackUrl);
                await EmailService.Instance.SendEmailAsync(model.Email, "Reset Password",
                    "We received a request to reset your password.  If you did not request a " +
                    "password change, then please dis-regard this email with our apologies.\n\n" +
                    "To reset your password, click here: <a href=\"" + callBackUrl + "\">link</a>");
                return View("ForgotConfirmation");
            }

            // If the model was not valid, re-display the form
            return View(model);
        }

I hope you are getting the sense of the patterns in workflows now. I start with a HTTP GET entry-point for the workflow. This displays a View to gather data using a form that is accepted to a POST action via a ViewModel unique to the form. The controller Action, ViewModel and View all have distinct patterns that I can repeat over and over.

This controller action is very similar to the Registration action. I make sure the user doesn’t exist. I don’t want to let on that the user may or may not exist, so if the user doesn’t exist, then I will output the exact same message as if the user did exist. When the user exists, however, I generate a code – the Identity Framework has a routine for this – and send the code to the user (maybe) using exactly the same method that I use to send out the registration message. Check out my last article for a discussion on the email technique.

Here is the ForgotConfirmation.cshtml view that goes along with this action:

@{
    Layout = "~/Views/Layout/LoginPage.cshtml";
    ViewBag.Title = "Forgot Password";
}

<div class="flex-container">
    <div class="login-form-outer" id="forgotconfirm">
        <h4>Check Your Email</h4>
        <p>
            We have sent a reset password link to your email.  Please check your email
            and click on the link in that email to reset your password.
        </p>
    </div>
</div>

Just like the registration process, I’m going to print out the link to the Debug window so I can cut and paste it into my browser. This code will not work until that receiving action exists. In the middle of the Forgot POST action is a call to Url.Action for /Account/ResetPassword The ASP.NET code is reasonable enough to barf there if the target action doesn’t exist. I need to write a receiver for this action:

        /**
         * GET /Account/ResetPassword
         */
        [HttpGet]
        [AllowAnonymous]
        public async Task<IActionResult> ResetPassword(string userId = null, string code = null)
        {
            Debug.WriteLine("ResetPassword: Checking for userId = " + userId);
            if (userId == null || code == null)
            {
                Debug.WriteLine("ResetPassword: Invalid Parameters");
                return View("ResetPasswordError");
            }
            Debug.WriteLine("ResetPassword: Looking for userId");
            var user = await UserManager.FindByIdAsync(userId);
            if (user == null)
            {
                Debug.WriteLine("ResetPassword: Could not find user");
                return View("ResetPasswordError");
            }
            return View();
        }

If you are thinking you recognize this code, you might be right. I copied a lot of this from the /Account/ConfirmEmail action that was developed during the registration process. I’ve got a new view – ResetPasswordError – just in case you have an error. The error message should not give away anything about the user id.

@{
    Layout = "~/Views/Layout/LoginPage.cshtml";
    ViewBag.Title = "Confirm Email: Error";
}

<div class="flex-container">
    <div class="login-form-outer">
        <h4>Reset Password Failed</h4>
        <p>
            We could not reset your password.
        </p>
    </div>
</div>

I also need a ResetPassword view for the successful lookup side of things:

@model AspNetIdentity.ViewModels.ResetPasswordViewModel

@{
    Layout = "~/Views/Layout/LoginPage.cshtml";
    ViewBag.Title = "Reset Password";
}

<div class="flex-container">
    <div class="login-form-outer" id="resetpassword">
        <h4>Reset Password</h4>
        @using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { id="resetpw-form", role = "form" }))
        {
        @Html.AntiForgeryToken()
        @Html.ValidationSummary(true)
        @Html.HiddenFor(model => model.Code)
        <div class="form-group">
            <div class="control-label"><span class="fa fa-envelope"></span></div>
            <div class="form-control">
                @Html.TextBoxFor(m => m.Email)
                @Html.ValidationMessageFor(m => m.Email)
            </div>
        </div>
        <div class="form-group">
            <div class="control-label"><span class="fa fa-key"></span></div>
            <div class="form-control">
                @Html.PasswordFor(m => m.Password)
                @Html.ValidationMessageFor(m => m.Password)
            </div>
        </div>
        <div class="form-group">
            <div class="control-label"><span class="fa fa-key"></span></div>
            <div class="form-control">
                @Html.PasswordFor(m => m.ConfirmPassword)
                @Html.ValidationMessageFor(m => m.ConfirmPassword)
            </div>
        </div>
        <div class="submit-btn" id="resetpw-btn">Reset Password</div>
        }
    </div>
</div>

@section scripts
{
<script src="~/lib/jquery/dist/jquery.min.js"></script>

<script>
        $(document).ready(function () {
            $("#resetpw-btn").click(function () {
                $("#resetpw-form").submit();
            });
        });
</script>
}

… which means I also need a ResetPasswordViewModel.

using System.ComponentModel.DataAnnotations;

namespace AspNetIdentity.ViewModels
{
    public class ResetPasswordViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }

        [Required]
        [StringLength(100, MinimumLength = 6, ErrorMessage = "Password must be at least 6 characters.")]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

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

        public string Code { get; set; }
    }
}

Note that the system does not fill in the email address for the user. If the email message is intercepted on the way through then attacker already has the email address and so can enter it easily. If the user has clicked on the link then the email address should already be loaded in. In both cases, I don’t see a compelling reason to not fill in the email address. You can fix this at the bottom of the HttpGet version of ResetPassword(). Instead of returning View(), return a View with a model:

        [HttpGet]
        [AllowAnonymous]
        public async Task<IActionResult> ResetPassword(string userId = null, string code = null)
        {
            Debug.WriteLine("ResetPassword: Checking for userId = " + userId);
            if (userId == null || code == null)
            {
                Debug.WriteLine("ResetPassword: Invalid Parameters");
                return View("ResetPasswordError");
            }
            Debug.WriteLine("ResetPassword: Looking for userId");
            var user = await UserManager.FindByIdAsync(userId);
            if (user == null)
            {
                Debug.WriteLine("ResetPassword: Could not find user");
                return View("ResetPasswordError");
            }

            ResetPasswordViewModel model = new ResetPasswordViewModel();
            model.Email = user.UserName;
            return View(model);
        }

It’s now time to process the password reset. The user has filled in the form and clicked on the password reset button to submit the form. The form is submitted as a POST to /Account/ResetPassword and processed like this:

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
        {
            if (ModelState.IsValid)
            {
                Debug.WriteLine("ResetPassword: Checking for user = " + model.Email);
                var user = await UserManager.FindByNameAsync(model.Email);
                if (user == null)
                {
                    Debug.WriteLine("ResetPassword: User does not exist - lie to the user");
                    return RedirectToAction("ResetConfirmation", "Account");
                }
                var result = await UserManager.ResetPasswordAsync(user, model.Code, model.Password);
                if (result.Succeeded)
                {
                    Debug.WriteLine("ResetPassword: Password is reset - confirm to the user");
                    return RedirectToAction("ResetConfirmation", "Account");
                }
                foreach (var error in result.Errors)
                {
                    Debug.WriteLine(string.Format("Register: Adding Error: {0}:{1}", error.Code, error.Description));
                    ModelState.AddModelError("", error.Description);
                }
                return View(model);
            }
            Debug.WriteLine("ResetPassword: Model is invalid - just re-state the form");
            return View(model);
        }

Look familiar again? It should as this is pretty much the same code as our confirm email. I need a controllaction for the password reset:

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

And I need a view called ResetConfirmation.cshtml:

@{
    Layout = "~/Views/Layout/LoginPage.cshtml";
    ViewBag.Title = "Reset Password";
}

<div class="flex-container">
    <div class="login-form-outer" id="resetconfirm">
        <h4>Password Reset Successful</h4>
        <p>
            Please @Html.ActionLink("click here to log in", "Login", "Account", routeValues: null)
        </p>
    </div>
</div>

This, again, is similar to the ConfirmEmail view. The registration and forgotten password workflows have many similarities, so it is expected that they share a lot of characteristics.

Designing Workflows

Most of these processes were adapted from the originals for MVC5 and if you are familiar with MVC5 you will note that these were pretty much the same. I found the way that they were presented in the starter kits was confusing which was why I ripped them apart and put them back together again. By doing that over the last five days I feel I now understand them.

When I am designing a workflow, I got through a process. First, I draw the interaction on a white board. I have a white board book just for this sort of thing. Writing the interactions down helps me visualize what I need to have in terms of controller actions, views and models. Each time a user fills in a form there is the following:

  1. A HttpGet controller action for presenting the form
  2. A view for presenting the form
  3. A ViewModel for receiving the data when the user pushes Submit
  4. A HttpPost controller action for processing the form data
  5. One or more views to present success or failure

If you can identify those up front, the code becomes a lot of boiler plate except where you are adding in the logic for what the workflow needs to do.

Pro Tip: The white board is your friend.

Want the code? Check out the resultant code from the last 5 articles on my GitHub Repository.

4 thoughts

  1. Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1841

  2. Pingback: Les liens de la semaine – Édition #128 | French Coding

  3. I have been working through user login/registration/password-reset in asp.net 5 recently. Thanks for posting this.

    One question, in the 4th from last example you show the GET action method for ResetPassword.

    [HttpGet]
    [AllowAnonymous]
    public async Task ResetPassword(string userId = null, string code = null)

    I noticed that the code argument isn’t used in that method. Why not?

    Like

    • That got me as well. It’s non-obvious, but it is used the the View that gets generated. Look for model => model.Code in the resulting View.

      Like

Comments are closed.