ASP.Net 5 AuthorizationHandler Fail Redirect - c#

I am trying to add a custom authorization policy that can check a delimited list of groups supplied in a json config file. I am using ASP.Net 5 - MVC 6, along with windows authentication.
Everything is working fine, except for when I call Fail. Then nothing happens. A Blank screen is shown. Here is my HandleRequirementAsync method. I have tried various values for the task result. I have been googling like a madman, but with no luck. Hopefully someone can help.
DESIRED RESULT: I would like to redirect to a custom page on failure, but if that is not possible, at least be able to redirect back to the login page. The only thing that seems to have any effect is to throw an exception.
The pertinent registration code in Startup:
var appSettings = Configuration.GetSection("AppSettings");
services.Configure<Models.AppSettings>(appSettings);
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("RoleAuth", policy => policy.Requirements.Add(new RolesRequirement(appSettings["AllowedGroups"])));
});
services.AddSingleton<IAuthorizationHandler, RoleAuthorizationHandler>();
And the authorization classes:
public class RolesRequirement : IAuthorizationRequirement
{
public RolesRequirement(string groups)
{
Groups = groups;
}
public string Groups { get; private set; }
}
public class RoleAuthorizationHandler : AuthorizationHandler<RolesRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesRequirement requirement)
{
if (!string.IsNullOrWhiteSpace(requirement.Groups))
{
Console.WriteLine(requirement.Groups);
var groups = requirement.Groups.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
//we could check for group membership here.... maybe???
foreach (var group in groups)
{
if (context.User.IsInRole(group))
{
context.Succeed(requirement);
return Task.FromResult(0);
}
}
}
else
{
context.Succeed(requirement);
}
context.Fail();
return Task.FromResult(0);
}
}

The only way i have found of doing this is, don't use context.Fail(), instead do this:
replace:
context.Fail();
with:
var mvcContext = context.Resource as AuthorizationFilterContext;
mvcContext.Result = new RedirectToActionResult("Action", "Controller", null);
context.Succeed(requirement);
allowing the context to succeed, will execute the context, which is now a redirect.

I went with what herostwist suggested, but Policies can challenge or forbid. After intesive research I came across what gives you direct access to the AuthorizationFilterContext like this (because they follow the naming convention and inherit from AuthorizeAttribute:
public class BudgetAccessFilterAttribute : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
//context.HttpContext.User.Identity.Name
//TODO: determine if user has access to budget controllers, all of them could inherit from a Common Controller with this Filter
if (false)
{
//if no access then
context.Result = new RedirectToActionResult("Index", "Home", null);
}
}
}
You can then decorate your controllers like this:
[BudgetAccessFilter]
public class BudgetItemController : Controller
{
}
And if you will have lots of controllers with the same checking, then they can all inherit from a base class with the annotation like this:
[BudgetAccessFilter]
public class BCommonController : Controller
{
}
And then clean controllers:
public class BudgetItemController : BCommonController
{
}

After trying Herotwists answer and seeing that it no longer works in .NET Core (context.Resource as AuthorizationFilterContext always returns NULL), I came up with this which seems to work fine in .NET 5. It's a bit hacky though .... I'd really like to see how this should be done. Surely it should be possible?
Anyway, here goes:
if (accessAllowed)
{
context.Succeed(requirement);
}
else
{
var mvcContext = (context.Resource as Microsoft.AspNetCore.Http.DefaultHttpContext);
if (mvcContext != null)
{
mvcContext.Response.Redirect("/the-url-you-want-to-redirect-to");
}
}

I'm using cookie authentication instead of windows but in my Configure method in the Startup.cs I have the following piece of code which tells it where to go
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = "/account/login",
AuthenticationScheme = "Cookies",
AutomaticAuthenticate = true,
AutomaticChallenge = true
});

I don't know where you can configure the redirect result, but at least I was able to create such a "Account/AccessDenied.cshtml" file which will be shown in the fail case. "Account" is my class name and when fail happened, browser was redirected to this Url: (http://localhost:39339/Account/AccessDenied?ReturnUrl=%2Fapp%2Fequipments)
Here is my controller code (Web/AccountController.cs) as well.
public class AccountController : Controller
{
public IActionResult AccessDenied()
{
return View();
}
}

Related

Authorize only certain Http methods in ASP.NET Core

I would like to require one policy for all actions on a controller, and I would like to also require a second policy for all calls to HTTP "edit methods" (POST, PUT, PATCH, and DELETE). That is, the edit methods should require both policies. Due to implementation requirements, and also a desire to keep the code DRY, I need the latter policy to be applied at the controller level, not duplicated on all the action methods.
As a simple example, I have a PeopleController, and I also have two permissions, implemented as Policies, ViewPeople and EditPeople. Right now I have:
[Authorize("ViewPeople")]
public class PeopleController : Controller { }
How do I go about adding the EditPeople policy/permission such that it "stacks" and only applies to the edit verbs?
I've run into two problems which both seem to be a real pain:
You can't have more than one AuthorizeAttribute or more than one Policy specified within the AuthorizeAttribute, AFAIK.
You can't access the Request in a custom AuthorizationHandler, so I can't check the HttpMethod to check it.
I tried working around the former with a custom Requirement and AuthorizationHandler, like so:
public class ViewEditRolesRequirement : IAuthorizationRequirement
{
public ViewEditRolesRequirement(Roles[] editRoles, Roles[] viewRoles)
=> (EditRoles, ViewRoles) = (editRoles, viewRoles);
public Roles[] EditRoles { get; }
public Roles[] ViewRoles { get; }
}
public class ViewEditRolesHandler : AuthorizationHandler<ViewEditRolesRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ViewEditRolesRequirement requirement)
{
if (context.User != null)
{
var canView = requirement.ViewRoles.Any(r => context.User.IsInRole(r.ToString()));
var canEdit = requirement.EditRoles.Any(r => context.User.IsInRole(r.ToString()));
if (context. // Wait, why can't I get to the bloody HttpRequest??
}
return Task.CompletedTask;
}
}
... but I got as far as if (context. before I realized that I didn't have access to the request object.
Is my only choice to override the OnActionExecuting method in the controller and do my authorization there? I assume that's frowned upon, at the very least?
You can't access the Request in a custom AuthorizationHandler, so I can't check the HttpMethod...
Actually, we can access the Request in an AuthorizationHandler. We do that by casting the context.Resource with the as keyword. Here is an example:
services.AddAuthorization(config =>
{
config.AddPolicy("View", p => p.RequireAssertion(context =>
{
var filterContext = context.Resource as AuthorizationFilterContext;
var httpMethod = filterContext.HttpContext.Request.Method;
// add conditional authorization here
return true;
}));
config.AddPolicy("Edit", p => p.RequireAssertion(context =>
{
var filterContext = context.Resource as AuthorizationFilterContext;
var httpMethod = filterContext.HttpContext.Request.Method;
// add conditional authorization here
return true;
}));
});
You can't have more than one AuthorizeAttribute....
Actually, we can have more than one AuthorizeAttribute. Note from the docs that the attribute has AllowMultiple=true. That allows us to "stack" them. Here is an example:
[Authorize(Policy="View")]
[Authorize(Policy="Edit")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
...
}
You can have an IHttpContextAccessor injected into your handler and use it in HandleRequirementAsync:
public class ViewEditRolesHandler : AuthorizationHandler<ViewEditRolesRequirement>
{
private readonly IHttpContextAccessor _contextAccessor;
public ViewEditRolesHandler(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ViewEditRolesRequirement requirement)
{
if (context.User != null)
{
var canView = requirement.ViewRoles.Any(r => context.User.IsInRole(r.ToString()));
var canEdit = requirement.EditRoles.Any(r => context.User.IsInRole(r.ToString()));
if (_contextAccessor.HttpContext.Request. // Now you have it!
}
return Task.CompletedTask;
}
}

Wrong status code for failed autorization requirement in ASP.NET Core

I have created the web api with simple autorization via autorization requirments. My requirments code looks like:
public class TestRequirement : IAuthorizationRequirement { }
public class TestHandler : AuthorizationHandler<TestRequirement> {
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context, TestRequirement requirement) {
//context.Succeed(requirement); --#1
//context.Fail(); --#2
/*if (context.Resource is AuthorizationFilterContext mvcContext) {--#3
mvcContext.Result = new UnauthorizedResult();
}*/
return Task.CompletedTask;
}
}
Also, I updated Startup.ConfigureServices(...):
services.AddAuthorization(o => o.AddPolicy("Test", p => p.Requirements.Add(new TestRequirement())));
services.AddSingleton<IAuthorizationHandler, TestHandler>();
And I added apropriate attribute to controller: [Authorize(Policy = "Test")]
If I uncomment block #1 - it works as expected (I get my data). But when my code fails requiremnt (I comment #1), I get 500 Internal Server Error.
Then, I tried to explicit fail the requirment (uncomment block #2) - same result. I know it isn't recommended but I wanted to try.
After this, I tried the more ugly workaround, I commented #2 and uncommented block #3. I got same 500 status code.
Just for fun, I implemented resources filter with the same behavior:
public class TestResourceFilterAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context) {
context.Result = new UnauthorizedResult();
}
public void OnResourceExecuted(ResourceExecutedContext context) {
}
}
Then, I replaced on controller my authorize attribute with [TestResourceFilter] and got 401 Unauthorized as expected. But it the bad way to use resource filters.
What is wrong with my requirement implementation? And why I get 500 instead of 401 (or 403)?
EDIT: I found InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. in my log.
I saw samples with cookies scheme, but it isn't suitable for me. Because I want to implement stateless calls.
poke's commnets pointed me that I implemented my functionality in wrong way. I tried to handle the security checking on authorization level, but I had to do it on authentication level. So my final code looks like:
public class TestHandlerOptions : AuthenticationSchemeOptions { }
internal class TestHandler : AuthenticationHandler<TestHandlerOptions> {
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
if (await SomeCheckAsync()) {
var identity = new ClaimsIdentity(ClaimsName);
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), null, ClaimsName);
return AuthenticateResult.Success(ticket);
}
return AuthenticateResult.Fail("Missing or malformed 'Authorization' header.");
}
}
Add next in ConfigureServices in Startup class:
services.AddAuthentication(options => options.AddScheme(SchemeName, o => o.HandlerType = typeof(TestHandler)));
And authorize attribute looks like [Authorize(AuthenticationSchemes = SchemeName)]

.NET Core MVC add role claim to local user when in development before authorization

I'm looking for an "easy" way to automatically add role claims to the local user in order to test my authorization logic; that is, wanting to add some specific claims to the local user before they are authorized by my controllers.
I've learned that in the past something akin to this could be done for the controllers:
#if DEBUG
protected override void OnAuthorization(AuthorizationContext filterContext)
{
var roles = new[] { "role-under-test"};
HttpContext.User = new GenericPrincipal(new GenericIdentity("DebugUser"), roles);
base.OnAuthorization(filterContext);
}
#endif
Yet that was prior to .NET Core which is what I'm working in now. Slowly I've worked my way to the code presented below, which seems to work, but is clearly much more of a hassle compared to the above example. So my question is whether anyone knows a better--more easy--way to achieve this?
The custom authorization attribute:
public class CustomAuthAttribute : RolesAuthorizationRequirement {
public CustomAuthAttribute(IEnumerable<string> allowedRoles) : base(allowedRoles) { }
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement) {
#if DEBUG
context.User.AddIdentity(new ClaimsIdentity(new GenericIdentity("DebugUser"), new[] {
new Claim(ClaimsIdentity.DefaultRoleClaimType, "users"),
new Claim(ClaimsIdentity.DefaultRoleClaimType, "admins")
}));
#endif
return base.HandleRequirementAsync(context, requirement);
}
}
In Startup.cs
public void ConfigureServices(IServiceCollection services) {
// ...
services.AddAuthorization( options =>
options.AddPolicy("UsersAndAdmins",
policy => policy.AddRequirements(new CustomAuthAttribute(new []{"users", "admins"}))));
}
And then using it in the controllers:
[Authorize(Policy = "UsersAndAdmins")]
public class HomeController : Controller {
// ...
You want claims transformation;
Write a claims transformation that looks like
public class ClaimsTransformer : IClaimsTransformer
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
((ClaimsIdentity)context.principal.Identity).AddClaim(new Claim("Admin", "true"));
return Task.FromResult(principal);
}
}
And wire it up inside Configure in startup.cs
app.UseClaimsTransformation(new ClaimsTransformationOptions
{
Transformer = new ClaimsTransformer()
});
Of course if it's for test you'd wrap it inside an environment check to ensure you're in your dev environment :)

ValidateAntiForgeryToken in Ajax request with AspNet Core MVC

I have been trying to recreate an Ajax version of the ValidateAntiForgeryToken - there are many blog posts on how to do this for previous versions of MVC, but with the latest MVC 6, none of the code is relevant. The core principle that I am going after, though, is to have the validation look at the Cookie and the Header for the __RequestVerificationToken, instead of comparing the Cookie to a form value. I am using MVC 6.0.0-rc1-final, dnx451 framework, and all of the Microsoft.Extensions libraries are 1.0.0-rc1-final.
My initial thought was to just inherit ValidateAntiForgeryTokenAttribute, but looking at the source code, I would need to return my own implementation of an an Authorization Filter to get it to look at the header.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateAjaxAntiForgeryTokenAttribute : Attribute, IFilterFactory, IFilterMetadata, IOrderedFilter
{
public int Order { get; set; }
public bool IsReusable => true;
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<ValidateAjaxAntiforgeryTokenAuthorizationFilter>();
}
}
As such, I then made my own version of ValidateAntiforgeryTokenAuthorizationFilter
public class ValidateAjaxAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy
{
private readonly IAntiforgery _antiforgery;
private readonly ILogger _logger;
public ValidateAjaxAntiforgeryTokenAuthorizationFilter(IAntiforgery antiforgery, ILoggerFactory loggerFactory)
{
if (antiforgery == null)
{
throw new ArgumentNullException(nameof(antiforgery));
}
_antiforgery = antiforgery;
_logger = loggerFactory.CreateLogger<ValidateAjaxAntiforgeryTokenAuthorizationFilter>();
}
public async Task OnAuthorizationAsync(AuthorizationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (IsClosestAntiforgeryPolicy(context.Filters) && ShouldValidate(context))
{
try
{
await _antiforgery.ValidateRequestAsync(context.HttpContext);
}
catch (AjaxAntiforgeryValidationException exception)
{
_logger.LogInformation(1, string.Concat("Ajax Antiforgery token validation failed. ", exception.Message));
context.Result = new BadRequestResult();
}
}
}
protected virtual bool ShouldValidate(AuthorizationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return true;
}
private bool IsClosestAntiforgeryPolicy(IList<IFilterMetadata> filters)
{
// Determine if this instance is the 'effective' antiforgery policy.
for (var i = filters.Count - 1; i >= 0; i--)
{
var filter = filters[i];
if (filter is IAntiforgeryPolicy)
{
return object.ReferenceEquals(this, filter);
}
}
Debug.Fail("The current instance should be in the list of filters.");
return false;
}
}
However, I cannot find the proper Nuget package and namespace that contains IAntiforgeryPolicy. While I found the interface on GitHub - what package do I find it in?
My next attempt was to instead go after the IAntiforgery injection, and replace the DefaultAntiforgery with my own AjaxAntiforgery.
public class AjaxAntiforgery : DefaultAntiforgery
{
private readonly AntiforgeryOptions _options;
private readonly IAntiforgeryTokenGenerator _tokenGenerator;
private readonly IAntiforgeryTokenSerializer _tokenSerializer;
private readonly IAntiforgeryTokenStore _tokenStore;
private readonly ILogger<AjaxAntiforgery> _logger;
public AjaxAntiforgery(
IOptions<AntiforgeryOptions> antiforgeryOptionsAccessor,
IAntiforgeryTokenGenerator tokenGenerator,
IAntiforgeryTokenSerializer tokenSerializer,
IAntiforgeryTokenStore tokenStore,
ILoggerFactory loggerFactory)
{
_options = antiforgeryOptionsAccessor.Value;
_tokenGenerator = tokenGenerator;
_tokenSerializer = tokenSerializer;
_tokenStore = tokenStore;
_logger = loggerFactory.CreateLogger<AjaxAntiforgery>();
}
}
I got this far before I stalled out because there is no generic method on ILoggerFactory for CreateLogger<T>(). The source code for DefaultAntiforgery has Microsoft.Extensions.Options, but I cannot find that namespace in any Nuget package. Microsoft.Extensions.OptionsModel exists, but that just brings in the IOptions<out TOptions> interface.
To follow all of this up, once I do get the Authorization Filter to work, or I get a new implementation of IAntiforgery, where or how do I register it with the dependency injection to use it - and only for the actions that I will be accepting Ajax requests?
I had similar issue. I don't know if any changes are coming regarding this in .NET but, at the time, I added the following lines to ConfigureServices method in Startup.cs, before the line services.AddMvc(), in order to validate the AntiForgeryToken sent via Ajax:
services.AddAntiforgery(options =>
{
options.CookieName = "yourChosenCookieName";
options.HeaderName = "RequestVerificationToken";
});
The AJAX call would be something like the following:
var token = $('input[type=hidden][name=__RequestVerificationToken]', document).val();
var request = $.ajax({
data: { 'yourField': 'yourValue' },
...
headers: { 'RequestVerificationToken': token }
});
Then, just use the native attribute [ValidadeAntiForgeryToken] in your Actions.
I've been wrestling with a similar situation, interfacing angular POSTs with MVC6, and came up with the following.
There are two problems that need to be addressed: getting the security token into MVC's antiforgery validation subsystem, and translating angular's JSON-formatted postback data into an MVC model.
I handle the first step via some custom middleware inserted in Startup.Configure(). The middleware class is pretty simple:
public static class UseAngularXSRFExtension
{
public const string XSRFFieldName = "X-XSRF-TOKEN";
public static IApplicationBuilder UseAngularXSRF( this IApplicationBuilder builder )
{
return builder.Use( next => context =>
{
switch( context.Request.Method.ToLower() )
{
case "post":
case "put":
case "delete":
if( context.Request.Headers.ContainsKey( XSRFFieldName ) )
{
var formFields = new Dictionary<string, StringValues>()
{
{ XSRFFieldName, context.Request.Headers[XSRFFieldName] }
};
// this assumes that any POST, PUT or DELETE having a header
// which includes XSRFFieldName is coming from angular, so
// overwriting context.Request.Form is okay (since it's not
// being parsed by MVC's internals anyway)
context.Request.Form = new FormCollection( formFields );
}
break;
}
return next( context );
} );
}
}
You insert this into the pipeline with the following line inside the Startup.Configure() method:
app.UseAngularXSRF();
I did this right before the call to app.UseMVC().
Note that this extension transfers the XSRF header on any POST, PUT or DELETE where it exists, and it does so by overwriting the existing form field collection. That fits my design pattern -- the only time the XSRF header will be in a request is if it's coming from some angular code I've written -- but it may not fit yours.
I also think you need to configure the antiforgery subsystem to use the correct name for the XSRF field name (I'm not sure what the default is). You can do this by inserting the following line into Startup.ConfigureServices():
services.ConfigureAntiforgery( options => options.FormFieldName = UseAngularXSRFExtension.XSRFFieldName );
I inserted this right before the line services.AddAntiforgery().
There are several ways of getting the XSRF token into the request stream. What I do is add the following to the view:
...top of view...
#inject Microsoft.AspNet.Antiforgery.IAntiforgery af
...rest of view...
...inside the angular function...
var postHeaders = {
'X-XSRF-TOKEN': '#(af.GetTokens(this.Context).FormToken)',
'Content-Type': 'application/json; charset=utf-8',
};
$http.post( '/Dataset/DeleteDataset', JSON.stringify({ 'siteID': siteID }),
{
headers: postHeaders,
})
...rest of view...
The second part -- translating the JSON data -- is handled by decorating the model class on your action method with [FromBody]:
// the [FromBody] attribute on the model -- and a class model, rather than a
// single integer model -- are necessary so that MVC can parse the JSON-formatted
// text POSTed by angular
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult DeleteDataset( [FromBody] DeleteSiteViewModel model )
{
}
[FromBody] only works on class instances. Even though in my case all I'm interested in is a single integer, I still had to dummy up a class, which only contains a single integer property.
Hope this helps.
Using a anti forgery token in a Ajax call is possible but if you are trying to secure a Api I really would suggest using a Access Token instead.
If you are relying on a identity token stored in a cookie as authentication for your Api, you will need to write code to compensate for when your cookie authentication times out, and your Ajax post is getting redirected to a login screen. This is especially important for SPAs and Angular apps.
Using a Access Token implementation instead, will allow you to refresh you access token (using a refresh token), to have long running sessions and also stop cookie thiefs from accessing your Apis.. and it will also stop XSRF :)
A access token purpose is to secure resources, like Web Apis.

Override global authorize filter in ASP.NET Core 1.0 MVC

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:
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.
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.

Categories