ASP.NET 8 - Cookie Authentication

What is Cookie ? In web context Cookies are objects that store user information to help identify, track, and personalize the user experience based on their actions on a website. How it's works ? When you login in a website the server that process your request, if the login is successfuly, creates a cookie with an ID, user data and some informations such as the cookie type, expiration time, refresh policy and so on. Once the brownser receive the cookie it becomes responsible to storing and send the cookie in every request to the server which will check the cookies properties on each request. How to implement in .NET8 ? For this tutorial you will need to create a ASPNET CORE Web API and install the following packages: I made a simple entity to represent User with Name, Email, Password, and Roles. You can check it in this link. For the table mapping, use this configuration: public class UserConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasKey(x => x.Id); builder.HasIndex(x => x.Email).IsUnique(); builder.Property(x => x.Name).HasMaxLength(100).IsRequired(); builder.Property(x => x.Email).HasMaxLength(100).IsRequired(); builder.Property(x => x.Password).HasMaxLength(250).IsRequired(); } } I set the Email property as an index and marked it as unique to ensure that different users cannot register with the same email address. This also allows it to be used efficiently as a filter parameter during login. In your DbContext, make sure to configure the UserRoles relationship like this: public class AppDbContext : DbContext { public DbSet Users { get; set; } public DbSet Roles { get; set; } public AppDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity() .HasMany(u => u.Roles) .WithMany(r => r.Users) .UsingEntity( "UserRoles", j => j.HasOne().WithMany().HasForeignKey("RoleId"), j => j.HasOne().WithMany().HasForeignKey("UserId"), j => { j.HasKey("UserId", "RoleId"); j.ToTable("UserRoles"); }); } } On the respository we only need to search user by his email: public class UserRepository : IUserRespository { private readonly AppDbContext _context; private readonly DbSet _users; public UserRepository(AppDbContext context) { _context = context; _users = _context.Set(); } public async Task GetByEmail(string email) { return await _users .Where(x => x.Email == email) .Include(x => x.Roles) .FirstOrDefaultAsync(); } } Now we can focus on the login service, which is pretty simple: it just receives the user input, searches for the user by email in the database, validates the password, and returns the user. Here's the implementation: public class LoginService : ILoginService { protected readonly IUserRespository _respository; public LoginService(IUserRespository respository) { _respository = respository; } public async Task Login(LoginDto dto) { var result = new TResult(); var user = await _respository.GetByEmail(dto.Email); if (user == null) { result.AddError($"User {dto.Email} not found!"); return result; } if (!user.IsPasswordMatch(dto.Password)) { result.AddError($"Password Incorrect!"); return result; } result.Success(user); return result; } } Time to configure the Cookie Authentication, move on to the Program.cs and add this configuration: builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.Cookie.Name = "CookieAuthSystem"; options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.Strict; options.ExpireTimeSpan = TimeSpan.FromMinutes(5); options.SlidingExpiration = true; options.LoginPath = "/auth/login"; options.AccessDeniedPath = "/auth/access-denied"; // Hanlder the unauthorized access options.Events.OnRedirectToLogin = context => { context.Response.StatusCode = 401; return Task.CompletedTask; }; }) Some configuration explained: Cookie.HttpOnly = true >> Ensures that the authentication cookie cannot be accessed by client-side JavaScript, helping to prevent XSS (Cross-Site Scripting) attacks. Cookie.SecurePolicy = CookieSecurePolicy.Always >> Cook

Jun 8, 2025 - 23:50
 0
ASP.NET 8 - Cookie Authentication

What is Cookie ?

In web context Cookies are objects that store user information to help identify, track, and personalize the user experience based on their actions on a website.

How it's works ?

When you login in a website the server that process your request, if the login is successfuly, creates a cookie with an ID, user data and some informations such as the cookie type, expiration time, refresh policy and so on. Once the brownser receive the cookie it becomes responsible to storing and send the cookie in every request to the server which will check the cookies properties on each request.

How to implement in .NET8 ?

For this tutorial you will need to create a ASPNET CORE Web API and install the following packages:



I made a simple entity to represent User with Name, Email, Password, and Roles. You can check it in this link.

For the table mapping, use this configuration:

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.HasKey(x => x.Id);
        builder.HasIndex(x => x.Email).IsUnique();

        builder.Property(x => x.Name).HasMaxLength(100).IsRequired();
        builder.Property(x => x.Email).HasMaxLength(100).IsRequired();
        builder.Property(x => x.Password).HasMaxLength(250).IsRequired();
    }
}

I set the Email property as an index and marked it as unique to ensure that different users cannot register with the same email address. This also allows it to be used efficiently as a filter parameter during login.

In your DbContext, make sure to configure the UserRoles relationship like this:

public class AppDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Role> Roles { get; set; }

    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<User>()
            .HasMany(u => u.Roles)
            .WithMany(r => r.Users)
            .UsingEntity<Dictionary<string, object>>(
                "UserRoles",
                j => j.HasOne<Role>().WithMany().HasForeignKey("RoleId"),
                j => j.HasOne<User>().WithMany().HasForeignKey("UserId"),
                j =>
                {
                    j.HasKey("UserId", "RoleId");
                    j.ToTable("UserRoles");
                });
    }
}

On the respository we only need to search user by his email:

public class UserRepository : IUserRespository
{
    private readonly AppDbContext _context;
    private readonly DbSet<User> _users;

    public UserRepository(AppDbContext context)
    {
        _context = context;
        _users = _context.Set<User>();
    }

    public async Task<User?> GetByEmail(string email)
    {
        return await _users
            .Where(x => x.Email == email)
            .Include(x => x.Roles)
            .FirstOrDefaultAsync();
    }
}

Now we can focus on the login service, which is pretty simple: it just receives the user input, searches for the user by email in the database, validates the password, and returns the user. Here's the implementation:

public class LoginService : ILoginService
{
    protected readonly IUserRespository _respository;

    public LoginService(IUserRespository respository)
    {
        _respository = respository;
    }

    public async Task<TResult<User>> Login(LoginDto dto)
    {
        var result = new TResult<User>();
        var user = await _respository.GetByEmail(dto.Email);

        if (user == null)
        {
            result.AddError($"User {dto.Email} not found!");
            return result;
        }

        if (!user.IsPasswordMatch(dto.Password))
        {
            result.AddError($"Password Incorrect!");
            return result;
        }

        result.Success(user);
        return result;
    }
}

Time to configure the Cookie Authentication, move on to the Program.cs and add this configuration:

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.Name = "CookieAuthSystem";
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.SameSite = SameSiteMode.Strict;
        options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
        options.SlidingExpiration = true;
        options.LoginPath = "/auth/login";
        options.AccessDeniedPath = "/auth/access-denied";
        // Hanlder the unauthorized access
        options.Events.OnRedirectToLogin = context =>
        {
            context.Response.StatusCode = 401;
            return Task.CompletedTask;
        };
    })

Some configuration explained:

Cookie.HttpOnly = true >> Ensures that the authentication cookie cannot be accessed by client-side JavaScript, helping to prevent XSS (Cross-Site Scripting) attacks.

Cookie.SecurePolicy = CookieSecurePolicy.Always >> Cookie will only send by HTTPS request.

Cookie.SameSite = SameSiteMode.Strict >> Prevent the cookies from being sent with cross-origin requests, helping to prevent CSRF (Cross-Site Request Forgery) attacks.

ExpireTimeSpan = TimeSpan.FromMinutes(5) >> Set the Cookie lifetime for 5 minutes.

SlidingExpiration = true >> Requests a new cookie if a request is made when the current cookie is halfway to expiration. This improves the user experience by preventing repeated logins during continuous site usage.

Once we configure the Cookie Authentication, let's implements the the service to generate cookie:

public class CookieService : ICookieService
{

    public CookieDto GenerateCookie(User user)
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, user.Name),
            new Claim(ClaimTypes.Email, user.Email),
        };

        foreach (var role in user.Roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role.Name));
        }

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

        var authProperties = new AuthenticationProperties
        {
            IsPersistent = true,
            AllowRefresh = true,
            ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(5),
        };

        return new CookieDto(claimsIdentity, authProperties);
    }

In Claims Identity we provide the user name, email and his roles and define the authentication type which is cookie-based authentication.

In Cookie propertites we set has the persistent, allow request new cookie and define the time expiration.

Now lets move on to the auth controller:

[ApiController]
[Route("auth")]
public class AuthController : Controller
{
    protected readonly ILoginService _loginService;
    protected readonly ICookieService _cookieService;

    public AuthController(ILoginService loginService, ICookieService cookieService)
    {
        _loginService = loginService;
        _cookieService = cookieService;
    }

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginDto request)
    {
        var userResult = await _loginService.Login(request);

        // Check if login is successful
        if (!userResult.IsSuccess || userResult.Value == null)
        {
            return BadRequest(userResult.ErrorMessage);
        }

        var cookie = _cookieService.GenerateCookie(userResult.Value);

        await HttpContext.SignInAsync(
            CookieAuthenticationDefaults.AuthenticationScheme,
            new ClaimsPrincipal(cookie.Claims),
            cookie.Properties);

        return LocalRedirect("/auth/logged");
    }

    [HttpPost("logout")]
    public async Task<IActionResult> Logout()
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        return Ok("User Logout Successful");
    }

    [HttpGet("access-denied")]
    public IActionResult NotAllowed()
    {
        return Unauthorized("You not allowed to access this resource");
    }

    [Authorize]
    [HttpGet("logged")]
    public IActionResult Logged()
    {
        var userName = User.FindFirst(ClaimTypes.Name)?.Value;

        return Ok($"Hello {userName}");
    }
}

Login: Creates a cookie with user information and redirect the user to the /logged page which contains the cookie stored in the brownser.

Logout: Turns the Cookie into invalid and clear from the brownser.

Access Denied: Every time someone try to access a resource which is not allowed, they are redirected to this path.

Protected Endpoints:

[ApiController]
[Route("api")]
public class ApiController : Controller
{

    [Authorize(Roles = "User")]
    [HttpGet("user")]
    public ActionResult UserController()
    {
        return Ok("Hello User");
    }

    [Authorize(Roles = "Manager")]
    [HttpGet("manager")]
    public ActionResult ManagerController()
    {
        return Ok("Hello Manager");
    }

    [Authorize(Roles = "Admin")]
    [HttpGet("admin")]
    public ActionResult AdminController()
    {
        return Ok("Hello Admin");
    }
}

Running the Project

On swagger when you login successful, the user will receive this:
Image description

You can check the cookie in the browser, open the browser inspect, goes to application and then cookies, you will able to see something like this:
Image description

But if you try to access the cookie using javascript in the browser console with the following:

document.cookie

You probably will receive a empty string, because the cookie configuration we made in the program.cs, Cookie.HttpOnly = true

If the user try to access a resource which who is not allowed them will be redirect to this path:

Image description

In this tutorial you noticited we configure cookie to works in https environment, but i run in http, it because browsers treat localhost as a special case.

Github Link