Skip to content

Instantly share code, notes, and snippets.

@carloswm85
Created December 16, 2025 17:35
Show Gist options
  • Select an option

  • Save carloswm85/2df8dc8c0c4d3e6e4544efdcde9d16d3 to your computer and use it in GitHub Desktop.

Select an option

Save carloswm85/2df8dc8c0c4d3e6e4544efdcde9d16d3 to your computer and use it in GitHub Desktop.

Full Example - Identity API: Pet Shop

Comprehensive Pet Shop authorization example demonstrating modern ASP.NET Core policy-based design. Here are the key highlights:

What's Included

*_1.Domain Models _ *-Pet and User entities with proper relationships

2. Authorization Requirements - Business rules as testable components:

  • MinimumAccountAgeRequirement - Checks account maturity
  • ResourceOwnerOrElevatedRequirement - Ownership OR elevated role
  • RoleRequirement - 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

Why This is Forward-Thinking

  1. No [Authorize(Roles = "...")]-Everything uses policies
  2. Centralized - All rules in Program.cs, not scattered in attributes
  3. Testable - Handlers can be unit tested independently
  4. Evolvable - Add complexity (IP checks, time restrictions) without touching controllers
  5. Resource-Based - Passes actual Pet objects to check ownership

This follows Microsoft's recommended approach where policies are the top-level abstraction, with claims and roles working underneath them!

Code: 🏪 PET SHOP AUTHORIZATION - POLICY-BASED DESIGN (Modern ASP.NET Core)

  • Forward-thinking: Uses policies as the top-level abstraction, even for roles

using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Security.Claims;

1️⃣ DOMAIN MODELS

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; }
}

2️⃣ AUTHORIZATION REQUIREMENTS (Business Rules)

/// <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;
	}
}

3️⃣ AUTHORIZATION HANDLERS (Business Logic)

/// <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;
	}
}

4️⃣ PROGRAM.CS - POLICY REGISTRATION (Modern approach)

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();
	}
}

5️⃣ CONTROLLER - POLICY USAGE

[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)
		});
	}
}

6️⃣ USAGE EXAMPLES & SCENARIOS

🎯 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)

✨ BENEFITS OF THIS MODERN APPROACH

  1. Centralized Authorization Logic

    • All rules defined in one place (Program.cs)
    • Easy to audit and modify
  2. Testable

    • Handlers can be unit tested independently
    • Policies can be tested without controllers
  3. Evolvable

    • Add complexity without changing controllers
    • Example: "AdminOnly" could later check IP whitelist
  4. Consistent

    • All authorization uses policies (no mix of [Roles] and policies)
    • Same pattern throughout the application
  5. Clear Intent

    • Policy names express business rules: "CanListPets", "EstablishedCustomer"
    • Better than[Authorize(Roles = "Employee,Admin")]
  6. Resource - Based Authorization

    • Can pass actual resources to policies
    • Enables ownership checks and complex scenarios

🚀 FORWARD-THINKING: Future Enhancements

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment