Override global authorize filter in ASP.NET Core 1.0 MVC
Asked Answered
S

2

27

I am trying to set up authorization in ASP.NET Core 1.0 (MVC 6) web app.

More restrictive approach - by default I want to restrict all controllers and action methods to users with Admin role. So, I am adding a global authorize attribute like:

AuthorizationPolicy requireAdminRole = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .RequireRole("Admin")
    .Build();
services.AddMvc(options => { options.Filters.Add(new AuthorizeFilter(requireAdminRole));});

Then I want to allow users with specific roles to access concrete controllers. For example:

[Authorize(Roles="Admin,UserManager")]
public class UserControler : Controller{}

Which of course will not work, as the "global filter" will not allow the UserManager to access the controller as they are not "admins".

In MVC5, I was able to implement this by creating a custom authorize attribute and putting my logic there. Then using this custom attribute as a global. For example:

public class IsAdminOrAuthorizeAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        ActionDescriptor action = filterContext.ActionDescriptor;
        if (action.IsDefined(typeof(AuthorizeAttribute), true) ||
            action.ControllerDescriptor.IsDefined(typeof(AuthorizeAttribute), true))
        {
            return;
        }

        base.OnAuthorization(filterContext);
    }
}

I tried to create a custom AuthorizeFilter, but no success. API seems to be different.

So my question is: Is it possible to set up default policy and then override it for specific controllers and actions. Or something similar. I don't want to go with this

[Authorize(Roles="Admin,[OtherRoles]")]

on every controller/action, as this is a potential security problem. What will happen if I accidentally forget to put the Admin role.

Sclerotic answered 6/3, 2016 at 9:5 Comment(0)
H
37

You will need to play with the framework a bit since your global policy is more restrictive than the one you want to apply to specific controllers and actions:

  • By default only Admin users can access your application
  • Specific roles will also be granted access to some controllers (like UserManagers accessing the UsersController)

As you have already noticied, having a global filter means that only Admin users will have access to a controller. When you add the additional attribute on the UsersController, only users that are both Admin and UserManager will have access.

It is possible to use a similar approach to the MVC 5 one, but it works in a different way.

  • In MVC 6 the [Authorize] attribute does not contain the authorization logic.
  • Instead the AuthorizeFilter is the one that has an OnAuthorizeAsync method calling the authorization service to make sure policies are satisfied.
  • A specific IApplicationModelProvider is used to add an AuthorizeFilter for every controller and action that has an [Authorize] attribute.

One option could be to recreate your IsAdminOrAuthorizeAttribute, but this time as an AuthorizeFilter that you will then add as a global filter:

public class IsAdminOrAuthorizeFilter : AuthorizeFilter
{
    public IsAdminOrAuthorizeFilter(AuthorizationPolicy policy): base(policy)
    {
    }

    public override Task OnAuthorizationAsync(Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext context)
    {
        // If there is another authorize filter, do nothing
        if (context.Filters.Any(item => item is IAsyncAuthorizationFilter && item != this))
        {
            return Task.FromResult(0);
        }

        //Otherwise apply this policy
        return base.OnAuthorizationAsync(context);
    }        
}

services.AddMvc(opts => 
{
    opts.Filters.Add(new IsAdminOrAuthorizeFilter(new AuthorizationPolicyBuilder().RequireRole("admin").Build()));
});

This would apply your global filter only when the controller/action doesn't have a specific [Authorize] attribute.


You could also avoid having a global filter by injecting yourself in the process that generates the filters to be applied for every controller and action. You can either add your own IApplicationModelProvider or your own IApplicationModelConvention. Both will let you add/remove specific controller and actions filters.

For example, you can define a default authorization policy and extra specific policies:

services.AddAuthorization(opts =>
{
    opts.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().RequireRole("admin").Build();
    opts.AddPolicy("Users", policy => policy.RequireAuthenticatedUser().RequireRole("admin", "users"));
});

Then you can create a new IApplicatioModelProvider that will add the default policy to every controller that doesn't have its own [Authorize] attribute (An application convention would be very similar and probably more aligned with the way the framework is intended to be extended. I just quickly used the existing AuthorizationApplicationModelProvider as a guide):

public class OverridableDefaultAuthorizationApplicationModelProvider : IApplicationModelProvider
{
    private readonly AuthorizationOptions _authorizationOptions;

    public OverridableDefaultAuthorizationApplicationModelProvider(IOptions<AuthorizationOptions> authorizationOptionsAccessor)
    {
        _authorizationOptions = authorizationOptionsAccessor.Value;
    }

    public int Order
    {
        //It will be executed after AuthorizationApplicationModelProvider, which has order -990
        get { return 0; }
    }

    public void OnProvidersExecuted(ApplicationModelProviderContext context)
    {
        foreach (var controllerModel in context.Result.Controllers)
        {
            if (controllerModel.Filters.OfType<IAsyncAuthorizationFilter>().FirstOrDefault() == null)
            {
                //default policy only used when there is no authorize filter in the controller
                controllerModel.Filters.Add(new AuthorizeFilter(_authorizationOptions.DefaultPolicy));
            }
        }
    }

    public void OnProvidersExecuting(ApplicationModelProviderContext context)
    {            
        //empty    
    }
}

//Register in Startup.ConfigureServices
services.TryAddEnumerable(
    ServiceDescriptor.Transient<IApplicationModelProvider, OverridableDefaultAuthorizationApplicationModelProvider>());

With this in place, the default policy will be used on these 2 controllers:

public class FooController : Controller

[Authorize]
public class BarController : Controller

And the specific Users policy will be used here:

[Authorize(Policy = "Users")]
public class UsersController : Controller

Notice that you still need to add the admin role to every policy, but at least all your policies will be declared in a single startup method. You could probably create your own methods for building policies that will always add the admin role.

Hippie answered 8/3, 2016 at 9:25 Comment(4)
Hah, thanks for the detailed explanations. Exactly what I wanted to achieve. I knew that I can override the AuthorizeFilter but simply didn't put enough efforts to do it. For the IApplicationModelProvider, I didn't know about it. Just implemented the IApplicationModelProvider approach and can confirm it is working. I think it is more in the spirit of the new MVC auth model, so I'll go with it.Sclerotic
I would also give the IApplicationModelConvention a try, there is even an option for adding those in the services.AddMvc() extension method. The main difference regarding the code is that you will need to pass somehow the auth options into it.Hippie
This isn't working for me. I'm trying the Filter approach, and in my OnAuthorizationAsync method, there are always two other filters in the context, so the IsAdminOrAuthorized filter never gets run. That's even without any policies/AuthorizeAttributes in my application.Cyma
Thanks, that answer helped me a lot with some other problem ;)Ziska
M
1

Using @Daniel's solution I ran into the same issue mentioned by @TarkaDaal in the comment (there's 2 AuthorizeFilter in the context for each call...not quite sure where they are coming from).

So my way to solve it is as follow:

public class IsAdminOrAuthorizeFilter : AuthorizeFilter
{
    public IsAdminOrAuthorizeFilter(AuthorizationPolicy policy): base(policy)
    {
    }

    public override Task OnAuthorizationAsync(Microsoft.AspNet.Mvc.Filters.AuthorizationContext context)
    {
        if (context.Filters.Any(f =>
        {
            var filter = f as AuthorizeFilter;
            //There's 2 default Authorize filter in the context for some reason...so we need to filter out the empty ones
            return filter?.AuthorizeData != null && filter.AuthorizeData.Any() && f != this;
        }))
        {
            return Task.FromResult(0);
        }

        //Otherwise apply this policy
        return base.OnAuthorizationAsync(context);
    }        
}

services.AddMvc(opts => 
{
    opts.Filters.Add(new IsAdminOrAuthorizeFilter(new AuthorizationPolicyBuilder().RequireRole("admin").Build()));
});

This is ugly but it works in this case because if you're only using the Authorize attribute with no arguments you're going to be handled by the new AuthorizationPolicyBuilder().RequireRole("admin").Build() filter anyway.

Martino answered 1/12, 2016 at 20:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.