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 1

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 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.

New Project

Create a new ASP.NET Core Web Application and change Authentication to Individual User Accounts.

New Web App

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.

New Scaffolded Item

Select Identity in the left menu.

Add Scaffolded Identity

Click Add.

Check Override all files and select ApplicationDbContext.

Override Identity

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
        {
            // Credentials
            var credentials = new NetworkCredential(_emailSettings.Sender, _emailSettings.Password);

            // Mail message
            var mail = new MailMessage()
            {
                From = new MailAddress(_emailSettings.Sender, _emailSettings.SenderName),
                Subject = subject,
                Body = message,
                IsBodyHtml = true
            };

            mail.To.Add(new MailAddress(email));

            // Smtp client
            var client = new SmtpClient()
            {
                Port = _emailSettings.MailPort,
                DeliveryMethod = SmtpDeliveryMethod.Network,
                UseDefaultCredentials = false,
                Host = _emailSettings.MailServer,
                EnableSsl = true,
                Credentials = credentials
            };

            // Send it...         
            client.Send(mail);
        }
        catch (Exception ex)
        {
            // TODO: handle exception
            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 Microsoft.AspNetCore.Identity.UI.Services;
using <YourProjectName>.Services;

Step 5 - Require Confirmed and Unique Email

Edit Startup.cs > ConfigureServices to use AddIdentity<IdentityUser, IdentityRole> instead of AddDefaultIdentity<IdentityUser>:

//services.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:

Add New Item

Then:

New Razor Page

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:

//await _signInManager.SignInAsync(user, isPersistent: false);
//return LocalRedirect(returnUrl);
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)
                {
                    // Don't reveal that the user does not exist
                    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)
{
    //throw new InvalidOperationException($"Error confirming email for user with ID '{userId}':");
    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

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