Introduction
Authentication answers the question, “Who is making this request?” Authorization answers a different question: “Is this identity allowed to perform this operation?”
ASP.NET Core’s policy-based authorization system provides a flexible way to express requirements beyond simple role checks. It can enforce permissions, account status, tenant boundaries, and other application-specific rules.
Authentication Comes First
Configure authentication using the mechanism appropriate for the application, such as cookies or bearer tokens. Authorization policies then evaluate the authenticated principal and its claims.
For example, a controller can require an authenticated user:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[Authorize]
[ApiController]
[Route("api/reports")]
public class ReportsController : ControllerBase
{
[HttpGet]
public IActionResult GetReports()
{
return Ok();
}
}
This requires authentication but does not define which authenticated users can view reports.
Define a Policy
Policies let you name a requirement and apply it consistently.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("CanViewReports", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("permission", "reports.read");
});
});
Apply the policy to an endpoint:
[Authorize(Policy = "CanViewReports")]
[HttpGet]
public IActionResult GetReports()
{
return Ok();
}
The permission claim must come from a trusted authentication process. Never accept a client-supplied request field as proof of authorization.
Use Custom Requirements for Business Rules
Some rules require more than a single claim. For example, access might depend on whether a user’s account is active.
A custom requirement can represent the rule:
using Microsoft.AspNetCore.Authorization;
public sealed class ActiveAccountRequirement
: IAuthorizationRequirement
{
}
A handler evaluates it:
using Microsoft.AspNetCore.Authorization;
public sealed class ActiveAccountHandler
: AuthorizationHandler<ActiveAccountRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ActiveAccountRequirement requirement)
{
var active = context.User.HasClaim(
"account_status",
"active"
);
if (active)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Register the handler and policy:
builder.Services.AddSingleton<
IAuthorizationHandler,
ActiveAccountHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ActiveAccount", policy =>
{
policy.RequireAuthenticatedUser();
policy.AddRequirements(
new ActiveAccountRequirement());
});
});
This example uses a claim for simplicity. If account status changes frequently, a claim inside a long-lived token may become stale. Depending on the security requirements, the application may need to check current account state in a trusted data source.
Authorize Individual Resources
A user may have permission to read reports generally but still be prohibited from accessing another tenant’s report.
This is resource-based authorization. Load the resource, then evaluate access to that specific resource.
var report = await repository.FindAsync(
reportId,
cancellationToken
);
if (report is null)
{
return NotFound();
}
var result = await authorizationService.AuthorizeAsync(
User,
report,
"CanReadReport"
);
if (!result.Succeeded)
{
return Forbid();
}
return Ok(report);
This example assumes the repository method and CanReadReport policy are defined elsewhere. A real handler should compare the report’s ownership or tenant information against trusted identity data.
Depending on your security policy, you may return NotFound instead of Forbid when a resource must not be discoverable. Make that decision consistently.
Keep Policies Understandable
Prefer small, descriptive policies that match meaningful capabilities, such as CanManageUsers, CanExportReports, CanEditProject, and ActiveAccount.
Resource-specific decisions should remain close to the logic that loads and authorizes the resource.
Hiding a button in the frontend is not authorization. Every protected API operation must enforce access rules on the server.
Test Authorization Paths
Test that anonymous users cannot access protected endpoints, users without the required permission are denied, authorized users can proceed, and users cannot access resources belonging to another tenant.
Also verify how revoked or stale permissions behave according to your security requirements.
Integration tests are especially useful because they exercise authentication, routing, middleware, and authorization together.
Conclusion
Policy-based authorization makes access rules explicit and reusable. Use claim requirements for straightforward permissions, custom handlers for richer rules, and resource-based authorization for ownership or tenant checks.
Most importantly, authorize the specific operation and resource on the server. A valid identity is not automatically entitled to every piece of application data.