Adding Email Confirmation to ASP.NET Identity in MVC 5

In a previous article I demonstrated how to customize the user profile information in ASP.NET Identity.  Specifically I showed how to add capturing an email address for the user. We will expand on this work and add email confirmation to the registration process.  This process will send an email to the user with a link they can click on to confirm their registration and log in to the system. Prior to confirmation they will not be able to log in.  This process will be similar to the one I described for adding email confirmation to SimpleMembership.

First we need to modify the user information to store a the confirmation token and a flag indicating whether confirmation was completed or not.  So now our ApplicationUser looks like this.

    public class ApplicationUser : IdentityUser
    {
        public string Email { get; set; }
        public string ConfirmationToken { get; set; }
        public bool IsConfirmed { get; set; }
    }

If you have already made changes to the ApplicationUser before you will need to enable migrations and add a migration for these changes to take affect.  Now we can capture this information during the registration process and send the email.

        private string CreateConfirmationToken()
        {
            return ShortGuid.NewGuid();
        }

        private void SendEmailConfirmation(string to, string username, string confirmationToken)
        {
            dynamic email = new Email("RegEmail");
            email.To = to;
            email.UserName = username;
            email.ConfirmationToken = confirmationToken;
            email.Send();
        }

        //
        // POST: /Account/Register
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Register(RegisterViewModel model)
        {
            if (ModelState.IsValid)
            {
                string confirmationToken = CreateConfirmationToken();
                var user = new ApplicationUser()
                {
                    UserName = model.UserName,
                    Email = model.Email,
                    ConfirmationToken = confirmationToken, 
                        IsConfirmed = false };
                var result = await UserManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    SendEmailConfirmation(model.Email, model.UserName, confirmationToken);
                    return RedirectToAction("RegisterStepTwo", "Account");
                }
                else
                {
                    AddErrors(result);
                }
            }

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


To generate the token I use a class called ShortGuid that takes a GUID and encodes it so that it is shorter and is URL safe. I got the code for this class from this blog.  I add the generated token to the new user and set IsConfirmed to false. Then I send the email.  I use Postal to generate the emails.  Postal uses the Razor engine to create your dynamic email content that can include content such as the user name and the custom URL with the confirmation token.  The redirect to the RegisterStepTwo action just displays a view to the user that tells them to look for the email to complete the registration process.

Once the user gets the email they click on the link that will take us back to the controller action RegisterConfirmation.

        private bool ConfirmAccount(string confirmationToken)
        {
            ApplicationDbContext context = new ApplicationDbContext();
            ApplicationUser user =  context.Users.SingleOrDefault(u => u.ConfirmationToken == confirmationToken);
            if (user != null)
            {
                user.IsConfirmed = true;
                DbSet<ApplicationUser> dbSet = context.Set<ApplicationUser>();
                dbSet.Attach(user);
                context.Entry(user).State = EntityState.Modified;
                context.SaveChanges();

                return true;
            }
            return false;
        }

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

The method ConfirmAccount does a query to see if we can find a user with the confirmation token that was passed in the URL. If we find a user we set IsConfirmed to true and return true from the method; otherwise we return false. If the user is confirmed they will be able to log in. To make sure that the user could not log in before confirmation we need to make a small change to the Login action.

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
        {
            if (ModelState.IsValid)
            {
                var user = await UserManager.FindAsync(model.UserName, model.Password);
                if (user != null && user.IsConfirmed)
                {
                    await SignInAsync(user, model.RememberMe);
                    return RedirectToLocal(returnUrl);
                }
                else
                {
                    ModelState.AddModelError("", "Invalid username or password.");
                }
            }

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

All we do is check that if we find a user that the IsConfirmed flag is set to true.  If it is false then we do not log the user in and generate an error message.

That is all there is to setting up email confirmation using ASP.NET Identity in MVC 5.  You can get the source code for this example from the SimpleSecurity Project. You can find it in the AspNetIdentity folder.

One observation is that I had to make low level EF calls using the DbContext to make this work.  The UserManager provided by ASP.NET Identity to encapsulate EF and provide access to ApplicationUser did not provide me with enough control to do what I needed to accomplish.  It makes sense to me to add another layer on top of ASP.NET Identity that makes it easier to use and decouples it from the web application.  I decoupled SimpleMembership from the web application and it worked well. Look for something like this for ASP.NET Identity in the future that will be open source and available in the SimpleSecurity Project.



Comments

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