Account confirmation and password recovery in ASP.NET Core

By Rick Anderson, Ponant, and Joe Audette

This tutorial shows how to build an ASP.NET Core app with email confirmation and password reset. This tutorial is not a beginning topic. You should be familiar with:

See this PDF file for the ASP.NET Core 1.1 version.

Prerequisites

.NET Core 3.0 SDK or later

Create and test a web app with authentication

Run the following commands to create a web app with authentication.

dotnet new webapp -au Individual -uld -o WebPWrecover
cd WebPWrecover
dotnet run

Run the app, select the Register link, and register a user. Once registered, you are redirected to the to /Identity/Account/RegisterConfirmation page which contains a link to simulate email confirmation:

  • Select the Click here to confirm your account link.
  • Select the Login link and sign-in with the same credentials.
  • Select the Hello YourEmail@provider.com! link, which redirects you to the /Identity/Account/Manage/PersonalData page.
  • Select the Personal data tab on the left, and then select Delete.

Configure an email provider

In this tutorial, SendGrid is used to send email. You need a SendGrid account and key to send email. You can use other email providers. We recommend you use SendGrid or another email service to send email. SMTP is difficult to secure and set up correctly.

Create a class to fetch the secure email key. For this sample, create Services/AuthMessageSenderOptions.cs:

public class AuthMessageSenderOptions
{
    public string SendGridUser { get; set; }
    public string SendGridKey { get; set; }
}

Configure SendGrid user secrets

Set the SendGridUser and SendGridKey with the secret-manager tool. For example:

dotnet user-secrets set SendGridUser RickAndMSFT
dotnet user-secrets set SendGridKey <key>

Successfully saved SendGridUser = RickAndMSFT to the secret store.

On Windows, Secret Manager stores keys/value pairs in a secrets.json file in the %APPDATA%/Microsoft/UserSecrets/<WebAppName-userSecretsId> directory.

The contents of the secrets.json file aren't encrypted. The following markup shows the secrets.json file. The SendGridKey value has been removed.

{
  "SendGridUser": "RickAndMSFT",
  "SendGridKey": "<key removed>"
}

For more information, see the Options pattern and configuration.

Install SendGrid

This tutorial shows how to add email notifications through SendGrid, but you can send email using SMTP and other mechanisms.

Install the SendGrid NuGet package:

From the Package Manager Console, enter the following command:

Install-Package SendGrid

See Get Started with SendGrid for Free to register for a free SendGrid account.

Implement IEmailSender

To Implement IEmailSender, create Services/EmailSender.cs with code similar to the following:

using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;
using System.Threading.Tasks;

namespace WebPWrecover.Services
{
    public class EmailSender : IEmailSender
    {
        public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor)
        {
            Options = optionsAccessor.Value;
        }

        public AuthMessageSenderOptions Options { get; } //set only via Secret Manager

        public Task SendEmailAsync(string email, string subject, string message)
        {
            return Execute(Options.SendGridKey, subject, message, email);
        }

        public Task Execute(string apiKey, string subject, string message, string email)
        {
            var client = new SendGridClient(apiKey);
            var msg = new SendGridMessage()
            {
                From = new EmailAddress("Joe@contoso.com", Options.SendGridUser),
                Subject = subject,
                PlainTextContent = message,
                HtmlContent = message
            };
            msg.AddTo(new EmailAddress(email));

            // Disable click tracking.
            // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
            msg.SetClickTracking(false, false);

            return client.SendEmailAsync(msg);
        }
    }
}

Configure startup to support email

Add the following code to the ConfigureServices method in the Startup.cs file:

  • Add EmailSender as a transient service.
  • Register the AuthMessageSenderOptions configuration instance.
public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
                  options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();

    // requires
    // using Microsoft.AspNetCore.Identity.UI.Services;
    // using WebPWrecover.Services;
    services.AddTransient<IEmailSender, EmailSender>();
    services.Configure<AuthMessageSenderOptions>(Configuration);

    services.AddRazorPages();
}

Register, confirm email, and reset password

Run the web app, and test the account confirmation and password recovery flow.

  • Run the app and register a new user
  • Check your email for the account confirmation link. See Debug email if you don't get the email.
  • Click the link to confirm your email.
  • Sign in with your email and password.
  • Sign out.

Test password reset

  • If you're signed in, select Logout.
  • Select the Log in link and select the Forgot your password? link.
  • Enter the email you used to register the account.
  • An email with a link to reset your password is sent. Check your email and click the link to reset your password. After your password has been successfully reset, you can sign in with your email and new password.

Change email and activity timeout

The default inactivity timeout is 14 days. The following code sets the inactivity timeout to 5 days:

services.ConfigureApplicationCookie(o => {
    o.ExpireTimeSpan = TimeSpan.FromDays(5);
    o.SlidingExpiration = true;
});

Change all data protection token lifespans

The following code changes all data protection tokens timeout period to 3 hours:

public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
                  options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.Configure<DataProtectionTokenProviderOptions>(o =>
       o.TokenLifespan = TimeSpan.FromHours(3));

    services.AddTransient<IEmailSender, EmailSender>();
    services.Configure<AuthMessageSenderOptions>(Configuration);

    services.AddRazorPages();
}

The built in Identity user tokens (see AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs )have a one day timeout.

Change the email token lifespan

The default token lifespan of the Identity user tokens is one day. This section shows how to change the email token lifespan.

Add a custom DataProtectorTokenProvider<TUser> and DataProtectionTokenProviderOptions:

public class CustomEmailConfirmationTokenProvider<TUser>
                                       : DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomEmailConfirmationTokenProvider(IDataProtectionProvider dataProtectionProvider,
        IOptions<EmailConfirmationTokenProviderOptions> options,
        ILogger<DataProtectorTokenProvider<TUser>> logger)
                                          : base(dataProtectionProvider, options, logger)
    {

    }
}
public class EmailConfirmationTokenProviderOptions : DataProtectionTokenProviderOptions
{
    public EmailConfirmationTokenProviderOptions()
    {
        Name = "EmailDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(4);
    }
}

Add the custom provider to the service container:

public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(config =>
    {
        config.SignIn.RequireConfirmedEmail = true;
        config.Tokens.ProviderMap.Add("CustomEmailConfirmation",
            new TokenProviderDescriptor(
                typeof(CustomEmailConfirmationTokenProvider<IdentityUser>)));
        config.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmation";
      }).AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddTransient<CustomEmailConfirmationTokenProvider<IdentityUser>>();

    services.AddTransient<IEmailSender, EmailSender>();
    services.Configure<AuthMessageSenderOptions>(Configuration);

    services.AddRazorPages();
}

Resend email confirmation

See this GitHub issue.

Debug email

If you can't get email working:

  • Set a breakpoint in EmailSender.Execute to verify SendGridClient.SendEmailAsync is called.
  • Create a console app to send email using similar code to EmailSender.Execute.
  • Review the Email Activity page.
  • Check your spam folder.
  • Try another email alias on a different email provider (Microsoft, Yahoo, Gmail, etc.)
  • Try sending to different email accounts.

A security best practice is to not use production secrets in test and development. If you publish the app to Azure, set the SendGrid secrets as application settings in the Azure Web App portal. The configuration system is set up to read keys from environment variables.

Combine social and local login accounts

To complete this section, you must first enable an external authentication provider. See Facebook, Google, and external provider authentication.

You can combine local and social accounts by clicking on your email link. In the following sequence, "RickAndMSFT@gmail.com" is first created as a local login; however, you can create the account as a social login first, then add a local login.

Web application: RickAndMSFT@gmail.com user authenticated

Click on the Manage link. Note the 0 external (social logins) associated with this account.

Manage view

Click the link to another login service and accept the app requests. In the following image, Facebook is the external authentication provider:

Manage your external logins view listing Facebook

The two accounts have been combined. You are able to sign in with either account. You might want your users to add local accounts in case their social login authentication service is down, or more likely they've lost access to their social account.

Enable account confirmation after a site has users

Enabling account confirmation on a site with users locks out all the existing users. Existing users are locked out because their accounts aren't confirmed. To work around existing user lockout, use one of the following approaches:

  • Update the database to mark all existing users as being confirmed.
  • Confirm existing users. For example, batch-send emails with confirmation links.

Prerequisites

.NET Core 2.2 SDK or later

Create a web app and scaffold Identity

Run the following commands to create a web app with authentication.

dotnet new webapp -au Individual -uld -o WebPWrecover
cd WebPWrecover
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet aspnet-codegenerator identity -dc WebPWrecover.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout;Account.ConfirmEmail"
dotnet ef database drop -f
dotnet ef database update
dotnet run

Test new user registration

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. After submitting the registration, you are logged into the app. Later in the tutorial, the code is updated so new users can't sign in until their email is validated.

View the Identity database

  • From the View menu, select SQL Server Object Explorer (SSOX).
  • Navigate to (localdb)MSSQLLocalDB(SQL Server 13). Right-click on dbo.AspNetUsers > View Data:

Contextual menu on AspNetUsers table in SQL Server Object Explorer

Note the table's EmailConfirmed field is False.

You might want to use this email again in the next step when the app sends a confirmation email. Right-click on the row and select Delete. Deleting the email alias makes it easier in the following steps.

Require email confirmation

It's a best practice to confirm the email of a new user registration. Email confirmation helps to verify they're not impersonating someone else (that is, they haven't registered with someone else's email). Suppose you had a discussion forum, and you wanted to prevent "yli@example.com" from registering as "nolivetto@contoso.com". Without email confirmation, "nolivetto@contoso.com" could receive unwanted email from your app. Suppose the user accidentally registered as "ylo@example.com" and hadn't noticed the misspelling of "yli". They wouldn't be able to use password recovery because the app doesn't have their correct email. Email confirmation provides limited protection from bots. Email confirmation doesn't provide protection from malicious users with many email accounts.

You generally want to prevent new users from posting any data to your web site before they have a confirmed email.

Update Startup.ConfigureServices to require a confirmed email:

public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<IdentityUser>(config =>
    {
        config.SignIn.RequireConfirmedEmail = true;
    })
        .AddDefaultUI(UIFramework.Bootstrap4)
        .AddEntityFrameworkStores<ApplicationDbContext>();

    // requires
    // using Microsoft.AspNetCore.Identity.UI.Services;
    // using WebPWrecover.Services;
    services.AddTransient<IEmailSender, EmailSender>();
    services.Configure<AuthMessageSenderOptions>(Configuration);

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

config.SignIn.RequireConfirmedEmail = true; prevents registered users from logging in until their email is confirmed.

Configure email provider

In this tutorial, SendGrid is used to send email. You need a SendGrid account and key to send email. You can use other email providers. ASP.NET Core 2.x includes System.Net.Mail, which allows you to send email from your app. We recommend you use SendGrid or another email service to send email. SMTP is difficult to secure and set up correctly.

Create a class to fetch the secure email key. For this sample, create Services/AuthMessageSenderOptions.cs:

public class AuthMessageSenderOptions
{
    public string SendGridUser { get; set; }
    public string SendGridKey { get; set; }
}

Configure SendGrid user secrets

Set the SendGridUser and SendGridKey with the secret-manager tool. For example:

C:/WebAppl>dotnet user-secrets set SendGridUser RickAndMSFT
info: Successfully saved SendGridUser = RickAndMSFT to the secret store.

On Windows, Secret Manager stores keys/value pairs in a secrets.json file in the %APPDATA%/Microsoft/UserSecrets/<WebAppName-userSecretsId> directory.

The contents of the secrets.json file aren't encrypted. The following markup shows the secrets.json file. The SendGridKey value has been removed.

{
  "SendGridUser": "RickAndMSFT",
  "SendGridKey": "<key removed>"
}

For more information, see the Options pattern and configuration.

Install SendGrid

This tutorial shows how to add email notifications through SendGrid, but you can send email using SMTP and other mechanisms.

Install the SendGrid NuGet package:

From the Package Manager Console, enter the following command:

Install-Package SendGrid

See Get Started with SendGrid for Free to register for a free SendGrid account.

Implement IEmailSender

To Implement IEmailSender, create Services/EmailSender.cs with code similar to the following:

using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;
using System.Threading.Tasks;

namespace WebPWrecover.Services
{
    public class EmailSender : IEmailSender
    {
        public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor)
        {
            Options = optionsAccessor.Value;
        }

        public AuthMessageSenderOptions Options { get; } //set only via Secret Manager

        public Task SendEmailAsync(string email, string subject, string message)
        {
            return Execute(Options.SendGridKey, subject, message, email);
        }

        public Task Execute(string apiKey, string subject, string message, string email)
        {
            var client = new SendGridClient(apiKey);
            var msg = new SendGridMessage()
            {
                From = new EmailAddress("Joe@contoso.com", "Joe Smith"),
                Subject = subject,
                PlainTextContent = message,
                HtmlContent = message
            };
            msg.AddTo(new EmailAddress(email));

            // Disable click tracking.
            // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
            msg.SetClickTracking(false, false);

            return client.SendEmailAsync(msg);
        }
    }
}

Configure startup to support email

Add the following code to the ConfigureServices method in the Startup.cs file:

  • Add EmailSender as a transient service.
  • Register the AuthMessageSenderOptions configuration instance.
public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<IdentityUser>(config =>
    {
        config.SignIn.RequireConfirmedEmail = true;
    })
        .AddDefaultUI(UIFramework.Bootstrap4)
        .AddEntityFrameworkStores<ApplicationDbContext>();

    // requires
    // using Microsoft.AspNetCore.Identity.UI.Services;
    // using WebPWrecover.Services;
    services.AddTransient<IEmailSender, EmailSender>();
    services.Configure<AuthMessageSenderOptions>(Configuration);

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

Enable account confirmation and password recovery

The template has the code for account confirmation and password recovery. Find the OnPostAsync method in Areas/Identity/Pages/Account/Register.cshtml.cs.

Prevent newly registered users from being automatically signed in by commenting out the following line:

await _signInManager.SignInAsync(user, isPersistent: false);

The complete method is shown with the changed line highlighted:

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    if (ModelState.IsValid)
    {
        var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
        var result = await _userManager.CreateAsync(user, Input.Password);
        if (result.Succeeded)
        {
            _logger.LogInformation("User created a new account with password.");

            var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            var callbackUrl = Url.Page(
                "/Account/ConfirmEmail",
                pageHandler: null,
                values: new { userId = user.Id, code = code },
                protocol: Request.Scheme);

            await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

            //await _signInManager.SignInAsync(user, isPersistent: false);
            return LocalRedirect(returnUrl);
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

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

Register, confirm email, and reset password

Run the web app, and test the account confirmation and password recovery flow.

  • Run the app and register a new user
  • Check your email for the account confirmation link. See Debug email if you don't get the email.
  • Click the link to confirm your email.
  • Sign in with your email and password.
  • Sign out.

View the manage page

Select your user name in the browser: browser window with user name

The manage page is displayed with the Profile tab selected. The Email shows a check box indicating the email has been confirmed.

Test password reset

  • If you're signed in, select Logout.
  • Select the Log in link and select the Forgot your password? link.
  • Enter the email you used to register the account.
  • An email with a link to reset your password is sent. Check your email and click the link to reset your password. After your password has been successfully reset, you can sign in with your email and new password.

Change email and activity timeout

The default inactivity timeout is 14 days. The following code sets the inactivity timeout to 5 days:

services.ConfigureApplicationCookie(o => {
    o.ExpireTimeSpan = TimeSpan.FromDays(5);
    o.SlidingExpiration = true;
});

Change all data protection token lifespans

The following code changes all data protection tokens timeout period to 3 hours:

public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<IdentityUser>(config =>
    {
        config.SignIn.RequireConfirmedEmail = true;
    })
        .AddDefaultUI(UIFramework.Bootstrap4)
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.Configure<DataProtectionTokenProviderOptions>(o =>
                o.TokenLifespan = TimeSpan.FromHours(3));

    services.AddTransient<IEmailSender, EmailSender>();
    services.Configure<AuthMessageSenderOptions>(Configuration);

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

The built in Identity user tokens (see AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs )have a one day timeout.

Change the email token lifespan

The default token lifespan of the Identity user tokens is one day. This section shows how to change the email token lifespan.

Add a custom DataProtectorTokenProvider<TUser> and DataProtectionTokenProviderOptions:

public class CustomEmailConfirmationTokenProvider<TUser> 
                                       : DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomEmailConfirmationTokenProvider(IDataProtectionProvider dataProtectionProvider,
        IOptions<EmailConfirmationTokenProviderOptions> options) 
                                                        : base(dataProtectionProvider, options)
    {

    }
}
public class EmailConfirmationTokenProviderOptions : DataProtectionTokenProviderOptions
{
    public EmailConfirmationTokenProviderOptions()
    {
        Name = "EmailDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(4);
    }
}

Add the custom provider to the service container:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<IdentityUser>(config =>
    {
        config.SignIn.RequireConfirmedEmail = true;
        config.Tokens.ProviderMap.Add("CustomEmailConfirmation",
            new TokenProviderDescriptor(
                typeof(CustomEmailConfirmationTokenProvider<IdentityUser>)));
        config.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmation";                
    })
        .AddDefaultUI(UIFramework.Bootstrap4)
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddTransient<CustomEmailConfirmationTokenProvider<IdentityUser>>();
    services.AddTransient<IEmailSender, EmailSender>();            
    services.Configure<AuthMessageSenderOptions>(Configuration); // For SendGrid key.

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

Resend email confirmation

See this GitHub issue.

Debug email

If you can't get email working:

  • Set a breakpoint in EmailSender.Execute to verify SendGridClient.SendEmailAsync is called.
  • Create a console app to send email using similar code to EmailSender.Execute.
  • Review the Email Activity page.
  • Check your spam folder.
  • Try another email alias on a different email provider (Microsoft, Yahoo, Gmail, etc.)
  • Try sending to different email accounts.

A security best practice is to not use production secrets in test and development. If you publish the app to Azure, you can set the SendGrid secrets as application settings in the Azure Web App portal. The configuration system is set up to read keys from environment variables.

Combine social and local login accounts

To complete this section, you must first enable an external authentication provider. See Facebook, Google, and external provider authentication.

You can combine local and social accounts by clicking on your email link. In the following sequence, "RickAndMSFT@gmail.com" is first created as a local login; however, you can create the account as a social login first, then add a local login.

Web application: RickAndMSFT@gmail.com user authenticated

Click on the Manage link. Note the 0 external (social logins) associated with this account.

Manage view

Click the link to another login service and accept the app requests. In the following image, Facebook is the external authentication provider:

Manage your external logins view listing Facebook

The two accounts have been combined. You are able to sign in with either account. You might want your users to add local accounts in case their social login authentication service is down, or more likely they've lost access to their social account.

Enable account confirmation after a site has users

Enabling account confirmation on a site with users locks out all the existing users. Existing users are locked out because their accounts aren't confirmed. To work around existing user lockout, use one of the following approaches:

  • Update the database to mark all existing users as being confirmed.
  • Confirm existing users. For example, batch-send emails with confirmation links.