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.
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:
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.
[Authorize]
attribute does not contain the authorization logic.AuthorizeFilter
is the one that has an OnAuthorizeAsync
method calling the authorization service to make sure policies are satisfied. 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With