Account confirmation and password recovery
This tutorial shows you how to build an ASP.NET Core app with email confirmation and password reset.
Create a New ASP.NET Core Project
The tutorial requires Visual Studio 2017 or higher.
- In Visual Studio, create a New Web Application Project.
- Select Change Authentication and set to Individual User Accounts.
Run the app, select the Register link, and register a user. Follow the instructions to run Entity Framework migrations. At this point, the only validation on the email is with the [EmailAddress] attribute. After you submit the registration, you are logged into the app. Later in the tutorial we'll change this so new users cannot log in until their email has been 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:
Note the 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 now will make it easier in the following steps.
Require SSL and setup IIS Express for SSL
See Enforcing SSL.
Require email confirmation
It's a best practice to confirm the email of a new user registration 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 "yli@example.com" from registering as "nolivetto@contoso.com." Without email confirmation, "nolivetto@contoso.com" could get 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 only limited protection from bots and doesn't provide protection from determined spammers who have many working email aliases they can use to register.
You generally want to prevent new users from posting any data to your web site before they have a confirmed email. In the sections below, we will enable email confirmation and modify the code to prevent newly registered users from logging in until their email has been confirmed.
Update ConfigureServices
to require a confirmed email:
// Requires using Microsoft.AspNetCore.Mvc;
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
});
// Add framework services.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.Configure<AuthMessageSenderOptions>(Configuration);
}
Configure email provider
In this tutorial we'll be using SendGrid to send email. You'll need a SendGrid account and key to send email. We'll use the Options pattern to access the user account and key settings. For more information, see configuration.
Create a class to fetch the secure email key. For this sample, the AuthMessageSenderOptions
class is created in the Services/AuthMessageSenderOptions.cs file.
public class AuthMessageSenderOptions
{
public string SendGridUser { get; set; }
public string SendGridKey { get; set; }
}
Set the SendGridUser
and SendGridKey
with the secret-manager tool. For example:
C:\WebAppl\src\WebApp1>dotnet user-secrets set SendGridUser RickAndMSFT
info: Successfully saved SendGridUser = RickAndMSFT to the secret store.
On Windows, Secret Manager stores your keys/value pairs in a secrets.json file in the %APPDATA%/Microsoft/UserSecrets/
The contents of the secrets.json file are not encrypted. The secrets.json file is shown below (the Add This tutorial shows how to add email notification through SendGrid, but you can send email using SMTP and other mechanisms. Install the See Get Started with SendGrid for Free to register for a free SendGrid account. The template already has the code for account confirmation and password recovery. Follow these steps to enable it: Note: We're also preventing a newly registered user from being automatically logged on by commenting out the following line: Uncomment the markup in Views/Account/ForgotPassword.cshtml: In this section, run the web app and show the account confirmation and password recovery flow. Run the app and register a new user Check your email for the account confirmation link. If you don't get the email notification: Click the link to confirm your email. With the current templates, once a user completes the registration form, they are logged in (authenticated). You generally want to confirm their email before logging them in. In the section below, we will modify the code to require new users have a confirmed email before they are logged in. Update the Note: 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 setup to read keys from environment variables. To complete this section, you must first enable an external authentication provider. See Enabling authentication using Facebook, Google and other external providers. 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, but you can create the account as a social login first, then add a local login. Click on the Manage link. Note the 0 external (social logins) associated with this account. Click the link to another login service and accept the app requests. In the image below, Facebook is the external authentication provider: The two accounts have been combined. You will be able to log on with either account. You might want your users to add local accounts in case their social log in authentication service is down, or more likely they have lost access to their social account.SendGridKey
value has been removed.){
"SendGridUser": "RickAndMSFT",
"SendGridKey": "<key removed>"
}
Configure startup to use
AuthMessageSenderOptions
AuthMessageSenderOptions
to the service container at the end of the ConfigureServices
method in the Startup.cs file:// Requires using Microsoft.AspNetCore.Mvc;
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
});
// Add framework services.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.Configure<AuthMessageSenderOptions>(Configuration);
}
Configure the AuthMessageSender class
SendGrid
NuGet package. From the Package Manager Console, enter the following the following command:Install-Package SendGrid
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;
using System.Threading.Tasks;
namespace WebApp1.Services
{
public class AuthMessageSender : IEmailSender, ISmsSender
{
public AuthMessageSender(IOptions<AuthMessageSenderOptions> optionsAccessor)
{
Options = optionsAccessor.Value;
}
public AuthMessageSenderOptions Options { get; } //set only via Secret Manager
public Task SendEmailAsync(string email, string subject, string message)
{
// Plug in your email service here to send an email.
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));
return client.SendEmailAsync(msg);
}
public Task SendSmsAsync(string number, string message)
{
// Plug in your SMS service here to send a text message.
return Task.FromResult(0);
}
}
}
Enable account confirmation and password recovery
[HttpPost] Register
method in the AccountController.cs file.//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
// Send an email with this link
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Action(nameof(ConfirmEmail), "Account",
new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
await _emailSender.SendEmailAsync(model.Email, "Confirm your account",
$"Please confirm your account by clicking this link: <a href='{callbackUrl}'>link</a>");
// Comment out following line to prevent a new user automatically logged on.
// await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation(3, "User created a new account with password.");
return RedirectToLocal(returnUrl);
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
//await _signInManager.SignInAsync(user, isPersistent: false);
ForgotPassword
action in the Controllers/AccountController.cs file.//
// POST: /Account/ForgotPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
{
// Don't reveal that the user does not exist or is not confirmed.
return View("ForgotPasswordConfirmation");
}
// Send an email with this link
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
var callbackUrl = Url.Action(nameof(ResetPassword), "Account",
new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
await _emailSender.SendEmailAsync(model.Email, "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);
}
@model ForgotPasswordViewModel
@{
ViewData["Title"] = "Forgot your password?";
}
<h2>@ViewData["Title"]</h2>
<p>
For more information on how to enable reset password please see this
<a href="https://go.microsoft.com/fwlink/?LinkID=532713">article</a>.
</p>
<form asp-controller="Account" asp-action="ForgotPassword" method="post" class="form-horizontal">
<h4>Enter your email.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<button type="submit" class="btn btn-default">Submit</button>
</div>
</div>
</form>
@section Scripts {
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}
Register, confirm email, and reset password
Test password reset
Prevent login at registration
[HttpPost] Login
action in the AccountController.cs file with the following highlighted changes.//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
// Require the user to have a confirmed email before they can log on.
var user = await _userManager.FindByEmailAsync(model.Email);
if (user != null)
{
if (!await _userManager.IsEmailConfirmedAsync(user))
{
ModelState.AddModelError(string.Empty,
"You must have a confirmed email to log in.");
return View(model);
}
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout,
// set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.Email,
model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation(1, "User logged in.");
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(SendCode),
new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning(2, "User account locked out.");
return View("Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
Combine social and local login accounts