I spent some time understanding Resource-based authorization in ASP.NET Core today and implemented a simple example using it (also see Policy-based authorization in ASP.NET Core). This post is an alternative example to what’s in the documentation and some notes for reference.
In this example, authorization is a simple check that the UserId
of the resource (an Article
) is equal to the ID of the user requesting it. The check happens in a controller action after the resource is retrieved from a database via a repository.
Create a custom requirement class, and implement a requirement handler class.
In my case the requirement class is an empty class implementing IAuthorizationRequirement
:
using Microsoft.AspNetCore.Authorization;
namespace AnkiBooks.WebApp.Policies.Requirements;
public class UserOwnsArticleRequirement : IAuthorizationRequirement { }
The requirement handler:
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using AnkiBooks.ApplicationCore.Entities;
using AnkiBooks.WebApp.Policies.Requirements;
namespace AnkiBooks.WebApp.Policies.Handlers;
public class UserOwnsArticleHandler : AuthorizationHandler<UserOwnsArticleRequirement, Article>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
UserOwnsArticleRequirement requirement,
Article resource)
{
if (context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value == resource.UserId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
ClaimTypes.NameIdentifier
here is the claim type for the user ID claim (it is the default value of the ClaimsIdentityOptions.UserIdClaimType
property).
Register the requirement and handler in Program.cs:
This is the setup needed in Program.cs
:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("UserOwnsArticle",
policy => policy.Requirements.Add(new UserOwnsArticleRequirement()));
});
builder.Services.AddSingleton<IAuthorizationHandler, UserOwnsArticleHandler>();
Inject IAuthorizationService
into the controller:
[ApiController]
public class ArticlesController(IArticleRepository repository,
IAuthorizationService authorizationService,
ILogger<ArticlesController> logger) : ApplicationController
{
private readonly IArticleRepository _repository = repository;
private readonly IAuthorizationService _authorizationService = authorizationService;
private readonly ILogger<ArticlesController> _logger = logger;
...
}
A controller action using this:
[HttpGet("api/Articles/{articleId}")]
public async Task<ActionResult<Article>> GetArticle(string articleId)
{
ClaimsPrincipal user = HttpContext.User;
if (user == null) return new ForbidResult();
Article? article = await _repository.GetArticleAsync(articleId);
if (article == null) return NotFound();
AuthorizationResult authorizationResult =
await _authorizationService.AuthorizeAsync(user, article, "UserOwnsArticle");
if (authorizationResult.Succeeded)
{
_logger.LogInformation("Article authorization was successful");
return article;
}
else
{
_logger.LogInformation("Article authorization failed");
return NotFound();
}
}
Signature of this IAuthenticationService
AuthorizeAsync
overload:
Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user,
object resource,
string policyName);
Other comments:
HttpContext.User
is one way to get the currentClaimsPrincipal
.- Adding
[Authorize]
to the controller/action would be somewhat similar to theuser == null
check. - Sending a Not Found response instead of Forbidden for when the resource was found but the user was not authorized to see it is a good practice as it prevents giving away the existence of the resource.