- Full Example - Identity API: Pet Shop
- What's Included
- Why This is Forward-Thinking
- Code: 🏪 PET SHOP AUTHORIZATION - POLICY-BASED DESIGN (Modern ASP.NET Core)
- 1️⃣ DOMAIN MODELS
- 2️⃣ AUTHORIZATION REQUIREMENTS (Business Rules)
- 3️⃣ AUTHORIZATION HANDLERS (Business Logic)
- 4️⃣ PROGRAM.CS - POLICY REGISTRATION (Modern approach)
- 5️⃣ CONTROLLER - POLICY USAGE
- 6️⃣ USAGE EXAMPLES & SCENARIOS
- ✨ BENEFITS OF THIS MODERN APPROACH
- 🚀 FORWARD-THINKING: Future Enhancements
Comprehensive Pet Shop authorization example demonstrating modern ASP.NET Core policy-based design. Here are the key highlights:
*_1.Domain Models _ *-Pet and User entities with proper relationships
2. Authorization Requirements - Business rules as testable components:
MinimumAccountAgeRequirement- Checks account maturityResourceOwnerOrElevatedRequirement- Ownership OR elevated roleRoleRequirement- Role checks (wrapped as requirements for consistency)
3. Authorization Handlers - Business logic implementation for each requirement
4. Policy Registration - All authorization defined as named policies:
- ✨ Even simple role checks use policies (
AdminOnly,EmployeeOrAdmin) - Complex business rules (
EstablishedCustomer,CanManagePets) - Resource-based policies that evaluate at runtime
5. Controller Implementation - Full CRUD operations:
-*_CREATE _ *: Employees / Admins can list pets
- READ: Any authenticated user can view
- UPDATE: Resource - based(owner OR elevated role)
- *_DELETE _ *: Admin - only
- *_PURCHASE _ *: Requires 30-day account age
6. Real Scenarios - 5 detailed examples showing how authorization flows
- No
[Authorize(Roles = "...")]-Everything uses policies - Centralized - All rules in
Program.cs, not scattered in attributes - Testable - Handlers can be unit tested independently
- Evolvable - Add complexity (IP checks, time restrictions) without touching controllers
- Resource-Based - Passes actual
Petobjects to check ownership
This follows Microsoft's recommended approach where policies are the top-level abstraction, with claims and roles working underneath them!
- Forward-thinking: Uses policies as the top-level abstraction, even for roles
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Security.Claims;
public class Pet
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Species { get; set; } = string.Empty;
public decimal Price { get; set; }
public string OwnerId { get; set; } = string.Empty; // User who added this pet
public bool IsAvailable { get; set; } = true;
}
public class User
{
public string Id { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Role { get; set; } = string.Empty; // "Admin", "Employee", "Customer"
public DateTime MemberSince { get; set; }
}/// <summary>
/// Requirement: User must have sufficient account age
/// </summary>
public class MinimumAccountAgeRequirement : IAuthorizationRequirement
{
public int MinimumDays { get; }
public MinimumAccountAgeRequirement(int minimumDays)
{
MinimumDays = minimumDays;
}
}
/// <summary>
/// Requirement: User must be the owner of the resource OR have elevated privileges
/// </summary>
public class ResourceOwnerOrElevatedRequirement : IAuthorizationRequirement
{
public string[] ElevatedRoles { get; }
public ResourceOwnerOrElevatedRequirement(params string[] elevatedRoles)
{
ElevatedRoles = elevatedRoles;
}
}
/// <summary>
/// Requirement: User must have specific role(s) - wrapped in a requirement for consistency
/// </summary>
public class RoleRequirement : IAuthorizationRequirement
{
public string[] AllowedRoles { get; }
public RoleRequirement(params string[] allowedRoles)
{
AllowedRoles = allowedRoles;
}
}/// <summary>
/// Handler: Validates account age from MemberSince claim
/// </summary>
public class MinimumAccountAgeHandler : AuthorizationHandler<MinimumAccountAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAccountAgeRequirement requirement)
{
var memberSinceClaim = context.User.FindFirst("MemberSince");
if (memberSinceClaim == null)
{
return Task.CompletedTask; // Fail silently
}
if (DateTime.TryParse(memberSinceClaim.Value, out var memberSince))
{
var accountAge = DateTime.UtcNow - memberSince;
if (accountAge.TotalDays >= requirement.MinimumDays)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
/// <summary>
/// Handler: Checks if user owns the resource OR has elevated role
/// </summary>
public class ResourceOwnerOrElevatedHandler : AuthorizationHandler<ResourceOwnerOrElevatedRequirement, Pet>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ResourceOwnerOrElevatedRequirement requirement,
Pet resource)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var userRole = context.User.FindFirst(ClaimTypes.Role)?.Value;
if (userId == null)
{
return Task.CompletedTask;
}
// Check if user is the owner
if (resource.OwnerId == userId)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
// Check if user has elevated role (Admin or Employee)
if (userRole != null && requirement.ElevatedRoles.Contains(userRole))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
/// <summary>
/// Handler: Simple role check
/// </summary>
public class RoleHandler : AuthorizationHandler<RoleRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
RoleRequirement requirement)
{
var userRole = context.User.FindFirst(ClaimTypes.Role)?.Value;
if (userRole != null && requirement.AllowedRoles.Contains(userRole))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddAuthentication(/* configure JWT/Cookie */);
// ========================================================================
// MODERN POLICY-BASED AUTHORIZATION CONFIGURATION
// ========================================================================
// Forward-thinking: Define ALL authorization as policies
// Even simple role checks are wrapped in policies for consistency
// ========================================================================
builder.Services.AddAuthorization(options =>
{
// ------------------------------------------------------------------
// ROLE-BASED POLICIES (replacing [Authorize(Roles="...")])
// ------------------------------------------------------------------
// ✨ Modern: Use policies instead of direct role checks
// Benefits: Centralized, testable, evolvable
options.AddPolicy("AdminOnly", policy =>
policy.Requirements.Add(new RoleRequirement("Admin")));
options.AddPolicy("EmployeeOrAdmin", policy =>
policy.Requirements.Add(new RoleRequirement("Employee", "Admin")));
options.AddPolicy("AnyAuthenticated", policy =>
policy.RequireAuthenticatedUser());
// ------------------------------------------------------------------
// BUSINESS LOGIC POLICIES
// ------------------------------------------------------------------
// ✨ Modern: Complex rules as reusable policies
options.AddPolicy("EstablishedCustomer", policy =>
policy.Requirements.Add(new MinimumAccountAgeRequirement(30)));
options.AddPolicy("CanListPets", policy =>
policy.Requirements.Add(new RoleRequirement("Employee", "Admin")));
options.AddPolicy("CanManagePets", policy =>
{
// Resource-based: requires the Pet resource at runtime
policy.Requirements.Add(new ResourceOwnerOrElevatedRequirement("Employee", "Admin"));
});
options.AddPolicy("CanDeletePets", policy =>
policy.Requirements.Add(new RoleRequirement("Admin")));
options.AddPolicy("CanPurchase", policy =>
{
policy.RequireAuthenticatedUser();
// Could add more: credit check, account standing, etc.
});
options.AddPolicy("CanViewAnalytics", policy =>
policy.Requirements.Add(new RoleRequirement("Admin")));
});
// Register handlers
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAccountAgeHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, ResourceOwnerOrElevatedHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, RoleHandler>();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}[ApiController]
[Route("api/[controller]")]
public class PetsController : ControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly List<Pet> _pets; // Mock database
public PetsController(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
_pets = new List<Pet>(); // In real app: inject DbContext
}
// ------------------------------------------------------------------------
// CREATE: Only employees and admins can list new pets
// ------------------------------------------------------------------------
/// <summary>
/// Add a new pet to the shop inventory
/// </summary>
[HttpPost]
[Authorize(Policy = "CanListPets")] // ✨ Policy-based (not Roles="Employee,Admin")
public IActionResult CreatePet([FromBody] Pet pet)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userId == null)
{
return Unauthorized();
}
pet.Id = _pets.Count + 1;
pet.OwnerId = userId;
_pets.Add(pet);
return CreatedAtAction(nameof(GetPet), new { id = pet.Id }, pet);
}
// ------------------------------------------------------------------------
// READ: Any authenticated user can view pets
// ------------------------------------------------------------------------
/// <summary>
/// Get all available pets
/// </summary>
[HttpGet]
[Authorize(Policy = "AnyAuthenticated")] // ✨ Policy-based (not just [Authorize])
public IActionResult GetAllPets()
{
return Ok(_pets.Where(p => p.IsAvailable));
}
/// <summary>
/// Get specific pet by ID
/// </summary>
[HttpGet("{id}")]
[Authorize(Policy = "AnyAuthenticated")]
public IActionResult GetPet(int id)
{
var pet = _pets.FirstOrDefault(p => p.Id == id);
if (pet == null)
{
return NotFound();
}
return Ok(pet);
}
// ------------------------------------------------------------------------
// UPDATE: Resource-based authorization (owner OR elevated role)
// ------------------------------------------------------------------------
/// <summary>
/// Update pet details - only owner, employees, or admins
/// </summary>
[HttpPut("{id}")]
[Authorize] // ✨ First check authentication
public async Task<IActionResult> UpdatePet(int id, [FromBody] Pet updatedPet)
{
var pet = _pets.FirstOrDefault(p => p.Id == id);
if (pet == null)
{
return NotFound();
}
// ✨ MODERN: Resource-based policy authorization
// Pass the actual resource to check ownership
var authResult = await _authorizationService.AuthorizeAsync(
User,
pet,
"CanManagePets");
if (!authResult.Succeeded)
{
return Forbid(); // 403
}
// Update pet
pet.Name = updatedPet.Name;
pet.Species = updatedPet.Species;
pet.Price = updatedPet.Price;
pet.IsAvailable = updatedPet.IsAvailable;
return Ok(pet);
}
// ------------------------------------------------------------------------
// DELETE: Only admins can permanently delete pets
// ------------------------------------------------------------------------
/// <summary>
/// Delete a pet from inventory - admin only
/// </summary>
[HttpDelete("{id}")]
[Authorize(Policy = "CanDeletePets")] // ✨ Policy-based (not Roles="Admin")
public IActionResult DeletePet(int id)
{
var pet = _pets.FirstOrDefault(p => p.Id == id);
if (pet == null)
{
return NotFound();
}
_pets.Remove(pet);
return NoContent();
}
// ------------------------------------------------------------------------
// SPECIAL: Purchase action with complex policy
// ------------------------------------------------------------------------
/// <summary>
/// Purchase a pet - requires established customer status
/// </summary>
[HttpPost("{id}/purchase")]
[Authorize(Policy = "CanPurchase")]
public async Task<IActionResult> PurchasePet(int id)
{
var pet = _pets.FirstOrDefault(p => p.Id == id && p.IsAvailable);
if (pet == null)
{
return NotFound("Pet not available");
}
// ✨ Additional runtime policy check
var authResult = await _authorizationService.AuthorizeAsync(
User,
"EstablishedCustomer");
if (!authResult.Succeeded)
{
return StatusCode(403, "Account must be at least 30 days old to purchase");
}
pet.IsAvailable = false;
return Ok(new
{
Message = "Purchase successful!",
Pet = pet
});
}
// ------------------------------------------------------------------------
// ANALYTICS: Admin-only endpoint
// ------------------------------------------------------------------------
/// <summary>
/// View sales analytics - admin only
/// </summary>
[HttpGet("analytics")]
[Authorize(Policy = "CanViewAnalytics")]
public IActionResult GetAnalytics()
{
return Ok(new
{
TotalPets = _pets.Count,
AvailablePets = _pets.Count(p => p.IsAvailable),
SoldPets = _pets.Count(p => !p.IsAvailable),
AveragePrice = _pets.Average(p => p.Price)
});
}
}🎯 SCENARIO 1: Customer tries to list a pet
----------------------------------------
Request: POST / api / pets
User Claims: { role: "Customer", userId: "cust_123" }
Policy Check: "CanListPets" → RoleRequirement("Employee", "Admin")
Result: ❌ 403 Forbidden(Customer not in allowed roles)🎯 SCENARIO 2: Employee updates their own listed pet
----------------------------------------
Request: PUT / api / pets / 42
User Claims: { role: "Employee", userId: "emp_456" }
Pet.OwnerId: "emp_456"
Policy Check: "CanManagePets" → ResourceOwnerOrElevatedRequirement
Result: ✅ 200 OK(Employee is the owner)🎯 SCENARIO 3: Admin updates someone else's pet
----------------------------------------
Request: PUT / api / pets / 42
User Claims: { role: "Admin", userId: "admin_789" }
Pet.OwnerId: "emp_456"
Policy Check: "CanManagePets" → ResourceOwnerOrElevatedRequirement
Result: ✅ 200 OK(Admin has elevated role)🎯 SCENARIO 4: New customer tries to purchase
----------------------------------------
Request: POST / api / pets / 42 / purchase
User Claims: { role: "Customer", userId: "cust_123", MemberSince: "2025-12-10" }
Policy Check: "EstablishedCustomer" → MinimumAccountAgeRequirement(30 days)
Result: ❌ 403 Forbidden(Account only 6 days old)🎯 SCENARIO 5: Established customer purchases
----------------------------------------
Request: POST / api / pets / 42 / purchase
User Claims: { role: "Customer", userId: "cust_999", MemberSince: "2025-10-01" }
Policy Check: "EstablishedCustomer" → MinimumAccountAgeRequirement(30 days)
Result: ✅ 200 OK(Account 76 days old)-
Centralized Authorization Logic
- All rules defined in one place (Program.cs)
- Easy to audit and modify
-
Testable
- Handlers can be unit tested independently
- Policies can be tested without controllers
-
Evolvable
- Add complexity without changing controllers
- Example: "AdminOnly" could later check IP whitelist
-
Consistent
- All authorization uses policies (no mix of [Roles] and policies)
- Same pattern throughout the application
-
Clear Intent
- Policy names express business rules: "CanListPets", "EstablishedCustomer"
- Better than[Authorize(Roles = "Employee,Admin")]
-
Resource - Based Authorization
- Can pass actual resources to policies
- Enables ownership checks and complex scenarios
Could easily add:
- IP-based restrictions
- Time-based access(business hours only)
- Rate limiting per user role
- Multi-factor authentication requirements
- Geolocation-based rules
- Dynamic permissions from database
- Attribute-based access control (ABAC)
All without changing controller code - just add new requirements and handlers!