Monday, July 31, 2023

ASP.NET Core - Cookie authentication without ASP.NET Core Identity


In this tutorial we will learn about cookie authentication without ASP.NET Core Identity. First, we create an empty ASP.NET application with a razor page.

Project

  • Open the Visual Studio. 
  • Create web project using ASP.NET Core Empty template.
  • Project name: CookieAuthDemo
  • Solution name: CookieAuthDemo
  • .NET Core version: 5.0
  • Add the AddRazorPages() into the IServiceCollection in Startup class.
  • Use MapRazorPages() as terminal middleware in Startup class.
Look at the below updated code in Startup class.
Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace CascadingDropdownsRP
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}

Add Pages folder in the root directory of the application and create a razor page named Index in it. 

Write some HTML tags for 'Hello Cookie' text in the Index page and run the application we get the Hello Cookie printed in the browser. Again add another folder named as Admin in the Pages folder. And inside this folder create another Index page, write some HTML tag in this page also and run the application. We reach to the Index page of Admin folder when we use the URL pointing to the Admin folder. Till now there is no restriction to use the Admin folder and its pages. Any user can access pages of this folder.

Restrict Access of folder

To restrict the access of this folder we use RazorPageOptions as given in the following code. The
RazorPageOptions provides options to authorize a razor page or folder or any folder in areas.


public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages(opt =>
            {
                opt.Conventions.AuthorizeFolder("/admin");
            });
        }
AuthorizeFolder, AuthorizeAreaFolder, AuthorizePage, AuthorizeAreaPage, AllowAnonymousToPage, AllowAnonymousToAreaPage, AllowAnonymousToFolder, AllowAnonymousToAreaFolder etc are extension methods of PageConventionCollection class. These methods are used to restrict access of a page or folder.
Run the application. We get the following exception

Since folder authorization is set in the Startup class in AddRazorPages but not middleware is added to support the authorization, we get error. To overcome this exception, we add the UseAuthorization middleware.
Run the application.We get the following exception.


Now add the following line of code  in ConfigureServices in Startup class which implies that authentication service is added in the app which is based on cookie authentication. But it is not enough because it just tells about authentication scheme which can be cookie or token etc. We must add cookie and its description. We will get error.

  services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);

We still get exception because we must add cookie and set its options, if needed.


With out setting cookie options, the default are applied. Cookie options are all about Login Path, Logout Path, Access denied path, cookie name, cookie domain, max age, http only, expire time span, sliding expiration, cookie manager etc. Cookie manager is used to request for a cookie or to delete a cookie etc.

In the following line of code, just cookie is added using AddCookie method.

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
Run the application. We get the following exception.
As we have not set the login path in the cookie, we are getting the default login path- Account/Login. It means that we must have Account/Login page. So, we create Account folder in the Pages folder and add a razor page called Login. Write <h1>Login here</h1> tag in this page and run the application. We get the following result.

Login form. Next task is to create a login form in the Login.cshtml razor page.


@page
@model CookieAuthDemo.Pages.Account.LoginModel


<h1>Login here</h1>

<form method="post">
    <table>
        <tr>
            <td><label>Name</label></td>
            <td><input type="text" /></td>
        </tr>
        <tr>
            <td><label>Password</label></td>
            <td><input type="password" /></td>
        </tr>
        <tr>
            <td colspan="2" style="text-align:right"> <input type="submit" /></td>
        </tr>
    </table>
</form>
<br />

@ViewData["Message"]

Login handler. When user clicks the submit button, the click event will be handled in the backing page of Login page. we write the following code which is without any login test.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;


namespace CookieAuthDemo.Pages.Account
{
    public class LoginModel : PageModel
    {
        public IActionResult OnPost(string returnUrl)
        {
            return LocalRedirect(returnUrl);
        }
    }
}
Run the application. We get the following result when we try to reach the admin folder.
Now fill the any name and password and click the submit button. We don’t reach the OnPost method. We get the following error. Why?
Add the following attribute in form tag.
action="/Pages/Account/Login.cshtml.cs"
Run the application. We get the following error.

Sign-in user class. We will use Tag helpers to overcome these limitations. Create a model class named UserSignin in the Models folder as given below.

namespace CookieAuthDemo.Models
{
    public class UserSignIn
    {
        public string Name { get; set; }
        public string Password { get; set; }
        public string Role { get; set; } 
    }
}

Update the Login page. Create controls by using tag helpers.



@page
@model CookieAuthDemo.Pages.Account.LoginModel
@using CookieAuthDemo.Models;
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Login here</h1>
<form method="post">
    <table>
        <tr>
            <td><label asp-for="UserSignIn.Name"></label></td>
            <td><input type="text" asp-for="UserSignIn.Name" /></td>
        </tr>
        <tr>
            <td><label asp-for="UserSignIn.Password" ></label></td>
            <td><input type="password" asp-for="UserSignIn.Password" /></td>
        </tr>
        <tr>
            <td colspan="2" style="text-align:right"> <input type="submit"  /></td>
        </tr>
        <tr>
            <td colspan="2"><input type="hidden" asp-for="UserSignIn.Role" value="@ViewData["role"]" /></td>
        </tr>
    </table>
</form>

<br />

Click Handler. Update the backing Login Razor page. When user submits the form, OnPost event handler will execute. All the sign-in information is stored in claims identity and is tied with default cookie authentication scheme. Finally, this claims identity is encapsulated inside claims principal. The SignInAsync method takes two parameters, first is authentication scheme and second is principal. SignInAsync creates an encrypted cookie and adds it to the current response. If AuthenticationScheme isn't specified, the default scheme is used.
using CookieAuthDemo.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace CookieAuthDemo.Pages.Account
{
    public class LoginModel : PageModel
    {
        [BindProperty]

        public UserSignIn UserSignIn { get; set; }

        public async Task<IActionResult> OnPostAsync(string returnUrl)
        {
	    // let username and password matches
            // check it here 
            // then we create claims principal
            List<Claim> claims = new List<Claim>();
            claims.Add(new Claim(ClaimTypes.Name, UserSignIn.Name));
            claims.Add(new Claim("Password", UserSignIn.Password));
            claims.Add(new Claim(ClaimTypes.Role, UserSignIn.Role));

            var ci = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
            var principle = new ClaimsPrincipal(ci);
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principle);

            return LocalRedirect(returnUrl);
        }


        public void OnGet()
        {
            ViewData["role"] = "admin";
        }
    }
}

UseAuthentication. One of the important task that is performed by the built-in authentication middleware is to read the cookies and construct the ClaimsPrincipal and update the User object in the HttpContext.

Startup class

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;

using System.Linq;
using System.Threading.Tasks;

namespace CookieAuthDemo
{
    public class Startup
    {

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages(opt =>
            {
                opt.Conventions.AuthorizeFolder("/admin");
            });
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme
               ).AddCookie();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())

            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}
Admin/Index page. Note that when sign-in is successful, username can be displayed on the page. We use User.Identity.IsAuthenticated to check if user is authenticated or not on any page.

@page
@model CookieAuthDemo.Pages.Admin.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>You are on Index page of Admin</h1>
<h3>Is user authenticated: @User.Identity.IsAuthenticated</h3>
<h3>User name: @User.Identity.Name</h3> 

<h1><a asp-page-handler="Signout">Signout</a></h1>
Signout.

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Threading.Tasks;

namespace CookieAuthDemo.Pages.Admin
{
    public class IndexModel : PageModel
    {
        public async Task<IActionResult> OnGetSignoutAsync()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return RedirectToPage("/Index");
        }
    }
}
Root Index page is also modified.

@page
@model CookieAuthDemo.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<div style="background-color:cyan;width:40%;margin-left:10px">
    <h3 style="text-align:right;margin-right:10px">Username: @User.Identity.Name</h3>
    <h1>Index of Root page</h1>
    <h3><a asp-page="Admin/Index">Go to Index page of Admin</a></h3>
    <br/>
    <br/>
</div>
Run the app. We get the following UI. On clicking the link, login form appears which is filled by user and button is clicked then Index page of admin opens and Username appears in the top right. We can sign-out on that page to delete the cookie also. Model validation is missing in this code which can be applied in the UserSingIn class.
REMARKS

  • Call UseAuthentication and UseAuthorization to set the HttpContext.User property.
  • AuthenticationScheme passed to AddAuthentication sets the default authentication scheme for the app. AuthenticationScheme is useful when there are multiple instances of cookie authentication and the app needs to authorize with a specific scheme. Setting the AuthenticationScheme to CookieAuthenticationDefaults.AuthenticationScheme provides a value of "Cookies" for the scheme. Any string value can be used that distinguishes the scheme.
  • The CookieAuthenticationOptions class is used to configure the authentication provider options. Configure CookieAuthenticationOptions in the AddCookie method:

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)

    .AddCookie(options =>

    {
        options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
        options.SlidingExpiration = true;
        options.AccessDeniedPath = "/Forbidden/";

    });

  • Use CookiePolicyOptions provided to the Cookie Policy Middleware to control global characteristics of cookie processing and hook into cookie processing handlers when cookies are appended or deleted.
  • To create a cookie holding user information, construct a ClaimsPrincipal. The user information is serialized and stored in the cookie.
  • Create a ClaimsIdentity with any required Claims and call SignInAsync to sign in the user.
  • SignInAsync creates an encrypted cookie and adds it to the current response. If AuthenticationScheme isn't specified, the default scheme is used.


Sign out

To sign out the current user and delete their cookie, call SignOutAsync:

// Clear the existing external cookie

    await HttpContext.SignOutAsync(
        CookieAuthenticationDefaults.AuthenticationScheme);


NOTE: If CookieAuthenticationDefaults.AuthenticationScheme or "Cookies" isn't used as the scheme, supply the scheme used when configuring the authentication provider.


// using Microsoft.AspNetCore.Authentication;

await HttpContext.SignInAsync(

    CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(claimsIdentity),
    new AuthenticationProperties
    {
        IsPersistent = true,
        ExpiresUtc = DateTime.UtcNow.AddMinutes(20)
    });

No comments:

Post a Comment

Hot Topics