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.
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 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.
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();
}
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();
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();
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.
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.
[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.Program.cs via RequireAuthorization() or Fallback Policies, and use [AllowAnonymous] explicitly for the few public routes. It prevents accidental data leaks.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."