Adding Email Confirmation to SimpleMembership

In a previous post I described how to seed and customize the SimpleMembership provider that is now the default membership provider for an ASP.NET MVC 4 Internet application.  In this post we will extend what we learned from the previous post and incorporate an email confirmation step to the registration process.  SimpleMembership actually makes this very straight forward.  I will assume that you have followed the steps in my previous post and have already modified the UserProfile class to include an Email property.

First we need to modify the RegisterModel in AccountModels.cs to include an Email property for capturing the email address during registration.

public class RegisterModel
{
    [Required]
    [Display(Name = "User name")]
    public string UserName { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [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 password do not match.")]
    public string ConfirmPassword { get; set; }

    [Required]
    [Display(Name = "EMail")]
    public string Email { get; set; }

}



Then we need to modify the view in Register.cshtml to include our new Email property by adding these lines write after the ConfirmPassword.

   <li>
       @Html.LabelFor(m => m.Email)
       @Html.TextBoxFor(m => m.Email)
   </li>



Next we need to modify the existing AccountController to this.

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Register(RegisterModel model)
{
    if (ModelState.IsValid)
    {
        // Attempt to register the user
        try
        {
            string confirmationToken = 
                WebSecurity.CreateUserAndAccount(model.UserName, model.Password, new { Email = model.Email }, true);
            dynamic email = new Email("RegEmail");
            email.To = model.Email;
            email.UserName = model.UserName;
            email.ConfirmationToken = confirmationToken;
            email.Send();

            return RedirectToAction("RegisterStepTwo", "Account");
        }
        catch (MembershipCreateUserException e)
        {
            ModelState.AddModelError("", ErrorCodeToString(e.StatusCode));
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

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

[AllowAnonymous]
public ActionResult RegisterConfirmation(string Id)
{
    if (WebSecurity.ConfirmAccount(Id))
    {
        return RedirectToAction("ConfirmationSuccess");
    }
    return RedirectToAction("ConfirmationFailure");
}

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

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

The Register action for the AccountController has been modified so that WebSecurity.CreateUserAndAccount now accepts the new Email property and update the UserProfile table with this information. It has also been modified to return a confirmation token. The boolean parameter tells the membership provider that a token confirmation process will be used.

The modified Register action sends an email to the user using the email address we captured during registration and the confirmation token that was generated by CreateUserAndAccount.  I am using Postal to handle sending the email. I like this package because it uses the Razor View Engine to generate your email. The view for my email look like this.

@{
    Layout = null;
}

To: @ViewBag.To
From: lolcats@website.com
Subject: Complete Registration Process

Hello @ViewBag.UserName,

To complete the registration process click on this link 
http://localhost/Account/RegisterConfirmation/@ViewBag.ConfirmationToken


Notice that when you test this out you will need to add a port number after localhost that matches where Visual Studio is running the application in debug mode.


For testing I use the default settings so that it sends the email to a directory that I can open and test the link for my confirmation page.  The default settings are in the web.config and they look like this.

  <system.net>
    <mailSettings>
      <smtp deliveryMethod="SpecifiedPickupDirectory">
        <specifiedPickupDirectory pickupDirectoryLocation="c:\email"/>
        <network host="localhost"/>
      </smtp>
    </mailSettings>
  </system.net>


Postal uses the SmtpClient to send the email and it uses these configuration settings. You can get more details on modifying these settings for an SMTP server on a network here. They would look something like this.

  <system.net>
    <mailSettings>
      <smtp>
        <network host="SMTPServer" port="" userName="username" password="password" />
      </smtp>
    </mailSettings>
  </system.net>


The Register action has also been modified to redirect to a RegisterStepTwo action which just provides a view that tells the user to look in their email for further instructions on the registration process. When you get to this page you can enter the link in your test email which uses the RegisterConfirmation action in the Account controller to confirm the user. It uses the WebSecurity.ConfirmAccount method which takes the confirmation token as a parameter.  If this is successful the user is redirected to the ConfirmationSuccess action which just presents a view indicating that they are confirmed and can now log into the system.

To test this out I modified the HomeController to use the AuthorizeAttribute for the Contact and About actions. It looks like this.

[Authorize(Roles = "Admin")]
public ActionResult About()
{
    ViewBag.Message = "Your app description page.";

    return View();
}

[Authorize]
public ActionResult Contact()
{
    ViewBag.Message = "Your contact page.";
    var context = new UsersContext();
    var username = User.Identity.Name;
    var user = context.UserProfiles.SingleOrDefault(u => u.UserName == username);
    ViewBag.Email = user.Email;

    return View();
}

I also modified the Contact action to add the users email to a ViewBag. This demonstrates how to retrieve information from the UserProfile table and display the information for verification.  The Contact view was modified to display the email with these view lines inserted in the body of the page.

 <p>
    <span class="label">Your Email:</span>
    <span><a href="mailto:@ViewBag.Email">@ViewBag.Email</a></span>
 </p>

If you try to access the Contact page before you log in you will be directed to the log in page. Log in with the credentials you used during the registration and it will redirect you to the Contact page.  Now you will see the email that you used during registration displayed.  If you try to access the About page when you are logged in you will redirected to the log in page. That is because the Authroize attribute for this action indicates you must be in the Admin role.  In this sample the user is not assigned any roles during the registration/account creation process.  If you log in again with the account that we seeded SimpleMembership with it will redirect you to the About page.

That is all there is to adding email confirmation to an ASP.NET MVC 4 Internet application. SimpleMembership makes it pretty... well simple.  This also demonstrates why you might want to customize the SimpleMembership UserProfile and how to access this custom data.

You can download the complete project here.




Comments

  1. Thanx for a great post!

    I have tried it out and all works well if the intent is that the user is directed to the login page after confirmation, but what if I want to automatically login the user upon confirmation? After all, the user has sent the confirmationtoken confirming the account. How do I log the user in att this stage?

    Offcourse I could pass the username in confirmationLink and fetch it from the querystring or something, but then what? How do I get users password?

    What's the pattern here, what am I missing?

    /Frederik

    ReplyDelete
    Replies
    1. Good question. I was thinking about automatically logging the person in on confirmation also, but after further research I determined there was no easy way to do this and it was probably not very secure. If you test the confirmation page you will notice that you can hit it any number of times and it will pass the confirmation process. This means that if you automatically log the person on during confirmation that anyone that gets a hold of that token (i.e. gets a hold of the confirmation email) will be able to log on as that person. For other security reasons the password stored in the database is encrypted using a one-way hash algorithm, so there is no way to retrieve the password to log them in, and it is the only way to log them in securely. So while having the users log in after confirmation may be a slight inconvenience, it makes your web site much more secure. I have noticed that most sights require this two step process.

      Delete
  2. This is good stuff Kevin, but i have few problems, i just can't figure out what i am suppose to add to my web.config with your tutorial. i have tried:

    Keep getting authentication error. What do you think?

    ReplyDelete
    Replies
    1. Nothing was modified in the web.config for this example. It uses the one generated when you use the MVC 4 project template in VS 2012, with the Internet option. I have updated this blog post to include a link to the complete project at the very end. Hopefully this will help. If you are still stuck provide more details on the specific error and what you are doing when it occurs, and hopefully I can assist.

      Delete
    2. After looking through your sample code, i can see the difference. Your smtp delivery method isn't Network, its rather your local drive. Did you try to deliver a confirmation email to an actual email address through a network?

      Delete
    3. Sorry; I forgot that Postal updated the web.config for the email settings. I have updated this post to include information on these configuration setting with an example of what a network setting may look like and links to more resources on this. I have used Postal before with a network SMTP server. I hope this helps.

      Delete
    4. I figured it out, thanks. Also i just realised that i posted some sensitive information in my first comment, thanks for omitting that when you approved the comment. This host="smtp.gmail.com" works if you also include enableSsl="true". I am assuming that, this approach applies to any other smtp hot you prefer.

      Delete
  3. Great post!

    How would I implement "re-send confirmation" for my users who lose the original mail?

    ReplyDelete
    Replies
    1. Excellent question. WebMatrix.WebData.WebSecurity does not provide any methods that let you retrieve the confirmation token after it is initially created. The only way to do this is to query the webpages_Membership table directly. I have updated SimpleSecurity.WebSecurity to include a method named GetConfirmationToken which accepts the user name as a parameter. I will be updating the reference web application to provide an example of how to use this. I have added a unit test to the project which tests and demonstrates how to call this method. You can get the source code at https://simplesecurity.codeplex.com/SourceControl/BrowseLatest.

      Delete
    2. I have added a new blog post that describes how to get the confirmation token to re-send the confirmation email. http://kevin-junghans.blogspot.com/2013/04/retrieving-confirmation-token-in.html

      Delete

Post a Comment

Popular posts from this blog

Using Claims in ASP.NET Identity

Seeding & Customizing ASP.NET MVC SimpleMembership

Customizing Claims for Authorization in ASP.NET Core 2.0