![ASP.NET Core Advanced Authorization: Policy-Based Security & Resource Protection Guide (Part-14 of 40) ASP.NET Core Advanced Authorization: Policy-Based Security & Resource Protection Guide (Part-14 of 40)]()
Previous Article: ASP.NET Core Identity Unleashed: Complete Guide to Registration, Roles, 2FA & Security
Master ASP.NET Core advanced authorization with policy-based security, resource protection, custom requirements, and real-world implementation examples for robust application security.
Table of Contents
Introduction to Advanced Authorization
Authorization vs Authentication
Policy-Based Authorization Fundamentals
Creating Custom Authorization Policies
Resource-Based Authorization
Custom Authorization Requirements & Handlers
Role-Based Authorization Deep Dive
Claims-Based Authorization
Permission-Based Authorization
Authorization in Razor Pages
Authorization in Web APIs
Advanced Scenarios & Patterns
Security Best Practices
Performance Considerations
Testing Authorization
Troubleshooting & Debugging
Real-World Implementation Example
Conclusion
1. Introduction to Advanced Authorization
Authorization is the process that determines what a user is allowed to do once they're authenticated. While basic authorization checks if a user is logged in, advanced authorization provides granular control over resources and operations.
Why Advanced Authorization Matters
In modern applications, simple role checks are insufficient. Consider these real-world scenarios:
Healthcare System: Doctors can view patient records, but only for their own patients
Banking Application: Users can transfer money, but only from their own accounts
E-commerce Platform: Sellers can edit product information, but only for their own products
Project Management: Team members can update tasks, but only those assigned to them
These scenarios require more sophisticated authorization than simple role-based checks.
2. Authorization vs Authentication
Authentication: Who Are You?
// Authentication confirms user identity
public async Task<IActionResult> Login(LoginModel model)
{
var result = await _signInManager.PasswordSignInAsync(
model.Email, model.Password, model.RememberMe, false);
if (result.Succeeded)
{
// User is authenticated
return RedirectToAction("Dashboard");
}
}
Authorization: What Can You Do?
// Authorization determines permissions
[Authorize(Roles = "Admin")]
public IActionResult ManageUsers()
{
// Only authenticated users in Admin role can access
return View();
}
3. Policy-Based Authorization Fundamentals
Policy-based authorization is the cornerstone of advanced security in ASP.NET Core. It provides a flexible, declarative way to define authorization rules.
Basic Policy Configuration
// Program.cs or Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
// Requirement: User must be authenticated
options.AddPolicy("Authenticated", policy =>
policy.RequireAuthenticatedUser());
// Requirement: User must be in Admin role
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
// Requirement: User must have specific claim
options.AddPolicy("Over18Only", policy =>
policy.RequireClaim("Age", "18", "19", "20")); // Values 18+
// Combined requirements
options.AddPolicy("SeniorEditor", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireRole("Editor");
policy.RequireClaim("ExperienceLevel", "Senior");
});
});
}
Using Policies in Controllers
[Authorize(Policy = "AdminOnly")]
public class AdminController : Controller
{
public IActionResult Dashboard() => View();
}
[Authorize(Policy = "Over18Only")]
public class AdultContentController : Controller
{
public IActionResult RestrictedContent() => View();
}
4. Creating Custom Authorization Policies
Complex Policy with Multiple Requirements
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy("PremiumUser", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("SubscriptionType", "Premium", "Enterprise");
policy.RequireClaim("AccountStatus", "Active");
policy.Requirements.Add(new MinimumSubscriptionDurationRequirement(30)); // 30 days
});
});
}
Policy with Custom Logic
public class MinimumSubscriptionDurationRequirement : IAuthorizationRequirement
{
public int MinimumDays { get; }
public MinimumSubscriptionDurationRequirement(int minimumDays)
{
MinimumDays = minimumDays;
}
}
public class MinimumSubscriptionDurationHandler
: AuthorizationHandler<MinimumSubscriptionDurationRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumSubscriptionDurationRequirement requirement)
{
if (!context.User.HasClaim(c => c.Type == "SubscriptionStartDate"))
{
return Task.CompletedTask;
}
var subscriptionStartDate = DateTime.Parse(
context.User.FindFirst(c => c.Type == "SubscriptionStartDate").Value);
var subscriptionDuration = DateTime.Now - subscriptionStartDate;
if (subscriptionDuration.Days >= requirement.MinimumDays)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// Registration
services.AddScoped<IAuthorizationHandler, MinimumSubscriptionDurationHandler>();
5. Resource-Based Authorization
Resource-based authorization evaluates permissions based on both the user and the specific resource being accessed.
Real-World Example: Document Management System
// Document entity
public class Document
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public string AuthorId { get; set; } // User who created the document
public DocumentStatus Status { get; set; }
public DateTime CreatedDate { get; set; }
}
public enum DocumentStatus
{
Draft,
Published,
Archived
}
Resource Authorization Requirement
public class DocumentAuthorizationRequirement : IAuthorizationRequirement
{
public string Operation { get; }
public DocumentAuthorizationRequirement(string operation)
{
Operation = operation;
}
}
public class DocumentAuthorizationHandler
: AuthorizationHandler<DocumentAuthorizationRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DocumentAuthorizationRequirement requirement,
Document resource)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
switch (requirement.Operation)
{
case "Read":
if (resource.Status == DocumentStatus.Published ||
resource.AuthorId == userId ||
context.User.IsInRole("Admin"))
{
context.Succeed(requirement);
}
break;
case "Edit":
if (resource.AuthorId == userId &&
resource.Status != DocumentStatus.Archived ||
context.User.IsInRole("Admin"))
{
context.Succeed(requirement);
}
break;
case "Delete":
if (resource.AuthorId == userId ||
context.User.IsInRole("Admin"))
{
context.Succeed(requirement);
}
break;
case "Publish":
if ((resource.AuthorId == userId &&
context.User.HasClaim("CanPublish", "true")) ||
context.User.IsInRole("Admin"))
{
context.Succeed(requirement);
}
break;
}
return Task.CompletedTask;
}
}
Using Resource Authorization in Controllers
public class DocumentsController : Controller
{
private readonly DocumentService _documentService;
private readonly IAuthorizationService _authorizationService;
public DocumentsController(DocumentService documentService,
IAuthorizationService authorizationService)
{
_documentService = documentService;
_authorizationService = authorizationService;
}
[HttpGet]
public async Task<IActionResult> Edit(int id)
{
var document = await _documentService.GetDocumentAsync(id);
if (document == null)
return NotFound();
var authorizationResult = await _authorizationService.AuthorizeAsync(
User, document, new DocumentAuthorizationRequirement("Edit"));
if (!authorizationResult.Succeeded)
{
return Forbid();
}
return View(document);
}
[HttpPost]
public async Task<IActionResult> Delete(int id)
{
var document = await _documentService.GetDocumentAsync(id);
if (document == null)
return NotFound();
var authorizationResult = await _authorizationService.AuthorizeAsync(
User, document, new DocumentAuthorizationRequirement("Delete"));
if (!authorizationResult.Succeeded)
{
return Forbid();
}
await _documentService.DeleteDocumentAsync(id);
return RedirectToAction("Index");
}
}
6. Custom Authorization Requirements & Handlers
Complex Business Rule: Department-Based Authorization
// Requirement: User must be in the same department as the resource
public class SameDepartmentRequirement : IAuthorizationRequirement
{
}
public class SameDepartmentHandler : AuthorizationHandler<SameDepartmentRequirement, DepartmentResource>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
SameDepartmentRequirement requirement,
DepartmentResource resource)
{
var userDepartment = context.User.FindFirst("Department")?.Value;
if (userDepartment != null &&
userDepartment.Equals(resource.Department, StringComparison.OrdinalIgnoreCase))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Time-Based Authorization
// Requirement: Access only during business hours
public class BusinessHoursRequirement : IAuthorizationRequirement
{
public TimeSpan StartTime { get; }
public TimeSpan EndTime { get; }
public BusinessHoursRequirement(TimeSpan startTime, TimeSpan endTime)
{
StartTime = startTime;
EndTime = endTime;
}
}
public class BusinessHoursHandler : AuthorizationHandler<BusinessHoursRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
BusinessHoursRequirement requirement)
{
var currentTime = DateTime.Now.TimeOfDay;
if (currentTime >= requirement.StartTime &&
currentTime <= requirement.EndTime)
{
context.Succeed(requirement);
}
else
{
context.Fail(new AuthorizationFailureReason(
this, "Access allowed only during business hours"));
}
return Task.CompletedTask;
}
}
Geographic Location Authorization
// Requirement: User must be accessing from specific countries
public class GeographicRequirement : IAuthorizationRequirement
{
public string[] AllowedCountries { get; }
public GeographicRequirement(params string[] allowedCountries)
{
AllowedCountries = allowedCountries;
}
}
public class GeographicHandler : AuthorizationHandler<GeographicRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IGeoLocationService _geoLocationService;
public GeographicHandler(IHttpContextAccessor httpContextAccessor,
IGeoLocationService geoLocationService)
{
_httpContextAccessor = httpContextAccessor;
_geoLocationService = geoLocationService;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
GeographicRequirement requirement)
{
var httpContext = _httpContextAccessor.HttpContext;
var ipAddress = httpContext.Connection.RemoteIpAddress?.ToString();
if (string.IsNullOrEmpty(ipAddress))
{
context.Fail();
return;
}
var country = await _geoLocationService.GetCountryFromIpAsync(ipAddress);
if (requirement.AllowedCountries.Contains(country, StringComparer.OrdinalIgnoreCase))
{
context.Succeed(requirement);
}
else
{
context.Fail(new AuthorizationFailureReason(
this, $"Access not allowed from {country}"));
}
}
}
7. Role-Based Authorization Deep Dive
Advanced Role Management
// Custom role service for complex role hierarchies
public interface IRoleService
{
Task<bool> UserIsInRoleAsync(string userId, string role);
Task<IEnumerable<string>> GetUserRolesAsync(string userId);
Task<bool> UserHasPermissionAsync(string userId, string permission);
}
public class RoleService : IRoleService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
public RoleService(UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager)
{
_userManager = userManager;
_roleManager = roleManager;
}
public async Task<bool> UserIsInRoleAsync(string userId, string role)
{
var user = await _userManager.FindByIdAsync(userId);
return await _userManager.IsInRoleAsync(user, role);
}
public async Task<IEnumerable<string>> GetUserRolesAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
return await _userManager.GetRolesAsync(user);
}
public async Task<bool> UserHasPermissionAsync(string userId, string permission)
{
var userRoles = await GetUserRolesAsync(userId);
foreach (var roleName in userRoles)
{
var role = await _roleManager.FindByNameAsync(roleName);
var claims = await _roleManager.GetClaimsAsync(role);
if (claims.Any(c => c.Type == "Permission" && c.Value == permission))
{
return true;
}
}
return false;
}
}
Hierarchical Role System
// Custom requirement for hierarchical roles
public class MinimumRoleRequirement : IAuthorizationRequirement
{
public string MinimumRole { get; }
private static readonly Dictionary<string, int> RoleHierarchy = new()
{
["User"] = 1,
["Moderator"] = 2,
["Admin"] = 3,
["SuperAdmin"] = 4
};
public MinimumRoleRequirement(string minimumRole)
{
MinimumRole = minimumRole;
}
public bool IsRoleSufficient(string userRole)
{
if (!RoleHierarchy.ContainsKey(userRole) ||
!RoleHierarchy.ContainsKey(MinimumRole))
return false;
return RoleHierarchy[userRole] >= RoleHierarchy[MinimumRole];
}
}
public class MinimumRoleHandler : AuthorizationHandler<MinimumRoleRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumRoleRequirement requirement)
{
var userRoles = context.User.Claims
.Where(c => c.Type == ClaimTypes.Role)
.Select(c => c.Value);
foreach (var role in userRoles)
{
if (requirement.IsRoleSufficient(role))
{
context.Succeed(requirement);
return Task.CompletedTask;
}
}
context.Fail();
return Task.CompletedTask;
}
}
8. Claims-Based Authorization
Advanced Claims Transformation
// Custom claims transformer
public class AdvancedClaimsTransformer : IClaimsTransformation
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IPermissionService _permissionService;
public AdvancedClaimsTransformer(UserManager<ApplicationUser> userManager,
IPermissionService permissionService)
{
_userManager = userManager;
_permissionService = permissionService;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = principal.Identity as ClaimsIdentity;
if (identity == null || !identity.IsAuthenticated)
return principal;
// Add additional claims based on business logic
var userId = identity.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
return principal;
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
return principal;
// Add subscription claims
if (user.SubscriptionExpiryDate > DateTime.UtcNow)
{
identity.AddClaim(new Claim("Subscription", "Active"));
identity.AddClaim(new Claim("SubscriptionType", user.SubscriptionType));
}
// Add permission claims
var permissions = await _permissionService.GetUserPermissionsAsync(userId);
foreach (var permission in permissions)
{
identity.AddClaim(new Claim("Permission", permission));
}
// Add department claims
if (!string.IsNullOrEmpty(user.Department))
{
identity.AddClaim(new Claim("Department", user.Department));
}
return principal;
}
}
Dynamic Claims Evaluation
// Custom requirement for dynamic claims
public class DynamicClaimRequirement : IAuthorizationRequirement
{
public string ClaimType { get; }
public Func<string, bool> ValueEvaluator { get; }
public DynamicClaimRequirement(string claimType, Func<string, bool> valueEvaluator)
{
ClaimType = claimType;
ValueEvaluator = valueEvaluator;
}
}
public class DynamicClaimHandler : AuthorizationHandler<DynamicClaimRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DynamicClaimRequirement requirement)
{
var claims = context.User.FindAll(requirement.ClaimType);
foreach (var claim in claims)
{
if (requirement.ValueEvaluator(claim.Value))
{
context.Succeed(requirement);
return Task.CompletedTask;
}
}
context.Fail();
return Task.CompletedTask;
}
}
9. Permission-Based Authorization
Permission Service Implementation
public interface IPermissionService
{
Task<bool> HasPermissionAsync(string userId, string permission);
Task<IEnumerable<string>> GetUserPermissionsAsync(string userId);
Task AssignPermissionAsync(string userId, string permission);
Task RemovePermissionAsync(string userId, string permission);
}
public class PermissionService : IPermissionService
{
private readonly ApplicationDbContext _context;
public PermissionService(ApplicationDbContext context)
{
_context = context;
}
public async Task<bool> HasPermissionAsync(string userId, string permission)
{
var userPermissions = await GetUserPermissionsAsync(userId);
return userPermissions.Contains(permission, StringComparer.OrdinalIgnoreCase);
}
public async Task<IEnumerable<string>> GetUserPermissionsAsync(string userId)
{
// Get direct user permissions
var directPermissions = await _context.UserPermissions
.Where(up => up.UserId == userId && up.IsActive)
.Select(up => up.Permission.Name)
.ToListAsync();
// Get role-based permissions
var rolePermissions = await _context.UserRoles
.Where(ur => ur.UserId == userId)
.Join(_context.RolePermissions,
ur => ur.RoleId,
rp => rp.RoleId,
(ur, rp) => rp.Permission.Name)
.ToListAsync();
return directPermissions.Union(rolePermissions).Distinct();
}
public async Task AssignPermissionAsync(string userId, string permission)
{
var permissionEntity = await _context.Permissions
.FirstOrDefaultAsync(p => p.Name == permission);
if (permissionEntity == null)
{
permissionEntity = new Permission { Name = permission };
_context.Permissions.Add(permissionEntity);
}
var userPermission = new UserPermission
{
UserId = userId,
PermissionId = permissionEntity.Id,
IsActive = true,
GrantedDate = DateTime.UtcNow
};
_context.UserPermissions.Add(userPermission);
await _context.SaveChangesAsync();
}
public async Task RemovePermissionAsync(string userId, string permission)
{
var userPermission = await _context.UserPermissions
.Include(up => up.Permission)
.FirstOrDefaultAsync(up => up.UserId == userId && up.Permission.Name == permission);
if (userPermission != null)
{
userPermission.IsActive = false;
userPermission.RevokedDate = DateTime.UtcNow;
await _context.SaveChangesAsync();
}
}
}
Permission-Based Authorization Handler
public class PermissionRequirement : IAuthorizationRequirement
{
public string Permission { get; }
public PermissionRequirement(string permission)
{
Permission = permission;
}
}
public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly IPermissionService _permissionService;
public PermissionHandler(IPermissionService permissionService)
{
_permissionService = permissionService;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
context.Fail();
return;
}
if (await _permissionService.HasPermissionAsync(userId, requirement.Permission))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
}
10. Authorization in Razor Pages
Page Model Authorization
// Using conventions in Program.cs
builder.Services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/Contact");
options.Conventions.AuthorizePage("/Privacy", "AdminOnly");
options.Conventions.AuthorizeFolder("/Products");
options.Conventions.AllowAnonymousToPage("/Products/Public");
options.Conventions.AuthorizeAreaFolder("Admin", "/", "AdminOnly");
});
// Page model with authorization
[Authorize(Policy = "EditProduct")]
public class EditProductModel : PageModel
{
private readonly ProductService _productService;
private readonly IAuthorizationService _authorizationService;
public EditProductModel(ProductService productService,
IAuthorizationService authorizationService)
{
_productService = productService;
_authorizationService = authorizationService;
}
[BindProperty]
public Product Product { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Product = await _productService.GetProductAsync(id);
if (Product == null)
return NotFound();
var authorizationResult = await _authorizationService.AuthorizeAsync(
User, Product, new ProductAuthorizationRequirement("Edit"));
if (!authorizationResult.Succeeded)
return Forbid();
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
return Page();
var authorizationResult = await _authorizationService.AuthorizeAsync(
User, Product, new ProductAuthorizationRequirement("Edit"));
if (!authorizationResult.Succeeded)
return Forbid();
await _productService.UpdateProductAsync(Product);
return RedirectToPage("./Details", new { id = Product.Id });
}
}
Razor Page Authorization in Views
@page
@model Products.IndexModel
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
<div class="container">
<h1>Products</h1>
@if ((await AuthorizationService.AuthorizeAsync(User, "CreateProduct")).Succeeded)
{
<a asp-page="Create" class="btn btn-primary">Create New Product</a>
}
<div class="row">
@foreach (var product in Model.Products)
{
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">@product.Name</h5>
<p class="card-text">@product.Description</p>
<p class="card-text">[email protected]</p>
@if ((await AuthorizationService.AuthorizeAsync(User, product, "Edit")).Succeeded)
{
<a asp-page="Edit" asp-route-id="@product.Id" class="btn btn-warning">Edit</a>
}
@if ((await AuthorizationService.AuthorizeAsync(User, product, "Delete")).Succeeded)
{
<form method="post" asp-page-handler="Delete" asp-route-id="@product.Id" style="display: inline;">
<button type="submit" class="btn btn-danger">Delete</button>
</form>
}
</div>
</div>
</div>
}
</div>
</div>
11. Authorization in Web APIs
API Controller Authorization
[ApiController]
[Route("api/[controller]")]
[Authorize] // All endpoints require authentication
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
private readonly IAuthorizationService _authorizationService;
public ProductsController(IProductService productService,
IAuthorizationService authorizationService)
{
_productService = productService;
_authorizationService = authorizationService;
}
[HttpGet]
[AllowAnonymous] // This endpoint is publicly accessible
public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts()
{
var products = await _productService.GetAllProductsAsync();
return Ok(products);
}
[HttpGet("{id}")]
[Authorize(Policy = "ViewProduct")]
public async Task<ActionResult<ProductDto>> GetProduct(int id)
{
var product = await _productService.GetProductAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
[HttpPost]
[Authorize(Policy = "CreateProduct")]
public async Task<ActionResult<ProductDto>> CreateProduct(CreateProductDto createDto)
{
var product = await _productService.CreateProductAsync(createDto);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, UpdateProductDto updateDto)
{
var product = await _productService.GetProductAsync(id);
if (product == null)
return NotFound();
var authorizationResult = await _authorizationService.AuthorizeAsync(
User, product, new ProductAuthorizationRequirement("Edit"));
if (!authorizationResult.Succeeded)
return Forbid();
await _productService.UpdateProductAsync(id, updateDto);
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
var product = await _productService.GetProductAsync(id);
if (product == null)
return NotFound();
var authorizationResult = await _authorizationService.AuthorizeAsync(
User, product, new ProductAuthorizationRequirement("Delete"));
if (!authorizationResult.Succeeded)
return Forbid();
await _productService.DeleteProductAsync(id);
return NoContent();
}
}
Custom Authorization Filters for APIs
public class PermissionAuthorizationFilter : IAuthorizationFilter
{
private readonly string _permission;
private readonly IPermissionService _permissionService;
public PermissionAuthorizationFilter(string permission, IPermissionService permissionService)
{
_permission = permission;
_permissionService = permissionService;
}
public async void OnAuthorization(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;
if (!user.Identity.IsAuthenticated)
{
context.Result = new UnauthorizedResult();
return;
}
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
context.Result = new ForbidResult();
return;
}
var hasPermission = await _permissionService.HasPermissionAsync(userId, _permission);
if (!hasPermission)
{
context.Result = new ForbidResult();
}
}
}
public class PermissionAttribute : TypeFilterAttribute
{
public PermissionAttribute(string permission)
: base(typeof(PermissionAuthorizationFilter))
{
Arguments = new object[] { permission };
}
}
// Usage in API controllers
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
[HttpGet("reports")]
[Permission("ViewReports")]
public IActionResult GetReports()
{
// Only users with "ViewReports" permission can access
return Ok();
}
[HttpPost("users")]
[Permission("ManageUsers")]
public IActionResult CreateUser()
{
// Only users with "ManageUsers" permission can access
return Ok();
}
}
12. Advanced Scenarios & Patterns
Multi-Tenant Authorization
// Tenant-based authorization requirement
public class TenantRequirement : IAuthorizationRequirement
{
public string RequiredTenant { get; }
public TenantRequirement(string requiredTenant)
{
RequiredTenant = requiredTenant;
}
}
public class TenantHandler : AuthorizationHandler<TenantRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
TenantRequirement requirement)
{
var userTenant = context.User.FindFirst("TenantId")?.Value;
if (userTenant != null && userTenant == requirement.RequiredTenant)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
// Multi-tenant resource authorization
public class MultiTenantResourceHandler
: AuthorizationHandler<IAuthorizationRequirement, ITenantResource>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
IAuthorizationRequirement requirement,
ITenantResource resource)
{
var userTenantId = context.User.FindFirst("TenantId")?.Value;
var resourceTenantId = resource.TenantId;
if (userTenantId == resourceTenantId)
{
context.Succeed(requirement);
}
else
{
context.Fail(new AuthorizationFailureReason(this,
"Access denied: Resource belongs to different tenant"));
}
return Task.CompletedTask;
}
}
Feature Flag Authorization
// Feature-based authorization
public class FeatureRequirement : IAuthorizationRequirement
{
public string FeatureName { get; }
public FeatureRequirement(string featureName)
{
FeatureName = featureName;
}
}
public class FeatureHandler : AuthorizationHandler<FeatureRequirement>
{
private readonly IFeatureService _featureService;
public FeatureHandler(IFeatureService featureService)
{
_featureService = featureService;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
FeatureRequirement requirement)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (await _featureService.IsFeatureEnabledAsync(requirement.FeatureName, userId))
{
context.Succeed(requirement);
}
else
{
context.Fail(new AuthorizationFailureReason(this,
$"Feature '{requirement.FeatureName}' is not enabled"));
}
}
}
Composite Authorization
// Composite requirement that combines multiple requirements
public class CompositeRequirement : IAuthorizationRequirement
{
public IAuthorizationRequirement[] Requirements { get; }
public CompositeRequirement(params IAuthorizationRequirement[] requirements)
{
Requirements = requirements;
}
}
public class CompositeHandler : AuthorizationHandler<CompositeRequirement>
{
private readonly IAuthorizationService _authorizationService;
public CompositeHandler(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
CompositeRequirement requirement)
{
foreach (var subRequirement in requirement.Requirements)
{
var result = await _authorizationService.AuthorizeAsync(
context.User, context.Resource, subRequirement);
if (!result.Succeeded)
{
context.Fail();
return;
}
}
context.Succeed(requirement);
}
}
13. Security Best Practices
Principle of Least Privilege
// Grant minimum required permissions
public class LeastPrivilegeService
{
public async Task AssignDefaultPermissionsAsync(string userId, string userType)
{
var permissions = GetDefaultPermissions(userType);
foreach (var permission in permissions)
{
await _permissionService.AssignPermissionAsync(userId, permission);
}
}
private IEnumerable<string> GetDefaultPermissions(string userType)
{
return userType switch
{
"Viewer" => new[] { "ReadContent", "ViewReports" },
"Editor" => new[] { "ReadContent", "EditContent", "ViewReports" },
"Admin" => new[] { "ReadContent", "EditContent", "DeleteContent",
"ViewReports", "ManageUsers" },
_ => new[] { "ReadContent" }
};
}
}
Secure Default Configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
// Default policy: deny all by default
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
// Fallback policy for unprotected endpoints
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
// Specific policies with clear naming
options.AddPolicy("Content.Read", policy =>
policy.RequireAuthenticatedUser()
.RequireClaim("Permission", "ReadContent"));
options.AddPolicy("Content.Write", policy =>
policy.RequireAuthenticatedUser()
.RequireClaim("Permission", "WriteContent")
.RequireAssertion(context =>
context.User.HasClaim("Status", "Active")));
});
}
Audit Logging for Authorization
public class AuditingAuthorizationHandler : IAuthorizationHandler
{
private readonly ILogger<AuditingAuthorizationHandler> _logger;
private readonly IAuthorizationHandler _innerHandler;
public AuditingAuthorizationHandler(ILogger<AuditingAuthorizationHandler> logger,
IAuthorizationHandler innerHandler)
{
_logger = logger;
_innerHandler = innerHandler;
}
public async Task HandleAsync(AuthorizationHandlerContext context)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var requirements = context.Requirements.ToList();
await _innerHandler.HandleAsync(context);
foreach (var requirement in requirements)
{
var succeeded = context.HasSucceeded(requirement);
_logger.LogInformation(
"Authorization audit - User: {UserId}, Requirement: {Requirement}, Result: {Result}",
userId, requirement.GetType().Name, succeeded ? "Granted" : "Denied");
}
}
}
14. Performance Considerations
Caching Authorization Results
public class CachingAuthorizationService : IAuthorizationService
{
private readonly IAuthorizationService _innerService;
private readonly IMemoryCache _cache;
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5);
public CachingAuthorizationService(IAuthorizationService innerService,
IMemoryCache cache)
{
_innerService = innerService;
_cache = cache;
}
public async Task<AuthorizationResult> AuthorizeAsync(
ClaimsPrincipal user, object resource, string policyName)
{
var cacheKey = $"auth_{user.Identity.Name}_{policyName}_{resource?.GetHashCode()}";
if (_cache.TryGetValue(cacheKey, out AuthorizationResult cachedResult))
{
return cachedResult;
}
var result = await _innerService.AuthorizeAsync(user, resource, policyName);
_cache.Set(cacheKey, result, _cacheDuration);
return result;
}
public async Task<AuthorizationResult> AuthorizeAsync(
ClaimsPrincipal user, object resource,
IEnumerable<IAuthorizationRequirement> requirements)
{
// Similar caching implementation for requirements-based authorization
var requirementsHash = string.Join("_", requirements.Select(r => r.GetType().Name));
var cacheKey = $"auth_req_{user.Identity.Name}_{requirementsHash}_{resource?.GetHashCode()}";
if (_cache.TryGetValue(cacheKey, out AuthorizationResult cachedResult))
{
return cachedResult;
}
var result = await _innerService.AuthorizeAsync(user, resource, requirements);
_cache.Set(cacheKey, result, _cacheDuration);
return result;
}
}
Optimized Permission Loading
public class OptimizedPermissionService : IPermissionService
{
private readonly ApplicationDbContext _context;
private readonly IMemoryCache _cache;
public OptimizedPermissionService(ApplicationDbContext context, IMemoryCache cache)
{
_context = context;
_cache = cache;
}
public async Task<IEnumerable<string>> GetUserPermissionsAsync(string userId)
{
var cacheKey = $"user_permissions_{userId}";
if (_cache.TryGetValue(cacheKey, out IEnumerable<string> permissions))
{
return permissions;
}
// Single database query to get all permissions
permissions = await _context.Users
.Where(u => u.Id == userId)
.SelectMany(u => u.UserPermissions.Where(up => up.IsActive)
.Select(up => up.Permission.Name)
.Union(u.UserRoles.SelectMany(ur => ur.Role.RolePermissions
.Select(rp => rp.Permission.Name))))
.Distinct()
.ToListAsync();
_cache.Set(cacheKey, permissions, TimeSpan.FromMinutes(30));
return permissions;
}
public async Task PreloadUserPermissionsAsync(string userId)
{
await GetUserPermissionsAsync(userId);
}
}
15. Testing Authorization
Unit Testing Authorization Handlers
public class DocumentAuthorizationHandlerTests
{
private DocumentAuthorizationHandler _handler;
private Mock<UserManager<ApplicationUser>> _userManagerMock;
[SetUp]
public void Setup()
{
_userManagerMock = new Mock<UserManager<ApplicationUser>>(
Mock.Of<IUserStore<ApplicationUser>>(), null, null, null, null, null, null, null, null);
_handler = new DocumentAuthorizationHandler(_userManagerMock.Object);
}
[Test]
public async Task HandleRequirementAsync_AuthorCanEditOwnDocument_Succeeds()
{
// Arrange
var document = new Document { AuthorId = "user123", Status = DocumentStatus.Draft };
var requirement = new DocumentAuthorizationRequirement("Edit");
var user = CreateUser("user123", "User");
var context = new AuthorizationHandlerContext(new[] { requirement }, user, document);
// Act
await _handler.HandleAsync(context);
// Assert
Assert.IsTrue(context.HasSucceeded);
}
[Test]
public async Task HandleRequirementAsync_NonAuthorCannotEditDocument_Fails()
{
// Arrange
var document = new Document { AuthorId = "user123", Status = DocumentStatus.Draft };
var requirement = new DocumentAuthorizationRequirement("Edit");
var user = CreateUser("user456", "User"); // Different user
var context = new AuthorizationHandlerContext(new[] { requirement }, user, document);
// Act
await _handler.HandleAsync(context);
// Assert
Assert.IsFalse(context.HasSucceeded);
}
[Test]
public async Task HandleRequirementAsync_AdminCanEditAnyDocument_Succeeds()
{
// Arrange
var document = new Document { AuthorId = "user123", Status = DocumentStatus.Draft };
var requirement = new DocumentAuthorizationRequirement("Edit");
var user = CreateUser("admin123", "Admin"); // Admin user
var context = new AuthorizationHandlerContext(new[] { requirement }, user, document);
// Act
await _handler.HandleAsync(context);
// Assert
Assert.IsTrue(context.HasSucceeded);
}
private ClaimsPrincipal CreateUser(string userId, string role)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim(ClaimTypes.Role, role)
};
return new ClaimsPrincipal(new ClaimsIdentity(claims, "Test"));
}
}
Integration Testing Authorization
public class AuthorizationIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public AuthorizationIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Test]
public async Task AdminEndpoint_WithAdminUser_ReturnsSuccess()
{
// Arrange
var client = _factory.CreateClient();
// Create admin user and get token
var token = await GetAdminTokenAsync();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await client.GetAsync("/api/admin/users");
// Assert
response.EnsureSuccessStatusCode();
}
[Test]
public async Task AdminEndpoint_WithRegularUser_ReturnsForbidden()
{
// Arrange
var client = _factory.CreateClient();
// Create regular user and get token
var token = await GetRegularUserTokenAsync();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await client.GetAsync("/api/admin/users");
// Assert
Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode);
}
private async Task<string> GetAdminTokenAsync()
{
// Implementation to get JWT token for admin user
// This would typically call your authentication endpoint
return "admin_jwt_token";
}
private async Task<string> GetRegularUserTokenAsync()
{
// Implementation to get JWT token for regular user
return "user_jwt_token";
}
}
16. Troubleshooting & Debugging
Authorization Diagnostics
public class AuthorizationDiagnosticsMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<AuthorizationDiagnosticsMiddleware> _logger;
public AuthorizationDiagnosticsMiddleware(RequestDelegate next,
ILogger<AuthorizationDiagnosticsMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Log authorization attempts
var originalBodyStream = context.Response.Body;
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
await _next(context);
// Log authorization results
if (context.Response.StatusCode == 403 || context.Response.StatusCode == 401)
{
_logger.LogWarning(
"Authorization failed - Path: {Path}, User: {User}, Status: {Status}",
context.Request.Path,
context.User.Identity.Name,
context.Response.StatusCode);
}
responseBody.Seek(0, SeekOrigin.Begin);
await responseBody.CopyToAsync(originalBodyStream);
}
}
// Custom authorization event source
public class AuthorizationEvents
{
public static readonly EventHandler<AuthorizationFailedEventArgs> AuthorizationFailed =
(sender, e) =>
{
var logger = (ILogger<AuthorizationEvents>)sender;
logger.LogWarning(
"Authorization failed for user {User} on resource {Resource}. Requirement: {Requirement}",
e.Context.User.Identity.Name,
e.Context.Resource?.GetType().Name,
e.Requirement.GetType().Name);
};
}
Common Issues and Solutions
// Problem: Policies not being evaluated
// Solution: Ensure policies are registered and handlers are scoped
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy("CustomPolicy", policy =>
policy.Requirements.Add(new CustomRequirement()));
});
// Register authorization handlers
services.AddScoped<IAuthorizationHandler, CustomRequirementHandler>();
}
// Problem: Resource-based authorization not working
// Solution: Ensure resource is passed to AuthorizeAsync
public async Task<IActionResult> EditDocument(int id)
{
var document = await _documentService.GetDocumentAsync(id);
// Correct: Pass the document resource
var result = await _authorizationService.AuthorizeAsync(User, document, "EditPolicy");
// Incorrect: Not passing the resource
// var result = await _authorizationService.AuthorizeAsync(User, "EditPolicy");
if (!result.Succeeded)
return Forbid();
return View(document);
}
17. Real-World Implementation Example
Complete E-commerce Authorization System
// Product entity with authorization properties
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public string VendorId { get; set; } // User ID of the vendor
public ProductStatus Status { get; set; }
public DateTime CreatedDate { get; set; }
public string Category { get; set; }
public bool IsFeatured { get; set; }
}
public enum ProductStatus
{
Draft,
Published,
Discontinued,
Archived
}
// Comprehensive product authorization requirements
public class ProductAuthorizationRequirement : IAuthorizationRequirement
{
public string Operation { get; }
public ProductAuthorizationRequirement(string operation)
{
Operation = operation;
}
}
public class ProductAuthorizationHandler
: AuthorizationHandler<ProductAuthorizationRequirement, Product>
{
private readonly IPermissionService _permissionService;
public ProductAuthorizationHandler(IPermissionService permissionService)
{
_permissionService = permissionService;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ProductAuthorizationRequirement requirement,
Product product)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
switch (requirement.Operation)
{
case "View":
await HandleViewRequirement(context, product, userId);
break;
case "Edit":
await HandleEditRequirement(context, product, userId);
break;
case "Delete":
await HandleDeleteRequirement(context, product, userId);
break;
case "Publish":
await HandlePublishRequirement(context, product, userId);
break;
case "Feature":
await HandleFeatureRequirement(context, product, userId);
break;
}
}
private async Task HandleViewRequirement(AuthorizationHandlerContext context,
Product product, string userId)
{
// Anyone can view published products
if (product.Status == ProductStatus.Published)
{
context.Succeed(new ProductAuthorizationRequirement("View"));
return;
}
// Vendor can view their own products
if (product.VendorId == userId)
{
context.Succeed(new ProductAuthorizationRequirement("View"));
return;
}
// Admins and moderators can view all products
if (context.User.IsInRole("Admin") || context.User.IsInRole("Moderator"))
{
context.Succeed(new ProductAuthorizationRequirement("View"));
}
}
private async Task HandleEditRequirement(AuthorizationHandlerContext context,
Product product, string userId)
{
// Vendor can edit their own non-archived products
if (product.VendorId == userId && product.Status != ProductStatus.Archived)
{
context.Succeed(new ProductAuthorizationRequirement("Edit"));
return;
}
// Admins can edit any product
if (context.User.IsInRole("Admin"))
{
context.Succeed(new ProductAuthorizationRequirement("Edit"));
return;
}
// Moderators can edit products in their category
if (context.User.IsInRole("Moderator") &&
await HasCategoryPermissionAsync(userId, product.Category))
{
context.Succeed(new ProductAuthorizationRequirement("Edit"));
}
}
private async Task HandleDeleteRequirement(AuthorizationHandlerContext context,
Product product, string userId)
{
// Vendor can delete their own draft products
if (product.VendorId == userId && product.Status == ProductStatus.Draft)
{
context.Succeed(new ProductAuthorizationRequirement("Delete"));
return;
}
// Admins can delete any product
if (context.User.IsInRole("Admin"))
{
context.Succeed(new ProductAuthorizationRequirement("Delete"));
}
}
private async Task HandlePublishRequirement(AuthorizationHandlerContext context,
Product product, string userId)
{
// Vendor can publish their own products if they have permission
if (product.VendorId == userId &&
await _permissionService.HasPermissionAsync(userId, "PublishProducts"))
{
context.Succeed(new ProductAuthorizationRequirement("Publish"));
return;
}
// Admins and moderators can publish any product
if (context.User.IsInRole("Admin") || context.User.IsInRole("Moderator"))
{
context.Succeed(new ProductAuthorizationRequirement("Publish"));
}
}
private async Task HandleFeatureRequirement(AuthorizationHandlerContext context,
Product product, string userId)
{
// Only admins and users with feature permission can feature products
if (context.User.IsInRole("Admin") ||
await _permissionService.HasPermissionAsync(userId, "FeatureProducts"))
{
context.Succeed(new ProductAuthorizationRequirement("Feature"));
}
}
private async Task<bool> HasCategoryPermissionAsync(string userId, string category)
{
var permissionName = $"Moderate{category}Category";
return await _permissionService.HasPermissionAsync(userId, permissionName);
}
}
// Complete products controller with authorization
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
private readonly IAuthorizationService _authorizationService;
public ProductsController(IProductService productService,
IAuthorizationService authorizationService)
{
_productService = productService;
_authorizationService = authorizationService;
}
[HttpGet]
[AllowAnonymous]
public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts(
[FromQuery] ProductQueryParameters parameters)
{
// Apply filters based on user permissions
if (!User.Identity.IsAuthenticated)
{
parameters.Status = ProductStatus.Published;
}
else
{
var canViewAll = await _authorizationService.AuthorizeAsync(
User, new ProductAuthorizationRequirement("ViewAll"));
if (!canViewAll.Succeeded)
{
parameters.VendorId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
}
var products = await _productService.GetProductsAsync(parameters);
return Ok(products);
}
[HttpPost]
[Authorize(Policy = "CreateProduct")]
public async Task<ActionResult<ProductDto>> CreateProduct(CreateProductDto createDto)
{
var product = await _productService.CreateProductAsync(createDto, User);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, UpdateProductDto updateDto)
{
var product = await _productService.GetProductAsync(id);
if (product == null)
return NotFound();
var authorizationResult = await _authorizationService.AuthorizeAsync(
User, product, new ProductAuthorizationRequirement("Edit"));
if (!authorizationResult.Succeeded)
return Forbid();
await _productService.UpdateProductAsync(id, updateDto);
return NoContent();
}
[HttpPost("{id}/publish")]
public async Task<IActionResult> PublishProduct(int id)
{
var product = await _productService.GetProductAsync(id);
if (product == null)
return NotFound();
var authorizationResult = await _authorizationService.AuthorizeAsync(
User, product, new ProductAuthorizationRequirement("Publish"));
if (!authorizationResult.Succeeded)
return Forbid();
await _productService.PublishProductAsync(id);
return NoContent();
}
[HttpPost("{id}/feature")]
public async Task<IActionResult> FeatureProduct(int id, bool isFeatured)
{
var product = await _productService.GetProductAsync(id);
if (product == null)
return NotFound();
var authorizationResult = await _authorizationService.AuthorizeAsync(
User, product, new ProductAuthorizationRequirement("Feature"));
if (!authorizationResult.Succeeded)
return Forbid();
await _productService.SetFeaturedAsync(id, isFeatured);
return NoContent();
}
}
18. Conclusion
Advanced authorization in ASP.NET Core provides a robust, flexible framework for securing your applications. By mastering policy-based authorization, resource protection, and custom requirements, you can implement sophisticated security models that match your business needs.
Key Takeaways
Policy-based authorization offers declarative security that's easy to maintain and test
Resource-based authorization enables fine-grained control over specific data entities
Custom requirements and handlers allow implementation of complex business rules
Claims transformation provides dynamic permission assignment
Performance optimization through caching and efficient querying is crucial for scalability
Comprehensive testing ensures your authorization logic works as expected
Continuing Your Journey
In our next post, we'll dive into API Development in ASP.NET Core, covering RESTful principles, API versioning, documentation, and advanced API patterns.
Remember: Security is not a feature you add at the end—it's a fundamental aspect that should be designed into your application from the beginning. The authorization patterns we've covered provide the foundation for building secure, enterprise-ready applications.