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!
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.
_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 Startup
class 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
withApplicationUser
- Replace
ILogger<LoginModel>
withILogger<RegisterModel>
(known bug that will get fixed in a future release) -
Update the
InputModel
to addName
andAge
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 createdApplicationUser
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:
Now let's update the account management page. In /Areas/Identity/Pages/Account/Manage/Index.cshtml.cs
make the following changes:
- Replace
IdentityUser
withApplicationUser
-
Update the
InputModel
to addName
andAge
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 theName
andAge
properties on theInputModel
: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.
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.