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

Authorization Strategies

Implement role-based and policy-based authorization

Authorization Strategies in ASP.NET with .NET 10

Master authorization strategies in ASP.NET with free flashcards and spaced repetition practice. This lesson covers role-based authorization, policy-based authorization, claims-based authorization, and resource-based authorizationโ€”essential concepts for building secure web applications with .NET 10.

Welcome ๐Ÿ”

Authorization determines what an authenticated user can do in your application. While authentication answers "who are you?", authorization answers "what can you do?" In ASP.NET with .NET 10, you have multiple powerful strategies to control access to resources, endpoints, and functionality.

Think of authorization like hotel key cards ๐Ÿจ: authentication confirms you're a guest, but authorization determines which floors, rooms, and facilities you can access based on your room type, membership status, or special permissions.

๐Ÿ’ก Key Insight: Authorization always happens AFTER authentication. You must know who someone is before you can determine what they're allowed to do.

Core Concepts ๐Ÿ“š

1. Role-Based Authorization (RBAC) ๐Ÿ‘ฅ

Role-Based Authorization is the simplest and most common strategy. Users are assigned to roles (like "Admin", "Manager", "User"), and access is granted based on role membership.

How it works:

  • Users belong to one or more roles
  • Controllers/actions are decorated with [Authorize(Roles = "RoleName")]
  • ASP.NET checks if the authenticated user has the required role
[Authorize(Roles = "Admin")]
public class AdminController : Controller
{
    public IActionResult Dashboard()
    {
        return View();
    }
}

Real-world analogy: Hospital staff access ๐Ÿฅ

  • Doctors can access patient records and prescribe medication
  • Nurses can view records and administer medication
  • Receptionists can only schedule appointments

Advantages:

  • โœ… Simple to understand and implement
  • โœ… Works well for small applications
  • โœ… Easy to communicate to non-technical stakeholders

Disadvantages:

  • โŒ Can become rigid and hard to maintain as roles multiply
  • โŒ Difficult to handle fine-grained permissions
  • โŒ Role explosion: you might end up with "AdminWhoCanDeleteUsers", "AdminWhoCanEditSettings", etc.

2. Claims-Based Authorization ๐ŸŽซ

Claims are key-value pairs that describe properties of a user (e.g., "DateOfBirth": "1990-05-15", "Department": "Engineering", "SecurityClearance": "Level3"). Claims-based authorization makes decisions based on these attributes.

How it works:

  • User identity contains a collection of claims
  • Authorization logic checks for specific claims and their values
  • More flexible than roles because claims can represent any attribute
[Authorize]
public class DocumentsController : Controller
{
    public IActionResult ViewClassified()
    {
        var user = HttpContext.User;
        
        // Check if user has security clearance claim
        if (user.HasClaim("SecurityClearance", "Level3"))
        {
            return View();
        }
        
        return Forbid();
    }
}

Real-world analogy: Airport security clearances โœˆ๏ธ

  • Your boarding pass (identity) contains claims: destination, seat class, boarding group
  • Security doesn't just check "are you a passenger?" (role)
  • They check specific attributes: "Is your destination international?" "Do you have TSA PreCheck?"

๐Ÿง  Mnemonic: Think "Claims = Characteristics" - they describe characteristics of the user

Common claim types:

  • ClaimTypes.Name - User's name
  • ClaimTypes.Email - Email address
  • ClaimTypes.Role - Roles (yes, roles are actually implemented as claims!)
  • ClaimTypes.DateOfBirth - Birthdate
  • Custom claims - Any domain-specific attribute

3. Policy-Based Authorization ๐Ÿ“‹

Policy-Based Authorization is the most flexible and recommended approach in modern ASP.NET. Policies encapsulate authorization logic in reusable, testable units.

How it works:

  • Define named policies in Program.cs
  • Policies contain one or more requirements
  • Requirements are evaluated by handlers
  • Apply policies with [Authorize(Policy = "PolicyName")]

Policy registration:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
    
    options.AddPolicy("EmployeeOnly", policy =>
        policy.RequireClaim("EmployeeNumber"));
    
    options.AddPolicy("SeniorEmployee", policy =>
        policy.RequireClaim("Department")
              .RequireClaim("YearsOfService")
              .RequireAssertion(context =>
              {
                  var years = context.User.FindFirst("YearsOfService")?.Value;
                  return int.TryParse(years, out int y) && y >= 5;
              }));
});

Policy application:

[Authorize(Policy = "AtLeast21")]
public IActionResult PurchaseAlcohol()
{
    return View();
}

Real-world analogy: Loan application approval ๐Ÿฆ

  • Instead of simple roles ("customer" vs "VIP"), banks use policies
  • "Prime Loan Policy" might require: credit score > 750, income > $50k, debt ratio < 30%
  • Multiple conditions evaluated together
  • Same policy applies consistently across all loan officers

๐Ÿ’ก Best Practice: Use policies for anything more complex than simple role checks. They're easier to test, maintain, and modify.

4. Resource-Based Authorization ๐Ÿ”’

Resource-Based Authorization makes decisions based on the specific resource being accessed. The authorization logic depends on properties of the resource itself.

When to use it:

  • Document editing (users can only edit their own documents)
  • Social media posts (users can delete their own posts)
  • Project management (team members can access their projects)

How it works differently:

  • Can't use [Authorize] attribute (we don't know the resource at compile time)
  • Must use IAuthorizationService programmatically
  • Authorization happens in the action method after retrieving the resource
public class DocumentsController : Controller
{
    private readonly IAuthorizationService _authorizationService;
    private readonly IDocumentRepository _documentRepository;
    
    public DocumentsController(
        IAuthorizationService authorizationService,
        IDocumentRepository documentRepository)
    {
        _authorizationService = authorizationService;
        _documentRepository = documentRepository;
    }
    
    public async Task<IActionResult> Edit(int id)
    {
        var document = await _documentRepository.GetByIdAsync(id);
        
        if (document == null)
            return NotFound();
        
        // Resource-based authorization check
        var authResult = await _authorizationService
            .AuthorizeAsync(User, document, "EditPolicy");
        
        if (!authResult.Succeeded)
            return Forbid();
        
        return View(document);
    }
}

Authorization handler for resource:

public class DocumentAuthorizationHandler 
    : AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OperationAuthorizationRequirement requirement,
        Document resource)
    {
        // Owner can do anything
        if (context.User.Identity?.Name == resource.Owner)
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }
        
        // Admins can edit any document
        if (requirement.Name == "Edit" && 
            context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
        }
        
        return Task.CompletedTask;
    }
}

Real-world analogy: Medical records access ๐Ÿฅ

  • A doctor can view their own patient's records
  • The patient can view their own records
  • A specialist can view records if referred by primary doctor
  • Authorization depends on the SPECIFIC record, not just the user's role

๐Ÿ”” Did you know? GitHub uses resource-based authorization extensively. Your ability to push code depends on the specific repository's permissions, not just your GitHub account type.

5. Combining Authorization Strategies ๐Ÿ”—

Real applications typically use multiple strategies together:

// Requires Admin role AND specific claim
[Authorize(Roles = "Admin")]
[Authorize(Policy = "TwoFactorEnabled")]
public class SecuritySettingsController : Controller
{
    // Must satisfy BOTH requirements
}

Combination patterns:

PatternUse CaseExample
Role + PolicyBase role with additional requirementsManager with budget approval authority
Policy + ResourceGeneral permissions + specific ownershipEditor role who can only edit their articles
Claims + ResourceAttribute-based + ownershipDepartment member accessing department files

Authorization Decision Flow ๐Ÿ”„

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  1. HTTP Request arrives                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  2. Authentication Middleware               โ”‚
โ”‚     - Validates token/cookie                โ”‚
โ”‚     - Populates User.Identity               โ”‚
โ”‚     - Extracts claims                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  3. Authorization Middleware                โ”‚
โ”‚     - Checks [Authorize] attributes         โ”‚
โ”‚     - Evaluates policies                    โ”‚
โ”‚     - Checks roles/claims                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ”‚
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”
        โ†“           โ†“
     โœ… Pass    โŒ Fail
        โ”‚           โ”‚
        โ†“           โ†“
  Execute      Return 403
  Action       Forbidden

Authorization Requirements Deep Dive ๐ŸŽฏ

Requirements are the building blocks of policies. ASP.NET provides several built-in requirement types:

Built-in requirement methods:

options.AddPolicy("ComprehensivePolicy", policy =>
{
    // Require authenticated user
    policy.RequireAuthenticatedUser();
    
    // Require specific claim
    policy.RequireClaim("Department", "HR", "Finance");
    
    // Require role
    policy.RequireRole("Manager", "Admin");
    
    // Require username
    policy.RequireUserName("alice@company.com");
    
    // Custom assertion
    policy.RequireAssertion(context =>
        context.User.HasClaim(c => c.Type == "BadgeNumber"));
});

Custom requirements:

// Requirement class
public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }
    
    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

// Handler
public class MinimumAgeHandler 
    : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dobClaim = context.User.FindFirst(
            c => c.Type == ClaimTypes.DateOfBirth);
        
        if (dobClaim == null)
            return Task.CompletedTask;
        
        var dateOfBirth = Convert.ToDateTime(dobClaim.Value);
        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;
    }
}

// Registration in Program.cs
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

๐Ÿ’ก Handler Tips:

  • Handlers should call context.Succeed(requirement) if the requirement is met
  • NEVER call context.Fail() - let other handlers run
  • Return Task.CompletedTask when done
  • Register handlers as singleton or scoped based on dependencies

Authorization in Different Contexts ๐ŸŒ

Razor Pages:

[Authorize(Policy = "AdminOnly")]
public class AdminPageModel : PageModel
{
    public void OnGet()
    {
        // Only accessible to users meeting AdminOnly policy
    }
}

Minimal APIs (.NET 10):

app.MapGet("/admin/users", () => "User list")
   .RequireAuthorization("AdminOnly");

app.MapPost("/documents", async (Document doc) => 
{
    // Create document
})
.RequireAuthorization(policy => 
    policy.RequireClaim("CanCreateDocuments"));

Blazor Components:

<AuthorizeView Policy="AdminOnly">
    <Authorized>
        <AdminPanel />
    </Authorized>
    <NotAuthorized>
        <p>You don't have access to this section.</p>
    </NotAuthorized>
</AuthorizeView>

SignalR Hubs:

[Authorize(Policy = "ChatUser")]
public class ChatHub : Hub
{
    [Authorize(Roles = "Moderator")]
    public async Task BanUser(string userId)
    {
        // Only moderators can ban
    }
}

Detailed Examples ๐Ÿ’ป

Example 1: Multi-Tier Access Control System ๐Ÿข

Let's build a document management system with three access levels:

// Models/Document.cs
public class Document
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public string Owner { get; set; }
    public SecurityLevel SecurityLevel { get; set; }
    public DateTime CreatedDate { get; set; }
}

public enum SecurityLevel
{
    Public = 0,
    Internal = 1,
    Confidential = 2,
    Secret = 3
}

// Program.cs - Policy setup
builder.Services.AddAuthorization(options =>
{
    // Basic authenticated user
    options.AddPolicy("ViewPublic", policy =>
        policy.RequireAuthenticatedUser());
    
    // Internal documents require employee claim
    options.AddPolicy("ViewInternal", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("EmployeeId");
    });
    
    // Confidential requires specific security clearance
    options.AddPolicy("ViewConfidential", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("SecurityClearance", "Level2", "Level3");
    });
    
    // Secret requires highest clearance
    options.AddPolicy("ViewSecret", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("SecurityClearance", "Level3");
        policy.RequireRole("SecretClearance");
    });
    
    // Edit requires ownership OR admin role
    options.AddPolicy("EditDocument", policy =>
    {
        policy.RequireAuthenticatedUser();
        // Custom requirement for resource-based auth
    });
});

// Authorization Handler
public class DocumentEditRequirement : IAuthorizationRequirement { }

public class DocumentEditHandler 
    : AuthorizationHandler<DocumentEditRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        DocumentEditRequirement requirement,
        Document resource)
    {
        var userName = context.User.Identity?.Name;
        
        // Owner can always edit
        if (resource.Owner == userName)
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }
        
        // Admins can edit anything
        if (context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }
        
        // Managers can edit documents in their department
        if (context.User.IsInRole("Manager"))
        {
            var userDept = context.User.FindFirst("Department")?.Value;
            var docDept = context.User.FindFirst("DocumentDepartment")?.Value;
            
            if (userDept == docDept)
            {
                context.Succeed(requirement);
            }
        }
        
        return Task.CompletedTask;
    }
}

// Controller implementation
public class DocumentsController : Controller
{
    private readonly IAuthorizationService _authService;
    private readonly IDocumentRepository _repo;
    
    public DocumentsController(
        IAuthorizationService authService,
        IDocumentRepository repo)
    {
        _authService = authService;
        _repo = repo;
    }
    
    public async Task<IActionResult> View(int id)
    {
        var document = await _repo.GetByIdAsync(id);
        if (document == null)
            return NotFound();
        
        // Determine required policy based on security level
        string requiredPolicy = document.SecurityLevel switch
        {
            SecurityLevel.Public => "ViewPublic",
            SecurityLevel.Internal => "ViewInternal",
            SecurityLevel.Confidential => "ViewConfidential",
            SecurityLevel.Secret => "ViewSecret",
            _ => "ViewPublic"
        };
        
        var authResult = await _authService
            .AuthorizeAsync(User, document, requiredPolicy);
        
        if (!authResult.Succeeded)
        {
            return Forbid();
        }
        
        return View(document);
    }
    
    [HttpPost]
    public async Task<IActionResult> Edit(int id, Document updatedDoc)
    {
        var document = await _repo.GetByIdAsync(id);
        if (document == null)
            return NotFound();
        
        var authResult = await _authService
            .AuthorizeAsync(User, document, "EditDocument");
        
        if (!authResult.Succeeded)
        {
            return Forbid();
        }
        
        // Update logic here
        document.Title = updatedDoc.Title;
        document.Content = updatedDoc.Content;
        await _repo.UpdateAsync(document);
        
        return RedirectToAction(nameof(View), new { id });
    }
}

What makes this example powerful:

  • โœ… Combines role-based, claims-based, and resource-based authorization
  • โœ… Scalable security levels
  • โœ… Separates authorization logic from business logic
  • โœ… Testable authorization handlers

Example 2: Time-Based Authorization ๐Ÿ•

Restrict access based on time windows (business hours, scheduled maintenance, etc.):

// Requirement
public class BusinessHoursRequirement : IAuthorizationRequirement
{
    public TimeSpan StartTime { get; }
    public TimeSpan EndTime { get; }
    public DayOfWeek[] AllowedDays { get; }
    
    public BusinessHoursRequirement(
        TimeSpan startTime, 
        TimeSpan endTime,
        params DayOfWeek[] allowedDays)
    {
        StartTime = startTime;
        EndTime = endTime;
        AllowedDays = allowedDays;
    }
}

// Handler
public class BusinessHoursHandler 
    : AuthorizationHandler<BusinessHoursRequirement>
{
    private readonly ISystemClock _clock;
    
    public BusinessHoursHandler(ISystemClock clock)
    {
        _clock = clock;
    }
    
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        BusinessHoursRequirement requirement)
    {
        var currentTime = _clock.UtcNow.LocalDateTime;
        var currentDay = currentTime.DayOfWeek;
        var currentTimeOfDay = currentTime.TimeOfDay;
        
        // Check if current day is allowed
        if (!requirement.AllowedDays.Contains(currentDay))
        {
            return Task.CompletedTask;
        }
        
        // Check if current time is within allowed window
        if (currentTimeOfDay >= requirement.StartTime && 
            currentTimeOfDay <= requirement.EndTime)
        {
            context.Succeed(requirement);
        }
        
        return Task.CompletedTask;
    }
}

// System clock abstraction (for testing)
public interface ISystemClock
{
    DateTimeOffset UtcNow { get; }
}

public class SystemClock : ISystemClock
{
    public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}

// Program.cs
builder.Services.AddSingleton<ISystemClock, SystemClock>();
builder.Services.AddSingleton<IAuthorizationHandler, BusinessHoursHandler>();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("BusinessHoursOnly", policy =>
    {
        policy.Requirements.Add(new BusinessHoursRequirement(
            startTime: new TimeSpan(9, 0, 0),   // 9 AM
            endTime: new TimeSpan(17, 0, 0),    // 5 PM
            DayOfWeek.Monday,
            DayOfWeek.Tuesday,
            DayOfWeek.Wednesday,
            DayOfWeek.Thursday,
            DayOfWeek.Friday
        ));
    });
    
    options.AddPolicy("MaintenanceWindow", policy =>
    {
        policy.RequireRole("Admin");
        policy.Requirements.Add(new BusinessHoursRequirement(
            startTime: new TimeSpan(2, 0, 0),   // 2 AM
            endTime: new TimeSpan(4, 0, 0),     // 4 AM
            DayOfWeek.Sunday
        ));
    });
});

// Usage
[Authorize(Policy = "BusinessHoursOnly")]
public class TradingController : Controller
{
    public IActionResult PlaceOrder()
    {
        // Only accessible during business hours
        return View();
    }
}

Use cases:

  • ๐Ÿ“ˆ Trading platforms (only during market hours)
  • ๐Ÿฆ Banking operations (business hours only)
  • ๐Ÿ”ง Maintenance windows (admin access during off-hours)
  • ๐ŸŽฎ Game servers (scheduled events)

Example 3: Hierarchical Role Authorization ๐Ÿ“Š

Implement role hierarchy where higher roles inherit lower role permissions:

// Role hierarchy definition
public class RoleHierarchy
{
    private static readonly Dictionary<string, int> _roleRanks = new()
    {
        { "User", 1 },
        { "PowerUser", 2 },
        { "Moderator", 3 },
        { "Manager", 4 },
        { "Admin", 5 },
        { "SuperAdmin", 6 }
    };
    
    public static bool IsInRoleOrHigher(ClaimsPrincipal user, string requiredRole)
    {
        if (!_roleRanks.TryGetValue(requiredRole, out int requiredRank))
            return false;
        
        foreach (var claim in user.Claims.Where(c => c.Type == ClaimTypes.Role))
        {
            if (_roleRanks.TryGetValue(claim.Value, out int userRank))
            {
                if (userRank >= requiredRank)
                    return true;
            }
        }
        
        return false;
    }
    
    public static int GetHighestRoleRank(ClaimsPrincipal user)
    {
        return user.Claims
            .Where(c => c.Type == ClaimTypes.Role)
            .Select(c => _roleRanks.TryGetValue(c.Value, out int rank) ? rank : 0)
            .DefaultIfEmpty(0)
            .Max();
    }
}

// Requirement
public class MinimumRoleRequirement : IAuthorizationRequirement
{
    public string MinimumRole { get; }
    
    public MinimumRoleRequirement(string minimumRole)
    {
        MinimumRole = minimumRole;
    }
}

// Handler
public class MinimumRoleHandler 
    : AuthorizationHandler<MinimumRoleRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumRoleRequirement requirement)
    {
        if (RoleHierarchy.IsInRoleOrHigher(context.User, requirement.MinimumRole))
        {
            context.Succeed(requirement);
        }
        
        return Task.CompletedTask;
    }
}

// Program.cs
builder.Services.AddSingleton<IAuthorizationHandler, MinimumRoleHandler>();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("MinimumModerator", policy =>
        policy.Requirements.Add(new MinimumRoleRequirement("Moderator")));
    
    options.AddPolicy("MinimumManager", policy =>
        policy.Requirements.Add(new MinimumRoleRequirement("Manager")));
});

// Usage
[Authorize(Policy = "MinimumModerator")]
public class ContentModerationController : Controller
{
    // Accessible by Moderator, Manager, Admin, SuperAdmin
    public IActionResult ReviewContent()
    {
        var userRank = RoleHierarchy.GetHighestRoleRank(User);
        ViewBag.RoleRank = userRank;
        return View();
    }
}

Benefits of role hierarchy:

  • โœ… Reduces policy duplication
  • โœ… Natural organizational structure
  • โœ… Easier to reason about permissions
  • โœ… Single source of truth for role ranks

Example 4: Feature Flag Authorization ๐Ÿšฉ

Control access to features based on flags (for A/B testing, gradual rollouts, etc.):

// Feature flag service
public interface IFeatureFlagService
{
    Task<bool> IsEnabledAsync(string featureName, ClaimsPrincipal user);
}

public class FeatureFlagService : IFeatureFlagService
{
    private readonly Dictionary<string, FeatureFlag> _flags = new()
    {
        ["NewDashboard"] = new FeatureFlag 
        { 
            Name = "NewDashboard",
            EnabledForRoles = new[] { "Admin", "BetaTester" },
            RolloutPercentage = 20 // 20% of other users
        },
        ["AdvancedSearch"] = new FeatureFlag
        {
            Name = "AdvancedSearch",
            EnabledForAll = false,
            EnabledForClaims = new Dictionary<string, string[]>
            {
                ["SubscriptionTier"] = new[] { "Premium", "Enterprise" }
            }
        }
    };
    
    public Task<bool> IsEnabledAsync(string featureName, ClaimsPrincipal user)
    {
        if (!_flags.TryGetValue(featureName, out var flag))
            return Task.FromResult(false);
        
        // Feature enabled for everyone
        if (flag.EnabledForAll)
            return Task.FromResult(true);
        
        // Check role-based enabling
        if (flag.EnabledForRoles?.Any() == true)
        {
            foreach (var role in flag.EnabledForRoles)
            {
                if (user.IsInRole(role))
                    return Task.FromResult(true);
            }
        }
        
        // Check claim-based enabling
        if (flag.EnabledForClaims?.Any() == true)
        {
            foreach (var kvp in flag.EnabledForClaims)
            {
                var userClaim = user.FindFirst(kvp.Key);
                if (userClaim != null && kvp.Value.Contains(userClaim.Value))
                    return Task.FromResult(true);
            }
        }
        
        // Rollout percentage
        if (flag.RolloutPercentage > 0)
        {
            var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            if (!string.IsNullOrEmpty(userId))
            {
                var hash = Math.Abs(userId.GetHashCode());
                var bucket = hash % 100;
                if (bucket < flag.RolloutPercentage)
                    return Task.FromResult(true);
            }
        }
        
        return Task.FromResult(false);
    }
}

public class FeatureFlag
{
    public string Name { get; set; }
    public bool EnabledForAll { get; set; }
    public string[] EnabledForRoles { get; set; }
    public Dictionary<string, string[]> EnabledForClaims { get; set; }
    public int RolloutPercentage { get; set; }
}

// Authorization requirement
public class FeatureEnabledRequirement : IAuthorizationRequirement
{
    public string FeatureName { get; }
    
    public FeatureEnabledRequirement(string featureName)
    {
        FeatureName = featureName;
    }
}

// Handler
public class FeatureEnabledHandler 
    : AuthorizationHandler<FeatureEnabledRequirement>
{
    private readonly IFeatureFlagService _featureFlagService;
    
    public FeatureEnabledHandler(IFeatureFlagService featureFlagService)
    {
        _featureFlagService = featureFlagService;
    }
    
    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        FeatureEnabledRequirement requirement)
    {
        var isEnabled = await _featureFlagService
            .IsEnabledAsync(requirement.FeatureName, context.User);
        
        if (isEnabled)
        {
            context.Succeed(requirement);
        }
    }
}

// Program.cs
builder.Services.AddSingleton<IFeatureFlagService, FeatureFlagService>();
builder.Services.AddSingleton<IAuthorizationHandler, FeatureEnabledHandler>();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("NewDashboardFeature", policy =>
        policy.Requirements.Add(new FeatureEnabledRequirement("NewDashboard")));
    
    options.AddPolicy("AdvancedSearchFeature", policy =>
        policy.Requirements.Add(new FeatureEnabledRequirement("AdvancedSearch")));
});

// Controller usage
public class DashboardController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
    
    [Authorize(Policy = "NewDashboardFeature")]
    public IActionResult NewDashboard()
    {
        return View();
    }
}

// View usage
@inject IAuthorizationService AuthService

@if ((await AuthService.AuthorizeAsync(User, "NewDashboardFeature")).Succeeded)
{
    <a href="/dashboard/newdashboard">Try New Dashboard Beta</a>
}

Real-world applications:

  • ๐Ÿงช A/B testing new features
  • ๐Ÿ“ˆ Gradual feature rollouts
  • ๐Ÿ’ฐ Premium feature gating
  • ๐Ÿ› Kill switches for problematic features

Common Mistakes โš ๏ธ

1. Forgetting to Register Handlers

โŒ Wrong:

// Defined custom requirement and handler
public class CustomRequirement : IAuthorizationRequirement { }
public class CustomHandler : AuthorizationHandler<CustomRequirement> { }

// But forgot to register!
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("MyPolicy", policy =>
        policy.Requirements.Add(new CustomRequirement()));
});
// Handler never runs!

โœ… Correct:

builder.Services.AddSingleton<IAuthorizationHandler, CustomHandler>();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("MyPolicy", policy =>
        policy.Requirements.Add(new CustomRequirement()));
});

2. Using Authorization Before Authentication

โŒ Wrong:

app.UseAuthorization();
app.UseAuthentication(); // TOO LATE!

โœ… Correct:

app.UseAuthentication(); // FIRST!
app.UseAuthorization();  // THEN!

๐Ÿ’ก Remember: "Authentication before Authorization" (alphabetically too!)

3. Calling context.Fail() in Handlers

โŒ Wrong:

protected override Task HandleRequirementAsync(
    AuthorizationHandlerContext context,
    CustomRequirement requirement)
{
    if (someCondition)
    {
        context.Succeed(requirement);
    }
    else
    {
        context.Fail(); // DON'T DO THIS!
    }
    return Task.CompletedTask;
}

โœ… Correct:

protected override Task HandleRequirementAsync(
    AuthorizationHandlerContext context,
    CustomRequirement requirement)
{
    if (someCondition)
    {
        context.Succeed(requirement);
    }
    // Just return - don't call Fail()!
    return Task.CompletedTask;
}

Why? Multiple handlers might evaluate the same requirement. Calling Fail() prevents other handlers from succeeding.

4. Not Checking for Null Claims

โŒ Wrong:

var age = int.Parse(context.User.FindFirst("Age").Value);
// NullReferenceException if claim doesn't exist!

โœ… Correct:

var ageClaim = context.User.FindFirst("Age");
if (ageClaim == null)
    return Task.CompletedTask;

if (!int.TryParse(ageClaim.Value, out int age))
    return Task.CompletedTask;

// Now safely use age

5. Mixing Up Roles and Policies

โŒ Wrong:

[Authorize(Policy = "Admin")] // "Admin" is a role, not a policy!
public class AdminController : Controller { }

โœ… Correct:

[Authorize(Roles = "Admin")] // For roles
// OR
[Authorize(Policy = "RequireAdmin")] // For policies
public class AdminController : Controller { }

6. Not Handling Resource-Based Auth Correctly

โŒ Wrong:

[Authorize] // Can't use attribute for resource-based auth!
public async Task<IActionResult> Edit(int id)
{
    var doc = await _repo.GetByIdAsync(id);
    // User might not be allowed to edit THIS specific document
    return View(doc);
}

โœ… Correct:

[Authorize] // Check authentication only
public async Task<IActionResult> Edit(int id)
{
    var doc = await _repo.GetByIdAsync(id);
    if (doc == null) return NotFound();
    
    // Check authorization for THIS specific resource
    var authResult = await _authService
        .AuthorizeAsync(User, doc, "EditPolicy");
    
    if (!authResult.Succeeded)
        return Forbid();
    
    return View(doc);
}

7. Over-Complicating Simple Scenarios

โŒ Wrong (overkill for simple role check):

public class AdminRequirement : IAuthorizationRequirement { }

public class AdminHandler : AuthorizationHandler<AdminRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        AdminRequirement requirement)
    {
        if (context.User.IsInRole("Admin"))
            context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

builder.Services.AddSingleton<IAuthorizationHandler, AdminHandler>();
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminPolicy", policy =>
        policy.Requirements.Add(new AdminRequirement()));
});

โœ… Correct (use built-in role check):

[Authorize(Roles = "Admin")]
// Or if you prefer policy syntax:
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminPolicy", policy =>
        policy.RequireRole("Admin"));
});

๐Ÿ’ก Tip: Use custom requirements/handlers only when you need complex logic. For simple role/claim checks, use built-in methods.

Key Takeaways ๐ŸŽฏ

โœ… Authorization happens AFTER authentication - you must know who the user is before determining what they can do

โœ… Choose the right strategy:

  • Role-Based: Simple scenarios with clear roles (Admin, User, Manager)
  • Claims-Based: Attribute-based decisions (department, age, subscription tier)
  • Policy-Based: Complex rules, reusable authorization logic
  • Resource-Based: Ownership checks, per-resource permissions

โœ… Policies are preferred over roles for anything beyond trivial authorization

โœ… Authorization handlers should:

  • Call context.Succeed(requirement) when satisfied
  • NEVER call context.Fail()
  • Return Task.CompletedTask
  • Check for null claims

โœ… Register handlers in DI container or they won't run

โœ… Middleware order matters: Authentication โ†’ Authorization โ†’ Endpoint execution

โœ… Use IAuthorizationService programmatically for resource-based authorization

โœ… Combine strategies for sophisticated access control

โœ… Test authorization logic separately from controllers

๐Ÿง  Memory Device - PARC:

  • Policies (most flexible)
  • Attributes (declarative)
  • Resource-based (ownership)
  • Claims (characteristics)

๐Ÿ“‹ Quick Reference Card

๐Ÿ“‹ Authorization Strategies Cheat Sheet

Strategy When to Use Syntax
Role-Based Simple role membership [Authorize(Roles = "Admin")]
Claims-Based User attributes/properties policy.RequireClaim("Department")
Policy-Based Complex/reusable rules [Authorize(Policy = "MyPolicy")]
Resource-Based Ownership/per-resource permissions await _authService.AuthorizeAsync(User, resource, "Policy")

Common Policy Methods

RequireAuthenticatedUser() Any authenticated user
RequireRole("Admin") Specific role
RequireClaim("Type", "Value") Specific claim value
RequireUserName("alice@co.com") Specific user
RequireAssertion(ctx => ...) Custom logic

Middleware Order

1. app.UseAuthentication();
2. app.UseAuthorization();
3. app.MapControllers(); / app.MapRazorPages();

Custom Handler Template

public class MyRequirement : IAuthorizationRequirement { }

public class MyHandler : AuthorizationHandler<MyRequirement> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, MyRequirement requirement) { if (/* condition */) context.Succeed(requirement);

    return Task.CompletedTask;
}

}

// Register: builder.Services.AddSingleton<IAuthorizationHandler, MyHandler>();

๐Ÿ“š Further Study

  1. Microsoft Docs - ASP.NET Core Authorization: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/introduction
  2. Policy-Based Authorization Deep Dive: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies
  3. Resource-Based Authorization Guide: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased

๐Ÿ”” Pro Tip: Start with simple role-based authorization and migrate to policies as your requirements grow. Don't over-engineer authorization earlyโ€”you can always refactor later!

๐Ÿ’ก Next Steps: Practice building custom authorization handlers, experiment with combining multiple requirements, and explore authorization in Blazor and Minimal APIs for a complete understanding of .NET 10 authorization.

Practice Questions

Test your understanding with these questions:

Q1: Complete the attribute to restrict access to Admin role: ```csharp [{{1}}(Roles = "Admin")] public class AdminController : Controller { public IActionResult Dashboard() => View(); } ```
A: Authorize
Q2: What method call is missing to enable authorization in this middleware pipeline? ```csharp app.UseAuthentication(); app.{{1}}(); app.MapControllers(); ```
A: UseAuthorization
Q3: What does this authorization policy code output when a user with SecurityClearance claim "Level2" tries to access it? ```csharp builder.Services.AddAuthorization(options => { options.AddPolicy("HighSecurity", policy => policy.RequireClaim("SecurityClearance", "Level3")); }); [Authorize(Policy = "HighSecurity")] public IActionResult SecretData() => View(); ``` A. Returns 200 OK and shows the view B. Returns 401 Unauthorized C. Returns 403 Forbidden D. Returns 500 Internal Server Error E. Returns 404 Not Found
A: C
Q4: Fill in the blanks for a custom authorization requirement: ```csharp public class AgeRequirement : {{1}} { public int MinAge { get; set; } } public class AgeHandler : {{2}}<AgeRequirement> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, AgeRequirement requirement) { // logic here return Task.CompletedTask; } } ```
A: ["IAuthorizationRequirement","AuthorizationHandler"]
Q5: What is the correct method to check authorization programmatically in resource-based authorization?
A: AuthorizeAsync