Skip to main content

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);
}
}

Resources