Implement Cookie Authentication in ASP.NET Core
If you have been working with ASP.NET Core, you are probably aware of ASP.NET
Core Identity. ASP.NET Core Identity is a full-fledged framework to secure your
websites. It comes with a lot of features such as external logins and Json Web
Tokens (JWT) support. Ay times, however, you need something that is simple and
gives you total control on various aspects of implementation such as data store
and account manageemnt. That is where ASP.NET Core's Cookie Authentication can
come handy. In this article you will learn what Cookie Authentication is and how
to configure it to secure your websites.
A bit of background
Before I go into the configuration and implementation details of cookie
authentication, I can't help but to highlight the parallel between ASP.NET
classic and ASP.NET Core in this regards.
When ASP.NET 1.x was introduced, there were two prominent ways of
implementing authentication - Windows based authentication and Forms
authentication. The Forms authentication is also called cookie authentication
because it works on the basis of an authentication ticket in the form of a
cookie. The Forms authentication doesn't do any user management by itself. It
simply checks whether an incoming request is authenticated or not based on the
presence of a special cookie. And accordingly allows or denies access to the end
user. User account management is the responsibility of the developer and one
needs to write custom code to accomplish that task. This simplistic approach
gave total control on the underlying data store and user management but on the
other hand expected you to write all that logic yourself.
In ASP.NET 2.0 Forms authentication was complemented with Membership, Roles,
and Profile providers. The membership framework takes care of user and role
management for you. This approach saves a lot of time otherwise spent on write
the necessary code. But has its own limitations such as rigid database structure
and fixed set of user management APIs.
Later, Microsoft released ASP.NET Identity - a new framework that takes care
of the modern requirements of website security such as external logins.
Under ASP.NET Core we have similar two options to implement website security
- ASP.NET Core Identity or simple Cookie Authentication. I have already
explained ASP.NET Core
identity in earlier articles and intend to discuss cookie authentication in
the remainder of this article.
Now that you some background of cookie authentication, let's get going.
Begin by creating a new ASP.NET Core web application and follow the steps
outlined below.
Create database table and DbContext
When we decide to use cookie authentication for our ASP.NET Core website, the
data store and data structures is our responsibility. As an example we will
create a simple table in SQL Server database but you can use any data store of
your choice (for example, a NoSQL database).
Create a database table with the following structure :
As you can see, we created a table named MyAppUsers to store user
information. The table has a simple structure consisting of four columns - Id,
UserName, Password, and Roles. For the same of simplicity we store all the
information without any encryption. Moreover, roles are stored in the same table
rather than having a separate table.
Now add DataAccess folder under the project root and add the Entity and the
DbContext classes as shown below :
[Table("MyAppUsers")]
public class MyAppUser
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string Roles { get; set; }
}
The MyAppUser class maps to the MyAppUsers table we just created and has
corresponding properties.
public class MyAppDbContext:DbContext
{
public MyAppDbContext(DbContextOptions<MyAppDbContext>
options) : base(options)
{
}
public DbSet<MyAppUser> MyAppUsers { get; set; }
}
The MyAppDbContext class inherits from DbContext and contains MyAppUsers
DbSet.
Configure cookie authentication
Ok. Now that we have DbContext ready, let's enable cookie authentication for
our web application. Open the Startup class and modify ConfigureServices()
method as shown below :
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddEntityFrameworkSqlServer();
services.AddDbContext<MyAppDbContext>(options =>
options.UseSqlServer("data source=.;initial
catalog=northwind;integrated security=true;
multipleactiveresultsets=true"));
services.AddAuthentication
(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
}
The ConfigureServices() method should look familiar to you except the code
marked in bold letters. The AddAuthentication() and AddCookie() methods register
cookie authentication service with the framework. Notice that AddAuthentication()
accepts a string parameter indicating name of the security scheme. This can be
any developer defined value or you can use the default as indicated by
AuthenticationScheme property of CookieAuthenticationDefaults
class.
Also, make sure to change the database connection string in the AddDbContext()
call as per your setup. You may also pick it up from a configuration file.
Next, modify the Configure() method to use cookie authentication middleware :
public void Configure(IApplicationBuilder app,
IHostingEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
}
We just completed configuring the cookie authentication for our website.
Create RegisterViewModel and LoginViewModel classes
We need two view models - RegisterViewModel and LoginViewModel - classes as
shown below :
public class RegisterViewModel
{
[Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
[Required]
[Compare("Password")]
public string ConfirmPassword { get; set; }
}
The RegisterViewModel class wraps the registration details as entered on
Register view (discussed later).
public class LoginViewModel
{
[Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
[Required]
public bool RememberMe { get; set; }
}
The LoginViewModel class wraps the login details as entered on Login view.
Notice the RememberMe property that indicates whether the user's logged in state
should be preserved even after closing the browser session.
Create AccountController
Once the view models are ready, add AccountController class under Controllers
folder. The AccountController will house five actions :
- Register() - GET and POST versions of Register() action take care of
creating a new user account.
- Login() - GET and POST versions of Login() action takes care of signing
the user in the system. This is where authentication cookie is issued to the
user.
- Logout() - POST version of Logout() removes the authentication cookie
issued earlier.
Let's examine these methods one by one.
The actions listed above require MyAppDbContext injected in the constructor.
So, begin by adding this code to AccountController :
private MyAppDbContext db;
public AccountController(MyAppDbContext db)
{
this.db = db;
}
Creating a user account
The following code shows two versions of Register() action :
public IActionResult Register()
{
return View();
}
[HttpPost]
public IActionResult Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
MyAppUser user = new MyAppUser();
user.UserName = model.UserName;
user.Password = model.Password;
user.Roles = "Manager,Administrator";
db.MyAppUsers.Add(user);
db.SaveChanges();
ViewData["message"] = "User created successfully!";
}
return View();
}
The POST version of Register() accepts RegisterViewModel object through model
binding. Inside, we transfer values from RegisterViewModel to MyAppUser and then
add a new user to MyAppUsers DbSet. Notice that we have also set the Roles
property to Manager and Administrator. In a more realistic situation you will
have a separate page on which roles are assigned to a user. Calling SaveChanges()
creates the user account in the MyAppUsers table we created initially.
We also set a success message in the ViewData that can be displayed on the
Register view. Note that for the sake of simplicity we haven't added any
validations and checks for the user account being created.
Sign-in into the application
The two versions of Login() are shown below :
public IActionResult Login(string returnUrl)
{
return View();
}
[HttpPost]
public IActionResult Login(LoginViewModel model,
string returnUrl)
{
bool isUservalid = false;
MyAppUser user = db.MyAppUsers.Where(usr =>
usr.UserName == model.UserName &&
usr.Password == model.Password).SingleOrDefault();
if(user!=null)
{
isUservalid = true;
}
if(ModelState.IsValid && isUservalid)
{
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, user.UserName));
string[] roles = user.Roles.Split(",");
foreach (string role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var identity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.
AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
var props = new AuthenticationProperties();
props.IsPersistent = model.RememberMe;
HttpContext.SignInAsync(
CookieAuthenticationDefaults.
AuthenticationScheme,
principal, props).Wait();
return RedirectToAction("Index", "Home");
}
else
{
ViewData["message"] = "Invalid UserName
or Password!";
}
return View();
}
The POST version of Login() is important for us because this is where an
authentication cookie is issued to the user.
The code first determines whether the UserName and Password are valid. This
is done by checking whether there is MyAppUser entity matching the given
UserName and Password. Accordingly isUserValid boolean variable is assigned a
true or false value.
If a user is valid then four objects are created :
- List of Claim objects
- ClaimsIdentity object
- ClaimsPrincipal object
- AuthenticationProperties object
The List of Claim objects hold all the claims for a user. The first claim
object we add is of type Name and indicates user's UserName. This claim is
necessary so that HttpContext.User.Identity.Name property returns the current
user's UserName.
Then we add a series of roles assigned to the user. This is done by splitting
the Roles property of MyAppUser and then adding Claim objects of type Role. This
is necessary so that HttpContext.User.IsInRole() method works as expected.
We will be using the HttpContext.User.Identity.Name and
HttpContext.User.IsInRole() later in the HomeController.
Then the code creates a ClaimsIdentity object by passing the list of Claim
objects and the authentication scheme name.
Then the code creates a ClaimsPrincipal object by passing the ClaimsIdentity
in the constructor.
Then the code creates an AuthenticationProperties object. The
AuthenticationProperties object holds values for certain properties such as
IsPersistent that are used by the current authentication session.
Finally, SignInAsync() method of HttpContext is called to issue the
authentication cookie to the user. The SignInAsync() method accepts
authentication scheme name, ClaimsPrincipal, and AuthenticationProperties.
Once a user is successfully signed in, the response is redirected to the
Index() action of HomeController. You can also use returnUrl parameter of the
Login() action for redirection purpose.
If the user is invalid then a ViewData message is set accordingly.
Sign-out from the application
The Logout() action that removes the authentication cookie is shown below :
[HttpPost]
public IActionResult Logout()
{
HttpContext.SignOutAsync(
CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Login", "Account");
}
The Logout() action simply calls SignOutAsync() method of HttpContext by
passing the authentication scheme name. This method removes the authentication
cookie. The user then redirected to the login page.
Create Register and Login views
Ok. So far so good. Now let's proceed to creating the views. Add two views
under Views > Account folder - Register.cshtml and Login.cshtml.
This is how the Register view looks like in the browser :
The markup that makes the Register view view is given below :
@model SimpleCookieAuth.ViewModels.RegisterViewModel
<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"
type="password" /></td>
</tr>
<tr>
<td><label asp-for="ConfirmPassword"></label></td>
<td><input asp-for="ConfirmPassword"
type="password"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit"
value="Register" />
</td>
</tr>
</table>
<div asp-validation-summary="All"></div>
<br />
<div>@ViewData["message"]</div>
</form>
The Register view is quite straightforward and simply displays the textboxes
for entering the user details such as UserName and Password. The form tag helper
submits the form to the Register() action of AccountController.
The Login view is shown below :
The markup that makes the Login view view is given below :
@model SimpleCookieAuth.ViewModels.LoginViewModel
<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" type="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="Login" />
</td>
</tr>
</table>
<div asp-validation-summary="All"></div>
<br />
<div>@ViewData["message"]</div>
<h3><a asp-controller="Account" asp-action="Register">
Create a user</a></h3>
</form>
The form tag helper submits the form to the Login() action of
AccountController. The login page has textboxes to enter UserName, Password, and
also a checkbox to indicate whether login status should be remembered or not.
This completes the Register and Login views.
Create HomeController and Index view
Now add HomeController and modify the Index() action as shown below :
[Authorize]
public IActionResult Index()
{
string userName = HttpContext.User.Identity.Name;
if(HttpContext.User.IsInRole("Administrator"))
{
ViewData["adminMessage"] = "You are an Administrator!";
}
if (HttpContext.User.IsInRole("Manager"))
{
ViewData["managerMessage"] = "You are a Manager!";
}
ViewData["username"] = userName;
return View();
}
Notice the code marked in bold letters. The Index() action is decorated with
[Authorize] attribute indicating that it's a secured action and can be invoked
only by authenticated users.
The HttpContext.User.Identity.Name property returns the name of current user.
Recollect that we added a Claim object of type Name in the Login() action
earlier to make this property work as expected.
The HttpContext.User.IsInRole() method returns true if the current user
belongs to the specified role. Recollect that we added Claim objects of type
Role in the Login() action earlier to make this method work as expected.
A sample run of the Index view produces this output :
The markup behind the Index view is shown below :
<h1>Welcome @ViewData["username"]!</h1>
<h2>@ViewData["adminMessage"]</h2>
<h2>@ViewData["managerMessage"]</h2>
<form asp-controller="Account" asp-action="Logout"
method="post">
<input type="submit" value="Logout" />
</form>
The markup simply outputs various ViewData entries added earlier. It also
renders the Logout button at the bottom. Clicking on the Logout button triggers
the Logout() action of AccountController.
This completes the sample application. Run the application. You will notice
that although you try to access Index() action of HomeController, you are taken
to the Login page. You will also have RetrnUrl query string parameter pointing
to the root of the web app. Use the Create a user link at the bottom of the
Login page to got to the Register view. Create a new user account and try
signing in with that account. Also check the working of Remember Me checkbox.
You may read more about cookie authentication discussed in this article
here.
That's it for now! Keep coding !!