Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Require Confirmed Email in ASP.NET Core 2.2 - Part 2

0.00/5 (No votes)
22 Dec 2018 1  
Scaffold and modify Identity in the new ASP.NET Core 2.2 Razor pages template

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)
        {
        }

        /// <summary>
        /// Sets the <paramref name="email"/> address for a <paramref name="user"/>.
        /// </summary>
        /// <param name="user">The user whose email should be set.</param>
        /// <param name="email">The email to set.</param>
        /// <returns>
        /// The <see cref="Task"/> that represents the asynchronous operation, 
        /// containing the <see cref="IdentityResult"/>
        /// of the operation.
        /// </returns>
        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);
        }

        /// <summary>
        /// Validates that an email confirmation token matches the specified 
        /// <paramref name="user"/> and if successful sets
        /// EmailConfirmed to true and if UnconfirmedEmail is not NULL or Empty, 
        /// copies the user's UnconfirmedEmail to user's
        /// Email and sets UnconfirmedEmail to NULL.
        /// </summary>
        /// <param name="user">The user to validate the token against.</param>
        /// <param name="token">The email confirmation token to validate.</param>
        /// <returns>
        /// The <see cref="Task"/> that represents the asynchronous operation, 
        /// containing the <see cref="IdentityResult"/>
        /// of the operation.
        /// </returns>
        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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here