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.
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...
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.
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...
Oh Daniel! Gosh this seems like a waste of everyone’s time.
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...
Worth mentioning that the code generation requires installing the global tool.
dotnet tool install –global dotnet-aspnet-codegenerator
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...
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.