ASP.NET Core MVC · Security

Authentication

Authentication answers “who are you?”. Before reaching for the full Identity system, it is worth building a login once with plain cookie authentication — you will understand exactly what Identity does for you later.

Advanced 8 min Lesson 30 of 42Examples target .NET 9.
0/42 in ASP.NET Core MVC
On this page · 8 min · Advanced

In simple words#

Authentication is the security guard checking your ID card at the gate: “Who are you?” Once checked, you get a visitor pass (cookie).

Authentication
Proving who you are.
Cookie
Pass sent with each request after login.
UseAuthentication
Middleware that reads the pass.

Learning objectives#

  • Configure cookie authentication in Program.cs
  • Sign users in with claims and sign them out
  • Protect actions with [Authorize] and handle ReturnUrl
  • Set safe cookie and expiry options

Explanation#

AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(…) registers the scheme. UseAuthentication() then reads the auth cookie on every request and fills HttpContext.User.

A signed-in user is a ClaimsPrincipal: a set of claims such as Name, Email and Role. HttpContext.SignInAsync(principal) encrypts them into the cookie; SignOutAsync deletes it.

[Authorize] on a controller or action requires a signed-in user. Anonymous users are redirected to LoginPath with ?ReturnUrl=… so they come back after logging in — always redirect with LocalRedirect or Url.IsLocalUrl to avoid open redirects.

Never store plain passwords. Verify against a hash — PasswordHasher<T> (PBKDF2) is built into ASP.NET Core. Give the same error for unknown users and wrong passwords.

Cookie options: HttpOnly (JavaScript cannot read it), SecurePolicy Always (HTTPS only), SameSite Lax, ExpireTimeSpan with SlidingExpiration, and AccessDeniedPath for signed-in users without permission.

Syntax#

C# Illustrative
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(o => { o.LoginPath = "/Account/Login"; o.ExpireTimeSpan = TimeSpan.FromHours(8); });
…
app.UseAuthentication();
app.UseAuthorization();


await HttpContext.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme)));

Code example: Login, a protected page and logout#

C#Controllers/AccountController.cs Illustrative
// Program.cs
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authentication.Cookies;


var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();                                   // keep the demo output clean
builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<UserStore>();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Account/Login";
        options.ExpireTimeSpan = TimeSpan.FromHours(8);
        options.SlidingExpiration = true;
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;   // demo runs on http; use Always in production
    });


var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.Urls.Add("http://127.0.0.1:5117");
await app.StartAsync();


using var http = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false }) { BaseAddress = new Uri("http://127.0.0.1:5117") };
async Task<HttpResponseMessage> Login(string email, string password)
{
    var form = await http.GetStringAsync("/Account/Login");
    var token = Regex.Match(form, "__RequestVerificationToken\" type=\"hidden\" value=\"([^\"]+)").Groups[1].Value;
    return await http.PostAsync("/Account/Login?ReturnUrl=%2FOrders", new FormUrlEncodedContent(
        [new("Email", email), new("Password", password), new("__RequestVerificationToken", token)]));
}
void Show(string step, HttpResponseMessage r, string body = "") =>
    Console.WriteLine(string.Join(' ', new[] { step.PadRight(24), 
quot;
{(int)r.StatusCode}", r.Headers.Location?.ToString(), body }
.Where(part => !string.IsNullOrEmpty(part)))); var anonymous = await http.GetAsync("/Orders"); Show("GET /Orders (anonymous)", anonymous); var wrong = await Login("asha@example.com", "guess"); Show("POST login (wrong)", wrong, Regex.Match(await wrong.Content.ReadAsStringAsync(), "<p class=\"error\">([^<]+)").Groups[1].Value); var ok = await Login("asha@example.com", "Pa55word!"); var cookie = ok.Headers.GetValues("Set-Cookie").First(); Show("POST login (correct)", ok, cookie.Contains("httponly") ? "(cookie: httponly)" : ""); var orders = await http.GetAsync("/Orders"); Show("GET /Orders (signed in)", orders, await orders.Content.ReadAsStringAsync()); Show("POST logout", await http.PostAsync("/Account/Logout", null)); Show("GET /Orders (after)", await http.GetAsync("/Orders")); await app.StopAsync(); // Services/UserStore.cs using Microsoft.AspNetCore.Identity; public record AppUser(string Email, string Name, string Role) { public string PasswordHash { get; set; } = ""; } public class UserStore { private readonly PasswordHasher<AppUser> _hasher = new(); private readonly Dictionary<string, AppUser> _users = new(StringComparer.OrdinalIgnoreCase); public UserStore() { var asha = new AppUser("asha@example.com", "Asha", "Customer"); asha.PasswordHash = _hasher.HashPassword(asha, "Pa55word!"); // store hashes, never passwords _users[asha.Email] = asha; } public AppUser? Validate(string email, string password) => _users.TryGetValue(email, out var user) && _hasher.VerifyHashedPassword(user, user.PasswordHash, password) != PasswordVerificationResult.Failed ? user : null; } // Controllers/AccountController.cs using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Mvc; public class AccountController(UserStore users) : Controller { [HttpGet] public IActionResult Login(string? returnUrl = null) => View(); [HttpPost, ValidateAntiForgeryToken] public async Task<IActionResult> Login(string email, string password, string? returnUrl = null) { var user = users.Validate(email, password); if (user is null) { ViewData["Error"] = "Invalid email or password."; // same message for both cases return View(); } Claim[] claims = [new(ClaimTypes.Name, user.Name), new(ClaimTypes.Email, user.Email), new(ClaimTypes.Role, user.Role)]; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity)); return LocalRedirect(Url.IsLocalUrl(returnUrl) ? returnUrl! : "/"); } [HttpPost] public async Task<IActionResult> Logout() { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return RedirectToAction("Index", "Home"); } } // Controllers/OrdersController.cs using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; [Authorize] // signed-in users only public class OrdersController : Controller { public IActionResult Index() => Content(
quot;Orders for
{User.Identity!.Name} ({User.FindFirstValue(ClaimTypes.Role)})");
} @* Views/Account/Login.cshtml *@ <form method="post"> @if (ViewData["Error"] is string error) { <p class="error">@error</p> } <input name="Email" /> <input name="Password" type="password" /> <button>Sign in</button> </form>
Expected output
GET /Orders (anonymous)  302 http://127.0.0.1:5117/Account/Login?ReturnUrl=%2FOrders
POST login (wrong)       200 Invalid email or password.
POST login (correct)     302 /Orders (cookie: httponly)
GET /Orders (signed in)  200 Orders for Asha (Customer)
POST logout              302 /
GET /Orders (after)      302 http://127.0.0.1:5117/Account/Login?ReturnUrl=%2FOrders

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. An anonymous request to /Orders is redirected (302) to the login page with ReturnUrl.
  2. A wrong password re-renders the form with a generic message — no hint whether the email exists.
  3. The correct password builds a ClaimsPrincipal and signs in; the response sets an HttpOnly auth cookie and redirects to the ReturnUrl.
  4. With the cookie, [Authorize] lets the request through and User.Identity.Name and the Role claim are available.
  5. Logout deletes the cookie, so /Orders redirects to login again.

Real-world use case#

ShopSmart’s first version used exactly this for a single admin account. When customer accounts arrived (registration, password reset, lockout), the team moved to ASP.NET Core Identity — next lesson.

Common mistakes#

Storing or comparing plain passwords

Hash with PasswordHasher (or Identity). Never write passwords to the database or logs.

Redirect(returnUrl)

An attacker can send /Account/Login?ReturnUrl=https://evil.example. Use LocalRedirect or Url.IsLocalUrl.

UseAuthorization without UseAuthentication

HttpContext.User stays anonymous and every [Authorize] request is redirected to login.

Best practices#

  • HttpOnly, Secure (Always), SameSite cookies
  • Generic login error messages
  • Hash passwords; consider Identity for real user accounts
  • Logout via POST with antiforgery

Remember this#

AddAuthentication().AddCookie() + UseAuthentication() + UseAuthorization().

SignInAsync(principal with claims) / SignOutAsync().

[Authorize] → anonymous users go to LoginPath?ReturnUrl=…

Practice exercise#

Your task: Add an AccessDeniedPath "/Account/Denied" and an action that shows “You do not have access to this page.”, then protect an AdminController with [Authorize(Roles = "Admin")].

Need a hint?
options.AccessDeniedPath in AddCookie; a Denied() action returning Content(...).
Show solution
C# Illustrative
// Program.cs
using Microsoft.AspNetCore.Authentication.Cookies;


var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(o =>
    {
        o.LoginPath = "/Account/Login";
        o.AccessDeniedPath = "/Account/Denied";
    });
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.Run();


// Controllers/AccountController.cs
using Microsoft.AspNetCore.Mvc;


public class AccountController : Controller
{
    public IActionResult Denied() => Content("You do not have access to this page.");
}


// Controllers/AdminController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;


[Authorize(Roles = "Admin")]
public class AdminController : Controller
{
    public IActionResult Index() => Content("Admin dashboard");
}

Mini quiz#

Authentication quizMultiple choice Question 1 of 3

What does HttpContext.SignInAsync(principal) do with cookie authentication?

Score so far: 0/0

Want more? Practise all ASP.NET Core MVC MCQs · ASP.NET Core MVC interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.