Implement Security using ASP.NET Core Identity in 10 Easy Steps
Many web applications need to authenticate and authorize its users. A common
approach is to accept user name and password from the user and validate them
against some data store. As far as ASP.NET Core web applications are concerned
the recommended way to implement such a security using ASP.NET Core Identity. In
this article you will learn to implement user authentication as well as role
based security using ASP.NET Core Identity. You will do so by building a sample
application from scratch using the empty project template.
The steps required to build this application are listed below:
- Step 1 : Create a new ASP.NET Core project using Empty project template
- Step 2 : Add the required NuGet packages
- Step 3 : Create Identity DbContext, user and role classes
- Step 4 : Configure application startup
- Step 5 : Create view models required by the application
- Step 6 : Create AccountController - the controller containing
registration / login / logout code.
- Step 7 : Create Register and Login views
- Step 8 : Create HomeController - the controller that contains actions to
be secured
- Step 9 : Create Index view
- Step 10 : Create database tables using dotnet EF Core migrations
commands
Let's begin!
Step 1 : Creating a new project
Create a new ASP.NET Core Web Application using Visual Studio 2015.
Make sure to select the Empty project template while creating the project.
Step 2 : Adding NuGet packages
Since you have selected the Empty project template, the Project.json won't
have any mention of the NuGet packages. Open the Project.json file and modify
the dependencies and tools sections as shown below:
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.0",
"type": "platform"
},
"Microsoft.AspNetCore.Authentication.Cookies": "1.0.0",
"Microsoft.AspNetCore.Diagnostics": "1.0.0",
"Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0",
"Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.0.0",
"Microsoft.AspNetCore.Mvc": "1.0.0",
"Microsoft.AspNetCore.Razor.Tools": {
"version": "1.0.0-preview2-final",
"type": "build"
},
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
"Microsoft.AspNetCore.StaticFiles": "1.0.0",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
"Microsoft.EntityFrameworkCore.SqlServer.Design": {
"version": "1.0.0",
"type": "build"
},
"Microsoft.EntityFrameworkCore.Tools": {
"version": "1.0.0-preview2-final",
"type": "build"
},
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.Extensions.Configuration.UserSecrets": "1.0.0",
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.Extensions.Logging.Debug": "1.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0",
"Microsoft.VisualStudio.Web.CodeGeneration.Tools": {
"version": "1.0.0-preview2-final",
"type": "build"
},
"Microsoft.VisualStudio.Web.CodeGenerators.Mvc": {
"version": "1.0.0-preview2-final",
"type": "build"
}
},
"tools": {
"BundlerMinifier.Core": "2.0.238",
"Microsoft.AspNetCore.Razor.Tools":
"1.0.0-preview2-final",
"Microsoft.AspNetCore.Server.IISIntegration.Tools":
"1.0.0-preview2-final",
"Microsoft.EntityFrameworkCore.Tools":
"1.0.0-preview2-final",
"Microsoft.Extensions.SecretManager.Tools":
"1.0.0-preview2-final",
"Microsoft.VisualStudio.Web.CodeGeneration.Tools": {
"version": "1.0.0-preview2-final",
"imports": [
"portable-net45+win8"
]
}
},
Notice the NuGet packages marked in bold letters. They are required for
ASP.NET Core MVC, Entity Framework Core and ASP.NET Core Identity.
Step 3 : Create Identity DbContext, User and Role classes
Our sample application being built stored user names and passwords in a local
SQL server database. To talk with this database you need an IdentityDbContext
class. The IdentityDbContext needs to know what "type" of users and roles it
will be dealing with. Thus you also need to create user and role classes.
The MyIdentityUser class shown below represents our application user.
public class MyIdentityUser:IdentityUser
{
public string FullName { get; set; }
public DateTime BirthDate { get; set; }
}
As you can see the MyIdentityUser class inherits from IdentityUser base class
(Microsoft.AspNetCore.Identity.EntityFrameworkCore namespace). The IdentityUser
base class contains basic user details such as UserName, Password and Email. We
also want to capture FullName and BirthDate of a user. So, we add these
additional properties in MyIdentityUser class.
Now add MyIdentityRole class as shown below:
public class MyIdentityRole:IdentityRole
{
public string Description { get; set; }
}
The MyIdentityRole class inherits from IdentityRole base class. The base
class provides details such as RoleName and we add Description as an extra piece
of information.
If you don't want any additional details to be captured you could have used
IdentityUser and IdentityRole classes directly.
Now that user and role classes are ready, let's create IdentityDbContext
class.
public class MyIdentityDbContext:
dentityDbContext<MyIdentityUser,MyIdentityRole,string>
{
public MyIdentityDbContext
(DbContextOptions<MyIdentityDbContext> options)
: base(options)
{
//nothing here
}
}
The MyIdentityDbContext class inherits from IdentityDbContext base class.
Notice how MyIdentityUser and MyIdentityRole types are passed while creating the
MyIdentityDbContext class. The third parameter is the data type of the primary
key for the user and role classes.
Step 4 : Configure application startup
Open Startup.cs file and modify the ConfigureServices() and Configure()
methods as shown below:
private IConfiguration config;
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder();
builder.SetBasePath(env.ContentRootPath);
builder.AddJsonFile("appsettings.json");
config = builder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyIdentityDbContext>(options =>
options.UseSqlServer(config.GetConnectionString
("DefaultConnection")));
services.AddIdentity<MyIdentityUser, MyIdentityRole>()
.AddEntityFrameworkStores<MyIdentityDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}
public void Configure(IApplicationBuilder app,
IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseStaticFiles();
app.UseIdentity();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/
{action=Index}/{id?}");
});
}
Notice the code marked in bold letters.
The Startup constructor loads AppSettings.json file containing a database
connection string. This database stores the user accounts and other details.
Make sure to add the AppSettings.json file and specify a valid database
connection string there. An example is given below:
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=MyDb;
Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
The ConfigureServices() method calls AddDbContext() method to add
MyIdentityDbContext to the services collection. Notice how the database
connection string is specified. Then AddIdentity() method is used to add ASP.NET
Core Identity services to the container. This is where MyIdentityUser and
MyIdentityRole classes are also mentioned.
The Configure() method calls UserIdentity() method to add ASP.NET Core
Identity to the request pipeline.
Step 5 : Create RegisterViewModel and LoginViewModel classes
We need two view models - RegisterViewModel and LoginViewModel - in our
application. These view models hold the data entered on the register and login
views respectively and are used by the AccountController we create later.
These two view model classes are shown below:
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; }
[Required]
public string FullName { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
}
public class LoginViewModel
{
[Required]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Required]
public bool RememberMe { get; set; }
}
As you can see, the view model classes are quite straightforward. The
RegisterViewModel class has six properties namely UserName, Password,
ConfirmPassword, Email, FullName and BirthDate. The LoginViewModel class
contains three properties namely UserName, Password and RememberMe. Various
properties of the view model classes are decorated with data annotation
attributes for the sake of model validation.
Step 6 : Create AccountController class
The AccountController class is a controller that performs registration, login
and logout operations. The AccountController uses the functionality of ASP.NET
Core Identity for creating user accounts and signing the user in and out of the
application.
We declare a few private members for this class and assign them in the
constructor as shown below:
public class AccountController : Controller
{
private readonly UserManager<MyIdentityUser> userManager;
private readonly SignInManager<MyIdentityUser> loginManager;
private readonly RoleManager<MyIdentityRole> roleManager;
public AccountController(UserManager<MyIdentityUser> userManager,
SignInManager<MyIdentityUser> loginManager,
RoleManager<MyIdentityRole> roleManager)
{
this.userManager = userManager;
this.loginManager = loginManager;
this.roleManager = roleManager;
}
....
....
}
The AccountController declares three private variables of type UserManager<T>,
SignInManager<T> and RoleManager<T> respectively. The UserManager class is used
for creating and managing users. The RoleManager class is used for creating and
managing roles. The SignInManager class is used to log a user in and out of the
system.
Object instances of these three classes are injected into the
AccountController constructor as shown above. The constructor code simply
assigns the injected objects to the respective variables declared earlier.
The AccountController has in all five actions :
- Two Register() actions - GET and POST
- Two Login() actions - GET and POST
- One Logout() actions.- POST
The Register() actions are responsible for creating user accounts and are
shown below:
public IActionResult Register()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Register(RegisterViewModel obj)
{
if (ModelState.IsValid)
{
MyIdentityUser user = new MyIdentityUser();
user.UserName = obj.UserName;
user.Email = obj.Email;
user.FullName = obj.FullName;
user.BirthDate = obj.BirthDate;
IdentityResult result = userManager.CreateAsync
(user, obj.Password).Result;
if (result.Succeeded)
{
if(!roleManager.RoleExistsAsync("NormalUser").Result)
{
MyIdentityRole role = new MyIdentityRole();
role.Name = "NormalUser";
role.Description = "Perform normal operations.";
IdentityResult roleResult = roleManager.
CreateAsync(role).Result;
if(!roleResult.Succeeded)
{
ModelState.AddModelError("",
"Error while creating role!");
return View(obj);
}
}
userManager.AddToRoleAsync(user,
"NormalUser").Wait();
return RedirectToAction("Login", "Account");
}
}
return View(obj);
}
The Register() action with no parameters simply returns a blank registration
page to the user. The second Register() action accepts RegisterViewModel object
as its parameter. Inside, the code checks whether the model contains valid data
or not using the IsValid property of ModelState object. If the model is valid we
create a new MyIdentityUser object and assign its UserName, Email, FullName and
BirthDate properties. Then the code calls the CreateAsync() method of the
UserManager class in an attempt to create a user account. The password is passed
to the CreateAsync() method. Since the CreateAsync() is an asynchronous call, we
block the execution by calling the Result property. The CreateAsync() method
returns IdentityResult - an object that tells us whether the user creation was
successful or not.
If the user creation succeeds (result.Succeeded property) we proceed to
create the roles required by the application. The role creation is a one time
operation. It should happen only if a role doesn't exist already. So, the code
uses RoleManager to check whether a role already exists or not. This is done
using RoleExistsAsync() method and by passing role name as its parameter.
If a role doesn't exist already, MyIdentityRole object representing that role
is created. Its RoleName and Description properties are assigned the required
values (NormalUser is the role name in this example). Then CreateAsync() is
called on RoleManager in an attempt to create the role. If the role creation
succeeds (Succeeded property returns true) we add the newly created user to the
NormalUser role. This is done using AddToRoleAsync() method of UserManager.
After creating the account and adding the new user to the NormalUser role we
redirect the user to the login page. The two Login() actions are discussed next.
public IActionResult Login()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Login(LoginViewModel obj)
{
if (ModelState.IsValid)
{
var result = loginManager.PasswordSignInAsync
(obj.UserName, obj.Password,
obj.RememberMe,false).Result;
if (result.Succeeded)
{
return RedirectToAction("Index", "Home");
}
ModelState.AddModelError("", "Invalid login!");
}
return View(obj);
}
The first Login() action simply returns a blank login view to the user. When
a user enters user name and password and submits the form the form is posted to
the second Login() action.
The code then attempts to log the user in by calling the PasswordSignInAsync()
method of the SignInManager. The PasswordSignInAsync() method accepts user name,
password and remember me flag and account lockout flag. ASP.NET Core Identity
uses cookie based authentication scheme to authenticate requests. The remember
me flag controls whether the authentication cookie is to be persisted on the
client machine when the browser is closed. A value of true will persist
the cookie. We accept this flag from the end user using a checkbox (as you will
see later). The lockout flag comes into picture only when you are using account
lockout feature (account is locked after certain number of unsuccessful
attempts). In this example we aren't using account lockout and hence we pass
false.
If the user name and password are valid the PasswordSignInAsync() method will
issue the authentication cookie and Succeeded property will be true. We then
redirect the user to the /home/index page.
Finally, the Logout() action of the AccountController() removes the
authentication cookie issued earlier. This action is shown below:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult LogOff()
{
loginManager.SignOutAsync().Wait();
return RedirectToAction("Login","Account");
}
The SignOutAsync() method of SignInManager removes the authentication cookie.
Since this method is a void method we call Wait() on it to block the call. Once
a user signs out of the application we redirect the control to the login page.
Step 7 : Create Register and Login views
The Register and Login views are relatively straightforward. The markup of
Register view is given below:
@model RegisterViewModel
<script src="~/Scripts/jquery.js"></script>
<script src="~/Scripts/jquery.validate.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.js">
</script>
<h1>Register</h1>
<form asp-controller="Account" asp-action="Register" method="post">
<table>
<tr>
<td><label asp-for="UserName"></label></td>
<td><input asp-for="UserName" /></td>
</tr>
<tr>
<td><label asp-for="Password"></label></td>
<td><input asp-for="Password" /></td>
</tr>
<tr>
<td><label asp-for="ConfirmPassword"></label></td>
<td><input asp-for="ConfirmPassword" /></td>
</tr>
<tr>
<td><label asp-for="Email"></label></td>
<td><input asp-for="Email" /></td>
</tr>
<tr>
<td><label asp-for="FullName"></label></td>
<td><input asp-for="FullName" /></td>
</tr>
<tr>
<td><label asp-for="BirthDate"></label></td>
<td><input asp-for="BirthDate" /></td>
</tr>
<tr>
<td colspan="2"><input type="submit"
value="Register" /></td>
</tr>
</table>
<div asp-validation-summary="All" ></div>
</form>
The Register view consists of a form tag helper that submits to Register
action of the Account controller. The form further uses label and input tag
helpers to render the respective data entry fields. Notice that the form also
uses certain script files that are used during the client side validation. You
can place these files inside the Scripts folder under wwwroot folder. There is a
<div> element at the bottom that uses validation summary tag helper and displays
all the validation related errors.
The following figure shows a sample run of the Register view:
The complete markup of the Login view is as follows:
@model LoginViewModel
<script src="~/Scripts/jquery.js"></script>
<script src="~/Scripts/jquery.validate.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.js">
</script>
<h1>Login</h1>
<form asp-controller="Account" asp-action="Login" method="post">
<table>
<tr>
<td><label asp-for="UserName"></label></td>
<td><input asp-for="UserName" /></td>
</tr>
<tr>
<td><label asp-for="Password"></label></td>
<td><input asp-for="Password" /></td>
</tr>
<tr>
<td><label asp-for="RememberMe"></label></td>
<td><input asp-for="RememberMe" /></td>
</tr>
<tr>
<td colspan="2"><input type="submit"
value="Register" /></td>
</tr>
</table>
<div asp-validation-summary="All"></div>
</form>
We won't go into the details of this markup as it's straightforward. A sample
run of Login view looks like this:
Step 8 : Create HomeController and a secured action
The HomeController contains one secured action. The secured action simply
forms messages based on current user's name and role. These actions are shown
below:
private readonly UserManager<MyIdentityUser> userManager;
public HomeController(UserManager<MyIdentityUser>
userManager)
{
this.userManager = userManager;
}
[Authorize]
public IActionResult Index()
{
MyIdentityUser user = userManager.GetUserAsync
(HttpContext.User).Result;
ViewBag.Message = $"Welcome {user.FullName}!";
if(userManager.IsInRoleAsync(user,"NormalUser").Result)
{
ViewBag.RoleMessage = "You are a NormalUser.";
}
return View();
}
The HomeController's constructor receives a UserManager object through
ASP.NET Core DI framework. It stores the injected UserManager in a private
variable. The Index() action is secured by decorating it with [Authorize]
attribute. This way you won't be able to invoke Index() unless you log-in first.
The Index() action retrieves the name of the current user. This is done using
the GetUserAsync() method of UserManager. Notice the use of HttpContext.User
property to retrieve the underlying security principle of the current user.
A welcome message is then formed using the FullName of the user. Further, the
IsInRoleAsync() method checks whether the current user belongs to NormalUser
role. If so, a message is stored in the RoleMessage property of the ViewBag.
Step 9 : Creating the Index view
The Index view simply outputs the Message and RoleMessage properties on the
page. A sample run of Index view is shown below:
Step 10 : Create the database tables
ASP.NET Core Identity stores the user and role information in certain
database tables. To create these tables you need to issue dotnet EF migrations
commands at the command prompt. So, open a command prompt and go to the
project's root folder. Then issue the following commands:
>> dotnet ef migrations add MyMigrations
>> dotnet ef database update
After issuing the first command Migrations folder will be added to your
project and EF migrations related code will be generated for you. The second
command applies those migrations to the underlying database.
That's it! You can now run the application. Test the functionality by
creating a couple of new user accounts and then try signing in with those
credentials.