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 utilize 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 the following commands:
dotnet ef migrations add
dotnet ef database update
Note: You could learn more about EF and migrations here and here.
You could 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 his/her 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 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, when displaying the reset password view, we send the token received by 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 code for view here, they all have simple fields that map to 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.