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

Ruby on-RailsAngularjsCsrfProtect From-Forgery

Ruby on-Rails Problem Overview


If the protect_from_forgery option is mentioned in application_controller, then I can log in and perform any GET requests, but on very first POST request Rails resets the session, which logs me out.

I turned the protect_from_forgery option off temporarily, but would like to use it with Angular.js. Is there some way to do that?

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

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

Solution 2 - Ruby on-Rails

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.

Solution 3 - Ruby on-Rails

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'

Solution 4 - Ruby on-Rails

The answer that merges all previous answers and it relies that you are using Devise authentication gem.

First of all, add the gem:

gem 'angular_rails_csrf'

Next, add rescue_from block into application_controller.rb:

protect_from_forgery with: :exception

rescue_from ActionController::InvalidAuthenticityToken do |exception|
  cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  render text: 'Invalid authenticity token', status: :unprocessable_entity
end

And the finally, add the interceptor module to you angular app.

# coffee script
app.factory 'csrfInterceptor', ['$q', '$injector', ($q, $injector) ->
  responseError: (rejection) ->
    if rejection.status == 422 && rejection.data == 'Invalid authenticity token'
        deferred = $q.defer()

        successCallback = (resp) ->
          deferred.resolve(resp)
        errorCallback = (resp) ->
          deferred.reject(resp)

        $http = $http || $injector.get('$http')
        $http(rejection.config).then(successCallback, errorCallback)
        return deferred.promise

    $q.reject(rejection)
]

app.config ($httpProvider) ->
  $httpProvider.interceptors.unshift('csrfInterceptor')

Solution 5 - Ruby on-Rails

I saw the other answers and thought they were great and well thought out. I got my rails app working though with what I thought was a simpler solution so I thought I'd share. My rails app came with this defaulted in it,

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
end

I read the comments and it seemed like that is what I want to use angular and avoid the csrf error. I changed it to this,

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :null_session
end

And now it works! I don't see any reason why this shouldn't work, but I'd love to hear some insight from other posters.

Solution 6 - Ruby on-Rails

I've used the content from HungYuHei's answer in my application. I found that I was dealing with a few additional issues however, some because of my use of Devise for authentication, and some because of the default that I got with my application:

protect_from_forgery with: :exception

I note the related stack overflow question and the answers there, and I wrote a much more verbose blog post that summarises the various considerations. The portions of that solution that are relevant here are, in the application controller:

  protect_from_forgery with: :exception

  after_filter :set_csrf_cookie_for_ng

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

  rescue_from ActionController::InvalidAuthenticityToken do |exception|
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
    render :error => 'Invalid authenticity token', {:status => :unprocessable_entity} 
  end

protected
  def verified_request?
    super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
  end

Solution 7 - Ruby on-Rails

I found a very quick hack to this. All I had to do is the following:

a. In my view, I initialize a $scope variable which contains the token, let's say before the form, or even better at controller initialization:

<div ng-controller="MyCtrl" ng-init="authenticity_token = '<%= form_authenticity_token %>'">

b. In my AngularJS controller, before saving my new entry, I add the token to the hash:

$scope.addEntry = ->
	$scope.newEntry.authenticity_token = $scope.authenticity_token 
	entry = Entry.save($scope.newEntry)
	$scope.entries.push(entry)
	$scope.newEntry = {}

Nothing more needs to be done.

Solution 8 - Ruby on-Rails

 angular
  .module('corsInterceptor', ['ngCookies'])
  .factory(
    'corsInterceptor',
    function ($cookies) {
      return {
        request: function(config) {
          config.headers["X-XSRF-TOKEN"] = $cookies.get('XSRF-TOKEN');
          return config;
        }
      };
    }
  );

It's working on angularjs side!

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPaulView Question on Stackoverflow
Solution 1 - Ruby on-RailsHungYuHeiView Answer on Stackoverflow
Solution 2 - Ruby on-RailsMichelle TilleyView Answer on Stackoverflow
Solution 3 - Ruby on-RailsjsandersView Answer on Stackoverflow
Solution 4 - Ruby on-RailsAnton OrelView Answer on Stackoverflow
Solution 5 - Ruby on-RailsBlaine HatabView Answer on Stackoverflow
Solution 6 - Ruby on-RailsPaulLView Answer on Stackoverflow
Solution 7 - Ruby on-RailsRuby RacerView Answer on Stackoverflow
Solution 8 - Ruby on-RailsEvgeniy KrokhmalView Answer on Stackoverflow