glasses-logo

<dominikgorecki> Web development on the Microsoft Stack <dominikgorecki>

Implementing Password Reset in MVC 4 EF Code First using Simple Membership – Part 2

I apologize for the long delay between part 1 and part 2, but I’ve been really busy. I’ve created 2 new jQuery plugins (scrolling related) and I’ve put out my own RWD Grid Framework called Bare Bones. Check them out!

So it took me a while to figure out where we left off and what the next step is. Before you continue with this tutorial, you should already have a working copy of where we ended up with Part 1: an extended account model that allows for email entry. That was the hard part.  In Part 2, I’m going to explain how to use WebSecurity.GeneratePasswordResetToken that will create a token used to reset a user’s password. This is currently the most common way to recover a password: an email is sent to the user’s verified email address with a link the user can use to reset their password.

Part II – Create the Reset Functionality

The great part is that Simple Membership already comes with the functionality built in so all we need to do is implement it. Now that we have an email in our membership from Part 1, we’ll first need to create the request for reset page, then send the email, and finally a page to actually reset the password.  As suggested by Alec on my Code First tutorial, I will wrap up all the DB contexts in “using” statements to ensure it is properly disposed of as soon as it’s not needed.

Step 1 : Setup the LostPassword Page

Create the GET action in AccountController for LostPassword that simply returns View():

// GET: Account/LostPassword
[AllowAnonymous]
public ActionResult LostPassword()
{
    return View();
}

Then create the view that allows for the user to input their email address to receive their reset link. Note: Some websites like you to type in your username instead of your email address; however, I like the ones that allow you to enter your email address because–as sometimes happens to me–the user may have forgotten their username as well! Also if you want to do things right, I would suggest creating a LostPasswordModel that can be used on the view page so you can create validate it against the Email DataType and create custom error messages:

LostPasswordModel in AccountModels.cs:

public class LostPasswordModel
 {
    [Required(ErrorMessage = "We need your email to send you a reset link!")]
    [Display(Name = "Your account email")]
    [EmailAddress(ErrorMessage= "Not a valid email--what are you trying to do here?")]
    public string Email { get; set; }
 }

Create a new view based on the GET action we created (LostPassword.cshtml):

@model PasswordResetApp.Models.LostPasswordModel
@{
    ViewBag.Title = "Lost Password";
}

<h2>Lost Password</h2>

@using (Html.BeginForm())
{
   @Html.AntiForgeryToken()
   @Html.ValidationSummary()
   <fieldset>
      <legend>Lost Password Form</legend>
      <ol>
         <li>
             @Html.LabelFor(m => m.Email)
             @Html.TextBoxFor(m => m.Email)
         </li>
      </ol>
      <input type="submit" value="Recover Account" />
   </fieldset>
}

Step 2: Process the LostPassword POST

Create the POST part of the LostPassword action. This is a little tricky because you would think that you could use the Membership.GetUserNameByEmail method, but you can’t since that in Simple Membership that is not implemented. We will need to first look-up the username with a Linq query to load the MembershipUser object with (line 11 in our example). After that you generate the token (line 26) and send the email (line 49). Read the comments in this example carefully. The first comment on line 58 deals with privacy: you may not want to let the end user know that the email has not been found since that gives a third party possibly valuable information. The next comment on line 69 talks about how you may want to redirect to a “success” page since, right now, this example does not do anything if the email is successful sent. I didn’t want to add too much superfluous crap to this example; I leave the UX\flow up to you.

Here is the POST action for LostPassword in AccountController.cs:

// POST: Account/LostPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult LostPassword(LostPasswordModel model)
{
   if (ModelState.IsValid)
   {
      MembershipUser user;
      using (var context = new UsersContext())
      {
         var foundUserName = (from u in context.UserProfiles
                              where u.Email == model.Email
                              select u.UserName).FirstOrDefault();
         if (foundUserName != null)
         {
            user = Membership.GetUser(foundUserName.ToString());
         }
         else
         {
            user = null;
         }
      }
      if (user != null)
      {
         // Generae password token that will be used in the email link to authenticate user
         var token = WebSecurity.GeneratePasswordResetToken(user.UserName);
         // Generate the html link sent via email
         string resetLink = "<a href='"
            + Url.Action("ResetPassword", "Account", new { rt = token }, "http") 
            + "'>Reset Password Link</a>";

         // Email stuff
         string subject = "Reset your password for asdf.com";
         string body = "You link: " + resetLink;
         string from = "[email protected]";

         MailMessage message = new MailMessage(from, model.Email);
         message.Subject = subject;
         message.Body = body;
         SmtpClient client = new SmtpClient();

         // Attempt to send the email
         try
         {
            client.Send(message);
         }
         catch (Exception e)
         {
            ModelState.AddModelError("", "Issue sending email: " + e.Message);
         }
      }         
      else // Email not found
      {
         /* Note: You may not want to provide the following information
         * since it gives an intruder information as to whether a
         * certain email address is registered with this website or not.
         * If you're really concerned about privacy, you may want to
         * forward to the same "Success" page regardless whether an
         * user was found or not. This is only for illustration purposes.
         */
         ModelState.AddModelError("", "No user found by that email.");
      }
   }

   /* You may want to send the user to a "Success" page upon the successful
   * sending of the reset email link. Right now, if we are 100% successful
   * nothing happens on the page. :P
   */
   return View(model);
}

Step 3: Set-up SMTP

Temporarily set the SMTP settings to create a file in a directory so you can test out your solution (later you’ll want to set-up a valid SMTP server here so your application can actually send the email). Go into you main Web.Config and add this at the bottom (I show the </configuration> tag to illustrate the placement):

   <system.net>
      <mailSettings>
         <smtp deliveryMethod="SpecifiedPickupDirectory">
            <specifiedPickupDirectory pickupDirectoryLocation="C:\email"/>
         </smtp>
      </mailSettings>
   </system.net>
</configuration>

You’ll also need to create a new directory, “C:\email”. Once you’re done with this step, you’re application should successfully create an email file in C:\email that should contain the reset URL. The reset URL will still not work as we’ll need to

Step 4: Reset Password Page View Model

The final part is to create a page that processes the username (un) and token (rt) to allow the user to enter a new password.  This isn’t too difficult. First I create a view model for the reset page that will ensure that a return token is entered and that a new password is entered and confirmed (AccoundModels.cs):

public class ResetPasswordModel
{
   [Required]
   [Display(Name = "New Password")]
   [DataType(DataType.Password)]
   public string Password { get; set; }

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

   [Required]
   public string ReturnToken { get; set; }
}

Step 5: Create the GET Action and Page

The actions are quite simple because Simple Membership exposes a handy method for reseting the password (WebSecurity.ResetPassword) that takes two parameters: 1. the generated token and 2. The new password. First, however, we’ll need to create the GET action that will taken in the necessary query parameter and assigns it to the ReturnToken of the ResetPasswordModel model (AccountController.cs):

// GET: /Account/ResetPassword
[AllowAnonymous]
public ActionResult ResetPassword(string rt)
{
   ResetPasswordModel model = new ResetPasswordModel();
   model.ReturnToken = rt;
   return View(model);
}

From this create the view based on the ResetPasswordModel model view. Everything here is pretty standard except for the hidden field on line 25. This hidden field is passed in through the GET action that assigns it from the query parameter and allows the field to be entered in as a regular form to the POST method (ResetPassword.cshtml):

@model PasswordResetApp.Models.ResetPasswordModel

@{
   ViewBag.Title = "ResetPassword";
}

<h2>Reset Password</h2>

@using (Html.BeginForm())
{
   @Html.AntiForgeryToken()
   @Html.ValidationSummary()

   <fieldset>
      <legend>Resetting password form</legend>
      <ol>
         <li>
            @Html.LabelFor(m => m.Password)
            @Html.PasswordFor(m => m.Password)
         </li>
         <li>
            @Html.LabelFor(m => m.ConfirmPassword)
            @Html.PasswordFor(m => m.ConfirmPassword)
         </li>
         @Html.HiddenFor(m => m.ReturnToken)
      </ol>
      <input type="submit" value="Reset" />
   </fieldset>
<h2>@ViewBag.Message</h2>
}

Step 6: Create the POST action for ResetPassword

For the final step we’ll use the WebSecurity.ResetPassword method to change the users password. One line. Simple as that (AccountController.cs):

// POST: /Account/ResetPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ResetPassword(ResetPasswordModel model)
   {
   if (ModelState.IsValid)
   {
      bool resetResponse = WebSecurity.ResetPassword(model.ReturnToken, model.Password);
      if (resetResponse)
      {
         ViewBag.Message = "Successfully Changed";
      }
      else
      {
         ViewBag.Message = "Something went horribly wrong!";
      }
   }
return View(model);
}

Notice a few things here. The expected parameter is the model being returned, which is the view model we created for the ResetPage. Next, notice the one line that does all the work here–line 9.

Step 7: Test

So now that you have a working password reset, it’s time to test it. If you don’t have a user created yet, then create one. Test it out with one password and log-out. The go to /Account/LostPassword and type in the email that you registered the account with. Check “C:\email” for the email (go by last modified) and copy the URL into your client. … well you know the rest.

Leave a Reply

Open Menu Button