The title says it all really, earlier this week I was attempting to submit data in the form of JSON to a controller method that implemented the ValidateAntiForgeryTokenAttribute.
On the client side of things I managed to get the validation input generated by Html.AntiForgeryToken into my JSON object but I just kept on getting the “A required anti-forgery token was not supplied or was invalid” exception.
After a bit of research I found the problem to lie in the Asp.Net MVC 2 source, if you look at the ValidateAntiForgeryTokenAttribute file specifically on line 56 (OnAuthorization method) you’ll see this:
string formValue = filterContext.HttpContext.Request.Form[fieldName];
The validation class always checks the form for the token but we don’t have any form data when we post JSON!
So my solution was to modify like so:
string value; if (filterContext.HttpContext.Request.ContentType.ToLower().Contains("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.ASCII.GetString(bytes); var jsonObject = JObject.Parse(json); value = (string)jsonObject[fieldName]; } else { value = filterContext.HttpContext.Request.Form[fieldName]; }
What happens here is I do a check to see if the post is json, if so I then pull the string from the input stream and using a JSON parser extract the token.
Since we’re modifying the ValidateAntiForgery class we might as allow it to work with (or restrict to) any http verb.
This is what I’ve ended up with (sorry about the formatting):
namespace Your.App { using System; using System.Linq; using System.Text; using System.Web; using System.Web.Mvc; using Newtonsoft.Json.Linq; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class ValidateJsonAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter { private string _salt; private AntiForgeryDataSerializer _serializer; private readonly AcceptVerbsAttribute _verbs; public string Salt { get { return _salt ?? String.Empty; } set { _salt = value; } } internal AntiForgeryDataSerializer Serializer { get { if (_serializer == null) { _serializer = new AntiForgeryDataSerializer(); } return _serializer; } set { _serializer = value; } } public ValidateJsonAntiForgeryTokenAttribute(HttpVerbs verbs = HttpVerbs.Post):this(null, verbs) { } public ValidateJsonAntiForgeryTokenAttribute(string salt, HttpVerbs verbs = HttpVerbs.Post) { this._verbs = new AcceptVerbsAttribute(verbs); this._salt = salt; } private bool ValidateFormToken(AntiForgeryData token) { return (String.Equals(Salt, token.Salt, StringComparison.Ordinal)); } private static HttpAntiForgeryException CreateValidationException() { return new HttpAntiForgeryException("A required anti-forgery token was not supplied or was invalid."); } public void OnAuthorization(AuthorizationContext filterContext) { if (filterContext == null) { throw new ArgumentNullException("filterContext"); } // We only need to validate this if it's a post string httpMethodOverride = filterContext.HttpContext.Request.GetHttpMethodOverride(); if (!this._verbs.Verbs.Contains(httpMethodOverride, StringComparer.OrdinalIgnoreCase)) { return; } string fieldName = AntiForgeryData.GetAntiForgeryTokenName(null); string cookieName = AntiForgeryData.GetAntiForgeryTokenName(filterContext.HttpContext.Request.ApplicationPath); HttpCookie cookie = filterContext.HttpContext.Request.Cookies[cookieName]; if (cookie == null || String.IsNullOrEmpty(cookie.Value)) { // error: cookie token is missing throw CreateValidationException(); } AntiForgeryData cookieToken = Serializer.Deserialize(cookie.Value); string value; if (filterContext.HttpContext.Request.ContentType.ToLower().Contains("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.ASCII.GetString(bytes); var jsonObject = JObject.Parse(json); value = (string)jsonObject[fieldName]; } else { value = filterContext.HttpContext.Request.Form[fieldName]; } if (String.IsNullOrEmpty(value)) { // error: form token is missing throw CreateValidationException(); } AntiForgeryData formToken = Serializer.Deserialize(value); if (!String.Equals(cookieToken.Value, formToken.Value, StringComparison.Ordinal)) { // error: form token does not match cookie token throw CreateValidationException(); } if (!ValidateFormToken(formToken)) { // error: custom validation failed throw CreateValidationException(); } } } }
Also I had to copy over 2 classes from the MVC source. The “AntiForgeryData” and “AntiForgeryDataSerializer” since they are interal and my project wouldn’t compile without them.
Final note, if you’re having trouble with the Serializer you copied over (in the Deserialize method and casting to a Triplet) then this may be of some help (.Net 4.0 only):
dynamic deserializedObj = formatter.Deserialize(serializedToken); return new AntiForgeryData() { Salt = deserializedObj[0], Value = deserializedObj[1], CreationDate = (DateTime)deserializedObj[2] };
8 Comments
Thanks!! It works perfect!
Fantastic, love it!
When you said you had to bring over the two sealed classes for your new Validation attribute, did you have to rename them or do anything so they wouldn’t conflict with the sealed versions within the MVC assembly?
It doesn’t look like you renamed them. I haven’t tried it out yet, was just curious if you had any issues with it.
Thanks again!
No it worked fine for me.
Thanks. This really came in handy this morning.
Hi !
Is it possible to have an example (source code, albertini.olivier@gmail.com) because i have this problem… and it’ll make 2 days that i search a solution.. i’m on .NET 4. Json with validationToken doesn’t work …
Please,
i really appreciate that !!!
Sorry for the late reply, I’m afraid I don;’t have an example (I used this in live code) but what I did above is essentially the same code I.
Tony,
Great article, thanks! One thing though, in my case I needed to get the MvcResources,resx and cs files as well. Other than that. It was a pretty easy fix.
MVC 5 changed the definition of the Attribute quite a bit. This is what I put together to allow getting the fields from either the header or the original form field.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class,
AllowMultiple = false, Inherited = true)]
public class ValidateJsonAntiForgeryTokenAttribute :
FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException(“filterContext”);
}
var formValue = filterContext.RequestContext.HttpContext.Request.Form[“__RequestVerificationToken”];
var headerValue = filterContext.RequestContext.HttpContext.Request.Headers[“__RequestVerificationToken”];
string cookieValue = null;
var cookieInstance = filterContext.RequestContext.HttpContext.Request.Cookies[“__RequestVerificationToken”];
if (cookieInstance != null)
{
cookieValue = cookieInstance.Value;
}
if (formValue == null)
{
formValue = headerValue;
}
AntiForgery.Validate(cookieValue, formValue);
}
}