Reputation: 2472
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
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