You are viewing a preview of this lesson. Sign in to start learning
Back to ASP.NET with .NET 10

Security & Authentication

Secure your APIs with authentication, authorization, and JWT token-based security mechanisms

Security & Authentication in ASP.NET with .NET 10

Secure your ASP.NET applications with authentication, authorization, and data protection using free flashcards and spaced repetition practice. This lesson covers Identity framework configuration, JWT tokens, role-based access control, and secure API endpointsβ€”essential concepts for building production-ready web applications.

Welcome πŸ”

Security is not optional in modern web developmentβ€”it's foundational. Whether you're building a simple blog or a complex enterprise system, protecting user data and controlling access to resources is paramount. ASP.NET with .NET 10 provides robust, built-in security features that handle authentication (verifying who users are) and authorization (determining what they can do).

In this lesson, you'll learn how to implement authentication strategies from basic cookie-based sessions to modern JWT tokens, configure ASP.NET Core Identity for user management, implement role-based and policy-based authorization, protect your APIs, and secure sensitive data. By the end, you'll be able to build secure applications that protect both your users and your business.

πŸ’‘ Tip: Security is a journey, not a destination. Stay updated with the latest vulnerabilities and best practices through regular security audits.

Core Concepts 🎯

Authentication vs Authorization

These two terms are often confused, but they serve distinct purposes:

Aspect Authentication πŸͺͺ Authorization 🚦
Definition Verifying identity ("Who are you?") Granting permissions ("What can you do?")
Process Login with credentials Checking roles/claims/policies
ASP.NET Middleware UseAuthentication() UseAuthorization()
Failure Result 401 Unauthorized 403 Forbidden
Example User logs in with username/password Admin can delete posts, regular users cannot

🧠 Memory Device: Think of a concert: Authentication is showing your ticket at the entrance (proving you're allowed in), Authorization is the VIP pass that determines whether you can access backstage (what you can do once inside).

ASP.NET Core Identity Framework

ASP.NET Core Identity is a membership system that provides complete user management functionality. It handles:

  • User registration and login
  • Password hashing and validation
  • Two-factor authentication (2FA)
  • Account lockout after failed attempts
  • Email confirmation
  • Password recovery
  • External login providers (Google, Facebook, etc.)

Key Identity Classes:

Class Purpose Common Methods
UserManager<TUser> Manages users CreateAsync(), FindByEmailAsync(), CheckPasswordAsync()
SignInManager<TUser> Handles sign-in operations PasswordSignInAsync(), SignOutAsync()
RoleManager<TRole> Manages roles CreateAsync(), RoleExistsAsync()
IdentityUser Default user entity Properties: UserName, Email, PasswordHash

Authentication Schemes πŸ”‘

.NET 10 supports multiple authentication schemes that can be used individually or combined:

Traditional session-based authentication for web applications.

How it works:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Client  β”‚                           β”‚  Server  β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜                           β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
     β”‚                                      β”‚
     β”‚  1. POST /login (credentials)        β”‚
     │─────────────────────────────────────→│
     β”‚                                      β”‚
     β”‚                    2. Validate       β”‚
     β”‚                       credentials βœ“  β”‚
     β”‚                                      β”‚
     β”‚  3. Set-Cookie: .AspNetCore.Cookies  β”‚
     │←─────────────────────────────────────│
     β”‚                                      β”‚
     β”‚  4. GET /profile (Cookie: ...)       β”‚
     │─────────────────────────────────────→│
     β”‚                                      β”‚
     β”‚                    5. Read cookie,   β”‚
     β”‚                       identify user  β”‚
     β”‚                                      β”‚
     β”‚  6. Response with user data          β”‚
     │←─────────────────────────────────────│
     β”‚                                      β”‚

Best for: Traditional web applications with server-side rendering (Razor Pages, MVC).

2. JWT (JSON Web Token) Authentication

Stateless token-based authentication for APIs and SPAs.

JWT Structure:

header.payload.signature

Header (Algorithm & Token Type):
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload (Claims):
{
  "sub": "user123",
  "name": "John Doe",
  "role": "Admin",
  "exp": 1735689600
}

Signature:
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret_key
)

JWT Flow:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Client  β”‚                           β”‚ API      β”‚
β”‚  (SPA)   β”‚                           β”‚ Server   β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜                           β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
     β”‚                                      β”‚
     β”‚  1. POST /api/auth/login             β”‚
     │─────────────────────────────────────→│
     β”‚                                      β”‚
     β”‚  2. JWT Token                        β”‚
     │←─────────────────────────────────────│
     β”‚                                      β”‚
     β”‚  3. Store token (localStorage/       β”‚
     β”‚     memory/sessionStorage)           β”‚
     β”‚                                      β”‚
     β”‚  4. GET /api/data                    β”‚
     β”‚     Authorization: Bearer     β”‚
     │─────────────────────────────────────→│
     β”‚                                      β”‚
     β”‚                    5. Validate token β”‚
     β”‚                       signature βœ“    β”‚
     β”‚                                      β”‚
     β”‚  6. Protected data                   β”‚
     │←─────────────────────────────────────│
     β”‚                                      β”‚

Best for: RESTful APIs, mobile apps, SPAs (React, Angular, Vue).

πŸ€” Did you know? JWT tokens are not encrypted by defaultβ€”they're only encoded (Base64). Anyone can decode and read the payload! Never store sensitive data like passwords in JWT claims. Use signatures to ensure integrity, not confidentiality.

3. OAuth 2.0 & OpenID Connect

Delegated authorization for third-party login ("Login with Google").

Common Providers:

  • πŸ”΅ Microsoft Account
  • πŸ”΄ Google
  • 🟦 Facebook
  • πŸ™ GitHub
  • πŸŸͺ Twitter/X

Authorization Strategies 🚦

Role-Based Authorization

Simple permission model based on user roles.

Example Hierarchy:

        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚  SuperAdmin  β”‚  (All permissions)
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”
        β”‚    Admin     β”‚  (Manage users, content)
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”
        β”‚   Moderator  β”‚  (Edit content, ban users)
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”
        β”‚     User     β”‚  (View, comment, post)
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Claims-Based Authorization

Fine-grained permissions based on claims (key-value pairs).

Example Claims:

  • Department: Engineering
  • ClearanceLevel: 3
  • CanApproveExpenses: true
  • Region: US-West

Policy-Based Authorization

Complex business logic combining roles, claims, and custom requirements.

Policy Evaluation Flow:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  User requests protected        β”‚
β”‚  resource                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Check Policy Requirements      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  βœ“ Must have role "Manager"     β”‚
β”‚  βœ“ Department claim = "Sales"   β”‚
β”‚  βœ“ Account age > 90 days        β”‚
β”‚  βœ“ Email verified               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
    β”‚             β”‚
    β–Ό             β–Ό
  βœ… All         ❌ Any
  requirements   requirement
  met           fails
    β”‚             β”‚
    β–Ό             β–Ό
  Allow         Deny
  access        (403)

Data Protection API πŸ”’

ASP.NET Core's Data Protection API provides cryptographic services for:

  • Encrypting cookies
  • Protecting authentication tokens
  • Generating secure random values
  • Time-limited data protection (expiring tokens)

Key Concepts:

Component Purpose
IDataProtector Encrypts/decrypts data with purpose strings
IDataProtectionProvider Creates protectors with specific purposes
Purpose Strings Namespace data protection (e.g., "UserTokens.PasswordReset")
Key Ring Stores encryption keys (rotate periodically for security)

πŸ’‘ Tip: Purpose strings isolate encrypted data. Data protected with purpose "A" cannot be decrypted using purpose "B", even with the same key.

Detailed Examples πŸ’»

Example 1: Setting Up ASP.NET Core Identity

Let's configure a complete Identity system with a SQL Server database.

Step 1: Install NuGet Packages

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

Step 2: Create User and DbContext

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

// Custom user class (extend IdentityUser for additional properties)
public class ApplicationUser : IdentityUser
{
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public DateTime DateOfBirth { get; set; }
    public string? ProfilePictureUrl { get; set; }
}

// Database context
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        
        // Customize Identity tables (optional)
        builder.Entity<ApplicationUser>(entity =>
        {
            entity.Property(u => u.FirstName).HasMaxLength(100).IsRequired();
            entity.Property(u => u.LastName).HasMaxLength(100).IsRequired();
        });
    }
}

Step 3: Configure Services in Program.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add DbContext with SQL Server
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection")
    ));

// Configure Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
    // Password settings
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequiredLength = 8;
    
    // Lockout settings
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.AllowedForNewUsers = true;
    
    // User settings
    options.User.RequireUniqueEmail = true;
    options.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

// Configure cookie authentication
builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Strict;
    options.ExpireTimeSpan = TimeSpan.FromDays(14);
    options.SlidingExpiration = true;
    options.LoginPath = "/Account/Login";
    options.LogoutPath = "/Account/Logout";
    options.AccessDeniedPath = "/Account/AccessDenied";
});

builder.Services.AddControllersWithViews();

var app = builder.Build();

// Middleware order is critical!
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();  // Must come before UseAuthorization
app.UseAuthorization();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Step 4: Implement Registration and Login

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

public class AccountController : Controller
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;

    public AccountController(
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager)
    {
        _userManager = userManager;
        _signInManager = signInManager;
    }

    [HttpPost]
    public async Task<IActionResult> Register(RegisterViewModel model)
    {
        if (!ModelState.IsValid)
            return View(model);

        var user = new ApplicationUser
        {
            UserName = model.Email,
            Email = model.Email,
            FirstName = model.FirstName,
            LastName = model.LastName,
            DateOfBirth = model.DateOfBirth
        };

        var result = await _userManager.CreateAsync(user, model.Password);

        if (result.Succeeded)
        {
            // Generate email confirmation token
            var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            var confirmationLink = Url.Action(
                "ConfirmEmail", "Account",
                new { userId = user.Id, token },
                protocol: HttpContext.Request.Scheme);

            // Send email (implementation not shown)
            // await _emailService.SendConfirmationEmail(user.Email, confirmationLink);

            return RedirectToAction("RegistrationSuccess");
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }

        return View(model);
    }

    [HttpPost]
    public async Task<IActionResult> Login(LoginViewModel model)
    {
        if (!ModelState.IsValid)
            return View(model);

        var result = await _signInManager.PasswordSignInAsync(
            model.Email,
            model.Password,
            model.RememberMe,
            lockoutOnFailure: true);

        if (result.Succeeded)
        {
            return RedirectToAction("Index", "Home");
        }

        if (result.IsLockedOut)
        {
            ModelState.AddModelError(string.Empty, 
                "Account locked due to multiple failed login attempts.");
        }
        else
        {
            ModelState.AddModelError(string.Empty, 
                "Invalid login attempt.");
        }

        return View(model);
    }

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

Why this works: Identity handles all the complex security detailsβ€”password hashing (using PBKDF2), salt generation, timing-safe comparisons, and secure token generation. You get enterprise-grade security without implementing cryptography yourself.

Example 2: JWT Authentication for APIs

Let's create a JWT-based authentication system for a RESTful API.

Step 1: Install JWT Package

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Step 2: Configure JWT in Program.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// JWT Configuration from appsettings.json
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
var secretKey = Encoding.UTF8.GetBytes(jwtSettings["SecretKey"]!);

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = jwtSettings["Issuer"],
        ValidAudience = jwtSettings["Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(secretKey),
        ClockSkew = TimeSpan.Zero // Remove default 5-minute tolerance
    };

    // Handle token from query string (for SignalR, downloads, etc.)
    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            var accessToken = context.Request.Query["access_token"];
            if (!string.IsNullOrEmpty(accessToken))
            {
                context.Token = accessToken;
            }
            return Task.CompletedTask;
        }
    };
});

builder.Services.AddAuthorization();
builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

appsettings.json:

{
  "JwtSettings": {
    "SecretKey": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!",
    "Issuer": "https://yourapi.com",
    "Audience": "https://yourapp.com",
    "ExpirationMinutes": 60
  }
}

Step 3: Create JWT Token Service

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

public interface ITokenService
{
    string GenerateToken(ApplicationUser user, IList<string> roles);
    ClaimsPrincipal? ValidateToken(string token);
}

public class TokenService : ITokenService
{
    private readonly IConfiguration _configuration;

    public TokenService(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public string GenerateToken(ApplicationUser user, IList<string> roles)
    {
        var jwtSettings = _configuration.GetSection("JwtSettings");
        var secretKey = Encoding.UTF8.GetBytes(jwtSettings["SecretKey"]!);

        var claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.Id),
            new Claim(JwtRegisteredClaimNames.Email, user.Email!),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim("firstName", user.FirstName),
            new Claim("lastName", user.LastName)
        };

        // Add roles as claims
        foreach (var role in roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role));
        }

        var key = new SymmetricSecurityKey(secretKey);
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var expiration = DateTime.UtcNow.AddMinutes(
            int.Parse(jwtSettings["ExpirationMinutes"]!));

        var token = new JwtSecurityToken(
            issuer: jwtSettings["Issuer"],
            audience: jwtSettings["Audience"],
            claims: claims,
            expires: expiration,
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public ClaimsPrincipal? ValidateToken(string token)
    {
        var jwtSettings = _configuration.GetSection("JwtSettings");
        var secretKey = Encoding.UTF8.GetBytes(jwtSettings["SecretKey"]!);

        var tokenHandler = new JwtSecurityTokenHandler();
        try
        {
            var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = jwtSettings["Issuer"],
                ValidAudience = jwtSettings["Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(secretKey)
            }, out SecurityToken validatedToken);

            return principal;
        }
        catch
        {
            return null;
        }
    }
}

Step 4: API Authentication Controller

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly ITokenService _tokenService;

    public AuthController(
        UserManager<ApplicationUser> userManager,
        ITokenService tokenService)
    {
        _userManager = userManager;
        _tokenService = tokenService;
    }

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginRequest request)
    {
        var user = await _userManager.FindByEmailAsync(request.Email);
        if (user == null)
            return Unauthorized(new { message = "Invalid credentials" });

        var isPasswordValid = await _userManager.CheckPasswordAsync(
            user, request.Password);
        if (!isPasswordValid)
            return Unauthorized(new { message = "Invalid credentials" });

        var roles = await _userManager.GetRolesAsync(user);
        var token = _tokenService.GenerateToken(user, roles);

        return Ok(new
        {
            token,
            expiration = DateTime.UtcNow.AddMinutes(60),
            user = new
            {
                user.Email,
                user.FirstName,
                user.LastName
            }
        });
    }

    [HttpPost("refresh")]
    public IActionResult RefreshToken([FromBody] RefreshTokenRequest request)
    {
        var principal = _tokenService.ValidateToken(request.ExpiredToken);
        if (principal == null)
            return Unauthorized();

        // Implement refresh token logic here
        // (store refresh tokens in database, validate, issue new access token)
        
        return Ok(new { token = "new_token_here" });
    }
}

public record LoginRequest(string Email, string Password);
public record RefreshTokenRequest(string ExpiredToken, string RefreshToken);

Step 5: Protect API Endpoints

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
[Authorize] // Require authentication for all actions
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll()
    {
        // Anyone authenticated can access
        return Ok(new[] { "Product1", "Product2" });
    }

    [HttpPost]
    [Authorize(Roles = "Admin,Manager")] // Require specific roles
    public IActionResult Create([FromBody] Product product)
    {
        // Only Admins or Managers can create
        return CreatedAtAction(nameof(GetById), new { id = 1 }, product);
    }

    [HttpGet("{id}")]
    [AllowAnonymous] // Override controller-level [Authorize]
    public IActionResult GetById(int id)
    {
        // Public endpoint
        return Ok(new Product { Id = id, Name = "Sample" });
    }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

Client-side usage (JavaScript):

// Login and store token
const response = await fetch('https://api.example.com/api/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'user@example.com', password: 'Password123!' })
});

const { token } = await response.json();
localStorage.setItem('jwt', token);

// Make authenticated request
const productsResponse = await fetch('https://api.example.com/api/products', {
  headers: {
    'Authorization': `Bearer ${localStorage.getItem('jwt')}`
  }
});

const products = await productsResponse.json();

⚠️ Security Note: Store JWT tokens securely! localStorage is vulnerable to XSS attacks. Consider using httpOnly cookies or in-memory storage for sensitive applications.

Example 3: Policy-Based Authorization

Create complex authorization rules using policies.

Step 1: Define Custom Requirements

using Microsoft.AspNetCore.Authorization;

// Requirement: User must be at least a certain age
public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

// Requirement: User must belong to specific department
public class DepartmentRequirement : IAuthorizationRequirement
{
    public string[] AllowedDepartments { get; }

    public DepartmentRequirement(params string[] departments)
    {
        AllowedDepartments = departments;
    }
}

// Requirement: User account must be active for minimum days
public class AccountAgeRequirement : IAuthorizationRequirement
{
    public int MinimumDays { get; }

    public AccountAgeRequirement(int minimumDays)
    {
        MinimumDays = minimumDays;
    }
}

Step 2: Implement Authorization Handlers

using Microsoft.AspNetCore.Authorization;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst(
            c => c.Type == "DateOfBirth");

        if (dateOfBirthClaim == null)
            return Task.CompletedTask;

        if (DateTime.TryParse(dateOfBirthClaim.Value, out var dateOfBirth))
        {
            var age = DateTime.Today.Year - dateOfBirth.Year;
            if (dateOfBirth.Date > DateTime.Today.AddYears(-age))
                age--;

            if (age >= requirement.MinimumAge)
            {
                context.Succeed(requirement);
            }
        }

        return Task.CompletedTask;
    }
}

public class DepartmentHandler : AuthorizationHandler<DepartmentRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        DepartmentRequirement requirement)
    {
        var departmentClaim = context.User.FindFirst(
            c => c.Type == "Department");

        if (departmentClaim != null &&
            requirement.AllowedDepartments.Contains(departmentClaim.Value))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

public class AccountAgeHandler : AuthorizationHandler<AccountAgeRequirement>
{
    private readonly UserManager<ApplicationUser> _userManager;

    public AccountAgeHandler(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        AccountAgeRequirement requirement)
    {
        var userId = context.User.FindFirst(
            c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;

        if (userId == null)
            return;

        var user = await _userManager.FindByIdAsync(userId);
        if (user == null)
            return;

        var accountAge = (DateTime.UtcNow - user.LockoutEnd?.UtcDateTime ?? DateTime.UtcNow).Days;
        
        // Note: Using LockoutEnd as proxy for account creation date
        // In production, add a CreatedDate property to ApplicationUser
        
        if (accountAge >= requirement.MinimumDays)
        {
            context.Succeed(requirement);
        }
    }
}

Step 3: Register Policies in Program.cs

builder.Services.AddAuthorization(options =>
{
    // Simple age policy
    options.AddPolicy("AtLeast18", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));

    // Department-based policy
    options.AddPolicy("EngineeringOnly", policy =>
        policy.Requirements.Add(new DepartmentRequirement("Engineering", "IT")));

    // Complex combined policy
    options.AddPolicy("SeniorEngineer", policy =>
    {
        policy.Requirements.Add(new MinimumAgeRequirement(25));
        policy.Requirements.Add(new DepartmentRequirement("Engineering"));
        policy.Requirements.Add(new AccountAgeRequirement(365));
        policy.RequireRole("Engineer", "SeniorEngineer");
    });

    // Policy with custom logic
    options.AddPolicy("CanDeletePosts", policy =>
        policy.RequireAssertion(context =>
            context.User.IsInRole("Admin") ||
            (context.User.IsInRole("Moderator") &&
             context.User.HasClaim("CanDelete", "true"))));
});

// Register handlers
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, DepartmentHandler>();
builder.Services.AddScoped<IAuthorizationHandler, AccountAgeHandler>();

Step 4: Apply Policies to Controllers

[Authorize(Policy = "SeniorEngineer")]
public class ArchitectureController : Controller
{
    [HttpGet]
    public IActionResult DesignReview()
    {
        // Only senior engineers with 1+ year account age can access
        return View();
    }
}

[Authorize]
public class ContentController : Controller
{
    private readonly IAuthorizationService _authorizationService;

    public ContentController(IAuthorizationService authorizationService)
    {
        _authorizationService = authorizationService;
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        // Manual policy check with resource
        var post = GetPostById(id); // Your data retrieval method
        
        var authResult = await _authorizationService.AuthorizeAsync(
            User, post, "CanDeletePosts");

        if (!authResult.Succeeded)
            return Forbid();

        // Perform deletion
        return NoContent();
    }

    private object GetPostById(int id) => new { Id = id }; // Placeholder
}

Resource-based authorization (checking ownership):

public class DocumentAuthorizationHandler :
    AuthorizationHandler<SameAuthorRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        SameAuthorRequirement requirement,
        Document resource)
    {
        if (context.User.Identity?.Name == resource.Author)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

public class SameAuthorRequirement : IAuthorizationRequirement { }

public class Document
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Author { get; set; } = string.Empty;
}

Example 4: Securing APIs with CORS and Rate Limiting

Protect your API from abuse and configure cross-origin access.

Step 1: Configure CORS

var builder = WebApplication.CreateBuilder(args);

// Define CORS policies
builder.Services.AddCors(options =>
{
    // Development policy (permissive)
    options.AddPolicy("DevelopmentPolicy", policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyMethod()
              .AllowAnyHeader();
    });

    // Production policy (restrictive)
    options.AddPolicy("ProductionPolicy", policy =>
    {
        policy.WithOrigins(
                "https://yourdomain.com",
                "https://app.yourdomain.com")
              .WithMethods("GET", "POST", "PUT", "DELETE")
              .WithHeaders("Authorization", "Content-Type")
              .AllowCredentials()
              .SetPreflightMaxAge(TimeSpan.FromMinutes(10));
    });

    // SignalR-specific policy
    options.AddPolicy("SignalRPolicy", policy =>
    {
        policy.WithOrigins("https://yourdomain.com")
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials();
    });
});

var app = builder.Build();

// Use appropriate policy based on environment
if (app.Environment.IsDevelopment())
{
    app.UseCors("DevelopmentPolicy");
}
else
{
    app.UseCors("ProductionPolicy");
}

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

Step 2: Implement Rate Limiting (.NET 7+)

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(options =>
{
    // Fixed window: 100 requests per minute
    options.AddFixedWindowLimiter("fixed", limiterOptions =>
    {
        limiterOptions.PermitLimit = 100;
        limiterOptions.Window = TimeSpan.FromMinutes(1);
        limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        limiterOptions.QueueLimit = 10;
    });

    // Sliding window: smoother rate limiting
    options.AddSlidingWindowLimiter("sliding", limiterOptions =>
    {
        limiterOptions.PermitLimit = 100;
        limiterOptions.Window = TimeSpan.FromMinutes(1);
        limiterOptions.SegmentsPerWindow = 6; // 10-second segments
        limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        limiterOptions.QueueLimit = 10;
    });

    // Token bucket: allows bursts
    options.AddTokenBucketLimiter("token", limiterOptions =>
    {
        limiterOptions.TokenLimit = 100;
        limiterOptions.ReplenishmentPeriod = TimeSpan.FromMinutes(1);
        limiterOptions.TokensPerPeriod = 100;
        limiterOptions.QueueLimit = 10;
    });

    // Concurrency limiter: max concurrent requests
    options.AddConcurrencyLimiter("concurrent", limiterOptions =>
    {
        limiterOptions.PermitLimit = 50;
        limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        limiterOptions.QueueLimit = 20;
    });

    // Per-user rate limiting
    options.AddPolicy("perUser", context =>
    {
        var username = context.User.Identity?.Name ?? "anonymous";
        return RateLimitPartition.GetFixedWindowLimiter(username, _ => new()
        {
            PermitLimit = 20,
            Window = TimeSpan.FromMinutes(1)
        });
    });

    // Global rejection response
    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.StatusCode = 429;
        await context.HttpContext.Response.WriteAsync(
            "Too many requests. Please try again later.", token);
    };
});

var app = builder.Build();

app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

Step 3: Apply Rate Limiting to Endpoints

using Microsoft.AspNetCore.RateLimiting;

[ApiController]
[Route("api/[controller]")]
public class ApiController : ControllerBase
{
    [HttpGet("public")]
    [EnableRateLimiting("fixed")]
    public IActionResult GetPublicData()
    {
        return Ok(new { data = "Public information" });
    }

    [HttpPost("search")]
    [EnableRateLimiting("sliding")]
    public IActionResult Search([FromBody] SearchRequest request)
    {
        // Expensive search operation
        return Ok(new { results = new[] { "result1", "result2" } });
    }

    [HttpGet("user-data")]
    [Authorize]
    [EnableRateLimiting("perUser")]
    public IActionResult GetUserData()
    {
        return Ok(new { userId = User.Identity?.Name });
    }

    [HttpGet("no-limit")]
    [DisableRateLimiting]
    public IActionResult NoLimit()
    {
        return Ok(new { message = "No rate limiting on this endpoint" });
    }
}

public record SearchRequest(string Query);

Custom rate limit key (by IP address):

builder.Services.AddRateLimiter(options =>
{
    options.AddPolicy("perIp", context =>
    {
        var ipAddress = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        return RateLimitPartition.GetFixedWindowLimiter(ipAddress, _ => new()
        {
            PermitLimit = 50,
            Window = TimeSpan.FromMinutes(1)
        });
    });
});

Common Mistakes ⚠️

1. Incorrect Middleware Order

❌ Wrong:

app.UseAuthorization();
app.UseAuthentication(); // Too late!
app.MapControllers();

βœ… Correct:

app.UseAuthentication();  // Must come first
app.UseAuthorization();   // Then check permissions
app.MapControllers();

Why: Authentication must establish user identity before authorization can check permissions.

2. Storing Sensitive Data in JWT Payload

❌ Wrong:

var claims = new[]
{
    new Claim("password", user.Password),        // Never!
    new Claim("ssn", user.SocialSecurityNumber), // Never!
    new Claim("creditCard", user.CardNumber)     // Never!
};

βœ… Correct:

var claims = new[]
{
    new Claim(JwtRegisteredClaimNames.Sub, user.Id),
    new Claim(JwtRegisteredClaimNames.Email, user.Email),
    new Claim(ClaimTypes.Role, "User")
};

Why: JWT tokens are only Base64-encoded, not encrypted. Anyone can decode and read the payload.

3. Weak JWT Secrets

❌ Wrong:

var secretKey = Encoding.UTF8.GetBytes("secret"); // Too short!

βœ… Correct:

// At least 256 bits (32 characters) for HS256
var secretKey = Encoding.UTF8.GetBytes(
    "YourSuperSecretKeyThatIsAtLeast32CharactersLong!");

Why: Short keys are vulnerable to brute-force attacks. Use at least 256 bits for HMAC-SHA256.

4. Not Validating Token Expiration

❌ Wrong:

options.TokenValidationParameters = new TokenValidationParameters
{
    ValidateLifetime = false // Security risk!
};

βœ… Correct:

options.TokenValidationParameters = new TokenValidationParameters
{
    ValidateLifetime = true,
    ClockSkew = TimeSpan.Zero // Remove 5-minute tolerance
};

5. Missing HTTPS in Production

❌ Wrong:

options.Cookie.SecurePolicy = CookieSecurePolicy.None; // Allows HTTP

βœ… Correct:

options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // HTTPS only
options.Cookie.SameSite = SameSiteMode.Strict;          // CSRF protection

Why: Cookies sent over HTTP can be intercepted. Always use HTTPS in production.

6. Checking Authorization in Business Logic Instead of Controllers

❌ Wrong:

public class ProductService
{
    public void DeleteProduct(int id, string userId)
    {
        if (!IsAdmin(userId)) // Authorization in wrong layer
            throw new UnauthorizedException();
        
        // Delete logic
    }
}

βœ… Correct:

[Authorize(Roles = "Admin")]
public class ProductsController : Controller
{
    private readonly ProductService _service;

    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        _service.DeleteProduct(id); // No user check needed
        return NoContent();
    }
}

Why: Authorization is a cross-cutting concern that belongs in the presentation layer. Business logic should remain authorization-agnostic.

7. Not Implementing Token Refresh

❌ Wrong: Using only long-lived access tokens (e.g., 30 days)

βœ… Correct: Short-lived access tokens (15 minutes) + refresh tokens (30 days)

Why: If an access token is compromised, the damage window is limited. Refresh tokens can be revoked centrally.

8. Trusting Client-Side Role Checks

❌ Wrong:

// JavaScript - client can modify this!
if (user.role === 'admin') {
  showDeleteButton();
}

βœ… Correct:

// Client-side (UI only)
if (user.role === 'admin') {
  showDeleteButton();
}

// Server-side (actual protection)
[Authorize(Roles = "Admin")]
public IActionResult Delete(int id) { ... }

Why: Always enforce authorization on the server. Client-side checks are only for UX.

Key Takeaways 🎯

βœ… Authentication verifies identity; Authorization grants permissions

βœ… Use ASP.NET Core Identity for comprehensive user management with built-in security

βœ… Cookie authentication for traditional web apps; JWT for APIs and SPAs

βœ… JWT tokens are encoded, not encryptedβ€”never store sensitive data in them

βœ… Middleware order matters: UseAuthentication() must come before UseAuthorization()

βœ… Use role-based authorization for simple permissions, policy-based for complex rules

βœ… Always enforce HTTPS in production with SecurePolicy.Always

βœ… Implement rate limiting to prevent abuse and DDoS attacks

βœ… Use strong secrets (256+ bits) for JWT signing keys

βœ… Apply defense in depth: combine multiple security layers (authentication, authorization, CORS, rate limiting, HTTPS)

πŸ”” Remember: Security is not a feature you add at the endβ€”it's a foundation you build from day one. Every endpoint, every data access, every user action should be authenticated and authorized.

πŸ“š Further Study

  1. Official Microsoft Documentation: ASP.NET Core Security - Comprehensive security guidance
  2. OWASP Top 10: https://owasp.org/www-project-top-ten/ - Essential web security vulnerabilities
  3. JWT.io Debugger: https://jwt.io/ - Decode and verify JWT tokens

πŸ“‹ Quick Reference Card

Authentication Schemes Cookie (web apps), JWT (APIs), OAuth (social login)
Authorization Types Role-based, Claims-based, Policy-based, Resource-based
Identity Core Classes UserManager, SignInManager, RoleManager
JWT Structure header.payload.signature (Base64-encoded)
Middleware Order Routing β†’ Authentication β†’ Authorization β†’ Endpoints
Status Codes 401 = Unauthenticated, 403 = Forbidden (unauthorized)
Cookie Security HttpOnly, Secure, SameSite=Strict
JWT Best Practices Short expiration (15-60 min), strong secret (256+ bits)
Rate Limiting Types Fixed window, Sliding window, Token bucket, Concurrency
CORS Policy Restrict origins, methods, headers; use AllowCredentials wisely

πŸ” Security Mindset: Always assume breach. Design systems where compromising one layer doesn't compromise everything. Use defense in depth, principle of least privilege, and fail securely.

Practice Questions

Test your understanding with these questions:

Q1: What method adds JWT Bearer authentication in Program.cs? ```csharp builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; }) .{{1}}(); ```
A: AddJwtBearer
Q2: Complete the JWT token validation parameters: ```csharp options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, {{1}} = true, {{2}} = new SymmetricSecurityKey(secretKey) }; ```
A: ["ValidateIssuerSigningKey","IssuerSigningKey"]
Q3: What does this authorization attribute do? ```csharp [Authorize(Roles = "Admin,Manager")] public IActionResult DeleteProduct(int id) { // Implementation } ``` A. Allows only users with both Admin AND Manager roles B. Allows users with either Admin OR Manager role C. Denies access to Admin and Manager roles D. Requires authentication but ignores roles E. Creates new Admin and Manager roles
A: B
Q4: Fill in the Identity password configuration: ```csharp builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options => { options.Password.{{1}} = 8; options.Password.{{2}} = true; }); ```
A: ["RequiredLength","RequireDigit"]
Q5: What HTTP status code is returned when an authenticated user lacks permission? ```csharp [Authorize(Policy = "AdminOnly")] public IActionResult DeleteUser(string id) { // Regular user tries to access this return Ok(); } ``` A. 400 Bad Request B. 401 Unauthorized C. 403 Forbidden D. 404 Not Found E. 500 Internal Server Error
A: C