views:

489

answers:

4

Is there any way to centralize enforcement that every action method must have a "ValidateAntiForgeryToken" attribute? I'm thinking it would have to be done by extending one the "routing" classes.

Edit: Or maybe do some reflection at application startup?

+6  A: 

Yes. You can do this by creating your own BaseController that inherits the Mvc Controller, and overloads the OnAuthorization(). You want to make sure it is a POST event before enforcing it:

public abstract class MyBaseController : Controller
{
  protected override void OnAuthorization(AuthorizationContext filterContext)
  {
    //enforce anti-forgery stuff for HttpVerbs.Post
    if (String.Compare(filterContext.HttpContext.Request.HttpMethod, "post", true) == 0)
    {
      var forgery = new ValidateAntiForgeryTokenAttribute();
      forgery.OnAuthorization(filterContext);
    }
    base.OnAuthorization(filterContext);
  }
}

Once you have that, make sure all of your controllers inherit from this MyBaseController (or whatever you call it). Or you can do it on each Controller if you like with the same code.

eduncan911
This solutions seams to not work on MVC2. It's working just fine for MVC1, but as soon as we upgraded to v2, it stopped working. Trying to find a solution now.
Glenn Nilsson
Thanks for bringing this to my attention. I'll look into as well.
eduncan911
I found _a_ solution at least, not the best one perhaps. See my answer below. The comment box wasn't very good with code formatting =)
Glenn Nilsson
could use System.Net.WebRequestMethods.Http.Post instead of "post"
JoelFan
+3  A: 

Sounds like you're trying to prevent "oops I forgot to set that" bugs. If so I think the best place to do this is with a custom ControllerActionInvoker.

Essentially what you want to do is stop MVC from even finding an action without a AntiForgery token:

public class MustHaveAntiForgeryActionInvoker : ControllerActionInvoker
{
    protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
    {
        var foundAction = base.FindAction(controllerContext, controllerDescriptor, actionName);

        if( foundAction.GetCustomAttributes(typeof(ValidateAntiForgeryTokenAttribute), true ).Length == 0 )
            throw new InvalidOperationException("Can't find a secure action method to execute");

        return foundAction;
    }
}

Then in your controller, preferably your base controller:

ActionInvoker = new MustHaveAntiForgeryActionInvoker();

Just wanted to add that custom Controller base classes tend to get "thick" and imo its always best practice to use MVC's brilliant extensibility points to hook in the features you need where they belong.

Here is a good guide of most of MVC's extensibility points: http://codeclimber.net.nz/archive/2009/04/08/13-asp.net-mvc-extensibility-points-you-have-to-know.aspx

jfar
+2  A: 

Ok, I just upgraded a project to MVC v2.0 here, and eduncan911's solution doesn't work anymore if you use the AuthorizeAttribute on your controller actions. It was somewhat hard to figure out why.

So, the culprit in the story is that the MVC team added the use of the ViewContext.HttpContext.User.Identity.Name property in the value for the RequestVerificationToken.

The overridden OnAuthorization in the base controller is executed before any filters on the controller action. So, the problem is that the Authorize attribute has not yet been invoked and therefore is the ViewContext.HttpContext.User not set. So the UserName is String.Empty whereas the AntiForgeryToken used for validation includes the real user name = fail.

We solved it now with this code:

public abstract class MyBaseController : Controller
{
    protected override void OnAuthorization(AuthorizationContext filterContext)
    {
        //enforce anti-forgery stuff for HttpVerbs.Post
        if (String.Compare(filterContext.HttpContext.Request.HttpMethod, "post", true) == 0)
        {
            var authorize = new AuthorizeAttribute();
            authorize.OnAuthorization(filterContext);
            if (filterContext.Result != null) // Short circuit validation
                return;
            var forgery = new ValidateAntiForgeryTokenAttribute();
            forgery.OnAuthorization(filterContext);
        }
        base.OnAuthorization(filterContext);
    }
}

Some references to the MVC code base:

ControllerActionInvoker#InvokeAuthorizationFilters() line 283. Same short circuiting. AntiForgeryData#GetUsername() line 98. New functionality.

Glenn Nilsson
I went ahead and gave ya upvotes (+3, wow!). But, I am using MVC 2.0 RTM with VS2010 RTM on the 3.5 framework (it's an Azure project, so I must remain 3.5 for now). The anti-forgery solution I posted still works. I wonder if the issue was that you were using MVC 2.0 RC2 or alike?
eduncan911
A: 

Hi

Thanks for the solution, as for eduncan911, the original solution works fine for me, I'm also using MVC 2.0 RTM and 4.0 framework. (Couldn't write a comment, thanks stackoverflow for not trusting me with my low rep)

Jeep