Account confirmation and password recovery with ASP.NET Identity (C#)

Before doing this tutorial you should first complete Create a secure ASP.NET MVC 5 web app with log in, email confirmation and password reset. This tutorial contains more details and will show you how to set up email for local account confirmation and allow users to reset their forgotten password in ASP.NET Identity.

A local user account requires the user to create a password for the account, and that password is stored (securely) in the web app. ASP.NET Identity also supports social accounts, which don't require the user to create a password for the app. Social accounts use a third party (such as Google, Twitter, Facebook, or Microsoft) to authenticate users. This topic covers the following:

New users register their email alias, which creates a local account.

Image of the account register window

Selecting the Register button sends a confirmation email containing a validation token to their email address.

Image showing email sent confirmation

The user is sent an email with a confirmation token for their account.

Image of confirmation token

Selecting the link confirms the account.

Image confirming email address

Password recovery/reset

Local users who forget their password can have a security token sent to their email account, enabling them to reset their password.

Image of forgot password reset window

The user will soon get an email with a link allowing them to reset their password.

Image showing reset password email
Selecting the link will take them to the Reset page.

Image showing user password reset window

Selecting the Reset button will confirm the password has been reset.

Image showing password reset confirmation

Create an ASP.NET web app

Start by installing and running Visual Studio 2017.

  1. Create a new ASP.NET Web project and select the MVC template. Web Forms also support ASP.NET Identity, so you could follow similar steps in a web forms app.

  2. Change the authentication to Individual User Accounts.

  3. Run the app, select the Register link and register a user. At this point, the only validation on the email is with the [EmailAddress] attribute.

  4. In Server Explorer, navigate to Data Connections\DefaultConnection\Tables\AspNetUsers, right-click and select Open table definition.

    The following image shows the AspNetUsers schema:

    Image showing A s p Net  Users schema

  5. Right-click on the AspNetUsers table and select Show Table Data.

    Image showing table data

    At this point the email has not been confirmed.

The default data store for ASP.NET Identity is Entity Framework, but you can configure it to use other data stores and to add additional fields. See Additional Resources section at the end of this tutorial.

The OWIN startup class ( Startup.cs ) is called when the app starts and invokes the ConfigureAuth method in App_Start\Startup.Auth.cs, which configures the OWIN pipeline and initializes ASP.NET Identity. Examine the ConfigureAuth method. Each CreatePerOwinContext call registers a callback (saved in the OwinContext) that will be called once per request to create an instance of the specified type. You can set a break point in the constructor and Create method of each type (ApplicationDbContext, ApplicationUserManager) and verify they are called on each request. A instance of ApplicationDbContext and ApplicationUserManager is stored in the OWIN context, which can be accessed throughout the application. ASP.NET Identity hooks into the OWIN pipeline through cookie middleware. For more information, see Per request lifetime management for UserManager class in ASP.NET Identity.

When you change your security profile, a new security stamp is generated and stored in the SecurityStamp field of the AspNetUsers table. Note, the SecurityStamp field is different from the security cookie. The security cookie is not stored in the AspNetUsers table (or anywhere else in the Identity DB). The security cookie token is self-signed using DPAPI and is created with the UserId, SecurityStamp and expiration time information.

The cookie middleware checks the cookie on each request. The SecurityStampValidator method in the Startup class hits the DB and checks security stamp periodically, as specified with the validateInterval. This only happens every 30 minutes (in our sample) unless you change your security profile. The 30 minute interval was chosen to minimize trips to the database. See my two-factor authentication tutorial for more details.

Per the comments in the code, the UseCookieAuthentication method supports cookie authentication. The SecurityStamp field and associated code provides an extra layer of security to your app, when you change your password, you will be logged out of the browser you logged in with. The SecurityStampValidator.OnValidateIdentity method enables the app to validate the security token when the user logs in, which is used when you change a password or use the external login. This is needed to ensure that any tokens (cookies) generated with the old password are invalidated. In the sample project, if you change the users password then a new token is generated for the user, any previous tokens are invalidated and the SecurityStamp field is updated.

The Identity system allow you to configure your app so when the users security profile changes (for example, when the user changes their password or changes associated login (such as from Facebook, Google, Microsoft account, etc.), the user is logged out of all browser instances. For example, the image below shows the Single signout sample app, which allows the user to sign out of all browser instances (in this case, IE, Firefox and Chrome) by selecting one button. Alternatively, the sample allows you to only log out of a specific browser instance.

Image showing the single sign-out sample app window

The Single signout sample app shows how ASP.NET Identity allows you to regenerate the security token. This is needed to ensure that any tokens (cookies) generated with the old password are invalidated. This feature provides an extra layer of security to your application; when you change your password, you will be logged out where you have logged into this application.

The App_Start\IdentityConfig.cs file contains the ApplicationUserManager, EmailService and SmsService classes. The EmailService and SmsService classes each implement the IIdentityMessageService interface, so you have common methods in each class to configure email and SMS. Although this tutorial only shows how to add email notification through SendGrid, you can send email using SMTP and other mechanisms.

The Startup class also contains boiler plate to add social logins (Facebook, Twitter, etc.), see my tutorial MVC 5 App with Facebook, Twitter, LinkedIn and Google OAuth2 Sign-on for more info.

Examine the ApplicationUserManager class, which contains the users identity information and configures the following features:

  • Password strength requirements.
  • User lock out (attempts and time).
  • Two-factor authentication (2FA). I'll cover 2FA and SMS in another tutorial.
  • Hooking up the email and SMS services. (I'll cover SMS in another tutorial).

The ApplicationUserManager class derives from the generic UserManager<ApplicationUser> class. ApplicationUser derives from IdentityUser. IdentityUser derives from the generic IdentityUser class:

//     Default EntityFramework IUser implementation
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
   where TLogin : IdentityUserLogin<TKey>
   where TRole : IdentityUserRole<TKey>
   where TClaim : IdentityUserClaim<TKey>
{
   public IdentityUser()
   {
      Claims = new List<TClaim>();
      Roles = new List<TRole>();
      Logins = new List<TLogin>();
   }

   ///     User ID (Primary Key)
   public virtual TKey Id { get; set; }

   public virtual string Email { get; set; }
   public virtual bool EmailConfirmed { get; set; }

   public virtual string PasswordHash { get; set; }

   ///     A random value that should change whenever a users credentials have changed (password changed, login removed)
   public virtual string SecurityStamp { get; set; }

   public virtual string PhoneNumber { get; set; }
   public virtual bool PhoneNumberConfirmed { get; set; }

   public virtual bool TwoFactorEnabled { get; set; }

   ///     DateTime in UTC when lockout ends, any time in the past is considered not locked out.
   public virtual DateTime? LockoutEndDateUtc { get; set; }

   public virtual bool LockoutEnabled { get; set; }

   ///     Used to record failures for the purposes of lockout
   public virtual int AccessFailedCount { get; set; }
   
   ///     Navigation property for user roles
   public virtual ICollection<TRole> Roles { get; private set; }

   ///     Navigation property for user claims
   public virtual ICollection<TClaim> Claims { get; private set; }

   ///     Navigation property for user logins
   public virtual ICollection<TLogin> Logins { get; private set; }
   
   public virtual string UserName { get; set; }
}

The properties above coincide with the properties in the AspNetUsers table, shown above.

Generic arguments on IUser enable you to derive a class using different types for the primary key. See the ChangePK sample which shows how to change the primary key from string to int or GUID.

ApplicationUser

ApplicationUser (public class ApplicationUserManager : UserManager<ApplicationUser>) is defined in Models\IdentityModels.cs as:

public class ApplicationUser : IdentityUser
{
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(
        UserManager<ApplicationUser> manager)
    {
        // Note the authenticationType must match the one defined in 
       //   CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, 
    DefaultAuthenticationTypes.ApplicationCookie);
        // Add custom user claims here
        return userIdentity;
    }
}

The highlighted code above generates a ClaimsIdentity. ASP.NET Identity and OWIN Cookie Authentication are claims-based, therefore the framework requires the app to generate a ClaimsIdentity for the user. ClaimsIdentity has information about all the claims for the user, such as the user's name, age and what roles the user belongs to. You can also add more claims for the user at this stage.

The OWIN AuthenticationManager.SignIn method passes in the ClaimsIdentity and signs in the user:

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties(){
       IsPersistent = isPersistent }, 
       await user.GenerateUserIdentityAsync(UserManager));
}

MVC 5 App with Facebook, Twitter, LinkedIn and Google OAuth2 Sign-on shows how you can add additional properties to the ApplicationUser class.

Email confirmation

It's a good idea to confirm the email a new user register with to verify they are not impersonating someone else (that is, they haven't registered with someone else's email). Suppose you had a discussion forum, you would want to prevent "bob@example.com" from registering as "joe@contoso.com". Without email confirmation, "joe@contoso.com" could get unwanted email from your app. Suppose Bob accidentally registered as "bib@example.com" and hadn't noticed it, he wouldn't be able to use password recover because the app doesn't have his correct email. Email confirmation provides only limited protection from bots and doesn't provide protection from determined spammers, they have many working email aliases they can use to register.In the sample below, the user won't be able to change their password until their account has been confirmed (by them selecting a confirmation link received on the email account they registered with.) You can apply this work flow to other scenarios, for example sending a link to confirm and reset the password on new accounts created by the administrator, sending the user an email when they have changed their profile and so on. You generally want to prevent new users from posting any data to your web site before they have been confirmed by email, a SMS text message or another mechanism.

Build a more complete sample

In this section, you'll use NuGet to download a more complete sample we will work with.

  1. Create a new empty ASP.NET Web project.

  2. In the Package Manager Console, enter the following commands:

    Install-Package SendGrid
    Install-Package -Prerelease Microsoft.AspNet.Identity.Samples
    

    In this tutorial, we'll use SendGrid to send email. The Identity.Samples package installs the code we will be working with.

  3. Set the project to use SSL.

  4. Test local account creation by running the app, selecting the Register link, and posting the registration form.

  5. Select the demo email link, which simulates email confirmation.

  6. Remove the demo email link confirmation code from the sample (The ViewBag.Link code in the account controller. See the DisplayEmail and ForgotPasswordConfirmation action methods and razor views ).

Warning

If you change any of the security settings in this sample, productions apps will need to undergo a security audit that explicitly calls the changes made.

Examine the code in App_Start\IdentityConfig.cs

The sample shows how to create an account and add it to the Admin role. You should replace the email in the sample with the email you will be using for the admin account. The easiest way right now to create an administrator account is programmatically in the Seed method. We hope to have a tool in the future that will allow you to create and administer users and roles. The sample code does let you create and manage users and roles, but you must first have an administrators account to run the roles and user admin pages. In this sample, the admin account is created when the DB is seeded.

Change the password and change the name to an account where you can receive email notifications.

Warning

Security - Never store sensitive data in your source code.

As mentioned previously, the app.CreatePerOwinContext call in the startup class adds callbacks to the Create method of the app DB content, user manager and role manger classes. The OWIN pipeline calls the Create method on these classes for each request and stores the context for each class. The account controller exposes the user manager from the HTTP context (which contains the OWIN context):

public ApplicationUserManager UserManager
{
    get
    {
        return _userManager ?? 
    HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
    }
    private set
    {
        _userManager = value;
    }
}

When a user registers a local account, the HTTP Post Register method is called:

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
            var callbackUrl = Url.Action(
               "ConfirmEmail", "Account", 
               new { userId = user.Id, code = code }, 
               protocol: Request.Url.Scheme);

            await UserManager.SendEmailAsync(user.Id, 
               "Confirm your account", 
               "Please confirm your account by clicking this link: <a href=\"" 
                                               + callbackUrl + "\">link</a>");
            // ViewBag.Link = callbackUrl;   // Used only for initial demo.
            return View("DisplayEmail");
        }
        AddErrors(result);
    }

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

The code above uses the model data to create a new user account using the email and password entered. If the email alias is in the data store, account creation fails and the form is displayed again. The GenerateEmailConfirmationTokenAsync method creates a secure confirmation token and stores it in the ASP.NET Identity data store. The Url.Action method creates a link containing the UserId and confirmation token. This link is then emailed to the user, the user can select on the link in their email app to confirm their account.

Set up email confirmation

Go to the SendGrid sign up page and register for free account. Add code similar to the following to configure SendGrid:

public class EmailService : IIdentityMessageService
{
   public Task SendAsync(IdentityMessage message)
   {
      return configSendGridasync(message);
   }

   private Task configSendGridasync(IdentityMessage message)
   {
      var myMessage = new SendGridMessage();
      myMessage.AddTo(message.Destination);
      myMessage.From = new System.Net.Mail.MailAddress(
                          "Joe@contoso.com", "Joe S.");
      myMessage.Subject = message.Subject;
      myMessage.Text = message.Body;
      myMessage.Html = message.Body;

      var credentials = new NetworkCredential(
                 ConfigurationManager.AppSettings["mailAccount"],
                 ConfigurationManager.AppSettings["mailPassword"]
                 );

      // Create a Web transport for sending email.
      var transportWeb = new Web(credentials);

      // Send the email.
      if (transportWeb != null)
      {
         return transportWeb.DeliverAsync(myMessage);
      }
      else
      {
         return Task.FromResult(0);
      }
   }
}

Note

Email clients frequently accept only text messages (no HTML). You should provide the message in text and HTML. In the SendGrid sample above, this is done with the myMessage.Text and myMessage.Html code shown above.

The following code shows how to send email using the MailMessage class where message.Body returns only the link.

void sendMail(Message message)
{
#region formatter
   string text = string.Format("Please click on this link to {0}: {1}", message.Subject, message.Body);
   string html = "Please confirm your account by clicking this link: <a href=\"" + message.Body + "\">link</a><br/>";

   html += HttpUtility.HtmlEncode(@"Or click on the copy the following link on the browser:" + message.Body);
#endregion

   MailMessage msg = new MailMessage();
   msg.From = new MailAddress("joe@contoso.com");
   msg.To.Add(new MailAddress(message.Destination));
   msg.Subject = message.Subject;
   msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(text, null, MediaTypeNames.Text.Plain));
   msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(html, null, MediaTypeNames.Text.Html));

   SmtpClient smtpClient = new SmtpClient("smtp.gmail.com", Convert.ToInt32(587));
   System.Net.NetworkCredential credentials = new System.Net.NetworkCredential("joe@contoso.com", "XXXXXX");
   smtpClient.Credentials = credentials;
   smtpClient.EnableSsl = true;
   smtpClient.Send(msg);
}

Warning

Security - Never store sensitive data in your source code. The account and credentials are stored in the appSetting. On Azure, you can securely store these values on the Configure tab in the Azure portal. See Best practices for deploying passwords and other sensitive data to ASP.NET and Azure.

Enter your SendGrid credentials, run the app, register with an email alias can select the confirm link in your email. To see how to do this with your Outlook.com email account, see John Atten's C# SMTP Configuration for Outlook.Com SMTP Host and hisASP.NET Identity 2.0: Setting Up Account Validation and Two-Factor Authorization posts.

Once a user selects the Register button a confirmation email containing a validation token is sent to their email address.

Image of email sent confirmation window

The user is sent an email with a confirmation token for their account.

Image of email received

Examine the code

The following code shows the POST ForgotPassword method.

public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = await UserManager.FindByNameAsync(model.Email);
        if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
        {
            // Don't reveal that the user does not exist or is not confirmed
            return View("ForgotPasswordConfirmation");
        }

        var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
        var callbackUrl = Url.Action("ResetPassword", "Account", 
    new { UserId = user.Id, code = code }, protocol: Request.Url.Scheme);
        await UserManager.SendEmailAsync(user.Id, "Reset Password", 
    "Please reset your password by clicking here: <a href=\"" + callbackUrl + "\">link</a>");        
        return View("ForgotPasswordConfirmation");
    }

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

The method fails silently if the user email has not been confirmed. If an error was posted for an invalid email address, malicious users could use that information to find valid userId (email aliases) to attack.

The following code shows the ConfirmEmail method in the account controller that is called when the user selects the confirmation link in the email sent to them:

public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
    if (userId == null || code == null)
    {
        return View("Error");
    }
    var result = await UserManager.ConfirmEmailAsync(userId, code);
    if (result.Succeeded)
    {
        return View("ConfirmEmail");
    }
    AddErrors(result);
    return View();
}

Once a forgotten password token has been used, it's invalidated. The following code change in the Create method (in the App_Start\IdentityConfig.cs file) sets the tokens to expire in 3 hours.

if (dataProtectionProvider != null)
 {
    manager.UserTokenProvider =
       new DataProtectorTokenProvider<ApplicationUser>
          (dataProtectionProvider.Create("ASP.NET Identity"))
          {                    
             TokenLifespan = TimeSpan.FromHours(3)
          };
 }

With the code above, the forgotten password and the email confirmation tokens will expire in 3 hours. The default TokenLifespan is one day.

The following code shows the email confirmation method:

// GET: /Account/ConfirmEmail
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
   if (userId == null || code == null)
   {
      return View("Error");
   }
   IdentityResult result;
   try
   {
      result = await UserManager.ConfirmEmailAsync(userId, code);
   }
   catch (InvalidOperationException ioe)
   {
      // ConfirmEmailAsync throws when the userId is not found.
      ViewBag.errorMessage = ioe.Message;
      return View("Error");
   }

   if (result.Succeeded)
   {
      return View();
   }

   // If we got this far, something failed.
   AddErrors(result);
   ViewBag.errorMessage = "ConfirmEmail failed";
   return View("Error");
}

To make your app more secure, ASP.NET Identity supports Two-Factor authentication (2FA). See ASP.NET Identity 2.0: Setting Up Account Validation and Two-Factor Authorization by John Atten. Although you can set account lockout on login password attempt failures, that approach makes your login susceptible to DOS lockouts. We recommend you use account lockout only with 2FA.

Additional resources