Anti-forgery Tokens in ASP.NET MVC and Knockout

As I am implementing a small application framework for my current project that is using ASP.NET MVC and Knockout JavaScript library, I had to rethink the approach to using anti forgery tokens.  Any forgery tokens in ASP.NET MVC are designed to prevent cross-site request forgery attacks. Cross site scripting is number 2 on list of attacks published by OWASP.  In MVC it is very easy to prevent these attacks, so there is not reason for you not to do it.  It is a two-step process.  You have to inject the token into your view and decorate the action method in your controller with appropriate attribute:

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

 

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Save(Product product)

As you can see above, the process is pretty easy

However, once I started using Knockout, I also started using Json based payloads to my methods.  Unfortunately, ValidateAntiForgeryToken only looks at the Form values submitted, so you will end up with an exception.  I searched the internet,m and the solutions to these problems in my opinion were way over-engineered.  Most of they required writing your own classes by copying classes from MVC.  Not my idea of fun.

So, my solution is based on two parts.  One, I have my own attribute that is way simpler than anything else I saw.  Two, I setup a pattern in JavaScript I can follow.  Let’s take a look at the each part.

First of all, the attribute.  I did not have to re-write any classes from MVC.  I simply had to parse anti forger token value based on predefined name that is a constant in MVC source code.

using System;
using System.Text;
using System.Web.Mvc;
using Newtonsoft.Json.Linq;

namespace MyApp.AntiForgery
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class MyValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
    {
        private readonly ValidateAntiForgeryTokenAttribute _validateAntiForgeryTokenAttribute;
        private const string FieldName = "__RequestVerificationToken";
        public MyValidateAntiForgeryTokenAttribute()
        {
            _validateAntiForgeryTokenAttribute = new ValidateAntiForgeryTokenAttribute();
        }

        #region Implementation of IAuthorizationFilter

        /// <summary>
        /// Called when authorization is required.
        /// </summary>
        /// <param name="filterContext">The filter context.</param>
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext.HttpContext.Request.ContentType.ToLower().Contains("application/json"))
            {
                var bytes = new byte[filterContext.HttpContext.Request.InputStream.Length];
                filterContext.HttpContext.Request.InputStream.Read(bytes, 0, bytes.Length);
                filterContext.HttpContext.Request.InputStream.Position = 0;
                var json = Encoding.UTF8.GetString(bytes);
                var jsonObject = JObject.Parse(json);
                var value = (string)jsonObject[FieldName];
                var httpCookie = filterContext.HttpContext.Request.Cookies[FieldName];
                if (httpCookie != null)
                {
                    System.Web.Helpers.AntiForgery.Validate(httpCookie.Value, value);
                }
                else
                {
                    throw new HttpAntiForgeryException("Anti forgery token cookie not found");
                }
            }
            else
            {
                _validateAntiForgeryTokenAttribute.OnAuthorization(filterContext);
            }
        }

        #endregion
    }
}

I used Json.NET library to parse Json from the request.  I also get the cookie value from the request.  Once that is done, I simply rely on built-in AntiForgery helper to compare two values.  This is exactly how the built-in process works.  You can always look at the ASP.NET MVC source code and you will see almost the same code as mine in the current ASP.NET MVC implementation as well.  This is the principle behind the validation – a cookie is injected into the browser when view calls Html.AntiForgeryToken().  At the same time the token is injected into html.  At the other end we compare the two values when they are posted back.  Simple and easy.  The other portion is a few lines of code I wrote into base view model JavaScript class I use with my screens.  I simply look for the token in the form, then add it to my data before calling mapping library for Knockout.  Once this is done, I can send my view model to the server and it will contain the token.

    function MyViewModel(jsObject, formName) {
        var antiForgeryToken = $('#' + formName + ' input[name="__RequestVerificationToken"]').val();
        if (antiForgeryToken) {
            jsObject.__RequestVerificationToken = antiForgeryToken;
        }
        var data = ko.mapping.fromJS(jsObject);
        this.data = viewModel;
    }

Above I am passing in Json data into my constructor along with form name.  Then I am getting the token value and adding it to Json object prior to creating Knockout view model.

Please let me know if you have any questions.

Thanks.

10 Comments

  1. I’m so glad I found this article. I also thought the approach to copy mvc internals was far too over engineered. This one is much much cleaner. Thanks!

    Btw, for anyone struggling to pass a javascript array along with the token, here’s what worked for me:

    var parameters = {
    __RequestVerificationToken: inputElement.closest(‘form’).find(‘input[name^=”__RequestVerificationToken”]’).val(),
    parameterName: [ “item1”, “item2”, “item3” ]
    };
    $.ajax({
    type: “POST”,
    url: “/Controller/Action”,
    contentType: “application/json”,
    data: JSON.stringify(parameters)
    });

  2. FYI, I had to change how the cookie is retrieved to use the AntiForgeryConfig.Name property instead of a hard coded name:
    var httpCookie = filterContext.HttpContext.Request.Cookies[AntiForgeryConfig.CookieName];

    Otherwise, works like a champ! Thanks for this post.

Leave a Reply to Daniel B Cancel reply

Your email address will not be published. Required fields are marked *