How would I write an ActionFilter that ensures AntiForgeryTokens are used for every Post operation?
Asked Answered
T

2

8

I want to use AntiForgeryTokens on every HttpPost Action using an ActionFilter that is in a controller named ControllerBase that every other controller inherits from.

I want to do this by creating an ActionFilter that inherits from ValidateAntiForgeryToken which takes an argument that tells it what HTTP verbs to apply itself to. I then want to apply that filter on ControllerBase to ensure that the AntiForgeryToken is checked for EVERY POST operation on the entire site.

I was looking into using this solution, but

  • AuthorizationContext Constructor (ControllerContext) is an obsolete constructor involved and I am not sure how to rebuild the code using the recommended AuthorizationContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor).

  • It does not appear to use the AntiForgeryToken by default as I get the following error: A required anti-forgery token was not supplied or was invalid after every post action.

How should I rewrite my ActionFilter to meet current non-obsolete standards and to properly use an anti-forgery token on every [HttpPost] verb?

Do I have to include an anti-forgery token in every form myself (I am thinking I do)? (as opposed to it being automatically generated - don't laugh, I'm curious) Update: As pointed out in the comments; Yes, this has to be done with every form.

Here is the code from my ControllerBase for reference:

[UseAntiForgeryTokenOnPostByDefault]
public class ControllerBase : Controller 
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class BypassAntiForgeryTokenAttribute : ActionFilterAttribute
    {
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class UseAntiForgeryTokenOnPostByDefault : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            if (ShouldValidateAntiForgeryTokenManually(filterContext))
            {
                var authorizationContext = new AuthorizationContext(filterContext.Controller.ControllerContext);

                //Use the authorization of the anti forgery token, 
                //which can't be inhereted from because it is sealed
                new ValidateAntiForgeryTokenAttribute().OnAuthorization(authorizationContext);
            }

            base.OnActionExecuting(filterContext);
        }

        /// <summary>
        /// We should validate the anti forgery token manually if the following criteria are met:
        /// 1. The http method must be POST
        /// 2. There is not an existing [ValidateAntiForgeryToken] attribute on the action
        /// 3. There is no [BypassAntiForgeryToken] attribute on the action
        /// </summary>
        private static bool ShouldValidateAntiForgeryTokenManually(ActionExecutingContext filterContext)
        {
            var httpMethod = filterContext.HttpContext.Request.HttpMethod;

            //1. The http method must be POST
            if (httpMethod != "POST") return false;

            // 2. There is not an existing anti forgery token attribute on the action
            var antiForgeryAttributes =
                filterContext.ActionDescriptor.GetCustomAttributes(typeof (ValidateAntiForgeryTokenAttribute), false);

            if (antiForgeryAttributes.Length > 0) return false;

            // 3. There is no [BypassAntiForgeryToken] attribute on the action
            var ignoreAntiForgeryAttributes =
                filterContext.ActionDescriptor.GetCustomAttributes(typeof (BypassAntiForgeryTokenAttribute), false);

            if (ignoreAntiForgeryAttributes.Length > 0) return false;

            return true;
        }
    }
}
Ticknor answered 2/8, 2012 at 15:47 Comment(8)
You can add a filter to the Global Filters collection. I'm not sure it would be necessary to put it on a base controller.Donnelly
I understand that, but would prefer to put it within the ControllerBase for my current architecture.Ticknor
Do I have to include an anti-forgery token in every form myself?... You might be able to write a custom BeginForm HtmlHelper that will emit this automatically. Otherwise, yes, you would need to output the token on each form manually.Rodrick
@Rodrick Thank you, that seems like what I should and will do.Ticknor
@Ticknor public static MvcForm BeginSecureForm(this HtmlHelper htmlHelper, RouteValueDictionary routeValues) { var mvcForm = htmlHelper.BeginForm(routeValues); htmlHelper.ViewContext.Writer.Write(htmlHelper.AntiForgeryToken().ToHtmlString()); return mvcForm; }Doralynn
@Doralynn this post is over a year old. Thanks anyway? :)Ticknor
@Ticknor .. not like they delete posts, trying to help the next guyDoralynn
@Doralynn That's a very good point. Keep it up!Ticknor
E
1

You don't need to instantiate any AuthorizationContext or call the OnAuthorization method, simply:

if (ShouldValidateAntiForgeryTokenManually(filterContext))
{
    AntiForgery.Validate(filterContext.HttpContext, null);
}
Eddington answered 3/8, 2012 at 6:30 Comment(1)
Times are a changin: [Obsolete("This method is deprecated. Use the Validate() method instead.", error: true)] ... this is now wrapped with _worker.Validate(new HttpContextWrapper(HttpContext.Current));Doralynn
P
7

I've used the following approach:

public class SkipCSRFCheckAttribute : Attribute
{
}

public class AntiForgeryTokenFilter : IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationContext filterContext)
    {
        if (IsHttpPostRequest(filterContext) && !SkipCsrfCheck(filterContext))
            AntiForgery.Validate();
    }

    private static bool IsHttpPostRequest(AuthorizationContext filterContext)
    {
        return filterContext.RequestContext.HttpContext.Request.HttpMethod == HttpMethod.Post.ToString();
    }

    private static bool SkipCsrfCheck(AuthorizationContext filterContext)
    {
        return filterContext.ActionDescriptor.GetCustomAttributes(typeof (SkipCSRFCheck), false).Any();
    }
}

Which enables us to disable it on a case-by-case basis using the SkipCSRFCheck attribute, and then registering it as a global filter in the Application_Start:

GlobalFilters.Filters.Add(new AntiForgeryTokenFilter());

Propylite answered 22/2, 2014 at 13:33 Comment(0)
E
1

You don't need to instantiate any AuthorizationContext or call the OnAuthorization method, simply:

if (ShouldValidateAntiForgeryTokenManually(filterContext))
{
    AntiForgery.Validate(filterContext.HttpContext, null);
}
Eddington answered 3/8, 2012 at 6:30 Comment(1)
Times are a changin: [Obsolete("This method is deprecated. Use the Validate() method instead.", error: true)] ... this is now wrapped with _worker.Validate(new HttpContextWrapper(HttpContext.Current));Doralynn

© 2022 - 2024 — McMap. All rights reserved.