Resource-based Authorization
Overview
Resource-based authorization makes authorization decisions based on the specific resource being accessed, not just user roles or policies.
When to Use
- User can only edit their own posts
- Document owners can delete documents
- Team members can access team resources
- Multi-tenant applications
Implementation
1. Create Requirement
public class SameAuthorRequirement : IAuthorizationRequirement { }
2. Create Handler
public class DocumentAuthorizationHandler :
AuthorizationHandler<SameAuthorRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
SameAuthorRequirement requirement,
Document resource)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userId == resource.AuthorId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
3. Register Handler
builder.Services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();
4. Use IAuthorizationService
app.MapPut("/api/documents/{id}", async (
int id,
Document updatedDocument,
IDocumentService documentService,
IAuthorizationService authorizationService,
HttpContext context) =>
{
var document = await documentService.GetByIdAsync(id);
if (document == null)
return Results.NotFound();
var authResult = await authorizationService.AuthorizeAsync(
context.User,
document,
new SameAuthorRequirement());
if (!authResult.Succeeded)
return Results.Forbid();
await documentService.UpdateAsync(document);
return Results.Ok(document);
}).RequireAuthorization();
Operations-based Authorization
public static class Operations
{
public static OperationAuthorizationRequirement Create =
new() { Name = nameof(Create) };
public static OperationAuthorizationRequirement Read =
new() { Name = nameof(Read) };
public static OperationAuthorizationRequirement Update =
new() { Name = nameof(Update) };
public static OperationAuthorizationRequirement Delete =
new() { Name = nameof(Delete) };
}
public class DocumentAuthorizationHandler :
AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Document resource)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (requirement.Name == Operations.Read.Name)
{
context.Succeed(requirement); // Anyone can read
}
else if (requirement.Name == Operations.Update.Name ||
requirement.Name == Operations.Delete.Name)
{
if (userId == resource.AuthorId)
context.Succeed(requirement);
}
else if (requirement.Name == Operations.Create.Name)
{
if (context.User.Identity.IsAuthenticated)
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// Usage
var authResult = await authorizationService.AuthorizeAsync(
User, document, Operations.Update);
Real-World Example
// Controller
[ApiController]
[Route("api/[controller]")]
public class PostsController : ControllerBase
{
private readonly IPostService _postService;
private readonly IAuthorizationService _authorizationService;
public PostsController(
IPostService postService,
IAuthorizationService authorizationService)
{
_postService = postService;
_authorizationService = authorizationService;
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, Post updatedPost)
{
var post = await _postService.GetByIdAsync(id);
if (post == null)
return NotFound();
var authResult = await _authorizationService.AuthorizeAsync(
User, post, Operations.Update);
if (!authResult.Succeeded)
return Forbid();
await _postService.UpdateAsync(updatedPost);
return Ok(updatedPost);
}
}