Search code examples
c#asp.net-coreasp.net-core-identityasp.net-authorization

Authorize only certain Http methods in ASP.NET Core


I would like to require one policy for all actions on a controller, and I would like to also require a second policy for all calls to HTTP "edit methods" (POST, PUT, PATCH, and DELETE). That is, the edit methods should require both policies. Due to implementation requirements, and also a desire to keep the code DRY, I need the latter policy to be applied at the controller level, not duplicated on all the action methods.

As a simple example, I have a PeopleController, and I also have two permissions, implemented as Policies, ViewPeople and EditPeople. Right now I have:

[Authorize("ViewPeople")]
public class PeopleController : Controller { }

How do I go about adding the EditPeople policy/permission such that it "stacks" and only applies to the edit verbs?

I've run into two problems which both seem to be a real pain:

  • You can't have more than one AuthorizeAttribute or more than one Policy specified within the AuthorizeAttribute, AFAIK.
  • You can't access the Request in a custom AuthorizationHandler, so I can't check the HttpMethod to check it.

I tried working around the former with a custom Requirement and AuthorizationHandler, like so:

public class ViewEditRolesRequirement : IAuthorizationRequirement
{
    public ViewEditRolesRequirement(Roles[] editRoles, Roles[] viewRoles)
        => (EditRoles, ViewRoles) = (editRoles, viewRoles);

    public Roles[] EditRoles { get; }
    public Roles[] ViewRoles { get; }
}

public class ViewEditRolesHandler : AuthorizationHandler<ViewEditRolesRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ViewEditRolesRequirement requirement)
    {
        if (context.User != null)
        {
            var canView = requirement.ViewRoles.Any(r => context.User.IsInRole(r.ToString()));
            var canEdit = requirement.EditRoles.Any(r => context.User.IsInRole(r.ToString()));
            if (context. // Wait, why can't I get to the bloody HttpRequest??
        }
        return Task.CompletedTask;
    }
}

... but I got as far as if (context. before I realized that I didn't have access to the request object.

Is my only choice to override the OnActionExecuting method in the controller and do my authorization there? I assume that's frowned upon, at the very least?


Solution

  • You can't access the Request in a custom AuthorizationHandler, so I can't check the HttpMethod...

    Actually, we can access the Request in an AuthorizationHandler. We do that by casting the context.Resource with the as keyword. Here is an example:

    services.AddAuthorization(config =>
    {
        config.AddPolicy("View", p => p.RequireAssertion(context =>
        {
            var filterContext = context.Resource as AuthorizationFilterContext;
            var httpMethod = filterContext.HttpContext.Request.Method;
            // add conditional authorization here
            return true; 
        }));
    
        config.AddPolicy("Edit", p => p.RequireAssertion(context =>
        {
            var filterContext = context.Resource as AuthorizationFilterContext;
            var httpMethod = filterContext.HttpContext.Request.Method;
            // add conditional authorization here
            return true;
        }));
    });
    

    You can't have more than one AuthorizeAttribute....

    Actually, we can have more than one AuthorizeAttribute. Note from the docs that the attribute has AllowMultiple=true. That allows us to "stack" them. Here is an example:

    [Authorize(Policy="View")]
    [Authorize(Policy="Edit")]
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        ...
    }