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.

Kevin

32 thoughts on “Adding Email Confirmation to ASP.NET MVC

  1. I stumbled upon your code while looking for some information on the MembershipProvider. I found your example helpful, but why did you modify the IsApproved property of the user directly after it’s created? Couldn’t you just set the cooresponding parameter on the call to the _provider.CreateUser in the IMembershipService implementation when the MembershipService.CreateUser is called?

    1. David – perfect! I didn’t see that overload in CreateUser… while my method *works* – yours is clearly more efficient/elegant. I’ll likely update the sample code and blog post for MVC3 – and will be sure to incorporate your change. Thanks for the catch!

    1. Hi Ryan – can you check/double-check/triple-check your web.config settings? Try using your gmail credentials… they work great for testing. There’s a thread on this topic here that shows how to do it. Please let me know if that helps.

      Regards,

      Kevin

  2. Great work Kevin: This is very helful for beginner like us, today I run the project and I was able to create three different user names under same email address, how can I stop that. where in accountModel I need to add validation, that it shouldn’t allow same email aswell?

    I think the below code doesn’t seem to work:
    case MembershipCreateStatus.DuplicateEmail:
    return “A username for that e-mail address already exists. Please enter a different e-mail address.”;

    1. Hi Green – in the AspNetSqlMembershipProvider section of the web.config file, you’ll find requiresUniqueEmail=”false”. Change that to “true” and it should throw an error when you try to create a second username with a duplicate email address.

  3. Thank you for this very well made tutorial!

    I think that to make it 100% perfect, you could apply the change suggested by David and add the creation of the “Verify” view (that is quite obvious, but missing).

    1. Andrea – I just incorporated David’s change in a new MVC3 post… and I’ve edited this post to incorporate those changes as well. I downloaded the zip file and the Verify View file is in there… am I missing something?

  4. Nice tutorial. This is what I have been looking for but I have a small problem when I try to resgister a new user. I get an exception saying unable to connect to SQL server database. I must be missing something obvious.

    1. Tandin – It looks like there may be something wrong with the sample as I got the same error the first time I ran it. Can you try executing the ‘Build | Clean’ Solution menu command and let me know if that works? If it doesn’t, I’ll dig further into it … but regardless, I need to fix the sample!

  5. Hi Kevin,

    Thank you for your prompt reply. It seemed like my SQL Express service wasn’t started. Now it is working. Cheers

  6. Good tutorial, everything was thoroughly explained.

    But I have a silly question about this line of code.

    Everything works fine if I give there my own data with gmail. So, how can other users can then register?(from other mail provider or just with different gmail account). Thank you in advance for your reply. This is my first approach with MVC, hence such a strange question:)

    1. Hi Lukas –

      I’m not sure that I completely understand your question – so please forgive me if I get this wrong. The data that you use to get gmail set up allows your application to be able to send emails to your users. It should only need to be setup once, and then will work for all of your potential users… if you want to point me to your application, I’ll register as well to confirm. Please let me know if you still have questions or if I didn’t answer that clearly enough.

      Regards,

      Kevin

      1. Thank you so much for your prompt reply. Yes, you understand me well. Now, of course, everything is clear and works well from the moment, I set the gmail account in the web.config file. Previously for other email accounts have been problems, hence my question appeared.

        Thanks again and best regards,

        Lukas

  7. When we start to run it giving error

    Server Error in ‘/’ Application.

    Configuration Error

    Description: An error occurred during the processing of a configuration file required to service this request. Please review the specific error details below and modify your configuration file appropriately.

    Parser Error Message: Could not load file or assembly ‘System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35′ or one of its dependencies. The system cannot find the file specified.

    Source Error:

    Line 25:
    Line 26:
    Line 27:
    Line 28:
    Line 29:

    Source File: D:\Downloads\EmailConfirmSample\EmailConfirmSample\EmailConfirmSample\web.config Line: 27

    Assembly Load Trace: The following information can be helpful to determine why the assembly ‘System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35′ could not be loaded.

    === Pre-bind state information ===
    LOG: User = Nirav-PC\Nirav
    LOG: DisplayName = System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
    (Fully-specified)
    LOG: Appbase = file:///D:/Downloads/EmailConfirmSample/EmailConfirmSample/EmailConfirmSample/
    LOG: Initial PrivatePath = D:\Downloads\EmailConfirmSample\EmailConfirmSample\EmailConfirmSample\bin
    Calling assembly : (Unknown).
    ===
    LOG: This bind starts in default load context.
    LOG: Using application configuration file: D:\Downloads\EmailConfirmSample\EmailConfirmSample\EmailConfirmSample\web.config
    LOG: Using host configuration file:
    LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config.
    LOG: Post-policy reference: System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
    LOG: Attempting download of new URL file:///C:/Users/Nirav/AppData/Local/Temp/Temporary ASP.NET Files/root/0b1a7f7f/ca308f35/System.Web.Mvc.DLL.
    LOG: Attempting download of new URL file:///C:/Users/Nirav/AppData/Local/Temp/Temporary ASP.NET Files/root/0b1a7f7f/ca308f35/System.Web.Mvc/System.Web.Mvc.DLL.
    LOG: Attempting download of new URL file:///D:/Downloads/EmailConfirmSample/EmailConfirmSample/EmailConfirmSample/bin/System.Web.Mvc.DLL.
    LOG: Attempting download of new URL file:///D:/Downloads/EmailConfirmSample/EmailConfirmSample/EmailConfirmSample/bin/System.Web.Mvc/System.Web.Mvc.DLL.
    LOG: Attempting download of new URL file:///C:/Users/Nirav/AppData/Local/Temp/Temporary ASP.NET Files/root/0b1a7f7f/ca308f35/System.Web.Mvc.EXE.
    LOG: Attempting download of new URL file:///C:/Users/Nirav/AppData/Local/Temp/Temporary ASP.NET Files/root/0b1a7f7f/ca308f35/System.Web.Mvc/System.Web.Mvc.EXE.
    LOG: Attempting download of new URL file:///D:/Downloads/EmailConfirmSample/EmailConfirmSample/EmailConfirmSample/bin/System.Web.Mvc.EXE.
    LOG: Attempting download of new URL file:///D:/Downloads/EmailConfirmSample/EmailConfirmSample/EmailConfirmSample/bin/System.Web.Mvc/System.Web.Mvc.EXE.

    Version Information: Microsoft .NET Framework Version:4.0.30319; ASP.NET Version:4.0.30319.272

    1. Hi Nirav,

      It *looks* like your error is related to not being able to find the MVC 2.0 assemblies… do you have ASP.NET MVC 2.0 Installed? What version of MVC are you using? You may just need to create a clean application and then port the code over. If you’re using MVC 3.0, there’s a nuget package you can install (blog post here). Please let me know regarding your progress.

      Kevin

Leave a Reply