SEARCH FINANCIAL SERVICES INFRASTRUCTURE SECURITY SCIENCE INTERVIEWS

 

     

ASP.NET Core 2.1.0-preview1: Introducing Identity UI as a library

By Daniel Roth, Microsoft

March 6, 2018

ASP.NET Core has historically provided project templates with code for setting up ASP.NET Core Identity, which enables support for identity related features like user registration, login, account management, etc. While ASP.NET Core Identity handles the hard work of dealing with passwords, two-factor authentication, account confirmation, and other hairy security concerns, the amount of code required to setup a functional identity UI is still pretty daunting. The most recent version of the ASP.NET Core Web Application template with Individual User Accounts setup has over 50 files and a couple of thousand lines of code dedicated to setting up the identity UI!

Identity files

Having all this identity code in your app gives you a lot of flexibility to update and change it as you please, but also imposes a lot of responsibility. It's a lot of security sensitive code to understand and maintain. Also if there is an issue with the code, it can't be easily patched.

The good news is that in ASP.NET Core 2.1 we can now ship Razor UI in reusable class libraries. We are using this feature to provide the entire identity UI as a prebuilt package (Microsoft.AspNetCore.Identity.UI) that you can simply reference from an application. The project templates in 2.1 have been updated to use the prebuilt UI, which dramatically reduces the amount of code you have to deal with. The one identity specific .cshtml file in the template is there solely to override the layout used by the identity UI to be the layout for the application.

Identity UI files

_ViewStart.cshtml

@{
    Layout = "/Pages/_Layout.cshtml";
}

The identity UI is enabled by both referencing the package and calling AddDefaultUI when setting up identity in the ConfigureServices method.

services.AddIdentity<IdentityUser, IdentityRole>(options => options.Stores.MaxLengthForKeys = 128)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultUI()
    .AddDefaultTokenProviders();

If you want the flexibility of having the identity code in your app, you can use the new identity scaffolder to add it back.

Currently you have to invoke the identity scaffolder from the command-line. In a future preview you will be able to invoke the identity scaffolder from within Visual Studio.

From the project directory run the identity scaffolder with the -dc option to reuse the existing ApplicationDbContext.

dotnet aspnet-codegenerator identity -dc WebApplication1.Data.ApplicationDbContext

The identity scaffolder will generate all of the identity related code in a new area under /Areas/Identity/Pages.

In the ConfigureServices method in Startup.cs you can now remove the call to AddDefaultUI.

services.AddIdentity<IdentityUser, IdentityRole>(options => options.Stores.MaxLengthForKeys = 128)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    // .AddDefaultUI()
    .AddDefaultTokenProviders();

Note that the ScaffoldingReadme.txt says to remove the entire call to AddIdentity, but this is a typo that will be corrected in a future release.

To also have the scaffolded identity code pick up the layout from the application, remove _Layout.cshtml from the identity area and update _ViewStart.cshtml in the identity area to point to the layout for the application (typically /Pages/_Layout.cshtml or /Views/Shared/_Layout.cshtml).

/Areas/Identity/Pages/_ViewStart.cshtml

@{
    Layout = "/Pages/_Layout.cshtml";
}

You should now be able to run the app with the scaffolded identity UI and log in with an existing user.

You can also use the code from the identity scaffolder to customize different pages of the default identity UI. For example, you can override just the register and account management pages to add some additional user profile data.

Let's extend identity to keep track of the name and age of our users.

Add an ApplicationUser class in the Data folder that derives from IdentityUser and adds Name and Age properties.

public class ApplicationUser : IdentityUser
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Update the ApplicationDbContext to derive from IdentityContext<ApplicationUser>.

    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }

In the Startupclass update the call to AddIdentity to use the new ApplicationUser and add back the call to AddDefaultUI if you removed it previously.

services.AddIdentity<ApplicationUser, IdentityRole>(options => options.Stores.MaxLengthForKeys = 128)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultUI()
    .AddDefaultTokenProviders();

Now let's update the register and account management pages to add UI for the two additional user properties.

In a future release we plan to update the identity scaffolder to support scaffolding only specific pages and provide a UI for selecting which pages you want, but for now the identity scaffolder is all or nothing and you have to remove the pages you don't want.

Remove all of the scaffolded files under /Areas/Identity except for:

  • /Areas/Identity/Pages/Account/Manage/Index.*
  • /Areas/Identity/Pages/Account/Register.*
  • /Areas/Identity/Pages/_ViewImports.cshtml
  • /Areas/Identity/Pages/_ViewStart.cshtml

Let's start with updating the register page. In /Areas/Identity/Pages/Account/Register.cshtml.cs make the following changes:

  • Replace IdentityUser with ApplicationUser
  • Replace ILogger<LoginModel> with ILogger<RegisterModel> (known bug that will get fixed in a future release)
  • Update the InputModel to add Name and Age properties:
      public class InputModel
      {
          [Required]
          [DataType(DataType.Text)]
          [Display(Name = "Full name")]
          public string Name { get; set; }
    
          [Required]
          [Range(0, 199, ErrorMessage = "Age must be between 0 and 199 years")]
          [Display(Name = "Age")]
          public string Age { get; set; }
    
          [Required]
          [EmailAddress]
          [Display(Name = "Email")]
          public string Email { get; set; }
    
          [Required]
          [StringLength(100, ErrorMessage = 
    "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
          [DataType(DataType.Password)]
          [Display(Name = "Password")]
          public string Password { get; set; }
    
          [DataType(DataType.Password)]
          [Display(Name = "Confirm password")]
          [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
          public string ConfirmPassword { get; set; }
      }
    
  • Update the OnPostAsync method to bind the new input values to the created ApplicationUser
      var user = new ApplicationUser()
      {
          Name = Input.Name,
          Age = Input.Age,
          UserName = Input.Email,
          Email = Input.Email
      };
    

Now we can update /Areas/Identity/Pages/Account/Register.cshtml to add the new fields to the register form.

<div class="row">
    <div class="col-md-4">
        <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h4>Create a new account.</h4>
            <hr />
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Input.Name"></label>
                <input asp-for="Input.Name" class="form-control" />
                <span asp-validation-for="Input.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Age"></label>
                <input asp-for="Input.Age" class="form-control" />
                <span asp-validation-for="Input.Age" class="text-danger"></span>
            </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>
            <div class="form-group">
                <label asp-for="Input.Password"></label>
                <input asp-for="Input.Password" class="form-control" />
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.ConfirmPassword"></label>
                <input asp-for="Input.ConfirmPassword" class="form-control" />
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-default">Register</button>
        </form>
    </div>
</div>

Run the app and click on Register to see the updates:

Register updated

Now let's update the account management page. In /Areas/Identity/Pages/Account/Manage/Index.cshtml.cs make the following changes:

  • Replace IdentityUser with ApplicationUser
  • Update the InputModel to add Name and Age properties:
      public class InputModel
      {
          [Required]
          [DataType(DataType.Text)]
          [Display(Name = "Full name")]
          public string Name { get; set; }
    
          [Required]
          [Range(0, 199, ErrorMessage = "Age must be between 0 and 199 years")]
          [Display(Name = "Age")]
          public int Age { get; set; }
    
          [Required]
          [EmailAddress]
          public string Email { get; set; }
    
          [Phone]
          [Display(Name = "Phone number")]
          public string PhoneNumber { get; set; }
      }
    
  • Update the OnGetAsync method to initialize the Name and Age properties on the InputModel:
      Input = new InputModel
      {
          Name = user.Name,
          Age = user.Age,
          Email = user.Email,
          PhoneNumber = user.PhoneNumber
      };
    
  • Update the OnPostAsync method to update the name and age for the user:
      if (Input.Name != user.Name)
      {
          user.Name = Input.Name;
      }
    
      if (Input.Age != user.Age)
      {
          user.Age = Input.Age;
      }
    
      var updateProfileResult = await _userManager.UpdateAsync(user);
      if (!updateProfileResult.Succeeded)
      {
          throw new InvalidOperationException($"Unexpected error ocurred updating the profile for user with ID '{user.Id}'");
      }
    

Now update /Areas/Identity/Pages/Account/Manage/Index.cshtml to add the additional form fields:

<div class="row">
    <div class="col-md-6">
        <form method="post">
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Username"></label>
                <input asp-for="Username" class="form-control" disabled />
            </div>
            <div class="form-group">
                <label asp-for="Input.Email"></label>
                @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 asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
                }
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Name"></label>
                <input asp-for="Input.Name" class="form-control" />
            </div>
            <div class="form-group">
                <label asp-for="Input.Age"></label>
                <input asp-for="Input.Age" class="form-control" />
            </div>
            <div class="form-group">
                <label asp-for="Input.PhoneNumber"></label>
                <input asp-for="Input.PhoneNumber" class="form-control" />
                <span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-default">Save</button>
        </form>
    </div>
</div>

Run the app and you should now see the updated account management page.

Manage account updated

You can find a complete version of this sample app on GitHub.

Summary

Having the identity UI as a library makes it much easier to get up and running with ASP.NET Core Identity, while still preserving the ability to customize the identity functionality. For complete flexibility you can also use the new identity scaffolder to get full access to the code. We hope you enjoy these new features! Please give them a try and let us know what you think about them on GitHub.

Terms of Use | Copyright 2002 - 2018 CONSTITUENTWORKS SM  CORPORATION. All rights reserved. | Privacy Statement