Click here to Skip to main content
15,885,757 members
Articles / Web Development / ASP.NET / ASP.NET Core
Article

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

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
22 Dec 2018MIT2 min read 20.9K   408   8   4
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:

C#
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:

C#
services.AddIdentity<ApplicationUser, IdentityRole>

Edit Areas\Identity\Pages\Account\Manage\EnableAuthenticator.cshtml.cs:

C#
private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user)

Edit Areas\Identity\Pages\Account\Manage\DownloadPersonalData.cshtml.cs:

C#
var personalDataProps = typeof(ApplicationUser).GetProperties().Where(
                        prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));

Edit Areas\Identity\Pages\Account\ExternalLogin.cshtml.cs:

C#
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };

Edit Register.cshtml.cs:

C#
var user = new ApplicationUser { UserName = Input.UserName, Email = Input.Email };

Resolve namespace issues where you replaced IdentityUser.

C#
using <YourProjectName>.Entities;

or for cshtml:

Razor
@using <YourProjectName>.Entities;

Build the project and check for errors.

Step 2 - Update the Database

Edit ApplicationDbContext in the Data folder, add ApplicationUser:

C#
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:

C#
public static string ChangeEmail => "ChangeEmail";

and:

C#
public static string ChangeEmailNavClass(ViewContext viewContext) =>
                                         PageNavClass(viewContext, ChangeEmail);

Edit _ManageNav.cshtml, add below Profile item:

Razor
<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:

Razor
@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:

C#
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:

C#
public string Email { get; set; }

Remove:

C#
public bool IsEmailConfirmed { get; set; }

Remove from InputModel:

C#
[Required]
[EmailAddress]
public string Email { get; set; }

Remove from OnGetAsync > Input:

C#
Email = email,

Remove from OnGetAsync:

C#
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);

Remove from OnPostAsync:

C#
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:

C#
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:

Razor
@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:

Razor
<input asp-for="Email" class="form-control" disabled />

Step 5 – Override UserManager

Add new class named ApplicationUserManager in Services folder:

C#
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>():

C#
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, along with any associated source code and files, is licensed under The MIT License


Written By
Web Developer Semi-Retired
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionNot adding UnconfirmedEmail Column Pin
Tom Mein27-Apr-19 2:29
Tom Mein27-Apr-19 2:29 
AnswerRe: Not adding UnconfirmedEmail Column Pin
Ken Haggerty13-May-19 6:07
professionalKen Haggerty13-May-19 6:07 
QuestionNot adding UnconfirmedEmail Column Pin
Tom Mein27-Apr-19 2:29
Tom Mein27-Apr-19 2:29 
QuestionMany appreciations Pin
mmisu23-Dec-18 22:32
mmisu23-Dec-18 22:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.