Introduction
Part 1 of 2 for an ASP.NET Core 2.2 web application to update a confirmed email. Here are the steps to scaffold and modify Identity to require a confirmed email before login.
Require Confirmed Email in ASP.NET Core 2.2 - Part 2
Using the Code
Prerequisites
- .NET Core 2.2 SDK
- One of the following:
- Visual Studio version 2017 15.9 or higher
- Visual Studio for Mac version 7.7 or higher
- Visual Studio Code C# extension version 1.17.1 or higher
You can download the VS 2017 project or follow these steps to modify your own project.
Step 1 - Create the Web Application
Create a new VS 2017 Project.
Create a new ASP.NET Core Web Application and change Authentication to Individual User Accounts.
Click OK.
Step 2 - Initialize the Database
The project uses SQL Server Express.
Edit appsettings.json > ConnectionStrings
> DefaultConnection
to set the database.
Run the command "Update-Database
" from the Package Manager Console in VS 2017.
Step 3 - Scaffold Identity
Right click the project name > Add > New Scaffolded Item.
Select Identity in the left menu.
Click Add.
Check Override all files and select ApplicationDbContext
.
Click Add.
Step 4 - Replace the Default EmailSender
Edit appsettings.json, add EmailSettings
with your email server settings:
"EmailSettings": {
"MailServer": "smtp.some_server.com",
"MailPort": 587,
"SenderName": "some name",
"Sender": "some_email@some_server.com",
"Password": "some_password"
}
Add new folder named Entities to project.
Add new class named EmailSettings in Entities
:
public class EmailSettings
{
public string MailServer { get; set; }
public int MailPort { get; set; }
public string SenderName { get; set; }
public string Sender { get; set; }
public string Password { get; set; }
}
Add new folder named Services to project.
Add new class named EmailSender
in Services:
public interface IEmailSender
{
Task SendEmailAsync(string email, string subject, string htmlMessage);
}
public class EmailSender : IEmailSender
{
private readonly EmailSettings _emailSettings;
public EmailSender(IOptions<emailsettings> emailSettings)
{
_emailSettings = emailSettings.Value;
}
public Task SendEmailAsync(string email, string subject, string message)
{
try
{
var credentials = new NetworkCredential(_emailSettings.Sender, _emailSettings.Password);
var mail = new MailMessage()
{
From = new MailAddress(_emailSettings.Sender, _emailSettings.SenderName),
Subject = subject,
Body = message,
IsBodyHtml = true
};
mail.To.Add(new MailAddress(email));
var client = new SmtpClient()
{
Port = _emailSettings.MailPort,
DeliveryMethod = SmtpDeliveryMethod.Network,
UseDefaultCredentials = false,
Host = _emailSettings.MailServer,
EnableSsl = true,
Credentials = credentials
};
client.Send(mail);
}
catch (Exception ex)
{
throw new InvalidOperationException(ex.Message);
}
return Task.CompletedTask;
}
}
Add namespaces to EmailSender.cs:
using Microsoft.Extensions.Options;
using <YourProjectName>.Entities;
using System.Net;
using System.Net.Mail;
Edit Startup.cs > ConfigureServices
, add EmailSettings
option:
services.AddOptions();
services.Configure<EmailSettings>(Configuration.GetSection("EmailSettings"));
Add to the bottom of Startup.cs > ConfigureServices
:
services.AddSingleton<IEmailSender, EmailSender>();
Add namespaces to Startup.cs:
using <YourProjectName>.Entities;
using <YourProjectName>.Services;
Edit Register.cshtml.cs, ForgotPassword.cshtml.cs and Manage\Index.cshtml.cs to use new EmailSender
's namespace
:
using <YourProjectName>.Services;
Step 5 - Require Confirmed and Unique Email
Edit Startup.cs > ConfigureServices
to use AddIdentity<IdentityUser, IdentityRole>
instead of AddDefaultIdentity<IdentityUser>
:
services.AddIdentity<IdentityUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
config.User.RequireUniqueEmail = true;
})
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
.AddDefaultTokenProviders();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddRazorPagesOptions(options =>
{
options.AllowAreas = true;
options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
options.Conventions.AuthorizeAreaPage("Identity", "/Account/Logout");
});
services.ConfigureApplicationCookie(options =>
{
options.LoginPath = $"/Identity/Account/Login";
options.LogoutPath = $"/Identity/Account/Logout";
options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
});
Add a razor page named CheckEmail
in Areas\Identity\Pages\Account:
Then:
Edit CheckEmail.cshtml:
@page
@model CheckEmailModel
@{
ViewData["Title"] = "Check email";
}
<h2>@ViewData["Title"]</h2>
<p>
Please check your inbox to confirm your account.
</p>
Edit CheckEmail.cshtml.cs, add AllowAnonymous
decoration:
[AllowAnonymous]
public class CheckEmailModel : PageModel
{
public void OnGet()
{
}
}
Add namespace to CheckEmail.cshtml.cs:
using Microsoft.AspNetCore.Authorization;
Edit Register.cshtml.cs > OnPostAsync
:
return RedirectToPage("./CheckEmail");
Step 6 - Add Login Name for UserName
Edit Areas\Identity\Pages\Account\Register.cshtml.cs, add UserName
property to Inputmodel
:
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and
at max {1} characters long.", MinimumLength = 6)]
[Display(Name = "Login Name")]
public string UserName { get; set; }
Edit Register.cshtml, add UserName
input:
<div class="form-group">
<label asp-for="Input.UserName"></label>
<input asp-for="Input.UserName" class="form-control" />
<span asp-validation-for="Input.UserName" class="text-danger"></span>
</div<
Edit Register.cshtml.cs > OnPostAsync
, use Input.UserName
in new IdentityUser
constructor:
var user = new IdentityUser { UserName = Input.UserName, Email = Input.Email };
Edit Login.cshtml.cs > InputModel
, replace Email
with UserName
:
public class InputModel
{
[Required]
[Display(Name = "Login Name")]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
Edit Login.cshtml.cs > OnPostAsync
, replace Input.Email
with Input.UserName
:
var result = await _signInManager.PasswordSignInAsync
(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: true);
Edit Login.cshtml, replace Email
with UserName
on the asp-for
:
<div class="form-group">
<label asp-for="Input.UserName"></label>
<input asp-for="Input.UserName" class="form-control" />
<span asp-validation-for="Input.UserName" class="text-danger"></span>
</div<
Step 7 - Add Unconfirmed Email Page
Add a razor page named UnconfirmedEmail
in Areas\Identity\Pages\Account:
Edit UnconfirmedEmail.cshtml:
@page "{userId}"
@model UnconfirmedEmailModel
@{
ViewData["Title"] = "Confirm your email.";
}
<h2>@ViewData["Title"]</h2>
<h4>Enter your email.</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
Edit UnconfirmedEmail.cshtml.cs:
using <YourProjectName>.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace <YourProjectName>.Areas.Identity.Pages.Account
{
[AllowAnonymous]
public class UnconfirmedEmailModel : PageModel
{
private readonly UserManager<IdentityUser> _userManager;
private readonly IEmailSender _emailSender;
public UnconfirmedEmailModel(UserManager<IdentityUser> userManager, IEmailSender emailSender)
{
_userManager = userManager;
_emailSender = emailSender;
}
[TempData]
public string UserId { get; set; }
[BindProperty(SupportsGet = true)]
public InputModel Input { get; set; }
public class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
}
public async Task OnGetAsync(string userId)
{
UserId = userId;
var user = await _userManager.FindByIdAsync(userId);
Input.Email = user.Email;
ModelState.Clear();
}
public async Task<IActionResult> OnPostAsync()
{
if (ModelState.IsValid)
{
var user = await _userManager.FindByIdAsync(UserId);
if (user == null)
{
return RedirectToPage("./CheckEmail");
}
if (user.Email != Input.Email)
{
var errors = new List<IdentityError>();
if (_userManager.Options.User.RequireUniqueEmail)
{
var owner = await _userManager.FindByEmailAsync(Input.Email);
if (owner != null && !string.Equals
(await _userManager.GetUserIdAsync(owner),
await _userManager.GetUserIdAsync(user)))
{
ModelState.AddModelError(string.Empty,
new IdentityErrorDescriber().DuplicateEmail(Input.Email).Description);
return Page();
}
}
await _userManager.SetEmailAsync(user, Input.Email);
}
var result = await _userManager.UpdateSecurityStampAsync(user);
if (!result.Succeeded)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
return Page();
}
}
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>.");
return RedirectToPage("./CheckEmail");
}
return Page();
}
}
}
Step 8 - Modify Login
Inject UserManager
to Areas\Identity\Pages\Account\Login.cshtml.cs:
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly ILogger<LoginModel> _logger;
public LoginModel(
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager,
ILogger<LoginModel> logger)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
}
Add ShowResend
and UserId
properties to Login.cshtml.cs:
public bool ShowResend { get; set; }
public string UserId { get; set; }
Add to Login.cshtml.cs > OnPostAsync
after result.IsLockedOut
:
if (result.IsNotAllowed)
{
_logger.LogWarning("User email is not confirmed.");
ModelState.AddModelError(string.Empty, "Email is not confirmed.");
var user = await _userManager.FindByNameAsync(Input.UserName);
UserId = user.Id;
ShowResend = true;
return Page();
}
Edit Login.cshtml after asp-validation-summary
:
@{
if (Model.ShowResend)
{
<p>
<a asp-page="./UnconfirmedEmail"
asp-route-userId="@Model.UserId">Resend verification?</a>
</p>
}
}
Step 9 - Modify Confirm Email
Add ShowInvalid
property to Areas\Identity\Pages\Account\ConfirmEmail.cshtml.cs:
public bool ShowInvalid { get; set; }
Edit ConfirmEmail.cshtml.cs > OnGetAsync
:
if (!result.Succeeded)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
ShowInvalid = true;
}
Edit ConfirmEmail.cshtml:
<div asp-validation-summary="All" class="text-danger"></div>
@{
if (Model.ShowInvalid)
{
<p>
Error confirming your email.
</p>
<p>
If you can login, try updating your email again.<br />
If you cannot login, try resend verification.
</p>
}
else
{
<p>
Thank you for confirming your email.
</p>
}
}
Build and test the project.
Points of Interest
Notice UpdateSecurityStampAsync(user)
before GenerateEmailConfirmationTokenAsync(user)
. This invalidates any previous codes sent to the user.
Forgot Password and Reset Password still uses Email
to find the user. Now you have the option of finding the user by UserName
.
History
- 2018-12-21: Initial post
- 2018-12-22: Updated SetCompatibilityVersion