March 2nd, 2018

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

Daniel Roth
Principal Product Manager

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.

Author

Daniel Roth
Principal Product Manager

Daniel Roth is a Program Manager on the ASP.NET team at Microsoft.

8 comments

Discussion is closed. Login to edit/delete existing comments.

  • barry wimlett

    Oh my this is horrid. I 've spent 20 doing ASP.net development and this is giving me PTSD flash backs to asp-webforms code-behind and VS2003.

    MVC development has controllers and views, any modules microsoft create for MVC should follow that pattern.

    Authentication and identification as fundamental features of websites, the first things most developers add and therefore the first 'real' MVC code new developers see, it should be a shining example of how (secure) MVC should be...

    Read more
  • Sandeep Bhaskar

    This is great and helpful, I need form authentication for my application so with the template VS providing I created web project.
    But now as @Mark said My customers don’t want to see “There are no external authentication services configured” Where I need to Update the UI.
    But I didn’t find a way to do that. Let me know if there is a way to update the existing scaffold UI.

  • Mark Sweat

    Daniel, my experience with this was the typical Microsoft tool setup and run experience. Fussing around installing tools and Nuget packages, opening and closing command windows and VS until it finally worked. I can't see how this UI system is usable in real, deployed application. My customers don't want to see "There are no external authentication services configured" and links to the Microsoft site. So I have to customize. For what? So you can do...

    Read more
  • Mark Sweat

    Oh Daniel! Gosh this seems like a waste of everyone’s time.

  • Charles Crawford

    This is probably one of the most frustrating and un-needed changes Microsoft has made since the Office Ribbon... Yes, I'm not a fan of either. The other frustrating issue I'm having is the different .net core frameworks for different versions of visual studio.On this specific change, what would posses Microsoft to have all of this in a class library? I don't know a single soul that uses the default views as-is. Almost every instance of...

    Read more
  • Simon Timms

    Worth mentioning that the code generation requires installing the global tool.
    dotnet tool install –global dotnet-aspnet-codegenerator

    • Mark Sweat

      After install I had to close and reopen my command window before the dotnet aspnet-codegenerator command would work. Once I did that I got another error running the command. It directed me to install microsoft.visualstudio.web.codegenerator.design NuGet package. After that it still didn't work. I closed and reopened both the command window and VS and it finally worked (maybe only one of these was necessary). That's a lot to do for something that's supposed to save...

      Read more
  • Admir Hodzic

    Any updates on this, can We now scarfold from VS.
    Are we able to swich fom code to Library GUI once when we scarfold gui.