Orchard's AntiForgeryAuthorizationFilter and Ajax POST requests

Topics: Core
Dec 15, 2012 at 6:51 AM

While working through Skywalker's webshop tutorials I noticed that the ajax requests being sent to update the shopping cart quantity (from the client-side knockoutjs based script) were failing with HTTP 500 errors due to the __RequestVerificationToken not being found.

I have AntiForgery enabled in the module, thus Orchard's AntiForgeryAuthorizationFilter is automatically attempting to validate POST requests. The ajax request being sent is a POST with JSON data.

I want to detail the solution I have come up with for feedback, and I also want to raise a few questions/concerns around the current implementation of Orchard's AntiForgeryAuthorizationFilter (Orchard.Framework/Mvc/AntiForgery/AntiForgeryAuthorizationFilter.cs)

As a background, the root issue is with the core System.Web.Mmvc.ValidateAntiForgeryTokenAttribute class which only looks for the _RequestVerificationToken within form fields in the post request. So if you are POSTing JSON data or using GET with the fields in a query string, the anti-forgery token validation will fail.

Orchard has a partial work-around to this (at least for GET requests) through the provision of the ValidateAntiForgeryTokenOrchardAttribute which can be used on GET actions in a controller to force the AntiForgeryAuthorizationFilter to validate the GET request. The AntiForgeryAuthorizationFilter then checks for this attribute on the current action, and if found it will search the request query string for the __RequestVerificationToken and use a hack to inject it into the HttpContext as a form field before validating with the core System.Web.Mmvc.ValidateAntiForgeryTokenAttribute.

Concern 1. If you're performing a POST (from AJAX), even if you include the __RequestVerificationToken in the query string, the code in this filter will not look for it unless you explicitly adorn your action with ValidateAntiForgeryTokenOrchardAttribute. This is a little counter-intuitive because if you have AntiForgery enabled in your module, then the assumption (at least my assumption) is that all POST requests are validated, and that the ValidateAntiForgeryTokenOrchardAttribute is only needed to force GET requests to be validated.


Concern 2. The first block of code in the filter's OnAuthorization method will drop-out (bypass validation) if the request is a POST request AND no user is currently authenticated (when the action is not adorned with ValidateAntiForgeryTokenOrchardAttribute). This is effectively saying "If it's a POST request and there's no authenticated user then check if the ValidateAntiForgeryTokenOrchardAttribute is on the action, and if it's not, then don't bother validating". I question why the state of the current user even comes into play here? Don't we want to do anti-forgery checks whether regardless of whether a user is authenticated or anonymous?


Concern 3. With the progession to MVC 4, we can now call System.Web.Helpers.AntiForgery.Validate() directly and pass in the cookie and the value contained in __RequestVerificationToken. This means I think we can do away with the HackHttpContext work-around in the AntiForgeryAuthorizationFilter alltogether and come up with a much cleaner approach.

Concern 4. Is the AntiForgeryAuthorizationFilter hit if requests are made via the new WebAPI features (which I believe Orchard now supports)? If so, then if someone has a module that exposes WebAPI actions and has AntiForgery enabled on the module, then the OnAuthorization routine possibly should appply validation to PUT and DELETE methods? I guess it could look for the validation token in the request header?

So here's the approach I've taken to the AntiForgeryAuthorizationFilter class...

  1. In OnAuthorization, if the request is a GET request and the action is not adorned with ValidateAntiForgeryTokenOrchardAttribute then dropout. So if it's a POST or DELETE or PUT, we'll keep going...
  2. Then check if AntiForgery is not enabled on the module in which case we'll dropout (unless the ValidateAntiForgeryTokenOrchardAttribute is on the action - in which case we always validate - allows us to selectively validate actions even when anti-forgery is disabled for the module by default).
  3. Now check the http request header for a validation token (I think this is a nice clean place to put it, and could also support WebAPI based requests - not that I'm sure there's a need for anti-forgery validation in this case). This will allow us to inject the token in the header of the http request when using ajax. In the sample code below, I've given the http header key "X-Request-Verification-Token" rather than "__RequestVerificationToken" to indicate the header is non-standard.
  4. Also check the query string for the validation token - supporting the case where a form has been submitted using GET (if we got past step 1 above).
  5. If we found a token from either of the checks in step 3/4 above then call System.Web.Helpers.AntiForgery.Validate() passing in the cookie and token, rather than hacking a form field into the HttpContext before using the core System.Web.Mmvc.ValidateAntiForgeryTokenAttribute method of validation.
  6. If we didn't find a token, then just call the core System.Web.Mmvc.ValidateAntiForgeryTokenAttribute to validate (this will cover all our standard POST forms) as we do now.

Here's the refactored AntiForgeryAuthorizationFilter class...

using System;
using System.Collections.Specialized;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using JetBrains.Annotations;
using Orchard.Environment.Extensions;
using Orchard.Mvc.Filters;
using Orchard.Security;
using Orchard.Settings;
using Orchard.Mvc.AntiForgery;

namespace Orchard.Mvc.AntiForgery {
    [UsedImplicitly]
    public class AntiForgeryAuthorizationFilter : FilterProvider, IAuthorizationFilter {
        private readonly ISiteService _siteService;
        private readonly IAuthenticationService _authenticationService;
        private readonly IExtensionManager _extensionManager;

        public AntiForgeryAuthorizationFilter(ISiteService siteService, IAuthenticationService authenticationService, IExtensionManager extensionManager)
        {
            _siteService = siteService;
            _authenticationService = authenticationService;
            _extensionManager = extensionManager;
        }

        public void OnAuthorization(AuthorizationContext filterContext) {
            var request = filterContext.HttpContext.Request;

            // TODO: We might want to add a new attribute to allow validation to be disabled
            // for particular actions when the module has AntiForgery enabled in general. For
            // example DoNotValidateAntiForgeryTokenOrchardAttribute. If we had this we would 
            // check here and drop out first.

            // If the action is adorned with the ValidateAntiForgeryTokenOrchard attribute then
            // we always validate regardless of the general module setting or the http method.
            // Otherwise we'll only validate if anti-forgery is enabled for the module and the
            // request is not a GET request.
            if (!ShouldValidate(filterContext))
            {
                if (request.HttpMethod == "GET" || !IsAntiForgeryProtectionEnabled(filterContext))
                    return;
            }

            // If the request header or the request query string contains the
            // verification token, then use this to validate the request.
            string requestVerificationToken = request.Headers.Get("X-Request-Verification-Token") ?? request.QueryString["__RequestVerificationToken"];
            if (!string.IsNullOrWhiteSpace(requestVerificationToken))
            {
                var cookie = request.Cookies[System.Web.Helpers.AntiForgeryConfig.CookieName];
                System.Web.Helpers.AntiForgery.Validate(cookie != null ? cookie.Value : null, requestVerificationToken);
            }
            else
            {
                var validator = new ValidateAntiForgeryTokenAttribute();
                validator.OnAuthorization(filterContext);
            }
        }

        private bool IsAntiForgeryProtectionEnabled(ControllerContext context) {
            string currentModule = GetArea(context.RouteData);
            if (!String.IsNullOrEmpty(currentModule)) {
                foreach (var descriptor in _extensionManager.AvailableExtensions()) {
                    if (String.Equals(descriptor.Id, currentModule, StringComparison.OrdinalIgnoreCase)) {
                        if (String.Equals(descriptor.AntiForgery, "enabled", StringComparison.OrdinalIgnoreCase)) {
                            return true;
                        }
                        return false;
                    }
                }
            }

            return false;
        }

        private static string GetArea(RouteData routeData) {
            if (routeData.Values.ContainsKey("area"))
                return routeData.Values["area"] as string;

            return routeData.DataTokens["area"] as string ?? "";
        }

        private static bool ShouldValidate(AuthorizationContext context) {
            var attributes =
                (ValidateAntiForgeryTokenOrchardAttribute[])
                context.ActionDescriptor.GetCustomAttributes(typeof (ValidateAntiForgeryTokenOrchardAttribute), false);

            if (attributes.Length > 0) 
            {
                return true;
            }

            return false;
        }

    }
}

You can then include the following javascript (needs jQuery) in your layout.cshtml to ensure all AJAX POST requests automatically include a validation token in the request header (just grabs the first one it can find in the DOM). Works a treat!

$(document).ready(function () {

    // Pass anti-forgery token through in the header of all ajax requests
    $.ajaxPrefilter(function (options, originalOptions, jqXHR) {
        var verificationTokenField = $("input[name='__RequestVerificationToken']").first();
        if (verificationTokenField) {
            jqXHR.setRequestHeader("X-Request-Verification-Token", verificationTokenField.val());
        }
    });

});

I've gone so far as writing a result filter (OnResultExecuting) in my own module to inject the javascript automatically so I don't need to explicitly include it in each theme I write. I think it would be nice if we had a core feature that did this (eg. AjaxAntiForgery or something) or maybe we could add a setting and this filter to the Orchard.JQuery module "Inject Anti-Forgery Token into AJAX request headers"?


I'd really appreciate any thoughts and feedback on this approach! I'll then submit a change based on the feedback.

Coordinator
Dec 16, 2012 at 6:20 AM

I think you should file a bug and make a pull request. Thanks for taking the time.

Dec 26, 2012 at 1:53 PM

thanks. works great.should go to core.

Jan 3, 2013 at 3:37 AM

I'm working on putting together a code change now.

As part of this change I'd also like to add a result filter (IResultFilter) to the jQuery module that will inject the small javascript (the javascript in the second code block in the post above) into each page. This will automate the client-side process of adding the verification token to all ajax POST request headers, so the entire anti-forgery process for ajax requests would be completely seamless.

I'm thinking this result filter should only inject the javascript into each page if a setting is enabled (so as not to effect existing deployments). Does anyone have objections to adding a new setting "Include anti-forgery token in ajax requests" which would be added to to the General settings page when the core jQuery module is enabled? Is this setting too complicated  for an end-user? Is there a better approach to enabling this? Another approach would be to make the new result filter a separate feature of the jQuery module (eg. Ajax Anti-Forgery)? Or I could just always inject the script (when jQuery module is enabled) however I'd expect that people would want the control to not have their pages load this additional script if they had no need for it. Any suggestions appreciated...

Jan 3, 2013 at 5:46 AM

I've decided against using a setting or adding an optional feature to the jQuery module that would cause the javascript to be included in every page. I think the control over which pages load this script needs to be more granular.

For now, I've simply added the above javascript to the jQuery module defined as a new script named "jQuery_AjaxAntiForgery" in the ResourceManifest.cs.

In the case that you wish to have the client side include the request verification token in your ajax requests, you can simply add the following to your Layout.cshtml in your theme, or to whichever specific view you need the token sent from...

Script.Require("jQuery_AjaxAntiForgery");

Alternatively, if you have a custom javascript in your own module that is performing the ajax requests, you could define a dependency on the above script within your ResourceManifest.cs as follows...

manifest.DefineScript("MyCustomScript").SetUrl("mycustomscript.js").SetDependencies("jQuery_AjaxAntiForgery");
Note. For the above script to work, you still need to ensure that an anti-forgery token is being generated in an input field (with name="__RequestVerificationToken") somewhere on your page. This would generally be the case if you are writing your pages with progressive enhancement in mind and have called Html.BeginFormAntiForgeryPost() somewhere.

Maybe in the long run a more transparent approach might be to somehow automatically include this script when the html helpers that generate anti-forgery tokens like Html.BeginFormAntiForgeryPost(), Html.AntiForgeryTokenOrchard() or Html.AntiForgeryTokenOrchardValue() are called.

Here's a link to the pull request for the changes...

http://orchard.codeplex.com/SourceControl/network/forks/mjy78/Issue19384/contribution/3876

Jan 14, 2013 at 11:05 PM
Edited Jan 14, 2013 at 11:06 PM

Wow, it would appear I ran into this very same issue. I am trying to unpublish queried content from a secured projection page using a custom link within each query. When that link is executed I get the error:

Exception Details: System.Web.Mvc.HttpAntiForgeryException: The required anti-forgery form field "__RequestVerificationToken" is not present.

Source Error: 

An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.  

Stack Trace: 


[HttpAntiForgeryException (0x80004005): The required anti-forgery form field "__RequestVerificationToken" is not present.]
   System.Web.Helpers.AntiXsrf.TokenValidator.ValidateTokens(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken sessionToken, AntiForgeryToken fieldToken) +757
   System.Web.Helpers.AntiXsrf.AntiForgeryWorker.Validate(HttpContextBase httpContext) +163
   Orchard.Mvc.AntiForgery.AntiForgeryAuthorizationFilter.OnAuthorization(AuthorizationContext filterContext) in c:\Users\sebros\My Projects\Orchard\src\Orchard\Mvc\AntiForgery\AntiForgeryAuthorizationFilter.cs:38
   System.Web.Mvc.ControllerActionInvoker.InvokeAuthorizationFilters(ControllerContext controllerContext, IList`1 filters, ActionDescriptor actionDescriptor) +156
   System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName) +720
   System.Web.Mvc.<>c__DisplayClass1d.b__19() +40
   System.Web.Mvc.Async.<>c__DisplayClass1.b__0() +15
   System.Web.Mvc.Controller.EndExecuteCore(IAsyncResult asyncResult) +53
   System.Web.Mvc.Async.<>c__DisplayClass4.b__3(IAsyncResult ar) +15
   System.Web.Mvc.<>c__DisplayClass8.b__3(IAsyncResult asyncResult) +42
   System.Web.Mvc.Async.<>c__DisplayClass4.b__3(IAsyncResult ar) +15
   Orchard.Mvc.Routes.HttpAsyncHandler.EndProcessRequest(IAsyncResult result) in c:\Users\sebros\My Projects\Orchard\src\Orchard\Mvc\Routes\ShellRoute.cs:162
   System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +607
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +288
 

Do you think your fix would work for my issue?

Coordinator
Jan 14, 2013 at 11:22 PM

Well, try it. But including the anti-forgery token is quite simple, and there are examples in the code (the media picker does it).

Mar 31, 2013 at 6:41 PM
If you open simultaneously two login pages and try to perform login action on the first and then on the second, you'll receive error, while executing second action here

validator.OnAuthorization(filterContext)

The provided anti-forgery token was meant for user "", but the current user is Y.
Coordinator
Mar 31, 2013 at 8:15 PM
So don't do that.
Mar 31 at 9:55 AM
Hello,

I just updated to Orchard 1.8 and I faced to same problem.

There were not pull request to have a way to post AntiForgeryToken in Ajax request ?
Mar 31 at 12:27 PM
I have a feeling the original pull request that I put in was lost in the migration from SVN to GIT. See the comments in the pull request here:

http://orchard.codeplex.com/SourceControl/network/forks/mjy78/Issue19384/contribution/3876

That said, some of my original concerns at the start of this thread I now believe were a little misguided. The code changes that I carry forward (in my own code base) nowdays differs somewhat to the original pull request. It is now my understanding that XSRF checks are only needed when a user is authenticated, as presumably any form that could be submitted by an anonymous user would provide no advantage for an attacker using a XSRF attack (they could simply submit the form themselves anyhow).

That said, the ability to provide an anti-forgery token in an AJAX request for an authenticated user is still something that we need to support cleanly.

I will submit a new pull-request with the changes that I believe now follow the best approach asap. I'll post a link back on this thread once I've done so.
Mar 31 at 1:16 PM
Here's a nice new pull request for this (Issue 19384)...

https://orchard.codeplex.com/SourceControl/network/forks/mjy78/Issue19384b/contribution/6508

As noted in previous comments above, with this change in place, you can then ensure your ajax requests send an anti forgery token by including the jQuery_AjaxAntiForgery script in your view.
Script.Require("jQuery_AjaxAntiForgery");
Also, as noted above, you still need to ensure that an anti forgery token is being generated in your view somewhere by a call to one of the html helpers: Html.BeginFormAntiForgeryPost(), Html.AntiForgeryTokenOrchard() or Html.AntiForgeryTokenOrchardValue().
Apr 16 at 6:58 AM
hi mjy78, how do i apply your pull request into my source?