While Role-Based and Policy-Based authorization evaluate permissions globally or against user claims, many enterprise applications require finer control: Resource-Based Authorization.

Resource-based authorization answers the question: Does the currently authenticated user have permission to modify or delete this specific database record? For example, a standard user should be allowed to edit their own blog post or profile, but not another user's.

This comprehensive guide covers how to implement resource-based authorization from scratch in ASP.NET Core MVC.

Step 1: Define the Domain Model and Database Context

Imagine an application where users can create and edit articles. Every article is tied to a specific user via an OwnerId string property (matching the ASP.NET Core Identity user identifier).

Create Models/Article.cs:

C#

namespace MvcAuthenticationDemo.Models
{
    public class Article
    {
        public int Id { get; set; }
        public string Title { get; set; } = string.Empty;
        public string Content { get; set; } = string.Empty;
        public string OwnerId { get; set; } = string.Empty; // User ID who created the article
    }
}

Step 2: Define Operations and the Authorization Requirement

Instead of creating separate policies for every action, resource-based authorization uses a combination of an operation requirement and the resource instance itself.

Create Authorization/ArticleOperations.cs and Authorization/ArticleAuthorizationRequirement.cs:

C#

using Microsoft.AspNetCore.Authorization;

namespace MvcAuthenticationDemo.Authorization
{
    public static class ArticleOperations
    {
        public static readonly string Update = "Update";
        public static readonly string Delete = "Delete";
    }

    public class ArticleAuthorizationRequirement : IAuthorizationRequirement
    {
        public string Name { get; }

        public ArticleAuthorizationRequirement(string name)
        {
            Name = name;
        }
    }
}

Step 3: Implement the Custom Authorization Handler

The authorization handler inspects the target resource (Article) and compares its OwnerId against the current user's ID. It can also include overrides, such as allowing users in the Admin role to bypass ownership checks.

Create Authorization/ArticleAuthorizationHandler.cs:

C#

using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using MvcAuthenticationDemo.Models;

namespace MvcAuthenticationDemo.Authorization
{
    public class ArticleAuthorizationHandler : AuthorizationHandler<ArticleAuthorizationRequirement, Article>
    {
        protected override Task HandleRequirementAsync(
            AuthorizationHandlerContext context,
            ArticleAuthorizationRequirement requirement,
            Article resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // 1. Administrators can manage any article regardless of ownership
            if (context.User.IsInRole("Admin"))
            {
                context.Succeed(requirement);
                return Task.CompletedTask;
            }

            // 2. Extract the current logged-in user's ID
            var currentUserId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);

            // 3. Check if the user owns the record and is attempting a valid operation
            if (resource.OwnerId == currentUserId && 
                (requirement.Name == ArticleOperations.Update || requirement.Name == ArticleOperations.Delete))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Step 4: Register the Handler in Program.cs

Register your custom resource authorization handler into the dependency injection container alongside your existing services:

C#

using Microsoft.AspNetCore.Authorization;
using MvcAuthenticationDemo.Authorization;

// Register the custom resource-based authorization handler
builder.Services.AddScoped<IAuthorizationHandler, ArticleAuthorizationHandler>();

Step 5: Enforcing Resource Authorization Inside Controllers

Because resource-based authorization depends on loading the specific record from the database first, you evaluate it imperatively inside your controller actions using IAuthorizationService instead of declarative attributes.

Create or update your ArticlesController.cs:

C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MvcAuthenticationDemo.Authorization;
using MvcAuthenticationDemo.Data;
using MvcAuthenticationDemo.Models;

[Authorize]
public class ArticlesController : Controller
{
    private readonly ApplicationDbContext _context;
    private readonly IAuthorizationService _authorizationService;

    public ArticlesController(ApplicationDbContext context, IAuthorizationService authorizationService)
    {
        _context = context;
        _authorizationService = authorizationService;
    }

    // GET: Articles/Edit/5
    [HttpGet]
    public async Task<IActionResult> Edit(int id)
    {
        var article = await _context.Articles.FindAsync(id);
        if (article == null)
        {
            return NotFound();
        }

        // Evaluate resource-based authorization
        var authResult = await _authorizationService.AuthorizeAsync(
            User, article, new ArticleAuthorizationRequirement(ArticleOperations.Update));

        if (!authResult.Succeeded)
        {
            return Forbid(); // Returns 403 Forbidden if user doesn't own the article
        }

        return View(article);
    }

    // POST: Articles/Edit/5
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Edit(int id, Article model)
    {
        var article = await _context.Articles.FindAsync(id);
        if (article == null)
        {
            return NotFound();
        }

        // Re-verify authorization before processing database updates
        var authResult = await _authorizationService.AuthorizeAsync(
            User, article, new ArticleAuthorizationRequirement(ArticleOperations.Update));

        if (!authResult.Succeeded)
        {
            return Forbid();
        }

        article.Title = model.Title;
        article.Content = model.Content;
        
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
}

Step 6: Conditionally Rendering UI Controls in Views

You can also use the IAuthorizationService directly inside your Razor views to hide edit or delete buttons for articles that the user does not own:

HTML

@model MvcAuthenticationDemo.Models.Article
@inject Microsoft.AspNetCore.Authorization.IAuthorizationService AuthorizationService

<div class="card p-3 my-3">
    <h3>@Model.Title</h3>
    <p>@Model.Content</p>

    @{
        // Check if the current user is authorized to update this specific article
        var updateAuth = await AuthorizationService.AuthorizeAsync(
            User, Model, new MvcAuthenticationDemo.Authorization.ArticleAuthorizationRequirement(MvcAuthenticationDemo.Authorization.ArticleOperations.Update));
    }

    @if (updateAuth.Succeeded)
    {
        <div class="mt-2">
            <a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-sm btn-primary">Edit Article</a>
        </div>
    }
</div>

Conclusion

By implementing resource-based authorization with custom requirements, handlers, and the IAuthorizationService, you secure record ownership down to individual database rows. This ensures that users retain fine-grained control over their own content while administrators maintain global oversight.