Resource-based Auth
Resource-Based Authorization
Resource-based authorization checks whether the current user has permission to perform an action on a specific resource instance — not just a resource type. It answers: "Can this user edit this specific document?" rather than "Can this user edit documents?"
When to Use
Role-based authorization ([Authorize(Roles = "Admin")]) checks what type of user you are. Resource-based authorization checks ownership or relationship to a specific resource. Use it when authorization depends on data — for example, only the document owner can edit it.
ASP.NET Core Implementation
// 1. Define a requirement
public class DocumentOwnerRequirement : IAuthorizationRequirement { }
// 2. Implement the handler
public class DocumentOwnerHandler : AuthorizationHandler<DocumentOwnerRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DocumentOwnerRequirement requirement,
Document resource)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (resource.OwnerId == userId)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
// 3. Register in DI
builder.Services.AddSingleton<IAuthorizationHandler, DocumentOwnerHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("DocumentOwner", policy =>
policy.Requirements.Add(new DocumentOwnerRequirement()));
});
// 4. Use in a controller
public async Task<IActionResult> Edit(int id)
{
var document = await _repo.GetAsync(id);
var authResult = await _authorizationService.AuthorizeAsync(User, document, "DocumentOwner");
if (!authResult.Succeeded) return Forbid();
// proceed with edit
}
Testing Authorization Handlers
Authorization handlers are plain classes and easy to unit test without spinning up ASP.NET Core:
// Unit test for DocumentOwnerHandler
public class DocumentOwnerHandlerTests
{
[Fact]
public async Task Succeeds_WhenUserIsOwner()
{
var handler = new DocumentOwnerHandler();
var userId = "user-123";
var document = new Document { OwnerId = userId };
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId) };
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
var requirement = new DocumentOwnerRequirement();
var context = new AuthorizationHandlerContext(
new[] { requirement }, user, document);
await handler.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Fact]
public async Task Fails_WhenUserIsNotOwner()
{
var handler = new DocumentOwnerHandler();
var document = new Document { OwnerId = "other-user" };
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, "user-123") };
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
var requirement = new DocumentOwnerRequirement();
var context = new AuthorizationHandlerContext(
new[] { requirement }, user, document);
await handler.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
}
Pitfalls
Missing Authorization Check After Fetching Resource
What goes wrong: the controller fetches the resource and returns it without checking ownership. Any authenticated user can access any resource by guessing the ID (Insecure Direct Object Reference, OWASP A01).
Why it happens: authorization is added as an afterthought, or developers assume role-based checks are sufficient.
Mitigation: always call IAuthorizationService.AuthorizeAsync(User, resource, policy) after fetching the resource and before returning it. Return 403 Forbidden (not 404 Not Found) when the resource exists but the user lacks permission — unless you want to hide resource existence.
Returning 404 vs 403
What goes wrong: returning 404 Not Found for unauthorized access hides resource existence but can confuse legitimate users who have the wrong ID.
Decision rule: return 403 Forbidden when the resource exists and the user is authenticated but lacks permission. Return 404 Not Found only when you intentionally want to hide resource existence from unauthorized users (e.g., private content).
Questions
Role-based authorization checks what type of user you are (e.g., Admin, Editor). Resource-based authorization checks your relationship to a specific resource instance (e.g., are you the owner of this document?). Use role-based for coarse-grained access control; use resource-based when the decision depends on data.
IAuthorizationService instead of checking ownership in the controller directly?
IAuthorizationService centralizes authorization logic in handlers, making it testable and reusable across controllers. Direct ownership checks in controllers scatter authorization logic, making it easy to miss a check or apply it inconsistently. The handler pattern also supports multiple requirements composing into a single policy.
References
- Microsoft — Resource-based authorization in ASP.NET Core — official guide with full implementation example including handler registration and controller usage
- Microsoft — Policy-based authorization — how to define and use authorization policies; covers requirement composition and handler ordering
- OWASP — Broken Access Control (A01:2021) — OWASP's top vulnerability category; Insecure Direct Object Reference (IDOR) is the canonical resource-based auth failure mode
- Microsoft — Authorization in ASP.NET Core (overview) — covers the full authorization model: simple, role-based, claims-based, and resource-based