Introduction
Part 2 of 2 for an ASP.NET Core 2.2 web application which allows the user to update a confirmed email. Here are the steps to allow the user to update their email.
Require Confirmed Email in ASP.NET Core 2.2 - Part 1
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 after completing the steps in Part 1.
Step 1 – Add UnconfirmedEmail Property to IdentityUser
Add new class named ApplicationUser
in Entities folder:
using Microsoft.AspNetCore.Identity;
namespace <YourProjectName>.Entities
{
public class ApplicationUser : IdentityUser
{
[PersonalData]
public string UnconfirmedEmail { get; set; }
}
}
Use Find and Replace to Replace <IdentityUser>
with <ApplicationUser>
in Current Project.
Edit Startup.cs > ConfigureServices
to use ApplicationUser
:
services.AddIdentity<ApplicationUser, IdentityRole>
Edit Areas\Identity\Pages\Account\Manage\EnableAuthenticator.cshtml.cs:
private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user)
Edit Areas\Identity\Pages\Account\Manage\DownloadPersonalData.cshtml.cs:
var personalDataProps = typeof(ApplicationUser).GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
Edit Areas\Identity\Pages\Account\ExternalLogin.cshtml.cs:
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
Edit Register.cshtml.cs:
var user = new ApplicationUser { UserName = Input.UserName, Email = Input.Email };
Resolve namespace issues where you replaced IdentityUser
.
using <YourProjectName>.Entities;
or for cshtml:
@using <YourProjectName>.Entities;
Build the project and check for errors.
Step 2 - Update the Database
Edit ApplicationDbContext
in the Data folder, add ApplicationUser
:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
Run the command "Add-Migration UnconfirmedEmail
" from the Package Manager Console in VS 2017.
Run the command "Update-Database
".
Step 3 – Add Change Email Page
Edit ManageNavPages.cs, add above ChangePassword
properties:
public static string ChangeEmail => "ChangeEmail";
and:
public static string ChangeEmailNavClass(ViewContext viewContext) =>
PageNavClass(viewContext, ChangeEmail);
Edit _ManageNav.cshtml, add below Profile item:
<li class="nav-item">
<a class="nav-link @ManageNavPages.ChangeEmailNavClass(ViewContext)"
id="change-email" asp-page="./ChangeEmail">Email</a></li>
Create a razor page named ChangeEmail
in Areas\Identity\Pages\Account\Manage.
Edit ChangeEmail.cshtml:
@page
@model ChangeEmailModel
@{
ViewData["Title"] = "Change Email";
ViewData["ActivePage"] = ManageNavPages.ChangeEmail;
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<form id="change-email-form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" disabled />
</div>
<h5>New email needs to be verified.</h5>
<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">Update Email</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
Edit ChangeEmail.cshtml.cs:
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using <YourProjectName>.Services;
using <YourProjectName>.Entities;
namespace <YourProjectName>.Areas.Identity.Pages.Account.Manage
{
public class ChangeEmailModel : PageModel
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<ChangeEmailModel> _logger;
private readonly IEmailSender _emailSender;
public ChangeEmailModel(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ILogger<ChangeEmailModel> logger,
IEmailSender emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
_emailSender = emailSender;
}
[BindProperty]
public InputModel Input { get; set; }
[TempData]
[Display(Name = "Verified Email")]
public string Email { get; set; }
[TempData]
public string StatusMessage { get; set; }
public class InputModel
{
[Required]
[EmailAddress]
[Display(Name = "New Email")]
public string Email { get; set; }
}
public async Task<IActionResult> OnGetAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var email = await _userManager.GetEmailAsync(user);
Email = email;
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var email = await _userManager.GetEmailAsync(user);
if (Input.Email != 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();
}
}
var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
if (!setEmailResult.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred
setting email for user with ID '{userId}'.");
}
if (Input.Email.ToUpper() != email.ToUpper())
{
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>.");
_logger.LogInformation("User updated their UnconfirmedEmail.");
StatusMessage = "Please check your inbox to confirm the new email.";
}
else
{
_logger.LogInformation("User updated their Email.");
StatusMessage = "Your email has been updated.";
}
}
return RedirectToPage();
}
}
}
Step 4 – Modify Profile
Edit Index.cshtml.cs in Areas\Identity\Pages\Account\Manage to use the new ChangeEmail
page.
Add:
public string Email { get; set; }
Remove:
public bool IsEmailConfirmed { get; set; }
Remove from InputModel
:
[Required]
[EmailAddress]
public string Email { get; set; }
Remove from OnGetAsync
> Input:
Email = email,
Remove from OnGetAsync
:
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
Remove from OnPostAsync
:
var email = await _userManager.GetEmailAsync(user);
if (Input.Email != email)
{
var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
if (!setEmailResult.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred
setting email for user with ID '{userId}'.");
}
}
Remove:
public async Task<IActionResult> OnPostSendVerificationEmailAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '
{_userManager.GetUserId(User)}'.");
}
var userId = await _userManager.GetUserIdAsync(user);
var email = await _userManager.GetEmailAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode
(callbackUrl)}' >clicking here</a>.");
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
}
Edit Index.cshtml.
Replace:
@if (Model.IsEmailConfirmed)
{
<div class="input-group">
<input asp-for="Input.Email" class="form-control" />
<span class="input-group-addon" aria-hidden="true">
<span class="glyphicon glyphicon-ok text-success"></span></span>
</div>
}
else
{
<input asp-for="Input.Email" class="form-control" />
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail"
class="btn btn-link">Send verification email</button>
}
<span asp-validation-for="Input.Email" class="text-danger"></span>
With:
<input asp-for="Email" class="form-control" disabled />
Step 5 – Override UserManager
Add new class named ApplicationUserManager
in Services folder:
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using <YourProjectName>.Entities;
namespace <YourProjectName>.Services
{
public class ApplicationUserManager : UserManager<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<ApplicationUser> passwordHasher,
IEnumerable<IUserValidator<ApplicationUser>> userValidators,
IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<ApplicationUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators,
passwordValidators, keyNormalizer, errors, services, logger)
{
}
public override async Task<IdentityResult> SetEmailAsync(ApplicationUser user, string email)
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.EmailConfirmed && user.Email.ToUpper() != email.ToUpper())
user.UnconfirmedEmail = email;
else
user.Email = email;
return await UpdateUserAsync(user);
}
public override async Task<IdentityResult>
ConfirmEmailAsync(ApplicationUser user, string token)
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IdentityResult result;
var provider = Options.Tokens.EmailConfirmationTokenProvider;
var isValidToken = await base.VerifyUserTokenAsync
(user, provider, "EmailConfirmation", token);
if (isValidToken)
{
if (!string.IsNullOrEmpty(user.UnconfirmedEmail))
{
user.Email = user.UnconfirmedEmail;
user.UnconfirmedEmail = null;
}
user.EmailConfirmed = true;
result = await UpdateUserAsync(user);
}
else
{
result = IdentityResult.Failed(new IdentityErrorDescriber().InvalidToken());
}
return result;
}
}
}
Edit Startup.cs > ConfigureServices
, add .AddUserManager<ApplicationUserManager>()
:
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
config.User.RequireUniqueEmail = true;
})
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddUserManager<ApplicationUserManager>()
.AddDefaultTokenProviders();
Build and test the project.
Points of Interest
I am not sure I had to "Override all files" when I scaffolded Identity
but I prefer to inspect and have full control over the user's experience.
Notice UpdateSecurityStampAsync(user)
before GenerateEmailConfirmationTokenAsync(user)
. This invalidates any previous codes sent to the user.
History
- 2018-12-21: Initial post
- 2018-12-22: Update History date