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.

// 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

References


Whats next