In this article we will take a look at a bugfix for the ODataAuthorization library. I think it's worth sharing and making some code available, if someone comes here from Google.
The Problem
There was recently a bug reported in the ODataAuthorization library, that made the Authorization failing for multiple requests:
The ODataAuthorization library basically works like this... From an incoming
HttpRequest
the IEdmModel
is resolved, then the permissions are resolved
using an ODataPath
(which is basically the parsed OData URL).
From these permissions an AuthorizationPolicy
is built and put into an
ASP.NET Core built-in AuthorizeFilter
. This Filter has then been added
to the controller like this:
// Licensed under the MIT License. See License.txt in the project root for license information.
// ...
namespace ODataAuthorization
{
/// <summary>
/// The OData authorization middleware
/// </summary>
public class ODataAuthorizationMiddleware
{
private static void ApplyRestrictions(IScopesEvaluator handler, HttpContext context)
{
var requirement = new ODataAuthorizationScopesRequirement(handler);
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(requirement)
.Build();
// We use the AuthorizeFilter instead of relying on the built-in authorization middleware
// because we cannot add new metadata to the endpoint in the middle of a request
// and OData's current implementation of endpoint routing does not allow for
// adding metadata to individual routes ahead of time
var authFilter = new AuthorizeFilter(policy);
var controllerActionDescriptor = context.GetEndpoint().Metadata.GetMetadata<ControllerActionDescriptor>();
if(controllerActionDescriptor != null)
{
controllerActionDescriptor.FilterDescriptors?.Add(new FilterDescriptor(authFilter, 0));
}
}
}
}
What the bug uncovered is, that a Filter
is cached this way and only the cached
filter is going to be used for all subsequent requests. I didn't find a way to
invalidate the cache.
The Solution
ASP.NET Core MVC comes with an IFilterProvider
, which is ...
A FilterItem provider. Implementations should update Results to make executable filters available.
So we basically shift the logic from the Middleware to an IFilterProvider
, which
allows us to dynamically provide a dynamically created AuthorizeFilter
. By registering
the FilterItem
as not reusable, we can finally prevent it from being cached.
Here is the solution in its glory:
// Licensed under the MIT License. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.OData.Abstracts;
using Microsoft.AspNetCore.OData.Extensions;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.OData.Edm;
using System;
using System.Linq;
namespace ODataAuthorization
{
public class ODataAuthorizeFilterProvider : IFilterProvider
{
public int Order => 0;
public void OnProvidersExecuted(FilterProviderContext context)
{
}
public void OnProvidersExecuting(FilterProviderContext context)
{
// ...
var permissions = model.ExtractPermissionsForRequest(httpContext.Request.Method, odataFeature.Path, odataFeature.SelectExpandClause);
var requirement = new ODataAuthorizationScopesRequirement(permissions);
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(requirement)
.Build();
var authFilter = new AuthorizeFilter(policy);
var authFilterDescriptor = new FilterDescriptor(authFilter, FilterScope.Global);
var authFilterItem = new FilterItem(authFilterDescriptor, authFilter)
{
IsReusable = false
};
context.Results.Add(authFilterItem);
}
}
}
And we need to add it to the Dependency Injection container:
// ...
services.AddSingleton<IFilterProvider, ODataAuthorizeFilterProvider>();