1

I'm trying to implement authorization in my ASP.NET Core 2.0 Web app.

This app has like 20 models, each with a controller implementing at least a CRUD. I found these two pages and I liked the idea of using a handler to authorize requisitions. I would like initially to implement authorization by user, i.e., a user has only permission to see/edit his own entities. All my database entities have an OwnerId field.

These examples I found seem to only work for one specific controller.

So, my question is: is it possible to create one authorization handler for all controllers?

Matheus Lacerda
  • 5,739
  • 11
  • 26
  • 41
  • 1
    Check this - https://stackoverflow.com/questions/31464359/how-do-you-create-a-custom-authorizeattribute-in-asp-net-core – CodeFuller Jan 26 '18 at 17:54

1 Answers1

1

Have you found a solution or workaround yet that works with the authorization handler or authorization attributes? I have the exact same setup as you do.

I was trying to create a generic attribute to serve all may Entity CRUD owner checks, but generic attributes are not allowed by design.

The only two (unsatisfying) solutions that I came up with are:

  1. Within the controller action, get the ownerId from the User, forward it all the way to your CRUD and include there a check for the ownerId. However, the code must be duplicated for every action in every controller.

    [HttpGet("{id}"]
    public async Task<IActionResult> GetById(int id)
    {
        var stringGuid = User.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
        if (String.IsNullOrWhiteSpace(stringGuid)) return Unauthorized();
        var ownerGuid = new Guid(stringGuid);
    
        var entity = _yourCrudInstance.GetById(id, ownerGuid);
    
        return Ok(entity);
    }
    
  2. Add a method to your CRUD repository like bool IsOwner(Guid ownerId) and use this method when creating the custom authorization handler (by creating a custom requirement together with a custom handler). This eliminates code duplication in the controller, because you can create a new policy with this custom authorization handler and consequently you can simply decorate every action with a [Authorize(Policy = "yourOwnershipPolicy")]. But still, there must be a service created for each and every controller. Moreover, the IsOwner(...) method adds an additional database call compared to solution 1 - one db call for checking the ownership (during authorization check) and one db call for actually getting the entity (by working through the controller action).

    [Authorize(Policy = "yourOwnershipPolicy")]
    public async Task<IActionResult> GetById(int id)
    {
        var entity = _yourCrudInstance.GetById(id);
    
        return Ok(entity);
    }
    

I am going with the first solution until I found a way to create a generic authorization handling for my generic CRUD repository, because one may forget creating the required authorization policy for a new entity, but one cannot forget to supply the parameter ownerId to .GetById(id, ownerGuid), provided there is no overload method, or the code doesn't compile.

Update:

  1. I found a third solution in which was able to create a kind of generic authorization attribute. The trick was to use the type of concrete repository as input parameter in the authorization attribute. Yet, there is still a limitation: The authorization attribute must be copied for every type of Id, for example int Id, Guid id, etc. But still, this reduces repeated code to the types of ids. In most cases, people only have one type of id, probably int or Guid.

Here some code that demonstrates my architecture. It is heavily summarized and redacted, but should compile successfully. My original code is working and in production:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

[Route("api/yourcontroller")]
public class YourApiController : Controller
{
    private readonly YourEntityXYZRepository _repo;

    public YourApiController(YourDbContext yourDbContext)
    {
        _repo = new YourEntityXYZRepository(yourDbContext);
    }

    [HttpGet("{id}")]
    [AuthorizeOwnerIntId(typeof(YourEntityXYZRepository), Policy = "YourCustomPolicy")]
    public async Task<IActionResult> GetById(int id)
    {
        var entity = _repo.GetById(id);
        return Ok(entity);
    }
}

// The "generic" authorization attribute for type int id
// Similar authorization attributes for every type of id must be created additionally, for example Guid
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class AuthorizeOwnerIntIdAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    private object _entityRepositoryObject;
    private IAsyncOwnerIntId _entityRepository;
    private readonly Type _TCrudRepository;

    public AuthorizeOwnerIntIdAttribute(Type TCrudRepository)
    {
        _TCrudRepository = TCrudRepository;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var yourDbContext = context.HttpContext.RequestServices.GetService<YourDbContext>();
        _entityRepositoryObject = Activator.CreateInstance(_TCrudRepository, yourDbContext);
        _entityRepository = _entityRepositoryObject as IAsyncOwnerIntId;

        var user = context.HttpContext.User;

        if (!user.Identity.IsAuthenticated)
        {
            // it isn't needed to set unauthorized result 
            // as the base class already requires the user to be authenticated
            // this also makes redirect to a login page work properly
            // context.Result = new UnauthorizedResult();
            return;
        }

        // get entityId from uri
        var idString = context.RouteData.Values["id"].ToString();
        if (!int.TryParse(idString, out var entityId))
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        // get subjectId from user claims
        var ownerIdString = context.HttpContext.User.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
        if (!Guid.TryParse(ownerIdString, out var ownerGuid))
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        if (!_entityRepository.IsEntityOwner(entityId, ownerGuid))
        {
            context.Result = new UnauthorizedResult();
        }
    }
}

// Your concrete repository
public class YourEntityXYZRepository : AsyncCrud<YourEntityXYZ, int>,
    IAsyncOwnerIntId // Note that type concrete IAsyncOwnerIntId is only implemented in concrete repository
{
    public YourEntityXYZRepository(YourDbContext yourDbContext) : base(yourDbContext)
    {

    }
}

// Your generic Crud repository
public abstract class AsyncCrud<TEntity, TId> : IAsyncCrud<TEntity, TId>
    where TEntity : class, IEntityUniqueIdentifier<TId>, IEntityOwner
    where TId : struct
{
    protected YourDbContext YourDbContext;

    public AsyncCrud(YourDbContext yourDbContext)
    {
        YourDbContext = yourDbContext;
    }

    // Note that the following single concrete implementation satisfies both interface members 
    // bool IsEntityOwner(TId id, Guid ownerGuid); from IAsyncCrud<TEntity, TId> and
    // bool IsEntityOwner(int id, Guid ownerGuid); from IAsyncOwnerIntId
    public bool IsEntityOwner(TId id, Guid ownerGuid)
    {
        var entity = YourDbContext.Set<TEntity>().Find(id);
        if (entity != null && entity.OwnerGuid == ownerGuid)
        {
            return true;
        }

        return false;
    }

    // Further implementations (redacted)
    public Task<bool> SaveContext() { throw new NotImplementedException(); }
    public Task<TEntity> Update(TEntity entity){ throw new NotImplementedException(); }
    public Task<TEntity> Create(TEntity entity, Guid ownerGuid) { throw new NotImplementedException(); }
    public Task<bool> Delete(TId id) { throw new NotImplementedException(); }
    public Task<bool> DoesEntityExist(TId id) { throw new NotImplementedException(); }
    public virtual Task<TEntity> GetById(TId id) { throw new NotImplementedException(); }
}

// The interface for the Crud operations
public interface IAsyncCrud<TEntity, TId>
    where TEntity : class, IEntityUniqueIdentifier<TId>
    where TId : struct
{
    bool IsEntityOwner(TId id, Guid ownerGuid);
    Task<bool> DoesEntityExist(TId id);
    Task<TEntity> GetById(TId id);
    Task<TEntity> Create(TEntity entity, Guid ownerGuid);
    Task<TEntity> Update(TEntity entity);
    Task<bool> Delete(TId id);
    Task<bool> SaveContext();
}

// The interface for the concrete type method for int id
// Similar interfaces for every type of id must be created additionally, for example Guid
public interface IAsyncOwnerIntId
{
    bool IsEntityOwner(int id, Guid ownerGuid);
}

// Typical db context
public class YourDbContext : DbContext
{
    public YourDbContext(DbContextOptions<YourDbContext> options) : base(options)
    {

    }

    public DbSet<YourEntityXYZ> YourEntityXYZ { get; set; }
}


public class YourEntityXYZ : IEntityUniqueIdentifier<int>, IEntityOwner
{
    public int Id { get; set; }
    public Guid? OwnerGuid { get; set; }
    // ... Additonal custom properties
}

public interface IEntityUniqueIdentifier<TId>
    where TId : struct
{
    TId Id { get; set; }
}

public interface IEntityOwner
{
    Guid? OwnerGuid { get; set; }
}
philipp-fx
  • 529
  • 1
  • 5
  • 18