ganders
ganders

Reputation: 7433

ASP.NET Core MVC ActionFilter execute before AuthorizationFilter

On my site I have a few controllers that are restricted to authenticated users. Beyond that, there are a few of those controllers that require authentication, that ALSO need to be restricted based on the time of year.

To handle that, I created a TimeRangeFilter ActionFilter/ResourceFilter.

Here's what it looks like:

public class TimeRangeFilter : Attribute, IActionFilter, IOrderedFilter
{
  public string AllowedMonths { get; set; } // like |3|4|5|
  public string RedirectUrl { get; set; }
  ...... // OnActionExecuting....
}

Then, on my Controller's class, I implement like this:

[TimeRangeFilter(AllowedMonths = "|3|4|", RedirectUrl = "/feature/welcome", Order = 1)]
[Authorize]
[IsNotNefarious]
public class HubController : BaseController
{...}

But, even with the IOrderedFilter interface on the filter, the AuthorizationFilter executes first, THEN my TimeRangeFilter.

For this welcome page, I don't want to require the user to be logged in to see it. But I don't want to have to change the URL that gets to my Hub page based on those allowed months.

How do I prioritize my ActionFilter/ResourceFilter to execute, and short-circuit, prior to the AuthorizationFilter execution?

Upvotes: 3

Views: 4079

Answers (2)

FWest98
FWest98

Reputation: 91

The solution from @Alexander does no longer work in .NET Core 3.1; now the Authorize attribute is being evaluated in the AuthorizationMiddleware, many steps before the filters are hit.

The best new approach is to make a custom middleware yourself, insert it after UseRouting(); in startup, and let it query the endpoint information manually. Example:

public class TimeRangeAttribute : Attribute {
    public string Info { get; set; }
}

public class TimeRangeMiddleware {
    private readonly RequestDelegate _next;
    public TimeRangeMiddleware(RequestDelegate next) => _next = next;

    public async Task Invoke(HttpContext context) {
        var endpoint = context.GetEndpoint();
        if (endpoint?.Metadata.GetMetadata<TimeRangeAttribute>() != null) {
            // check allowed or not
        }
        if(_next != null) await _next(context);
    }
}

// In Startup
public void Configure(...) {
    // ....
    app.UseRouting();
    app.UseMiddleware<TimeRangeMiddleware>();
    app.UseAuthentication();
    app.UseAuthorization();
    // ...
}

Upvotes: 5

Alexander
Alexander

Reputation: 9632

Short answer is "You can't make ActionFilter execute before AuhtorizeFilter". But you can turn TimeRangeFilter into authorization filter

public class TimeRangeFilterAttribute : Attribute, IAuthorizationFilter, IOrderedFilter
{
    public string AllowedMonths { get; set; } // like |3|4|5|
    public string RedirectUrl { get; set; }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (not allowed) {
            context.Result = new RedirectResult(RedirectUrl);
        }
    }
}

Specify Order = 0 to make it run before other Authorize checks, or try not implement IOrderedFilter for it and it will be executed first as well.

Upvotes: 3

Related Questions