Reputation: 24522
I have a controller decorated with an AuthorizeAttribute. The controller contains several actions that all require authentication apart from one action that requires some custom authentication provided by CustomAuthorizeAttribute.
My question is once I've added [Authorize] at the controller level can I override it (or remove it) with [CustomAuthorize] on just one action? Or do I have to remove [Authorize] from the controller level and add it individually to every other action?
I'm asking purely for convenience because I'm lazy and don't want to decorate every action with the AuthorizeAttribute.
[Authorize]
public class MyController : Controller {
//requires authentication
public ViewResult Admin() {
return View();
}
//... a lot more actions requiring authentication
//requires custom authentication
[CustomAuthorize] //never invoked as already failed at controller level
public ViewResult Home() {
return View();
}
}
Upvotes: 78
Views: 43356
Reputation: 1827
Stuff changed over time, and .NET 6 doesn't do the AuthorizeAttribute.Order
property or any way to override it other than [AllowAnonymous]
which takes priority. Basically, you can have the controller say "I require the Sales policy" and have one of the actions say "I require the Admin policy on top of that" or "Forget all that, allow Everyone to see this", but you can't have it say "Forget Sales, just require Login".
That being said, if you know what you're getting into and can afford the dirty solution, you can use reflection to access a private field within the HttpContext. (The alternative is to try and re-implement AuthorizationMiddleware
entirely)
First, define the override:
public class OverrideAuthorizeAttribute : AuthorizeAttribute
{
public OverrideAuthorizeAttribute(string policy) : base(policy) { }
}
Then define the middleware to go between UseAuthentication
and UseAuthorization
:
public class CustomAuthorizationMiddleware
{
private static readonly AuthorizeAttribute emptyAuthorize = new();
private static readonly FieldInfo itemsField = typeof(EndpointMetadataCollection)
.GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("Custom Authorization Middleware could not be initialized");
private readonly RequestDelegate _next;
public CustomAuthorizationMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var endpoint = context.GetEndpoint();
if (endpoint != null)
{
var overridesPolicy = endpoint.Metadata.GetMetadata<OverrideAuthorizeAttribute>() is not null;
if (overridesPolicy)
{
if (itemsField.GetValue(endpoint.Metadata) is object[] items)
{
for (int i = 0; i < items.Length; ++i)
{
if (items[i] is not OverrideAuthorizeAttribute)
{
items[i] = emptyAuthorize;
}
}
}
}
}
await _next(context);
}
}
And what this does is neuter all the AuthorizeAttribute
entries that are not OverrideAuthorizeAttribute
if it detects the override is present, but only for this one request. It then passes the modified context on to the actual Authorize middleware, which interprets it normally. Then just tag the endpoint like so:
[Authorize("Sales")]
public class LicenseController : Controller
{
// This action requires the user to be in the Sales group
public IActionResult SalesOnlyAction()
{
return View();
}
// This action only requires the user to be authenticated
[OverrideAuthorize("Login")]
public IActionResult LoginRequiredAction()
{
return View();
}
}
Upvotes: 0
Reputation: 11
[Applying @bmavity answer to .Net 6]
In .Net 6, we can register a global overridable Authorization Filter
Program.cs
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireRole("Normal")
.Build();
services.AddControllers(options => {
options.Filters.Add(new OverridableAuthorizeFilter(policy));
})
OverridableAuthorizeFilter.cs
public class OverridableAuthorizeFilter : AuthorizeFilter, IAsyncAuthorizationFilter
{
public OverridableAuthorizeFilter(AuthorizationPolicy policy) : base(policy)
{
}
public override async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var action = context.ActionDescriptor;
if (action.EndpointMetadata.Any(em => em.GetType() == typeof(AuthorizeAttribute)))
{
return;
}
var controller = action.FilterDescriptors.FirstOrDefault()?.Filter?.GetType();
if (controller != null && controller.IsDefined(typeof(AuthorizeAttribute), true))
{
return;
}
await base.OnAuthorizationAsync(context);
}
}
We can then override the global Authorization Filter by specifying a Authorization Attribute at the controller of action method level:
[Authorize(Roles = "Admin,Employee")] // admin or employee, ignores normal from global authorization filter
public class XController : Controller
{
[Authorize(Roles = "Admin")] // only admin, ignores normal from global authorization filter
public ActionResult ActionX() { ... }
[AllowAnonymous] // anyone
public ActionResult ActionX() { ... }
}
Upvotes: 1
Reputation: 1
Override for all controllers when handling prototype and production environment.
So there is no need to remove the authorize of each controller.
app.UseEndpoints(endpoint =>
{
endpoint.MapControllers().WithMetadata(new AllowAnonymousAttribute());
});
Upvotes: 0
Reputation: 6782
All you need to override the [Authorize] from the controller, for a specific action is to add
[AllowAnonymous]
to the action you want to not be authorized (then add your custom attribute as required).
See the comments / intellisense :
Represents an attribute that marks controllers and actions to skip the System.Web.Mvc.AuthorizeAttribute during authorization.
Full Example
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Diagnostics;
using System.Threading.Tasks;
namespace Website
{
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (true)//Perform own authorization logic
return; //simply return if request is authorized
context.Result = new UnauthorizedResult();
return; //this is not authorized
}
}
[Authorize]
public class WebsiteController : Controller
{
[HttpGet]
[AllowAnonymous]//When this is added our Custom Attribute is hit, without it our attribute is not used as request already gets 401 from controller's Authorize
[CustomAuthorize]
public IActionResult Index()
{
return View(new ViewModel());
}
}
Note
This approach will not work if you want to use the standard [Authorize] attribute on your action, with a custom policy e.g.
[Authorize]
public class WebsiteController : Controller
{
[HttpGet]
[AllowAnonymous]
[Authorize("CustomPolicyName")] //Will not be run
public IActionResult Index()
{
return View(new ViewModel());
}
}
services.AddAuthorization(options =>
{
options.AddPolicy("BadgeEntry", policy =>
policy.RequireAssertion(context =>
false //Custom logic here
));
});
...but if like the OP you want a Custom Attribute then you are good to go with my solution.
Upvotes: 10
Reputation: 13767
In MVC 5 you can override the authorization for any action using the new attribute OverrideAuthorization. Basically, you add it to an action that has a different authorization configuration than the one defined in the controller.
You do it like this:
[OverrideAuthorization]
[Authorize(Roles = "Employee")]
public ActionResult List() { ... }
More information at http://www.c-sharpcorner.com/UploadFile/ff2f08/filter-overrides-in-Asp-Net-mvc-5/
In ASP.NET Core 2.1 there's no OverrideAuthorization attribute and the only thing you can do is make an action anonymous, even if the controller is not. More information at https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-2.1
One option is to do it this way:
[Authorize(Roles = "Admin,Employee")] // admin or employee
public class XController : Controller
{
[Authorize(Roles = "Admin")] // only admin
public ActionResult ActionX() { ... }
[AllowAnonymous] // anyone
public ActionResult ActionX() { ... }
}
Upvotes: 132
Reputation: 2508
After way too much time, I came up with a solution. You need to decorate your controller with a custom AuthorizeAttribute.
public class OverridableAuthorize : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
var action = filterContext.ActionDescriptor;
if(action.IsDefined(typeof(IgnoreAuthorization), true)) return;
var controller = action.ControllerDescriptor;
if(controller.IsDefined(typeof(IgnoreAuthorization), true)) return;
base.OnAuthorization(filterContext);
}
}
Which can be paired with AllowAnonymous
on an Action
[AllowAnonymous]
Upvotes: 14
Reputation: 532435
You can change the Order in which the attributes run (using the Order property), but I believe that in this case they will still both run unless one generates a result with immediate effect. The key is to have the least restrictive attribute applied at the highest level (class) and get more restrictive for the methods. If you wanted the Home
action to be publicly available, for instance, you would need to remove the Authorize attribute from the class, and apply it to each of the other methods.
If the action has the same level of permissiveness, but has a different result, changing the order may be sufficient. For example, you would normally redirect to the Logon
action, but for Home
you want to redirect to the About
action. In this, case give the class attribute Order=2
and the Home
action attribute Order=1
.
Upvotes: 29