ASP.NET Core 2.2 - Scaffold Identity UI

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:
Ken Haggerty
Created 01/30/19
Updated 02/21/19 23:33 GMT

Log In or Reset Quota to read more.

Successfully completed. Thank you for contributing.
Contribute to enjoy content without advertisments.
Something went wrong. Please try again.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?