# 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 November 3, 2026

## 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:

```csharp
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:

```csharp
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:

```csharp
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:

```csharp
[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:

```csharp
[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:

```csharp
// 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:

```cshtml
@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

- [Cookie Authentication and \[Authorize\]](https://www.ask-codey.com/cheat-sheets/security-and-sessions-cookie-authentication-and-authorize-cheat-sheet)

Source: https://www.ask-codey.com/lessons/security-and-sessions-cookie-authentication-and-authorize
