1

I googled and stackoverflowed this question a lot and its different variants, but I'm still confused if it's possible at all. I just want to add a custom header to all actions having specific attribute. Sounds simple? But it's not. I have just written following:

[AttributeUsage(AttributeTargets.Method)]
public class HelloWorldAttribute : ActionFilterAttribute
{
    /// <inheritdoc />
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        filterContext.HttpContext.Response.Headers["X-HelloWorld"] = string.Empty;
    }
}

And it works fine for all requests except when they are forbidden by [Authorize] on Controller level.

I tried use to use this attribute for Controller level and pass names of methods that have to add this header to it, but it doesn't work too. It seems that Authorize has always a higher priority. And you can agree that it's ugly.

How can it be done?

Alex Zhukovskiy
  • 9,565
  • 11
  • 75
  • 151
  • What if you register it globally? If haven't yet. – dcg Jun 26 '17 at 13:43
  • @dcg I din't because I need in only for a several methods on couple of controllers. So it's easier to have a white list of controllers that have to add this header than register it globally and blaclist all except them. – Alex Zhukovskiy Jun 26 '17 at 13:45
  • 3
    Well, take a step back and think about what you are doing. Why would you want the code for the header to execute when someone is NOT authorized to see something? By definition, if you are not authorized, you should not get any functionality out of it and that will include your header. It's not that authorization has "higher priority". It's that it executes earlier in the pipeline in order to accomplish the aforementioned. – JuanR Jun 26 '17 at 13:49
  • Never used this before, but maybe handle the [PreSendRequestHeaders event](https://msdn.microsoft.com/en-us/library/system.web.httpapplication.presendrequestheaders.aspx)? There seems to be [several events in the pipeline](https://msdn.microsoft.com/en-us/library/bb470252.aspx) that you may be able to access. – Kenneth K. Jun 26 '17 at 13:49
  • @Juan this header itself doesn't provide any functionallity. It just say let's say `This action can be saved as a txt document`. If user doesn't have an access he can't access this resource and can't download this document. However, I don't see any problems if he knows that this resource can be downloaded, but haven't an access to it. – Alex Zhukovskiy Jun 26 '17 at 13:54
  • 1
    @AlexZhukovskiy: So there is no reason to add this header then. It looks to me like you are trying to solve a problem you don't have. You are looking to force a header that is not needed. – JuanR Jun 26 '17 at 14:07
  • @Juan you are partially right. In this case users are tiered, and here we have a `ClaimsAuthorize` that checks if user's tier is enough. And if it's not he gets `403`, but I'd also like to add `X-User-Can-Be-Promouted-To-A-Higher-Tier`. It's defenitly a use case. – Alex Zhukovskiy Jun 26 '17 at 14:22
  • Ha, now that is a different story and I think you just answered your own question. You should be adding this header in the ClaimsAuthorize attribute code. If you don't have access to the code, create your own that derives from it and implement there. – JuanR Jun 26 '17 at 14:33

2 Answers2

2

You can write you own authorize attribute, and handle you code if note autorized or you can add Policy.

Oh i'm soo lazy...

here example

First add policy

public class ReplaceHeaderPolicy : IAuthorizationRequirement
{

}

public class ReplaceHeaderHandler : AuthorizationHandler<ReplaceHeaderPolicy>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ReplaceHeaderPolicy requirement)
    {

        if (!context.User.Identity.IsAuthenticated)
        { 
            var fc = (FilterContext)context.Resource;
            fc.HttpContext.Response.Headers["X-HelloWorld"] = string.Empty;
        }

        context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

then register your policy

services.AddAuthorization(options =>
        {
            options.AddPolicy("ReplaceHeader",
                              policy => policy.Requirements.Add(new ReplaceHeaderPolicy()));
        });

        services.AddSingleton<IAuthorizationHandler, ReplaceHeaderHandler>();

and use it on controller

[Authorize]
    [Authorize(Policy = "ReplaceHeader")]
    public IActionResult Index()
    {
        return View();
    }

Remove second [Authorize] to allow access for Unauthorized access

I hope it's helps

itikhomi
  • 1,560
  • 13
  • 13
  • Add more details rather referencing the url. From [reviews](https://stackoverflow.com/review/first-posts/16531641). – harshavmb Jun 26 '17 at 14:28
  • IIRC in practice we just replaced AuthorisationAttribute with FilterAttribute, which could be ran after header is added. However this approach seems to be working one too. – Alex Zhukovskiy May 01 '18 at 15:38
2

The problem is that the Authorize attribute is a guard against the controller method from actually running so your other attribute is never going to get run.

You can add a custom attribute like this

public class HandleUnauthorizedAttribute : HandleErrorAttribute
{
    public override void OnException(ExceptionContext filterContext)
    {
        base.OnException(filterContext);

        if (filterContext.Exception.GetType() != typeof(SecurityException)) return;

        var controllerName = (string)filterContext.RouteData.Values["controller"];
        var actionName = (string)filterContext.RouteData.Values["action"];
        var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);

        filterContext.Result = new ViewResult
        {
            ViewName = "Unauthorized",
            ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
            TempData = filterContext.Controller.TempData
        };
        filterContext.ExceptionHandled = true;
        filterContext.HttpContext.Response.Clear();
        filterContext.HttpContext.Response.StatusCode = 403;
        filterContext.HttpContext.Response.Headers.Add("X-HelloWorld", "");
        filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
    }
}

which will handle the security exception that will get thrown from your Authorize attribute.

Or you could write a custom Authorize attribute that then adds you headers on response.

Fran
  • 6,440
  • 1
  • 23
  • 35