2
votes

I have an ASP.NET Core 2.2 application where I am displaying some documents. Most documents are public, so anonymous access is fine. However, some documents are private (i.e. they require authentication/authorization), and in the future some documents might also require a valid subscription. All documents are retrieved using the same actions, so we only know the required permissions after the documents have been loaded. We also load some resources as static files (IApplicationBuilder.UseStaticFiles), but I guess that shouldn't really be an issue as StaticFileOptions.OnPrepareResponse can be used for custom authorization code.

The logic for who gets access to private documents is currently really simple. And at the moment, we only display documents, we don't allow any other kind of operation on them (editing, deletion etc.). To me, this sounds like a pretty standard case of resource-based authorization.

Anyway, I have found this article and from what I've understood, I need to define a policy (identified by a magic string - what's up with that?!) as well as a requirement and an AuthorizationHandler<MyRequirement, MyResource> which will perform the actual authorization logic. Then, inside my controller action, I will need to call IAuthorizationService.AuthorizeAsync and pass in the user, the resource and the policy name (the magic string) and, based on the result from that method, allow or deny access. That seems more than convoluted for what I'm trying to accomplish. It would probably be easier if I simply defined my own kind of "authorization service" and simply dropped the whole policy and requirement stuff. I also think it's less than ideal that I would have to replicate the if-else logic in all affected controller actions.

Surely I'm not the only one with this issue. Is there something I've missed? If there are indeed good reasons for using policies and requirements, how would you name them in a case like this? I'm really feeling a little lost. Maybe it would make sense to use the type of document (public, private, subscribers-only) as the policy name?

2
Same requirements here... ever found a suitable solution? - Alessandro
@Alessandro I posted our solution as an answer now. - TravelingFox
Thanks a lot, very interesting! - Alessandro

2 Answers

1
votes

In the end, we didn't want to deal with this stuff and just wrote our own AuthorizationService, which is injected into the controller like any other service. It loads the required permissions for all documents the first time it is used and caches them. Our controller methods then look something like this:

    [HttpGet("[action]")]
    public async Task<Document> GetDocument(string documentId)
    {
        if (_authorizationService.MayAccess(User, documentId))
        {
            return _documentRepository.GetDocument(documentId);
        }
        else
        {
            Response.StatusCode = StatusCodes.Status403Forbidden;
            return null;
        }
    }
0
votes

I recommend the last approach explained in this article - https://www.red-gate.com/simple-talk/dotnet/c-programming/policy-based-authorization-in-asp-net-core-a-deep-dive/

Allows you to keep you controller clean, by just applying annotation with the name of the policy. In the handler you must implement the logic checking if person can access the resource - it can be based for example on checking a property ownerId in a resource(for example in database table column) or a member of a certain group in AD, or anything else.

EDIT:

Using Requirements and RequirementsHandlers - I have done something similiar. I don't know how should your logic exactly work, so I am just going to assume some.

lets say you have a get endpoint: documents/documentId

You want to apply logic which will make this document accessible only to the document owner. Obviously, you need somewhere to store who is the owner of the document, so lets keep that in property of a document entity.

protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IsDocumentOwner requirement, DocumentRepository documentRepository)
{
    if (context.Resource is AuthorizationFilterContext ctx)
    {
        var documentId = ctx.RouteData.Values["documentId"]?.ToString();

        //here load document from repo and check if the property ownerId is equal to current user id

        var userId = context.User.Claims.FirstOrDefault(x => x.ToString().Contains(oid))?.Value;
        //if yes, make the request pass to the body of a controller with the attribute
        context.Succeed(requirement);
    }

}

implement IsDocumentOwner:

public class IsDocumentOwner : IAuthorizationRequirement 
    {
    }

in your Startup.cs add:

services.AddAuthorization(options =>
            {
                options.AddPolicy(
                    nameof(IsDocumentOwner),
                    policyBuilder => policyBuilder.AddRequirements(
                        new IsDocumentOwner()));
            });

then, last step, apply attribute on your controller method

[Authorize(Policy = "IsDocumentOwner")]
[HttpGet("{documentId}")]
public YourDocumentObjectResultClass GetDocument([FromRoute]string documentId)
{
//stuff you do when current user is owner of the document, probably just display the doc
}

To your IsDocumentOwner handler you can inject any service by constructor(visualised by repository above), for example, to check if the user is a member of a group on azure ad