Request Metrics v3.0

Are you ready for INP?

Search update happens on March 12th. Is your INP fast?

Episode 15: Simple Cookie Based Authentication in ASP.NET Core

ASP.Net Core Identity is too magical. Will rolling authentication ourselves finally catch up to us? There are as many ways to set up authentication as there are to build the application itself. Core Identity is the officially encouraged method of authenticating users in ASP.NET Core. As you might have guessed, we’re not fans of the heavy handed, black box approach needed to make Core Identity “Just work”.

We want to use as much of the ASP.NET Authorization framework as we can while avoiding Core Identity. Thankfully, ASP.NET Supports cookie authorization without Core Identity and there are usage examples in the official repository.

Some responsibilities change with Core Identity out of the picture. We become responsible for validating user credentials which are stored in Redis. ASP.NET continues to handle the messy parts like cookie encryption and determining whether a user is currently authenticated.

First, the Authentication Service is configured to support cookie based authorization. This is done when the ASP.NET application starts up:


using System;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace MyApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            // Configure cookie based authentication
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                    .AddCookie(options =>
                    {
                        // Specify where to redirect un-authenticated users
                        options.LoginPath = "/login";

                        // Specify the name of the auth cookie.
                        // ASP.NET picks a dumb name by default.
                        options.Cookie.Name = "my_app_auth_cookie";
                    });

            // ...snip... Other ASP.NET Core configuration here!
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // Configure authentication.
            // This should be done before app.UseEndpoints() is called!
            app.UseAuthentication();
            app.UseAuthorization();

            // ...snip... Other ASP.NET Core configuration here!
        }
    }
}
Startup.cs

Log In a User Without ASP.NET Core Identity

We are responsible for authenticating the user’s credentials. Doing it ourselves means we can more easily support different authorization methods without worrying about whether Core Identity supports them:


using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Controllers
{
    [AllowAnonymous]
    public class LoginController : Controller
    {
        [HttpGet]
        [Route("/login")]
        public IActionResult GetLoginScreen()
        {
            return View("Login");
        }

        [HttpPost]
        [Route("/login")]
        public async Task<IActionResult> LoginUser(
            string username, string password, string returnUrl)
        {
            var loginValid = ValidateLogin(username, password);

            if (!loginValid)
            {
                TempData["LoginFailed"] = $"The username or password is incorrect.";

                return Redirect("/login");
            }
            else
            {
                await SignInUser(username);

                if (string.IsNullOrWhiteSpace(returnUrl) || !returnUrl.StartsWith("/"))
                {
                    returnUrl = "/";
                }

                return this.Redirect(returnUrl);
            }
        }

        private bool ValidateLogin(string username, string password)
        {
            // Check the user/pass here!
            // For example's sake, the credentials are always valid.
            return true;
        }

        private async Task SignInUser(string username)
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, username),
                new Claim("MyCustomClaim", "my claim value")
            };

            var claimsIdentity = new ClaimsIdentity(
                claims, CookieAuthenticationDefaults.AuthenticationScheme);

            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(claimsIdentity));
        }
    }
}
LoginController.cs

Read a Signed In User’s Claims

An authenticated user isn’t very interesting if we can’t find out who they are. The User Identity contains all the information supplied when they were originally signed in:


using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Controllers
{
    public class TestController : Controller
    {
        [HttpGet]
        [Route("/read-claims")]
        public IActionResult TestClaimReading()
        {
            var username = HttpContext.User.Identity.Name;
            var customClaim = HttpContext.User.FindFirst("MyCustomClaim");

            return Content($"User {username} has custom claim value: {customClaim.Value}");
        }
    }
}
TestController.cs

Only Allow Authenticated Users On A Page

There is not much point in having authenticated users if we can’t restrict access to pages. An attribute makes this quick to do in ASP.NET:


using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Controllers
{
    // The Authorize attribute can also be used on individual controller methods.
    [Authorize]
    public class BillingController : Controller
    {
        [HttpGet]
        [Route("/authorized-page")]
        public IActionResult TestAuthorizedPage()
        {
            return Content("Only an authorized user can see this page!");
        }
    }
}
SecureController.cs

The examples above may look like a lot of code, but it is still better than using Core Identity! With any luck, we’ll never have to look at this code again.

Our production environment consists of more than one webserver. If we want to store session data for users, we will need distributed session. Next, we’ll try using Redis as a distributed session store.