Each of those sub-apps is not really an app, but rather a set of APIs.
To illustrate here is the super-basic layout:
├── API
│ └── v1
│ ├── airports
│ │ ├── app.rb
│ │ ├── config
│ │ │ └── boot.rb
│ │ ├── controllers
│ │ │ └── airports.rb
│ │ ├── db
│ │ │ └── seed.rake
│ │ └── models
│ │ └── airport.rb
│ └── sessions
│ ├── app.rb
│ ├── config
│ │ └── boot.rb
│ ├── controllers
│ │ └── sessions.rb
│ ├── db
│ │ └── seed.rake
│ └── models
├── Capfile
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
│ ├── app.rb
│ ├── controllers
│ ├── helpers
│ └── views
├── config
│ ├── apps.rb
│ ├── boot.rb
│ ├── database.rb
│ └── deploy.rb
├── config.ru
├── db
├── lib
├── library
├── log
├── models
├── public
│ ├── airports.html
│ ├── favicon.ico
│ ├── home.html
│ ├── images
│ │ └── logo.jpg
│ ├── javascripts
│ │ ├── application.js
│ │ ├── jquery-ujs.js
│ │ └── jquery.js
│ ├── login.html
│ ├── logout.html
│ └── stylesheets
│ ├── application.css
│ ├── bootstrap.css
│ └── reset.css
├── spec
│ ├── spec.rake
│ └── spec_helper.rb
└── tmp
Notice that folder 'API'? That's what holds all the sub-apps.
In this simple case, there are just two.
Each has a minimal set of folders and what not.
The bulk of the work is done in the public html files via jQuery.
So for example, you want a html file to have jQuery make a call to login a user:
The html:
<div id="ProfileLogin"> <h1>LOGIN</h1> <div id="ProfileLoginErrors"></div> <form id="ProfileLoginForm" method="post"> <label for="ProfileLoginUsername">Username:</label> <input id="ProfileLoginUsername" name="username" type="text"/> <label for="ProfileLoginPassword">Password:</label> <input id="ProfileLoginPassword" name="password" type="text"/> <input id="ProfileLoginSubmit" type="submit" value="Login"/> </form> </div>
The jQuery:
<script type="text/javascript"> $(document).ready(function () { $('#ProfileLoginForm').on('submit', function (event) { event.preventDefault(); var postData = $('#ProfileLoginForm').serialize() $.ajax({ type: 'POST', url: '/api/v1/sessions/login', data: postData, success: function (data, textStatus, jqXHR) { if (data.errors) { $('#ProfileLoginErrors').html(data.errors); } else { window.location.href = '/home.html'; } }, fail: function (data, textStatus, jqXHR) { $('#ProfileLoginErrors').html(data); } }) }); }); </script>
Now when you do a POST, and look at the development log, you see this:
WARN - attack prevented by Rack::Protection::AuthenticityToken
DEBUG - POST (0.0035s) /api/v1/sessions/login - 403 Forbidden
Damn.
Rack::Protection::AuthenticityToken
is stopping us getting in.The problem is that Rack::Protection uses a hidden value to ensure you're being called from where it expects.
The code responsible is in
/Users/[you]/.rvm/gems/ruby-2.0.0-p0/gems/rack-protection-1.5.0/lib/rack/protection/authenticity_token.rb
.It looks like this:
require 'rack/protection' module Rack module Protection ## # Prevented attack:: CSRF # Supported browsers:: all # More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery # # Only accepts unsafe HTTP requests if a given access token matches the token # included in the session. # # Compatible with Rails and rack-csrf. class AuthenticityToken < Base def accepts?(env) return true if safe? env session = session env token = session[:csrf] ||= session['_csrf_token'] || random_string env['HTTP_X_CSRF_TOKEN'] == token or Request.new(env).params['authenticity_token'] == token end end end end
And the reason is returns false is because 'authenticity_token' hasn't been provided.
Don't bother going into and mucking about with:
set :protection, true set :protect_from_csrf, true set :allow_disabled_csrf, true
It won't help. The problem is you're not passing the 'authenticity_token' in the POST request.
The cure:
// REQUIRES the authenticity token appended! var postData = $('#ProfileLoginForm').serialize() + '&authenticity_token=' + CSRF_TOKEN;
Now where did that CSRF_TOKEN come from?
Ah. And here's the trick.
First modify your public/javascripts/applications.js and add this:
var CSRF_TOKEN = ''; function configureCSRF() { $.ajax({ type: 'GET', url: '/api/v1/sessions/csrf_token', async: false, cache: false, success: function (data, textStatus, jqXHR) { CSRF_TOKEN = data.csrf; }, fail: function (data, textStatus, jqXHR) { } }) }
Just see that a call is being made to '/api/v1/sessions/csrf_token' and on success, a global CSRF_TOKEN is being set.
So what is the call '/api/v1/sessions/csrf_token' look like?
get :csrf_token, :map => '/csrf_token', :provides => :json do logger.debug 'Retrieving csrf_token' result = { :csrf => session[:csrf] } JSON.pretty_generate result end
Ok. So how do you use it?
You change your javascript to:
<script type="text/javascript"> configureCSRF(); $(document).ready(function () { $('#ProfileLoginForm').on('submit', function (event) { event.preventDefault(); // REQUIRES the authenticity token appended! var postData = $('#ProfileLoginForm').serialize() + '&authenticity_token=' + CSRF_TOKEN; $.ajax({ type: 'POST', url: '/api/v1/sessions/login', data: postData, success: function (data, textStatus, jqXHR) { if (data.errors) { $('#ProfileLoginErrors').html(data.errors); } else { window.location.href = '/home.html'; } }, fail: function (data, textStatus, jqXHR) { $('#ProfileLoginErrors').html(data); } }) }); }); </script>
You could also add:
headers: { 'HTTP_X_CSRF_TOKEN': CSRF_TOKEN },
To the call, but I found it was not necessary.
YMMV.
Thanks! I also struggled with this, seems like you described my last day at work with this post.
ReplyDeleteBy the way, I noticed in rack-protection (1.5.3) the function accepts? checks for env['HTTP_X_CSRF_TOKEN'] before it checks for the env parameters, therefore setting the jquery XHR header of 'X_CSRF_TOKEN': 'your_token' would also work, instead of a parameter.
In fact, something like this could work for most people (using Padrino-framework):
$('body').bind('ajaxSend', function(elm, xhr, s){
if (s.type == 'POST' || s.type == 'PUT' || s.type == 'DELETE') {
xhr.setRequestHeader('X_CSRF_TOKEN', '#{csrf_token}');
}
});
Cheers!