Rails CSRF Protection + Angular.js: protect_from_forgery makes me to log out on POST
Ruby on-RailsAngularjsCsrfProtect From-ForgeryRuby 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!