Rails CSRF Protection + Angular.js: protect_from_forgery makes me to log out on POST

I think reading CSRF-value from DOM is not a good solution, it's just a workaround.

Here is a document form angularJS official website http://docs.angularjs.org/api/ng.$http :

Since only JavaScript that runs on your domain could read the cookie, your server can be assured that the XHR came from JavaScript running on your domain.

To take advantage of this (CSRF Protection), your server needs to set a token in a JavaScript readable session cookie called XSRF-TOKEN on first HTTP GET request. On subsequent non-GET requests the server can verify that the cookie matches X-XSRF-TOKEN HTTP header

Here is my solution based on those instructions:

First, set the cookie:

# app/controllers/application_controller.rb

# Turn on request forgery protection
protect_from_forgery

after_action :set_csrf_cookie

def set_csrf_cookie
  cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end

Then, we should verify the token on every non-GET request.
Since Rails has already built with the similar method, we can just simply override it to append our logic:

# app/controllers/application_controller.rb

protected
  
  # In Rails 4.2 and above
  def verified_request?
    super || valid_authenticity_token?(session, request.headers['X-XSRF-TOKEN'])
  end

  # In Rails 4.1 and below
  def verified_request?
    super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
  end

If you're using the default Rails CSRF protection (<%= csrf_meta_tags %>), you can configure your Angular module like this:

myAngularApp.config ["$httpProvider", ($httpProvider) ->
  $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content')
]

Or, if you're not using CoffeeScript (what!?):

myAngularApp.config([
  "$httpProvider", function($httpProvider) {
    $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content');
  }
]);

If you prefer, you can send the header only on non-GET requests with something like the following:

myAngularApp.config ["$httpProvider", ($httpProvider) ->
  csrfToken = $('meta[name=csrf-token]').attr('content')
  $httpProvider.defaults.headers.post['X-CSRF-Token'] = csrfToken
  $httpProvider.defaults.headers.put['X-CSRF-Token'] = csrfToken
  $httpProvider.defaults.headers.patch['X-CSRF-Token'] = csrfToken
  $httpProvider.defaults.headers.delete['X-CSRF-Token'] = csrfToken
]

Also, be sure to check out HungYuHei's answer, which covers all the bases on the server rather than the client.


The angular_rails_csrf gem automatically adds support for the pattern described in HungYuHei's answer to all your controllers:

# Gemfile
gem 'angular_rails_csrf'