How to properly control access in an ASP.NET Core application
Series: Security by Design in .NET: From JWT to Certificate-based Authentication
Authentication answers the question:
βWho is the user?β
Authorization answers the question:
βIs the user allowed to perform this action?β
In ASP.NET Core there are several ways to implement authorization:
-
Role-based authorization
-
Policy-based authorization
-
Resource-based authorization
Used correctly, these mechanisms can create a very flexible permission system.
π§© 1οΈβ£ Role-based Authorization
Role-based authorization is the simplest model.
Access is controlled through roles such as:
-
Admin
-
Manager
-
User
-
Moderator
In ASP.NET Core the attribute used is:
[Authorize(Roles = "Admin")]
Example:
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteUser(Guid id)
{
await _userService.DeleteUser(id);
return Ok();
}
Only users with the Admin role can access the endpoint.
Multiple roles
[Authorize(Roles = "Admin,Manager")]
The user must have at least one of the roles.
Disadvantages of Role-based
Role-based quickly becomes limited:
Common problems:
β too many roles
β hardcoded roles
β complex business logic
β lack of flexibility
Real example:
βThe user can edit the invoice only if it belongs to their company.β
Role-based does not solve this.
π‘οΈ 2οΈβ£ Policy-based Authorization
Policy-based authorization is much more flexible.
Instead of simple roles, we define security policies.
Configuration in Program.cs:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdmin", policy =>
policy.RequireRole("Admin"));
});
Usage:
[Authorize(Policy = "RequireAdmin")]
Policies based on claims
JWT can contain claims:
{
"role": "Admin",
"department": "Finance"
}
We define a policy:
options.AddPolicy("FinanceOnly",
policy => policy.RequireClaim("department", "Finance"));
Endpoint:
[Authorize(Policy = "FinanceOnly")]
π§ 3οΈβ£ Resource-based Authorization
Sometimes access depends on the accessed resource.
Example:
A user can only edit invoices of their own company.
This cannot be solved with roles.
The object must be verified.
Example:
public async Task<bool> CanEditInvoice(User user, Invoice invoice)
{
return user.CompanyId == invoice.CompanyId;
}
ASP.NET Core allows integrating this logic into the authorization system.
βοΈ 4οΈβ£ Authorization Handlers
Authorization Handlers allow custom logic.
We define a requirement:
public class InvoiceOwnerRequirement : IAuthorizationRequirement
{
}
Handler:
public class InvoiceOwnerHandler
: AuthorizationHandler<InvoiceOwnerRequirement, Invoice>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
InvoiceOwnerRequirement requirement,
Invoice resource)
{
var userCompanyId = context.User.FindFirst("companyId")?.Value;
if (resource.CompanyId.ToString() == userCompanyId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Registering the handler
builder.Services.AddScoped<
IAuthorizationHandler,
InvoiceOwnerHandler>();
Usage
await _authorizationService.AuthorizeAsync(
User,
invoice,
new InvoiceOwnerRequirement());
If the check passes:
β access granted.
π§± Recommended Architecture
In enterprise applications:
β Role-based for general access
β Policy-based for rules
β Resource-based for objects
Example:
Admin β general access
Policy β FinanceOnly
Resource β invoice belongs to the company
π¨ Common Mistakes
1οΈβ£ Only roles everywhere
2οΈβ£ Hardcoded roles in code
3οΈβ£ Business logic in controllers
4οΈβ£ No resource ownership verification
5οΈβ£ Authorization done only in UI
Important rule:
Authorization must be done on the server.
π― Conclusion
Role-based authorization is useful for simple scenarios.
But real applications need:
β policies
β handlers
β resource-based authorization
ASP.NET Core provides a very powerful system for this.
If used correctly, you can build an enterprise permission model.