For anyone that uses the jQuery AJAX library to send POST requests to their Rails application, you have probably found that the built in Rails CSRF protection does not play nice with your jQuery POST requests. By the way, if you don’t know what CSRF is, check out my Ruby on Rails security presentation. You need to send an Authenticity Token with each POST request and jQuery does not do that for you auto-magically. This post explains how I got jQuery, Rails, and Authenticity Tokens to play nice together preventing the dreaded error:
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken)
Step 1: Set up an AUTH_TOKEN Javascript variable
Add this to your application’s layout file (in the head):
<%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? %>
Step 2: Send the request with an AUTH_TOKEN
$.ajax({
type: "POST",
data:"authenticity_token="+$.URLEncode(AUTH_TOKEN),
url: "http://"+HOST+"/users/login",
success: function(html){
alert("woohoo!");
}
});
One thing to note is the $.URLEncode function. I started using this jQuery plugin when I noticed “escape” and “encodeURIComponent” functions were not properly encoding the “+” character. I forgot where I found the plugin so I’ll just post it right here since it’s really lite:
$.extend({URLEncode:function(c){var o='';var x=0;c=c.toString();var r=/(^[a-zA-Z0-9_.]*)/;
while(x<c.length){var m=r.exec(c.substr(x));
if(m!=null && m.length>1 && m[1]!=''){o+=m[1];x+=m[1].length;
}else{if(c[x]==' ')o+='+';else{var d=c.charCodeAt(x);var h=d.toString(16);
o+='%'+(h.length<2?'0':'')+h.toUpperCase();}x++;}}return o;},
URLDecode:function(s){var o=s;var binVal,t;var r=/(%[^%]{2})/;
while((m=r.exec(o))!=null && m.length>1 && m[1]!=''){b=parseInt(m[1].substr(1),16);
t=String.fromCharCode(b);o=o.replace(m[1],t);}return o;}
});
This is the bare minimum involved to play nicely with Rails but I prefer to keep things DRY.
Step 3: DRY up your jQuery POST requests
Any good Rails programmer will notice a flaw with this solution – you have to include the authenticity token in each request. This is not DRY. You can obviously create a wrapper function but I prefer the following method:
$(document).ajaxSend(function(event, request, settings) {
if (typeof(AUTH_TOKEN) == "undefined") return;
if (settings.type == 'GET' || settings.type == 'get') return;
settings.data = settings.data || "";
settings.data += (settings.data ? "&" : "") + "authenticity_token=" + $.URLEncode(AUTH_TOKEN);
});
ajaxSend is a Global jQuery event that allows you to modify the request before it gets sent. There is one issue with the method above that caused a bit of debugging on my end. When no data is provided in your post request:
$.ajax({
type: "POST",
//data:"some_data="+some_data,
url: "http://"+HOST+"/users/login",
success: function(html){
alert("woohoo!");
}
});
jQuery will not properly set the Content-Type of the request header. If we look at line 3522 of jQuery (1.3.2), we see the following:
if ( s.data )
xhr.setRequestHeader("Content-Type", s.contentType);
This says that if no data is in the request, jQuery will not set the Content-Type header and a quick Firebug will show you that the browser defaults to
” text/plain; charset=UTF-8 ”
which results in Rails not decoding the authenticity token properly.
Step 4: Set the Content-Type before you POST
The easy fix is to set the Content-Type in your ajaxSend global event handler resulting in the function shown below:
$(document).ajaxSend(function(event, request, settings) {
if (typeof(AUTH_TOKEN) == "undefined") return;
if (settings.type == 'GET' || settings.type == 'get') return;
settings.data = settings.data || "";
settings.data += (settings.data ? "&" : "") + "authenticity_token=" + $.URLEncode(AUTH_TOKEN);
request.setRequestHeader("Content-Type", settings.contentType);
});
Enjoy!
If you enjoyed this post, make sure you subscribe to my RSS feed!
Getting jQuery, Rails, and Authenticity Tokens to play nice
For anyone that uses the jQuery AJAX library to send POST requests to their Rails application, you have probably found that the built in Rails CSRF protection does not play nice with your jQuery POST requests. By the way, if you don’t know what CSRF is, check out my Ruby on Rails security presentation. You need to send an Authenticity Token with each POST request and jQuery does not do that for you auto-magically. This post explains how I got jQuery, Rails, and Authenticity Tokens to play nice together preventing the dreaded error:
Step 1: Set up an AUTH_TOKEN Javascript variable
Add this to your application’s layout file (in the head):
Step 2: Send the request with an AUTH_TOKEN
One thing to note is the $.URLEncode function. I started using this jQuery plugin when I noticed “escape” and “encodeURIComponent” functions were not properly encoding the “+” character. I forgot where I found the plugin so I’ll just post it right here since it’s really lite:
This is the bare minimum involved to play nicely with Rails but I prefer to keep things DRY.
Step 3: DRY up your jQuery POST requests
Any good Rails programmer will notice a flaw with this solution – you have to include the authenticity token in each request. This is not DRY. You can obviously create a wrapper function but I prefer the following method:
ajaxSend is a Global jQuery event that allows you to modify the request before it gets sent. There is one issue with the method above that caused a bit of debugging on my end. When no data is provided in your post request:
jQuery will not properly set the Content-Type of the request header. If we look at line 3522 of jQuery (1.3.2), we see the following:
This says that if no data is in the request, jQuery will not set the Content-Type header and a quick Firebug will show you that the browser defaults to
” text/plain; charset=UTF-8 ”
which results in Rails not decoding the authenticity token properly.
Step 4: Set the Content-Type before you POST
The easy fix is to set the Content-Type in your ajaxSend global event handler resulting in the function shown below:
Enjoy!
If you enjoyed this post, make sure you subscribe to my RSS feed!