Cookie Authentication and [Authorize]

Turn on cookie authentication in Program.cs, sign users in with claims and SignInAsync, protect actions with [Authorize] and roles, and show who's signed in with User.

Journeyman · ASP.NET Core · C# · Updated

After this lesson you can

  • Turn on cookie authentication, and sign a user in and out with SignInAsync and SignOutAsync
  • Protect actions with [Authorize] and [Authorize(Roles = "...")], and read the signed-in user with User

Key takeaways

  • AddAuthentication(...).AddCookie(...) turns on cookie authentication, and app.UseAuthentication() comes before app.UseAuthorization().
  • To sign a user in, put their name and role in claims and call HttpContext.SignInAsync; SignOutAsync deletes the cookie.
  • Mark an action [Authorize] for signed-in users, or [Authorize(Roles = "Admin")] for one role, on the POST action too: hiding a link isn't protection.
  • User.Identity.Name is the signed-in user's name, and User.IsInRole("Admin") checks a role, in a controller or a view.
  • Redirect to returnUrl only if Url.IsLocalUrl says it's on this site, and never say whether the user name or the password was wrong.

This lesson adds sign-in to NorthwindWeb: anyone can browse products, but only an administrator can add one.

1. Turn it on

In Program.cs, register cookie authentication with the services:

using Microsoft.AspNetCore.Authentication.Cookies;

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Account/Login";
        options.AccessDeniedPath = "/Account/Denied";
    });

and add it to the pipeline, before authorization:

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();
  • UseAuthentication reads the sign-in cookie on every request, and works out who the user is.
  • UseAuthorization checks [Authorize] rules, using what authentication found.
  • LoginPath is where someone who isn't signed in is sent; AccessDeniedPath is where a signed-in user without permission is sent.

2. Check the password

The users and their password hashes would live in a database table. To keep this lesson about sign-in, NorthwindWeb keeps two users in memory, in a UserService registered as a singleton. It hashes each password with ASP.NET Core's PasswordHasher, and checks a password like this:

public AppUser? SignIn(string userName, string password)
{
    AppUser? user = users.Find(u => u.UserName == userName);
    if (user is null)
    {
        return null;
    }

    PasswordVerificationResult result = hasher.VerifyHashedPassword(user, user.PasswordHash, password);
    return result == PasswordVerificationResult.Failed ? null : user;
}

3. Sign in

The AccountController's POST action checks the password, then signs the user in:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(string userName, string password, string? returnUrl)
{
    AppUser? user = userService.SignIn(userName, password);
    if (user is null)
    {
        ViewData["ReturnUrl"] = returnUrl;
        ViewData["Error"] = "That user name and password don't match.";
        return View();
    }

    List<Claim> claims = new List<Claim>
    {
        new Claim(ClaimTypes.Name, user.UserName),
        new Claim(ClaimTypes.Role, user.Role)
    };
    ClaimsIdentity identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));

    if (Url.IsLocalUrl(returnUrl))
    {
        return Redirect(returnUrl);
    }
    return RedirectToAction("Index", "Home");
}
  • A claim is one fact about the user: their name, their role. The claims go into the cookie.
  • SignInAsync writes the cookie. From the next request on, the user is signed in.
  • async and await: signing in is asynchronous, so the action returns a Task<IActionResult> and waits for it with await. (Tasks, async and await, in the HTTP and Web APIs chapter, explains async properly.)
  • returnUrl: when [Authorize] sends someone to sign in, it adds where they were going, such as /Account/Login?ReturnUrl=%2Fproducts%2Fnew. After signing in, they're sent back there. Url.IsLocalUrl makes sure it's an address on this site, so a crafted link can't send them somewhere else.
  • The error message doesn't say whether the user name or the password was wrong. That would help someone guessing.

Signing out deletes the cookie:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
    await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    return RedirectToAction("Index", "Home");
}

4. Protect actions

[Authorize] on an action (or a whole controller) lets only signed-in users in. Add Roles to narrow it further:

// GET /products/new
[Authorize(Roles = "Admin")]
[HttpGet("new")]
public IActionResult Create() { ... }
Who asks for /products/new What happens
Nobody signed in Redirected to /Account/Login?ReturnUrl=%2Fproducts%2Fnew
grace, role Staff Redirected to /Account/Denied?ReturnUrl=%2Fproducts%2Fnew
ada, role Admin The form

Put the same [Authorize(Roles = "Admin")] on the POST action too. Hiding a link isn't protection: anyone can send a POST without your form. [AllowAnonymous] opens one action inside a controller marked [Authorize].

5. Who's signed in: User

In a controller or a view, User is the signed-in user, built from the cookie's claims:

@if (User.Identity?.IsAuthenticated == true)
{
    <form method="post" asp-controller="Account" asp-action="Logout" class="d-flex align-items-center gap-2">
        <span class="navbar-text">Hi, @User.Identity.Name</span>
        <button type="submit" class="btn btn-link nav-link">Sign out</button>
    </form>
}
else
{
    <a class="nav-link" asp-controller="Account" asp-action="Login">Sign in</a>
}

User.Identity.Name is the name claim, and User.IsInRole("Admin") checks a role, for showing the Add a Product link only to administrators. Sign-out is a POST form, with its anti-forgery token, so no other site can sign your users out with a link.

Check yourself

2 questions to try on the lesson page.

So what

Turn it on, check the password, sign in with claims, and mark actions [Authorize]: ASP.NET Core sends strangers to sign in, turns away users without the role, and tells every page who's there.

Key terms

  • Cookie Authentication
  • [Authorize]
  • Claims and ClaimsPrincipal
  • Authentication Middleware

Cheat sheets