Tutorials ASP.NET Core MVC Mastery
Authorize Filters
On this page
Authorization Filters in ASP.NET Core — Guarding the Gates
Authentication answers "Who are you?". Authorization answers "Are you allowed to do this?". In ASP.NET Core MVC, Authorization Filters are the ultimate security checkpoint—running earlier than any other filter in the pipeline to immediately reject unprivileged requests before they hit controllers, validation, or database logic.
1. WHAT Are Authorization Filters?
In the ASP.NET Core Filter Pipeline, Authorization Filters (IAsyncAuthorizationFilter) are the very first filters to execute. If an authorization filter rejects a request (e.g., setting the result to ForbidResult or ChallengeResult), the entire rest of the pipeline is short-circuited. Your model binding won't run, action filters won't run, and the controller action definitely won't run.
The [Authorize] Attribute
The [Authorize] attribute is the built-in implementation of an Authorization Filter. You can apply it at the Controller level (guarding all actions) or Action level. Use [AllowAnonymous] to carve out public paths (like login pages) inside an authorized controller.
2. REAL-TIME PRODUCTION EXAMPLES
Example 1: Role-Based Authorization
The simplest form is declarative role-checking. The framework automatically parses the user's claims to check for specific roles.
[Authorize(Roles = "Admin,SuperUser")] // OR logic: Admin OR SuperUser
public class DashboardController : Controller
{
public IActionResult Index() => View();
// Requires the user to have BOTH Admin AND Finance roles
[Authorize(Roles = "Admin")]
[Authorize(Roles = "Finance")]
public IActionResult Revenue() => View();
// Overrides class-level Authorize
[AllowAnonymous]
public IActionResult PublicStatus() => View();
}
Example 2: Policy-Based Authorization (Enterprise Standard)
Role-based authorization breaks down in complex domains (e.g., "User can edit a document only if they are the author AND the document is unlocked"). Policies encapsulate complex logic.
// 1. Program.cs - Define Policies
builder.Services.AddAuthorization(options =>
{
// Simple Policy: Must have specific claim
options.AddPolicy("RequireEmployeeId", policy =>
policy.RequireClaim("EmployeeId"));
// Complex Policy: Must be Admin OR be in HR department and be over 21
options.AddPolicy("CanViewPayroll", policy =>
policy.RequireAssertion(context =>
context.User.IsInRole("Admin") ||
(context.User.HasClaim("Department", "HR") &&
context.User.HasClaim("AgeCategory", "Over21"))
));
});
// 2. Controller - Apply the policy cleanly
[Authorize(Policy = "CanViewPayroll")]
public IActionResult Payroll() => View();
Example 3: Custom IAsyncAuthorizationFilter
If you need to validate authorization against a live database (e.g., checking if the user's API key is actively billed/unlocked), you must build a custom filter.
public class SubscriptionCheckFilter : IAsyncAuthorizationFilter
{
private readonly ISubscriptionService _subscription;
public SubscriptionCheckFilter(ISubscriptionService subscription)
=> _subscription = subscription;
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;
if (!user.Identity.IsAuthenticated) return;
// Extract tenant ID from route or user claims
var tenantId = user.FindFirst("TenantId")?.Value;
// Custom database authorization logic
var isActive = await _subscription.IsTenantActiveAsync(tenantId);
if (!isActive)
{
// Short-circuit the request immediately with a 403 Forbidden
context.Result = new ForbidResult();
// Alternative: Redirect to payment page
// context.Result = new RedirectToActionResult("Billing", "Tenant", null);
}
}
}
// Applying the custom filter
[TypeFilter(typeof(SubscriptionCheckFilter))]
public IActionResult PremiumFeature() => View();
3. Challenge vs Forbid
Challenge (401 Unauthorized)
Occurs when the user is not authenticated at all (not logged in). The authentication scheme intercepts this 401 and usually redirects the user to the /Login page.
Forbid (403 Forbidden)
Occurs when the user is logged in, but does not have the required Role/Policy. The framework returns a 403 or redirects to an "Access Denied" view. They are not sent clearly to login again, because they already are.
4. Best Practices
- Use Policies over Roles. Hardcoding roles like
[Authorize(Roles="Manager")]makes code brittle. Encapsulate it in a Policy ([Authorize(Policy="CanApproveExpense")]). Later, you can map the Manager role to that policy without touching controller code. - Global Authorization: In modern APIs, apply authorization globally in
Program.csviaRequireAuthorization()or Fallback Policies, and use[AllowAnonymous]explicitly for the few public routes. It prevents accidental data leaks. - Keep filters fast: Authorization filters run on every single matching request. If you do a database lookup inside an auth filter, ensure it is heavily cached (e.g., Redis or in-memory array).
5. Interview Mastery
Q: "If an Authorization Filter and an Action Filter both throw exceptions, which order do they execute?"
Architect Answer: "Authorization Filters always run first in the MVC pipeline, immediately after Routing. If an Authorization filter (like `[Authorize]`) denies the request, it sets a Result to short-circuit the pipeline. Action Filters, Resource Filters, and Model Binding will completely skip execution. This is a critical security and performance optimization — you don't want the server expending CPU cycles binding a massive JSON payload into complex C# objects if the user doesn't even hold the appropriate JWT token to execute the action."