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 nameClaimTypes.Email- Email addressClaimTypes.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
IAuthorizationServiceprogrammatically - 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:
| Pattern | Use Case | Example |
|---|---|---|
| Role + Policy | Base role with additional requirements | Manager with budget approval authority |
| Policy + Resource | General permissions + specific ownership | Editor role who can only edit their articles |
| Claims + Resource | Attribute-based + ownership | Department 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.CompletedTaskwhen 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
- Microsoft Docs - ASP.NET Core Authorization: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/introduction
- Policy-Based Authorization Deep Dive: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies
- 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.