Authentication and Authorisation
Who you are versus what you may do: claims, identities and principals, and the two middleware calls that must run in order.
app.UseAuthentication(); app.UseAuthorization(); // always in this orderTwo different questions
Authentication answers one question: who is making this request. Authorisation answers a different one: is that person allowed to do this. They are separate steps and they fail differently. A request with no usable credential gets status 401. A request from a known user who is not permitted gets status 403.
ASP.NET Core describes a user with three small types. A Claim is one statement about them, such as name is ada. A ClaimsIdentity is a set of claims plus the authentication type that produced them. A ClaimsPrincipal holds one or more identities, and that is what HttpContext.User hands you on every request.
The two middleware calls
Middleware is one step in the chain every request passes through. app.UseAuthentication() reads the incoming credential and sets HttpContext.User. app.UseAuthorization() then measures that user against whatever the endpoint demands. The second cannot do its job before the first has run, so the order is fixed.
A scheme is a named way of authenticating, such as "Cookies" or "Bearer". Passing a name to AddAuthentication is shorthand for setting the default scheme by hand. These two lines mean the same thing:
builder.Services.AddAuthentication("Bearer");
builder.Services.AddAuthentication(options => options.DefaultScheme = "Bearer");
What trips people up
HttpContext.User is never null. An anonymous request still gets a principal; it simply carries no claims and reports IsAuthenticated as false. An identity built with a null authentication type also reports false, even when it does carry claims.
Example
using System.Security.Claims;
// A claim is one statement about a user: a type and a value.
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, "u-1042"),
new(ClaimTypes.Name, "ada"),
new(ClaimTypes.Role, "editor"),
new("department", "payroll")
};
// An identity is a set of claims plus how they were proved. Passing an
// authentication type is what makes the identity count as authenticated.
var identity = new ClaimsIdentity(claims, authenticationType: "demo");
// A principal wraps one or more identities. HttpContext.User is one of these.
var user = new ClaimsPrincipal(identity);
Console.WriteLine($"authenticated: {user.Identity?.IsAuthenticated}");
Console.WriteLine($"name: {user.Identity?.Name}");
Console.WriteLine($"id: {user.FindFirst(ClaimTypes.NameIdentifier)?.Value}");
Console.WriteLine($"editor: {user.IsInRole("editor")}");
Console.WriteLine($"admin: {user.IsInRole("admin")}");
Console.WriteLine($"department: {user.FindFirst("department")?.Value}");
// Output:
// authenticated: True
// name: ada
// id: u-1042
// editor: True
// admin: False
// department: payrollWhen to use it
- Work out whether a failing request needs a 401 or a 403 before writing the fix.
- Put the authentication and authorisation middleware in the right place in a new service.
- Read the signed-in user's id inside an endpoint without passing it in the request body.
- Debug a sign-in that succeeds while every protected endpoint still rejects the caller.
More examples
The two calls in a real pipeline
The smallest program that has both steps. GET /open always returns "anyone". GET /closed with no cookie is redirected to the login path instead of returning the body. Move UseAuthorization above UseAuthentication and /closed rejects every request, including ones with a valid cookie.
using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
builder.Services.AddAuthorization();
var app = builder.Build();
// Reads the cookie and sets HttpContext.User. Must come first.
app.UseAuthentication();
// Checks HttpContext.User against what the endpoint demands.
app.UseAuthorization();
app.MapGet("/open", () => "anyone");
app.MapGet("/closed", () => "signed-in only").RequireAuthorization();
app.Run();Reading the current user inside an endpoint
Two ways to reach the same object. Both endpoints are unprotected on purpose, which is why every property is read with a null-conditional: on an anonymous request the principal exists but has no claims, so name and id come back as null rather than throwing.
using System.Security.Claims;
// Asking for a ClaimsPrincipal parameter is shorthand: the framework
// passes HttpContext.User. The longhand asks for the context itself.
app.MapGet("/whoami", (ClaimsPrincipal user) => new
{
authenticated = user.Identity?.IsAuthenticated ?? false,
name = user.Identity?.Name,
id = user.FindFirst(ClaimTypes.NameIdentifier)?.Value
});
app.MapGet("/whoami-long", (HttpContext http) =>
{
ClaimsPrincipal user = http.User;
return new
{
authenticated = user.Identity?.IsAuthenticated ?? false,
name = user.Identity?.Name,
id = user.FindFirst(ClaimTypes.NameIdentifier)?.Value
};
});Two identities on one principal
Useful when a sign-in looks like it worked but an authorisation check still fails. Printing every identity shows which scheme produced which claims, and whether the one you expected ended up as the primary identity that IsAuthenticated and Name read from.
using System.Security.Claims;
namespace Diagnostics;
public static class PrincipalReport
{
// A principal can hold several identities, for example when a service
// accepts both a cookie and a client certificate on the same request.
public static string Describe(ClaimsPrincipal principal)
{
var lines = new List<string>();
foreach (ClaimsIdentity identity in principal.Identities)
{
lines.Add($"{identity.AuthenticationType ?? "(none)"}: " +
$"{identity.Claims.Count()} claims");
}
// Identity returns the first identity, which is the primary one.
lines.Add($"primary: {principal.Identity?.AuthenticationType ?? "(none)"}");
return string.Join("\n", lines);
}
}
Discussion