.NET Academy

Authentication and authorization in ASP.NET Core

Protect endpoints with JWT bearer tokens or cookies, ASP.NET Core Identity, roles and policies.

13 min read

You will learn

  • Configure authentication schemes (JWT bearer, cookies)
  • Protect endpoints with RequireAuthorization and [Authorize]
  • Write role-, claim- and policy-based authorization rules
  • Read the current user's claims

Concepts first

You learned the theory in Track 7: authentication establishes who the user is (sessions with cookies, or bearer tokens like JWTs; possibly via OpenID Connect), authorization decides what they may do. ASP.NET Core implements both as middleware plus a few attributes and extension methods.

Authentication: schemes

An authentication scheme knows how to read identity from a request. Register one (or several) and the middleware:

// An API accepting JWT access tokens (from your own issuer or an identity provider)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://login.example.com";   // issuer: keys are discovered automatically
        options.Audience = "shop-api";                      // tokens must be meant for this API
    });
 
builder.Services.AddAuthorization();
 
app.UseAuthentication();
app.UseAuthorization();

On every request, the JWT handler validates the token's signature, issuer, audience and expiry, and turns its claims into HttpContext.User.

For server-rendered websites, use cookies (AddCookie()), usually together with ASP.NET Core Identity — a complete user system (registration, password hashing, lockout, 2FA, email confirmation) stored via EF Core:

builder.Services.AddIdentity<AppUser, IdentityRole>()
    .AddEntityFrameworkStores<ShopDb>();

Tip

For APIs used by SPAs and mobile apps, prefer an external identity provider (Microsoft Entra ID, Auth0, Keycloak, Duende IdentityServer) issuing tokens via OpenID Connect. Your API then only validates tokens — it never handles passwords.

Protecting endpoints

app.MapGet("/products", GetProducts);                           // public
app.MapPost("/orders", PlaceOrder).RequireAuthorization();      // any signed-in user
app.MapDelete("/products/{id}", DeleteProduct)
   .RequireAuthorization(p => p.RequireRole("Admin"));          // inline rule
 
var admin = app.MapGroup("/admin").RequireAuthorization("AdminOnly");   // whole group

With controllers:

[Authorize]                                   // the whole controller requires sign-in
public class OrdersController : ControllerBase
{
    [AllowAnonymous] [HttpGet("public-stats")] public IActionResult Stats() => ...;
    [Authorize(Roles = "Admin")] [HttpDelete("{id}")] public IActionResult Delete(int id) => ...;
}

Outcomes: no valid identity → 401 (challenge); identity but rule fails → 403 (forbid).

Policies

Define rules once, by name:

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("AdminOnly", p => p.RequireRole("Admin"))
    .AddPolicy("CanManageCatalog", p => p.RequireAssertion(ctx =>
        ctx.User.IsInRole("Admin") || ctx.User.HasClaim("department", "catalog")))
    .AddPolicy("Adult", p => p.Requirements.Add(new MinimumAgeRequirement(18)));   // custom handler

Custom requirements are implemented by an AuthorizationHandler<TRequirement> — perfect for rules like "minimum age" or "subscription active".

Resource-based authorization

"Can edit this order?" depends on the order. Check it in the handler with IAuthorizationService:

app.MapPut("/orders/{id}", async (int id, ClaimsPrincipal user, IAuthorizationService auth, ShopDb db) =>
{
    var order = await db.Orders.FindAsync(id);
    if (order is null) return Results.NotFound();
 
    var result = await auth.AuthorizeAsync(user, order, "IsOrderOwner");
    return result.Succeeded ? Results.Ok(/* update */) : Results.Forbid();
}).RequireAuthorization();

Reading the current user

app.MapGet("/me", (ClaimsPrincipal user) => new
{
    Id = user.FindFirstValue(ClaimTypes.NameIdentifier),   // the "sub" claim
    Name = user.Identity?.Name,
    IsAdmin = user.IsInRole("Admin"),
    Claims = user.Claims.Select(c => new { c.Type, c.Value }),
}).RequireAuthorization();

To see what a token contains, paste it into the JWT decoder from Track 7 — those claims are exactly what ends up in ClaimsPrincipal.

Development tips

  • dotnet user-jwts create --role Admin generates a valid local JWT for testing protected endpoints.
  • Keep signing keys and client secrets in user secrets / a vault, never in appsettings.json.

Key takeaways

  • Register authentication schemes (AddJwtBearer, AddCookie, Identity, OIDC) plus UseAuthentication/UseAuthorization.
  • Protect endpoints with RequireAuthorization() / [Authorize]; open specific ones with [AllowAnonymous].
  • Centralize rules in named policies; use resource-based checks for per-object permissions.
  • The user is a ClaimsPrincipal (HttpContext.User) built from the token's or cookie's claims.

Quiz

4 questions · 3 correct to pass
  1. 1.An endpoint has RequireAuthorization() and the request has no token. What's the response?

  2. 2.Where does ASP.NET Core put the authenticated user's identity?

  3. 3.What's the advantage of named policies over checking roles inline everywhere?

  4. 4.What does AddJwtBearer validate on each request by default?

To complete this lesson

  • ○ Pass the quiz (3/4 correct)