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:
1. Cookie Authentication
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: EngineeringClearanceLevel: 3CanApproveExpenses: trueRegion: 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
- Official Microsoft Documentation: ASP.NET Core Security - Comprehensive security guidance
- OWASP Top 10: https://owasp.org/www-project-top-ten/ - Essential web security vulnerabilities
- 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.