Michele mpp Marostica
Michele mpp Marostica

Reputation: 2472

Global Resource Authorization based on route value

I'm working on an ASP.Net Core 3.1 web API.

The API users are from an Azure AD. The users can access the API if they have a license, each user can be assigned to multiple licenses and the same license can be assigned to multiple users.

The client want me to structure the API routes with a template like <api_url>/{licenseId}/controller/action.

My controllers are like:

[Authorize]
[Route("{licenseId}/Foo")]
public class FooController : ControllerBase
{

If I think of the License as a Resource, I can use the Resource based authorization as detailed here. It works but I find myself copying and pasting the auth check for all the actions.

Is there a better way to authorize an user using the route values?

Here what I've got so far:

public class LicenseRequirement : IAuthorizationRequirement
{        
    public Guid LicenseId { get; private set; }

    public LicenseRequirement(Guid licenseId)
    {
        LicenseId = licenseId;
    }
}

public class LicenseAuthorizationHandler : AuthorizationHandler<LicenseRequirement>
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ILogger<LicenseAuthorizationHandler> _logger;
    private readonly DBContext _db;

    public LicenseAuthorizationHandler(DBContext context, ILogger<LicenseAuthorizationHandler> logger, IHttpContextAccessor httpContextAccessor)
    {
        _logger = logger;
        _db = context;
        _httpContextAccessor = httpContextAccessor;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, LicenseRequirement requirement)
    {
        var userId = new Guid(context.User.GetUserId());
        var licenseId = _httpContextAccessor.HttpContext.GetRouteData().Values["licenseId"];

        if (await _db.ApiUsers.SingleOrDefaultAsync(x => x.LicenseId == new Guid(licenseId as string) && x.UserId == userId) is ApiUser user)
            context.Succeed(requirement);
    }
}

Now I'm a bit stuck as I don't know how to set it up in Startup.cs and use it as an attribute or area filter creating those requirement at runtime with the licenseId route's value.

Upvotes: 4

Views: 691

Answers (1)

Michele mpp Marostica
Michele mpp Marostica

Reputation: 2472

I found IAuthorizationFilter pretty straightforward to implement. When I tried it yesterday, I couldn't found the RouteValues but they're there:

public class LicenseAuthorizationFilter : IAuthorizationFilter
{
    private readonly ILogger<LicenseAuthorizationFilter> _logger;
    private readonly DBContext _db;

    public LicenseAuthorizationFilter(DBContext context, ILogger<LicenseAuthorizationFilter> logger)
    {
        _logger = logger;
        _db = context;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var userId = new Guid(context.HttpContext.User.GetUserId());
        var licenseId = new Guid(context.HttpContext.Request.RouteValues["licenseId"] as string);

        if (!(_db.ApiUsers.SingleOrDefault(x => x.LicenseId == licenseId && x.UserId == userId) is ApiUser user))
        {
            context.Result = new ForbidResult();
        }
    }
}

public class LicenseAuthorizationAttribute : TypeFilterAttribute
{
    public LicenseAuthorizationAttribute() : base(typeof(LicenseAuthorizationFilter))
    { }
}

And the controller can neatly become:

[Authorize]
[LicenseAuthorization]
[Route("{licenseId}/Items")]
public class ItemsController : ControllerBase
{
    [...]
}

Upvotes: 2

Related Questions