MVC Confirmation Email – Nuget

install

Problem

I’ve received several requests to update my original post dealing with adding a confirmation step to the standard ASP.NET MVC registration process so that it can work with ASP.NET MVC3. This seemed like a perfect place to use (and learn) Nuget, so you don’t have to deal with all the steps of creating the methods, modifying the views, etc.

Solution

For now, I’m going to assume that you already have Nuget installed and you have some idea of how to use it. If you don’t, read through the instructions at the Nuget site, or check out blog posts by Scott Hanselman or Phil Haack. If you still need help, post something in the comments and I’ll expand this post to include a Getting Started section.

Step 1a – Create a New Project

In Visual Studio, create a new ASP.NET MVC 3 Internet Application.

createnewproject

Step 1b – Work with an Existing Project

This will work with an existing project, but I’m assuming that you’re using default everything. If you’ve modified the AccountController or AccountModels in an uber custom way, you’ll likely experience pain. If you’re using out-of-the-box functionality, you won’t have any issues. And by “won’t”, I mean you’re completely on your own.

Step 2 – A little preparation

Now that you got your project, rename Controllers/AccountController.cs and Views/Account/LogOn.cshtml by adding “.old” to each file.

rename

This needs to be done because Nuget cannot modify existing files (or at least I don’t know how to do it). These files are included in the Nuget package, and the default behavior of Nuget is to ignore any files that it finds on disk that match files in the package. By renaming them, if you have any custom code in there, you won’t lose it – you’ll just need to port it over to the new files.

Step 3 – Installing the package

A. From the console:

If you’re a fan of the console, Select Tools | Library Package Manager | Package Manager Console. At the console prompt type: install-package MvcConfirmationEmail. Nuget will download the package and modify your project accordingly.

Console

B. From the User Interface:

Right-click on your project root, and select “Manage Nuget Packages…”. When the dialog window pops-up, enter “ConfirmationEmail” (one word) in the search text box in the upper right corner.

gui

After a few seconds, it will find the MvcConfirmationEmail package (shown above). Click the install button, and it will download and install the package into your project.

WARNING!

If you didn’t rename the files as noted in Step 2 – this won’t work, and you will need to uninstall the package (PM>uninstall-package MvcConfirmationEmail), rename the files, and do this again.

Step 4 – Update web.config

When Nuget installed the package, a few additions were made to web.config so that you can configure your email service provider’s settings.

A. Email From Name

When you send an email to someone during the registration process, it has to come from a named account. Open up web.config and find the configuration\appSettings section. There is a key named MvcConfirmationEmailFromAccount. Change the value to the account name that will be sending the email. I’ve been using Gmail as my test account and it works great.

B.  Mail Settings

You need to configure the settings for your email service provider. Head down to the bottom of the config file, and you’ll find the system.net\mailSettings\smtp node. Set the appropriate values for host, userName, password, and port, Gmail requires port 587 for SMTP – if you’re not using Gmail – you will need to check with your service provider to see what this should be.

That’s it! You should now be able to run the application and have the same functionality/workflow available in the original post. If you run into any problems or have any questions, please post down in the comments section and I’ll be happy to walk through your issues.

UPDATE 10/18/2011: I created a sample site on AppHarbor that you can use to test the functionality/workflow. http://confirm.apphb.com.

 

Adding Email Confirmation to ASP.NET MVC

UPDATE October 8th, 2011: I’ve written a new post that uses ASP.NET MVC3 and Nuget.

UPDATE 2 October 9th, 2011: I’ve re-written this post to incorporate the improvements suggested by David in the comments. Wow – they really simplified the code!
[Sample Application Code]

Problem

On one of the sites I run, I ask people to register in order to gain access to current data. The site is based on a book by Martin Zweig and calculates his Zweig Model – or at least it will. Right now, it only calculates one of the constituent components – the Prime Rate Indicator. The book was published in 1993 and I only permit unregistered users to have access to the data through the publication of the book. If the user wants to see current data, she has to register using the stock ASP.NET MVC registration process. Out of the box, there isn’t any email confirmation procedure, so I end up with a fair amount of test@test.com addresses.

Now, the only reason that I ask people to register is to gauge interest in the site. I currently have zero plans to do anything with the registration – but may elect do more if and when I ever add more to the site. This is how world domination begins. Or something.

So – how to cut down on the faux registrations, while not adding too many hoops for the user to jump through? A common pattern is to send an email to the address provided by the user, with a link that, when clicked, activates their registration. Sounds easy.

Solution

In the out-of-the-box code template for ASP.NET MVC, the Register action verifies the MembershipCreateStatus, and if all is kosher, it signs the user in (line 11 below).

        [HttpPost]
        public ActionResult Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                // Attempt to register the user
                MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName,
                       model.Password, model.Email);
                if (createStatus == MembershipCreateStatus.Success)
                {
                    FormsService.SignIn(model.UserName, false /* createPersistentCookie */);
                    return RedirectToAction("Index", "Home");
                }
                else
                {
                    ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus));
                }
            }

            // If we got this far, something failed, redisplay form
            ViewData["PasswordLength"] = MembershipService.MinPasswordLength;
            return View(model);
        }

To override this behavior, we can make use of the IsApproved property on the MembershipUser class. We want to set the property to “false” until the user has clicked the confirmation link. To do that, we need to dig into the implementation of the MembershipUser.CreateUser method, which is buried in AccountMembershipService class of the AccountModels.cs file.

public MembershipCreateStatus CreateUser(string userName, string password, string email)
{
	if (String.IsNullOrEmpty(userName)) throw
                new ArgumentException("Value cannot be null or empty.", "userName");
	if (String.IsNullOrEmpty(password)) throw
                new ArgumentException("Value cannot be null or empty.", "password");
	if (String.IsNullOrEmpty(email)) throw
                new ArgumentException("Value cannot be null or empty.", "email");

	MembershipCreateStatus status;
        // ORIGINAL: 6th Parameter is IsApproved property - which defaults to true
	    //_provider.CreateUser(userName, password, email, null, null, true, null, out status);
        // MODIFICATION: Set the IsApproved property to false
        _provider.CreateUser(userName, password, email, null, null, false, null, out status);
	return status;
}

With that done, the user will not be approved when they are created – so we’ll send the user an email with a hyperlink in it that, when clicked, will set their status to Approved.

        [HttpPost]
        public ActionResult Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                // Attempt to register the user
                MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName,
                           model.Password, model.Email);

                if (createStatus == MembershipCreateStatus.Success)
                {
                    /* TODO: At this point, the user has created a valid account (but is unapproved)
                     * We need to send the user a confirmation email and then
                     * redirect them to a confirmation page
                     * that says, 'thank you for registering, please check your
                     * email form a confirmation link. */

                    MembershipService.SendConfirmationEmail(model.UserName);

                    //FormsService.SignIn(model.UserName, false /* createPersistentCookie */);
                    return RedirectToAction("confirmation");
                }
                else
                {
                    ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus));
                }
            }

            // If we got this far, something failed, redisplay form
            ViewData["PasswordLength"] = MembershipService.MinPasswordLength;
            return View(model);
        }

Now, this won’t compile because the MembershipService interface does not include a SendConfirmationEmail method. So, mosey on over to Models\AccountModels.cs and look for IMembershipService definition. We’re going to add the method to the interface’s definition, like this:

    public interface IMembershipService
    {
        int MinPasswordLength { get; }

        bool ValidateUser(string userName, string password);
        MembershipCreateStatus CreateUser(string userName, string password, string email);
        bool ChangePassword(string userName, string oldPassword, string newPassword);

        // Additions to Interface for EmailConfirmation...
        void SendConfirmationEmail(string userName);
    }

Now let’s implement the method in the AccountMembershipService class:

public void SendConfirmationEmail(string userName)
        {
            MembershipUser user = Membership.GetUser(userName);
            string confirmationGuid = user.ProviderUserKey.ToString();
            string verifyUrl = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) +
                             "/account/verify?ID=" + confirmationGuid;

            var message = new MailMessage("YOUR_USER_ACCOUNT_HERE@YOUR_DOMAIN_HERE", user.Email)
            {
                Subject = "Please confirm your email",
                Body = verifyUrl

            };

            var client = new SmtpClient();

            client.Send(message);
        }

Here, we’re setting the confirmationGuid to the user’s ProviderUserKey property. This is the GUID stored in the database that uniquely identifies the user. We then set the verifyUrl to the Verify action on the Account controller – passing the confirmationGuid as the ID parameter. We then new-up a simple email message that just contains a hyperlink consisting of the verifyUrl.

In order for this email to work, you’ll need to import the

using System.Net.Mail;
using System.Configuration;

namespaces, and you’ll need to add the following to your web.config file:

<system.net>
  <mailSettings>
    <smtp deliveryMethod="Network">
                <network host="YOUR_MAIL_HOST" userName="YOUR_USER_NAME@YOUR_DOMAIN"
                                     password="YOUR_PASSWORD" port="YOUR_PORT">
    </smtp>
  </mailSettings>
</system.net>

Add this directly to the configuration root, changing the settings as appropriate. For testing purposes, you can use a Gmail account – which uses port 587. Be sure to double-check your settings!

With that configured, when the user clicks the register button, they’ll be sent an email with the confirmation link it, and they’ll be redirected to the confirmation page. That page doesn’t exist yet, so right click on the Views\Account folder and select Add | View. Name the view ‘confirmation’, and set your master page. The view does not need to be strongly typed, as you won’t be rendering any data here (though you certainly could). Add a simple message like this:

<@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
                                 Inherits="System.Web.Mvc.ViewPage<dynamic>>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
	Confirmation
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Confirmation</h2>
    <p>Thank you for registering. Please check your email for a confirmation request with a
                 link that will confirm your account. Once you click the link,
                 your registration will be complete.</p>

</asp:Content>

While you’re at it, you also need to add corresponding action methods for the Confirmation and Welcome views:

        public ActionResult Confirmation()
        {
            return View();
        }

        public ActionResult Welcome()
        {
            return View();
        }

The welcome view don’t exist yet, we’ll add it closer to the end.

At this point, everything should be good – and you’ll just need to implement the Verify action on the Account controller.

        public ActionResult Verify(string ID)
        {
            if (string.IsNullOrEmpty(ID) || (!Regex.IsMatch(ID,
                           @"[0-9a-f]{8}\-([0-9a-f]{4}\-){3}[0-9a-f]{12}")))
            {
                TempData["tempMessage"] =
                        "The user account is not valid. Please try clicking the link in your email again."
                return View();
            }

            else
            {
                MembershipUser user = Membership.GetUser(new Guid(ID));

                if (!user.IsApproved)
                {
                    user.IsApproved = true;
                    Membership.UpdateUser(user);
                    FormsService.SignIn(user.UserName, false);
                    return RedirectToAction("welcome");
                }
                else
                {
                    FormsService.SignOut();
                    TempData["tempMessage"] = "You have already confirmed your email address... please log in.";
                    return RedirectToAction("LogOn");
                }
             }
        }

This takes the id passed in the querystring and checks to ensure that it’s not null and that it’s a valid GUID. You’ll need to import the

using System.Text.RegularExpressions;

namespace for the regex to work. Once those criteria are met, the code instantiates a MembershipUser with the ID by calling the GetUser method of the Membership class. The code then checks if the user is not currently approved, and in that case, sets the IsApproved property to true, updates the User object store, signs the user in and redirects the user to a ‘welcome’ page. If the user is already approved (meaning this is not the first time the user has clicked the link), it signs any user out and redirects the user to the LogOn page, adding a message to TempData that the account is already registered. This last part is optional, and I’m not sure what the ‘proper’ experience should be for a user that clicks on the link multiple times – but this seemed to work for me.

The last thing you’ll need to do is add the welcome view, again, right-clicking on Views\Account and selecting Add | View:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
                                 Inherits="System.Web.Mvc.ViewPage<dynamic>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
	Welcome
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Welcome</h2>

    <p>
        Thank you <%: User.Identity.Name %> for verifying your email address.
                          You now have access to the site, and can poke around as you please.
    </p>

</asp:Content>

With this done, you should have a workflow as follows:

When the user clicks on the register link, they enter their registration data:

When they click the register button, they’re redirected to the confirmation page with instructions to check their email:

When they open their email client, they’ll have an email containing their verification link:

Finally, clicking the link will verify their account, sign them in, and redirect them to the welcome page:

Now, I could have chosen to just verify the account and then redirect them to the sign-on page. But that’s always annoyed me, and since there isn’t money on the line, I decided to be a nice guy and save my user from the hassle of having to do more work. I hate it when I call the phone company and have to enter all of my data – only to end up talking to an operator whose first question is: “And may I have your account number please?” Umm… didn’t I just type that in?

If the user tries to confirm their account more than once, they’re taken to:

This requires that we check for the existence of tempData in the Accounts\LogOn view:

<asp:Content ID="loginContent" ContentPlaceHolderID="MainContent" runat="server">
    <h2>Log On</h2>
    <p>
        Please enter your username and password.
                <%: Html.ActionLink("Register", "Register") %> if you don't have an account.
    </p>
    <%  if (!string.IsNullOrEmpty(TempData["tempMessage"] as string)) { %>
        <p>
            <b class="error"><%: TempData["tempMessage"].ToString() %></b>
        </p>
    <% } %>

    <% using (Html.BeginForm()) { %>

Wrap Up

That’s all there is to it. I’ve included a working sample application here. I didn’t include the database, as it will be created for you in the App_Data folder when you register your first user. Please let me know if you have any questions or issues.