ASP.NET Core 2.2 - Scaffold Identity UI


Ken Haggerty
Created 01/30/2019 - Updated 02/21/2019 23:33

The article I wrote for Code Project Require Confirmed Email in ASP.NET Core 2.2 - Part 1 briefly describes scaffolding Identity UI. This article will cover just scaffolding Identity UI and provide solutions to a couple of layout issues. I will assume you have created a new ASP.NET Core 2.2 Razor Pages project with Individual User Accounts and updated the database with the CreateIdentitySchema migration.

Scaffold Identity

Right click the project name > Add > New Scaffolded Item.
New Scaffolded Item
Select Identity in the left menu.
Add Identity Scaffold
Check Override all files and select ApplicationDbContext.
Override Identity
Edit Startup.cs > ConfigureServices to use AddIdentity<IdentityUser, IdentityRole> instead of AddDefaultIdentity<IdentityUser>.
//services.AddDefaultIdentity<IdentityUser>()
services.AddIdentity<IdentityUser, IdentityRole>()
    .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";
});
Section references:

Test the project, it should build and run. You should be able to register a user but there are a few issues which work in the Default Identity UI class but are not scaffolded. I found some of these issues have been addressed in the source code on GitHub but have not reached the templating feature.

First the logout behavior which I documented in ASP.NET Core 2.2 - Razor Pages Logout Behavior.

Edit Logout.cshtml.cs > OnPost() to refresh the Logout page.
public async Task<IActionResult> OnPost(string returnUrl = null)
{
    await _signInManager.SignOutAsync();
    _logger.LogInformation("User logged out.");
    if (returnUrl != null)
    {
        return LocalRedirect(returnUrl);
    }
    else
    {
        //return Page();
        return RedirectToPage();
    }
}    

With the Default Identity UI Class, before you accept the cookie banner, the TwoFactorAuthentication page under Manage your account displays a warning (navigate to Identity/Account/Manage/ by clicking Hello USERNAME! then click the Two-factor authentication).

Two-Factor Authentication Warning

The scaffolded page doesn't have this feature yet, but you should add it.

Edit TwoFactorAuthentication.cshtml.
@page
@using Microsoft.AspNetCore.Http.Features
@model TwoFactorAuthenticationModel
@{
    ViewData["Title"]="Two-factor authentication(2FA)";
    ViewData["ActivePage"]=ManageNavPages.TwoFactorAuthentication;
}

<partial name="_StatusMessage" for="StatusMessage" />
<h4>@ViewData["Title"]</h4>
@{
    var consentFeature=HttpContext.Features.Get<ITrackingConsentFeature>();
        @if(consentFeature?.CanTrack ?? true)
        {
            @if(Model.Is2faEnabled)
            {
                if (Model.RecoveryCodesLeft == 0)
                {
                <div class="alert alert-danger">
                    <strong>You have no recovery codes left.</strong>
                    <p>You must <a asp-page= "./GenerateRecoveryCodes" > generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
                </div>
            }
            else if (Model.RecoveryCodesLeft == 1)
            {
                <div class="alert alert-danger">
                    <strong>You have 1 recovery code left.</strong>
                    <p>You can<a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
                </div>
            }
            else if (Model.RecoveryCodesLeft <= 3)
            {
                <div class="alert alert-warning">
                    <strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
                    <p>You should <a asp-page= "./GenerateRecoveryCodes" > generate a new set of recovery codes</a>.</p>
                </div>
            }

            if (Model.IsMachineRemembered)
            {
                <form method="post" style="display: inline-block">
                    <button type="submit" class="btn btn-primary">Forget this browser</button>
                  </form>
            }
            <a asp-page="./Disable2fa" class="btn btn-primary">Disable 2FA</a>
            <a asp-page="./GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
        }

        <h5>Authenticator app</h5>
        @if (!Model.HasAuthenticator)
        {
            <a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
        }
        else
        {
            <a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Setup authenticator app</a>
            <a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-primary">Reset authenticator app</a>
        }
    }
    else
    {
        <div class="alert alert-danger">
            <strong>Privacy and cookie policy have not been accepted.</strong>
            <p>You must accept the policy before you can enable two factor authentication.</p>
        </div>
    }
}

@section Scripts
{
    <partial name="_ValidationScriptsPartial" />
} 
Section references:

After you enable two factor authentication, the ShowRecoveryCodes page should be displayed. This page doesn't get scaffolded.

Add ShowRecoveryCodes.cshtml.
@page
@model ShowRecoveryCodesModel
@{
    ViewData["Title"] = "Recovery codes";
    ViewData["ActivePage"] = "TwoFactorAuthentication";
}

<partial name="_StatusMessage" for="StatusMessage" />
<h4>@ViewData["Title"]</h4>
<div class="alert alert-warning" role="alert">
    <p>
        <span class="glyphicon glyphicon-warning-sign"></span>
        <strong>Put these codes in a safe place.</strong>
    </p>
    <p>
        If you lose your device and don't have the recovery codes you will lose access to your account.
    </p>
</div>
<div class="row">
    <div class="col-md-12">
        @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2)
        {
        <code class="recovery-code">@Model.RecoveryCodes[row]</code><text> </text><code class="recovery-code">@Model.RecoveryCodes[row + 1]</code><br />
        }
    </div>
</div>
Section references:
Add ShowRecoveryCodes.cshtml.cs.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
{
    /// <summary>
    ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
    ///     directly from your code. This API may change or be removed in future releases.
    /// </summary>
    public class ShowRecoveryCodesModel : PageModel
    {
        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        [TempData]
        public string[] RecoveryCodes { get; set; }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        [TempData]
        public string StatusMessage { get; set; }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public IActionResult OnGet()
        {
            if (RecoveryCodes == null || RecoveryCodes.Length == 0)
            {
                return RedirectToPage("./TwoFactorAuthentication");
            }

            return Page();
        }
    }
}
Section references:

Be sure to edit the namespace to match your project.

You may have noticed the Register.cshtml.cs > OnPost() attempts to send an email to confirm the new user's email address. This function is not configured but does not fail because it references the IEmailSender in Microsoft.AspNetCore.Identity.UI.Services. If you want to take full control of the Identity UI by removing .AddDefaultUI(UIFramework.Bootstrap4) from AddIdentity in Startup.cs you need to provide an IEmailSender interface and EmailSender class.

Add a Services folder to the project and add an EmailSender class to Services.
public interface IEmailSender
{
    Task SendEmailAsync(string email, string subject, string message);
}

public class EmailSender : IEmailSender
{
    public Task SendEmailAsync(string email, string subject, string message)
    {
        return Task.CompletedTask;
    }
} 

This just stubs out the EmailSender. The next article will cover implementing a functional email service.

Add services.AddSingleton<IEmailSender, EmailSender>(); to the bottom of ConfigureServices in Startup.cs then remove .AddDefaultUI(UIFramework.Bootstrap4) from services.AddIdentity. Replace the reference using Microsoft.AspNetCore.Identity.UI.Services with using YOURPROJECTNAME.Services in the ForgotPassword.cshtml.cs and Register.cshtml.cs pages in the Account folder and the Index.cshtml.cs page in the Account\Manage folder.

With the Default Identity UI completely removed you will find the Manage your account pages were dependent on a layout which was not scaffolded.

Profile No Layout

The GitHub source has an elaborate layout scheme which relies on its own master layout page. I believe to support IdentityHostingStartup independently. I am not using IdentityHostingStartup yet and don't want to maintain two layout schemes. Here is my solution for now.

Add a _ViewStart.cshtml to the Account\Manage folder.
@{
    Layout = "_Layout.cshtml";
}
Edit _Layout.cshtml in the Account\Manage folder.

@{
    //Layout = "/Areas/Identity/Pages/_Layout.cshtml";
    Layout = "/Pages/Shared/_Layout.cshtml";
}
Section references:
The Identity Layout From GitHub. AspNetCore Identity V4/_Layout.cshtml

Fix the paths for src and asp-fallback-src in _ValidationScriptsPartial.cshtml in the Identity\Pages folder by removing /Identity before lib. You might think I am nitpicking here, but I have seen a Googlebot 404 error on this.

Edit _ValidationScriptsPartial.cshtml in the Identity\Pages folder.

<environment include="Development">
    <script src="~/Identity/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</environment>

<environment exclude="Development">
    <script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.17.0/jquery.validate.min.js"
            asp-fallback-src="~/Identity/lib/jquery-validation/dist/jquery.validate.min.js"
            asp-fallback-test="window.jQuery && window.jQuery.validator"
            crossorigin="anonymous"
            integrity="sha384-rZfj/ogBloos6wzLGpPkkOr/gpkBNLZ6b6yLy4o+ok+t/SAKlL5mvXLr0OXNi1Hp">
    </script>
    <script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.9/jquery.validate.unobtrusive.min.js"
            asp-fallback-src="~/Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
            asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"
            crossorigin="anonymous"
            integrity="sha384-ifv0TYDWxBHzvAk2Z0n8R434FL1Rlv/Av18DXE43N/1rvHyOG4izKst0f2iSLdds">
    </script>
</environment>

Now you have a completely scaffolded Identity UI. Look for my next article which will demonstrate a SMTP EmailSender.

Profile With Layout
Article references:


6 Following


Comment Count = 11

TinoMc
TinoMc
Member Since 04/03/2019
Posted 04/03/2019 16:13

Great article, registered to say thanks. I thought I was going crazy when the nav title changed when entering Identity Area Pages. Searched all code but it was like voodoo, search brought me here and I have my answer so thank you very much Ken, I have implemented pretty much all your ui edits and all working fine.

Ken Haggerty
Ken Haggerty *
Member Since 01/04/2019
Posted 04/03/2019 16:33

You're welcome.

Nick Downes
Nick Downes
Member Since 04/15/2019
Posted 04/15/2019 07:18

Ken, this article is one of the best I have ever seen. I too registered to say thanks and how well written your explanation is. I will be looking at the other articles you have written. Thank you so much. Everything worked perfectly first time which is pretty rare these days.

Ken Haggerty
Ken Haggerty *
Member Since 01/04/2019
Posted 04/15/2019 09:47

Thank you for the kind words. As a registered member, you may download the Bootstrap Native Project which implements most of the articles. The download is listed under Manage > Assets.

Szoke Laszlo
Szoke Laszlo
Member Since 05/30/2019
Posted 05/30/2019 11:47

Hello Ken, thank you! (from Hungary)

Ken Haggerty
Ken Haggerty *
Member Since 01/04/2019
Posted 05/30/2019 14:23

You're welcome.

Serguei Khous
Serguei Khous
Member Since 05/31/2019
Posted 05/31/2019 08:48

Many thanks, Ken! Great article, nicely done. I've found all answers to my questions. Will read all your articles, for sure.

solarseven
solarseven
Member Since 06/18/2019
Posted 06/19/2019 12:40

Thanks for these crystal clear and clean guides. More people should write like you do. Pure awesomeness!

Esteban
Esteban
Member Since 07/16/2019
Posted 07/16/2019 22:45

Hi Ken, I added successfully an scaffolded Identity UI to my Solution. I want to edit the titles to Spanish and also stylize them. With titles, I am referring to Manage your account, Change your account settings, Profile, Username, Email and Phone number. Is there any way to achieve this?

Ken Haggerty
Ken Haggerty *
Member Since 01/04/2019
Posted 07/16/2019 23:49

For a multilingual website:

Globalization and localization in ASP.NET Core

For a Spanish only website:

You will probably have to inspect/edit every page for English dialog. The main navbar text is in /Pages/Shared/_Layout.cshtml and the Manage account text is in _Layout.cshtml and _ManageNav.cshtml in the /Areas/Identity/Pages/Account/Manage/ folder.

Esteban
Esteban
Member Since 07/16/2019
Posted 08/05/2019 22:07

Thanks Ken, I followed your instructions and I achieved what I wanted.

Please log in to comment or follow.

Login Register