Instructor-led Online Courses in ASP.NET Core and Angular by Bipin Joshi. Read more...
Registration open for ASP.NET Core and Angular instructor-led online courses. Courses conducted by Bipin Joshi on weekends. Click here for more details.

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 !!


Bipin Joshi is a software consultant, trainer, author and yoga mentor having 22+ years of experience in software development. He also conducts online courses on ASP.NET MVC, ASP.NET Core and Design Patterns. He is a published author and has authored or co-authored books for Apress and Wrox press. Having embraced the Yoga way of life he also teaches Ajapa Yoga to interested individuals. To know more about him click here.

Get connected : Twitter  Facebook  Google+  LinkedIn

Posted On : 19 February 2018


Tags : ASP.NET ASP.NET Core MVC C# Visual Studio