Problem
How to use ASP.NET Core Identity to authenticate users and manage their accounts.
Solution
In a previous post, I showed how to use cookie authentication middleware to protect your web application. ASP.NET Core also provides a richer set of services, called Identity, to work with user authentication and management scenarios. For instance, in addition to authentication and password hashing, it provides features for registering new users, creating forgot & reset password tokens and their validation, two-factor authentication and authentication using external providers.
In this post, I will discuss the following,
- Setting up Identity to provide authentication, including setting up database to store user details.
- Registering new user accounts, including how to confirm user email address before allowing them access to the application.
- Forgot and reset password feature.
Setup Identity
Create classes to represent role, user and database context by inheriting from framework classes IdentityRole, IdentityUser and IdentityDbContext,
- public class AppIdentityRole : IdentityRole
- { }
-
- public class AppIdentityUser : IdentityUser
- {
- public int Age { get; set; }
- }
-
- public class AppIdentityDbContext
- : IdentityDbContext<AppIdentityUser, AppIdentityRole, string>
- {
- public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options)
- : base(options)
- { }
- }
Note
The advantage of inheriting from framework classes is that you can add your own custom properties (e.g. Age in my case) to user entity. Also by inheriting database context you could modify the database schema, if required.
Configure services for Identity in Startup class, including configuration of cookie middleware,
- private IConfiguration configuration;
-
- public Startup(IConfiguration configuration)
- {
- this.configuration = configuration;
- }
-
- public void ConfigureServices(
- IServiceCollection services)
- {
- services.AddDbContext<AppIdentityDbContext>(options =>
- options.UseSqlServer(configuration["DB_CONN"]));
-
- services.AddIdentity<AppIdentityUser, AppIdentityRole>()
- .AddEntityFrameworkStores<AppIdentityDbContext>()
- .AddDefaultTokenProviders();
-
- services.ConfigureApplicationCookie(options =>
- {
- options.LoginPath = "/Security/Login";
- options.LogoutPath = "/Security/Logout";
- options.AccessDeniedPath = "/Security/AccessDenied";
- options.SlidingExpiration = true;
- options.Cookie = new CookieBuilder
- {
- HttpOnly = true,
- Name = ".Fiver.Security.Cookie",
- Path = "/",
- SameSite = SameSiteMode.Lax,
- SecurePolicy = CookieSecurePolicy.SameAsRequest
- };
- });
-
- services.AddMvc();
- }
-
- public void Configure(
- IApplicationBuilder app,
- IHostingEnvironment env)
- {
- if (env.IsDevelopment())
- app.UseDeveloperExceptionPage();
-
- app.UseAuthentication();
-
- app.UseMvcWithDefaultRoute();
- }
Note
setting up cookie authentication middleware was discussed in a previous post, where cookie middleware was used directly rather than via Identity services.
Add appsettings.json file with a DB_CONN setting, which will point to the connection string of database where you want to store tables used by Identity. Learn more about configuration here.
Create a controller for Login/Logout actions,
- private readonly SignInManager<AppIdentityUser> signInManager;
-
- public SecurityController(
- SignInManager<AppIdentityUser> signInManager)
- {
- this.signInManager = signInManager;
- }
- public IActionResult Login()
- {
- return View();
- }
-
- [HttpPost]
- [ValidateAntiForgeryToken]
- public async Task<IActionResult> Login(LoginViewModel model)
- {
- if (!ModelState.IsValid)
- return View(model);
-
- var result = await this.signInManager.PasswordSignInAsync(
- model.Username, model.Password,
- isPersistent: false, lockoutOnFailure: false);
-
- if (result.Succeeded)
- return RedirectToAction("Index", "Home");
-
- ModelState.AddModelError(string.Empty, "Login Failed");
- return View(model);
- }
-
- public async Task<IActionResult> Logout()
- {
- await this.signInManager.SignOutAsync();
- return RedirectToAction("Index", "Home");
- }
Here we are using the built-in SignInManager class to authenticate the user and also to sign them out. Framework is taking care of going to the database and validating password hashes.
We’re ready to run EF database migrations to create identity database. To utilise EF migrations, add NuGet package to ASP.NET Core Web Application project: Microsoft.EntityFrameworkCore.Design
Add CLI tools by editing .csproj file,
- <ItemGroup>
- <DotNetCliToolReference
- Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
- </ItemGroup>
Now run following commands,
- dotnet ef migrations add
- dotnet ef database update
Note
You can learn more about EF and migrations here and here.
You can see the tables in your database now,
We’re done setting up Identity but we can’t login yet, we have no users. Let’s add registration of new users next.
Registering Users
Inject an instance of UserManager into the constructor, which is a built-in class that provides features to manage user accounts,
- private readonly UserManager<AppIdentityUser> userManager;
- private readonly SignInManager<AppIdentityUser> signInManager;
- private readonly IEmailSender emailSender;
-
- public SecurityController(
- UserManager<AppIdentityUser> userManager,
- SignInManager<AppIdentityUser> signInManager,
- IEmailSender emailSender)
- {
- this.userManager = userManager;
- this.signInManager = signInManager;
- this.emailSender = emailSender;
- }
Create a class to act as view model for the registration view,
- public class RegisterViewModel
- {
- [Required]
- public string UserName { get; set; }
-
- [Required]
- [DataType(DataType.Password)]
- public string Password { get; set; }
-
- [Required]
- [DataType(DataType.Password)]
- public string ConfirmPassword { get; set; }
-
- [Required]
- [DataType(DataType.EmailAddress)]
- public string Email { get; set; }
-
- public int Age { get; set; }
- }
Next add action methods for registration,
- public IActionResult Register()
- {
- return View();
- }
-
- [HttpPost]
- [ValidateAntiForgeryToken]
- public async Task<IActionResult> Register(RegisterViewModel model)
- {
- if (!ModelState.IsValid)
- return View(model);
-
- var user = new AppIdentityUser
- {
- UserName = model.UserName,
- Email = model.Email,
- Age = model.Age
- };
-
- var result = await this.userManager.CreateAsync(user, model.Password);
- if (result.Succeeded)
- {
- var confrimationCode =
- await this.userManager.GenerateEmailConfirmationTokenAsync(user);
-
- var callbackurl = Url.Action(
- controller: "Security",
- action: "ConfirmEmail",
- values: new { userId = user.Id, code = confrimationCode },
- protocol: Request.Scheme);
-
- await this.emailSender.SendEmailAsync(
- email: user.Email,
- subject: "Confirm Email",
- message: callbackurl);
-
- return RedirectToAction("Index", "Home");
- }
-
- return View(model);
- }
Here we are first instantiating a new user and then using UserManager adding it to the database. If the database update is successful, we are creating a new token to send to user so that they can confirm their email address by clicking on the link. Finally we’re sending the email to the user. In the sample application I am not sending an actual email but rather just logging it to the console.
Next we’ll create the action method required to confirm the email,
- public async Task<IActionResult> ConfirmEmail(string userId, string code)
- {
- if (userId == null || code == null)
- return RedirectToAction("Index", "Home");
-
- var user = await this.userManager.FindByIdAsync(userId);
- if (user == null)
- throw new ApplicationException($"Unable to load user with ID '{userId}'.");
-
- var result = await this.userManager.ConfirmEmailAsync(user, code);
- if (result.Succeeded)
- return View("ConfirmEmail");
-
- return RedirectToAction("Index", "Home");
- }
Here we first try to find the user, if it exists we update the database indicating that the user has confirmed their email address.
Forgot and Reset Password
We’ve created functionality to register new users and authenticate them. Now what happens when a user forgets their password? This feature has two parts to it, first we send them an email with a link (containing token) and then once they click on the link we present them with a view to enter new password.
First we’ll implement a simple view with an email input box and action methods,
- public IActionResult ForgotPassword()
- {
- return View();
- }
-
- [HttpPost]
- [ValidateAntiForgeryToken]
- public async Task<IActionResult> ForgotPassword(string email)
- {
- if (string.IsNullOrEmpty(email))
- return View();
-
- var user = await this.userManager.FindByEmailAsync(email);
- if (user == null)
- return RedirectToAction("ForgotPasswordEmailSent");
-
- if (!await this.userManager.IsEmailConfirmedAsync(user))
- return RedirectToAction("ForgotPasswordEmailSent");
-
- var confrimationCode =
- await this.userManager.GeneratePasswordResetTokenAsync(user);
-
- var callbackurl = Url.Action(
- controller: "Security",
- action: "ResetPassword",
- values: new { userId = user.Id, code = confrimationCode },
- protocol: Request.Scheme);
-
- await this.emailSender.SendEmailAsync(
- email: user.Email,
- subject: "Reset Password",
- message: callbackurl);
-
- return RedirectToAction("ForgotPasswordEmailSent");
- }
-
- public IActionResult ForgotPasswordEmailSent()
- {
- return View();
- }
Highlighted lines are worth noting, we first find a user based on email and then generate a token to send via link. Also note that if the user isn’t found, we still give the user a success message and do not disclose the fact that database does not contain the email.
Next we’ll add action methods that users will reach once they click the link,
- public IActionResult ResetPassword(string userId, string code)
- {
- if (userId == null || code == null)
- throw new ApplicationException("Code must be supplied for password reset.");
-
- var model = new ResetPasswordViewModel { Code = code };
- return View(model);
- }
-
- [HttpPost]
- [ValidateAntiForgeryToken]
- public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
- {
- if (!ModelState.IsValid)
- return View(model);
-
- var user = await this.userManager.FindByEmailAsync(model.Email);
- if (user == null)
- return RedirectToAction("ResetPasswordConfirm");
-
- var result = await this.userManager.ResetPasswordAsync(
- user, model.Code, model.Password);
- if (result.Succeeded)
- return RedirectToAction("ResetPasswordConfirm");
-
- foreach (var error in result.Errors)
- ModelState.AddModelError(string.Empty, error.Description);
-
- return View(model);
- }
-
- public IActionResult ResetPasswordConfirm()
- {
- return View();
- }
Again highlighted lines are worth noting and when displaying the reset password view, we send the token received by the user to the view via a view model. When the user posts a new password we use UserManager to save the new password.
Note
I’ve not copied the code for View here, they all have simple fields that map to the respective View Models. You could explore these in the source code.
Summary
With relatively simple and few lines of code we’ve added registration, login, logout, forgot and reset password features to our application. We’re not dealing with password hashes, validating tokens, finding users etc. ASP.NET Core Identity takes care of all this for us. Most of the code in the sample is for views and models but the two classes of interest are UserManager and SigninManager and are doing all the heavy lifting for us. There are even more features provided by identity, for instance, two-factor authentication and authenticating via external identity providers. I’ll demonstrate these in future posts, stay tuned.
Source Code
GitHub