Cookie Authentication

Cookie sign-in for a server-rendered app: the Secure, HttpOnly and SameSite flags, sliding expiry, and when a cookie beats a token.

Syntaxbuilder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options => { ... })

What cookie authentication does

A cookie is a small value the browser stores for one site and sends back on every request to it. With cookie authentication the application signs the user in once, writes their claims into an encrypted cookie, and the browser carries it from then on. No code of yours has to attach it.

That is the difference from a bearer token. The browser sends cookies by itself, while a token has to be stored by your JavaScript and added to each request by hand. For a server-rendered site, or a single-page app served from the same site, a cookie is less code and less to get wrong.

The three flags that matter

  • HttpOnly: page scripts cannot read the cookie, so a cross-site scripting bug cannot steal the session.
  • Secure: the browser only sends it over HTTPS. Set CookieSecurePolicy.Always.
  • SameSite: Lax sends the cookie on normal navigation to your site but not on a cross-site POST, which stops most request forgery.

Expiry

ExpireTimeSpan is how long a session lasts. With SlidingExpiration true, continued use reissues the cookie with a fresh window, so an active user is not signed out mid-task. AddCookie() with no name is shorthand for AddCookie(CookieAuthenticationDefaults.AuthenticationScheme), the string "Cookies".

Against the program below, curl -ik -X POST https://localhost:5001/login returns 200 with a Set-Cookie header; sending that cookie to /me returns ada. Without it, /me returns a 302 redirect to the login path, which defaults to /Account/Login and is why an API wants a bearer scheme instead.

Example

Example · csharp
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.HttpOnly = true;                           // scripts cannot read it
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;   // HTTPS only
        options.Cookie.SameSite = SameSiteMode.Lax;                // not sent cross-site
        options.ExpireTimeSpan = TimeSpan.FromHours(8);
        options.SlidingExpiration = true;
    });
builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

app.MapPost("/login", async (HttpContext http) =>
{
    // A real endpoint checks the submitted password before this point.
    var claims = new List<Claim> { new(ClaimTypes.Name, "ada") };
    var identity = new ClaimsIdentity(
        claims, CookieAuthenticationDefaults.AuthenticationScheme);

    // Writes the encrypted cookie into the response.
    await http.SignInAsync(new ClaimsPrincipal(identity));
    return Results.Ok();
});

app.MapGet("/me", (ClaimsPrincipal user) =>
    user.Identity?.Name ?? "(no name)").RequireAuthorization();

app.Run();

When to use it

  • Sign users in to a server-rendered site or a single-page app hosted on the same origin.
  • Choose between a cookie and a bearer token for a new browser-facing application.
  • Keep an active user signed in for a working day without forcing a re-login mid-form.
  • Fix a deployment where users are signed out at random because instances reject each other's cookies.

More examples

Sign out, and a session that survives the browser closing

The "remember me" checkbox, and the sign-out that pairs with it. ExpiresUtc here overrides ExpireTimeSpan for this one sign-in, and sliding expiry does not apply to a cookie given an explicit absolute expiry.

Example · csharp
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

// Program.cs (excerpt)

// IsPersistent writes an expiry into the cookie itself, so it survives the
// browser closing. Without it the cookie lasts for the browser session.
app.MapPost("/login-remembered", async (HttpContext http) =>
{
    ClaimsPrincipal user = BuildPrincipal("ada");

    await http.SignInAsync(
        CookieAuthenticationDefaults.AuthenticationScheme,
        user,
        new AuthenticationProperties
        {
            IsPersistent = true,
            ExpiresUtc = DateTimeOffset.UtcNow.AddDays(14)
        });

    return Results.Ok();
});

// Sign-out deletes the cookie. There is nothing else to clean up, because
// the cookie was the whole session.
app.MapPost("/logout", async (HttpContext http) =>
{
    await http.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    return Results.Ok();
});

Throwing away a session for a disabled user

The cookie answer to the revocation problem that bearer tokens have. It costs a lookup per request, so most applications cache the active flag or only check it when more than a minute has passed since the cookie was issued.

Example · csharp
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

// Program.cs (excerpt). The cookie carries its own claims, so a user who
// is disabled keeps working until it expires. This event runs on every
// request and can reject the session.
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Events = new CookieAuthenticationEvents
        {
            OnValidatePrincipal = async context =>
            {
                var users = context.HttpContext.RequestServices
                    .GetRequiredService<UserStore>();

                string? id = context.Principal?
                    .FindFirst(ClaimTypes.NameIdentifier)?.Value;

                if (id is null || !await users.IsActiveAsync(id))
                {
                    context.RejectPrincipal();
                    await context.HttpContext.SignOutAsync(
                        CookieAuthenticationDefaults.AuthenticationScheme);
                }
            }
        };
    });

Cookies and bearer tokens in one service

Reach for this when one deployment serves both pages and a machine-facing API. Naming the scheme on the policy matters: without AddAuthenticationSchemes the API endpoint falls back to the cookie scheme, so the caller never gets the WWW-Authenticate: Bearer challenge it is waiting for. Since .NET 10 a minimal API endpoint that writes a JSON response is recognised as an API endpoint, so the cookie handler answers it with 401 rather than the 302 login redirect it sent in earlier versions.

Example · csharp
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;

// Program.cs (excerpt). The web pages use the cookie; /api is called by
// other services with a bearer token. Both schemes are registered, and
// each endpoint names the one it accepts.
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie()
    .AddJwtBearer(options =>
    {
        options.Authority = "https://login.example.com";
        options.Audience = "orders-api";
    });

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ServiceCallers", policy =>
    {
        policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
        policy.RequireAuthenticatedUser();
    });

// Uses the default scheme, the cookie: a browser is redirected to sign in.
app.MapGet("/orders", () => "the orders page").RequireAuthorization();

// Bearer only: a missing token gives 401 rather than a redirect.
app.MapGet("/api/orders", () => new[] { "A-1", "A-2" })
   .RequireAuthorization("ServiceCallers");

Discussion

  • Be the first to comment on this lesson.