131

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?

Rimian
  • 36,864
  • 16
  • 117
  • 117
Paul
  • 25,812
  • 38
  • 124
  • 247
  • See if this helps any, its about setting HTTP headers http://stackoverflow.com/questions/14183025/setting-application-wide-http-headers-in-angularjs – Mark Rajcok Feb 06 '13 at 17:21

8 Answers8

278

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
Community
  • 1
  • 1
HungYuHei
  • 2,974
  • 1
  • 15
  • 8
  • 18
    I like this technique, as you don't have to modify any client-side code. – Michelle Tilley Apr 12 '13 at 16:16
  • 2
    Thank you. I like this. It is worth to mention that protect_from_forgery must be still present to make this code work. – Paul Apr 12 '13 at 18:22
  • 11
    How does this solution preserve the usefulness of CSRF protection? By setting the cookie, the marked user's browser will send that cookie on all subsequent requests including cross-site requests. I could set up a malicious third party site that send a malicious request and the user's browser would send 'XSRF-TOKEN' to the server. It seems like this solution is tantamount to turning off CSRF protection altogether. – Steven Apr 19 '13 at 14:42
  • 10
    From the Angular docs: "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." @StevenXu - How would the third party site read the cookie? – Jimmy Baker Apr 20 '13 at 19:22
  • 9
    @JimmyBaker: yes, you're right. I've reviewed the documentation. The approach is conceptually sound. I confused the setting of the cookie with the validation, not realizing that Angular the framework was setting a custom header based on the value of the cookie! – Steven Apr 20 '13 at 21:08
  • just come across this solution and it's great. I wonder why other solutions still use client-side code though. – giosakti May 08 '13 at 01:54
  • 1
    not sure if it's a change in AngularJS, but the header is X-XSRF-TOKEN (notice the dashes instead of the underscores). Anyway both this method and the one below works like a charm. THANKS! – Jacob May 26 '13 at 18:28
  • @Kubee Yes, this's confused, and even the Rails source is using dashe, but in my solution only the underscore works. https://github.com/rails/rails/blob/7fb99e5743d88c04357e09960d112376428a6faa/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L93 – HungYuHei Jun 15 '13 at 06:41
  • should be: request.headers['X-XSRF-TOKEN'] with dashes instead of underscores. I submitted an edit but not peer reviewed yet – Evan Jul 09 '13 at 23:42
  • @Evan I tested, request.headers['X_XSRF_TOKEN'] works while request.headers['X-XSRF-TOKEN'] doesn't – HungYuHei Jul 23 '13 at 10:17
  • Worked for me when I used X-XSRF-TOKEN and it broke when using underscore. Possibly version issue? – Daniel Fischer Aug 13 '13 at 09:57
  • For me, it is "HTTP_X_XSRF_TOKEN". You can pry on the headers from within "verified_request?" definition before the Boolean expression. – Jasper Sep 18 '13 at 19:36
  • shouldn't we check that the request is made via xhr (thus can be trusted as from same domain) before running the after filter and set the csrf token? – nakhli Jun 13 '14 at 07:16
  • 5
    form_authenticity_token generates new values on each call in Rails 4.2, so this doesn't appear to work anymore. – Dave Dec 22 '14 at 18:30
  • This does break client side code for me on rails 4.2 even with the edit above. Adding/deleting entries hit the database but I'm getting routing error when responding with json through an ajax request. Sadly the "workaround" in the below answer works. – ctilley79 Mar 02 '15 at 17:54
  • Does the cookie need to be set as httponly per http://blog.codinghorror.com/protecting-your-cookies-httponly/? If so, how would you do that? – Jonathan Mui Mar 13 '15 at 23:22
  • 1
    I just added automated tests against Rails versions from 3.0 to 4.2 in my [angular_rails_csrf gem](https://github.com/jsanders/angular_rails_csrf) that uses this pattern. A few things: @HungYuHei: Using dashes like `X-XSRF-TOKEN` seems to work fine. @Sinbadsoft.com: The call to `super` in `verified_request?` handles checking for `xhr?`. @Dave: The new values on each request in 4.2 are just "masked" and they are handled by `valid_authenticity_token?`. @Jonathon Mui: The cookie is not `HttpOnly` because it needs to be read by javascript and injected into a header for this pattern to work. – jsanders Jun 04 '15 at 18:11
  • Though the above way works , i prefer @jsanders way [3 rd answer] – poorva Aug 25 '15 at 09:31
  • 2
    For me (Rails 4.2.6), the verified request method had to read from cookies variable: `super || valid_authenticity_token?(session, cookies['XSRF-TOKEN'])` – tomascharad Jun 14 '16 at 21:42
  • sorry but It sounds to me that CSRF cookie would not solve this scenario: http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf can you expand on that ? – equivalent8 Oct 12 '17 at 14:25
78

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.

Community
  • 1
  • 1
Michelle Tilley
  • 157,729
  • 40
  • 374
  • 311
  • Let me explain. The base document is a plain HTML, not .erb therefore I cannot use `<%= csrf_meta_tags %>`. I thought that there should be enough to mention `protect_from_forgery` only. What to do? The base document must be a plain HTML (I am here not the one who chooses). – Paul Feb 06 '13 at 19:15
  • 3
    When you use `protect_from_forgery` what you're saying is "when my JavaScript code makes Ajax requests, I promise to send an `X-CSRF-Token` in the header that corresponds to the current CSRF token." In order to get this token, Rails injects it into the DOM with `<%= csrf_meta_token %>` and get gets the contents of the meta tag with jQuery whenever it makes Ajax requests (the default Rails 3 UJS driver does this for you). If you're not using ERB, there's no way to get the current token from Rails into the page and/or the JavaScript--and thus you cannot use `protect_from_forgery` in this manner. – Michelle Tilley Feb 06 '13 at 19:29
  • Thank you for explanation. What I thought that in a classic server-side application the client side receives `csrf_meta_tags` each time the server generates a response, and each time these tags are different from previous ones. So, these tags are unique for each request. The question is: how the application receives these tags for an AJAX request (without angular)? I used protect_from_forgery with jQuery POST requests, never bothered myself with getting this CSRF token, and it worked. How? – Paul Feb 06 '13 at 19:47
  • 1
    The Rails UJS driver uses [`jQuery.ajaxPrefilter`](http://api.jquery.com/jQuery.ajaxPrefilter/) as shown here: https://github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets/javascripts/jquery_ujs.js#L294 You can peruse this file and see all the hoops Rails jumps through to make it work pretty much without having to worry about it. – Michelle Tilley Feb 06 '13 at 19:54
  • @BrandonTilley wouldn't it make sense do only do this on `put` and `post` instead of on `common`? From the [rails security guide](http://guides.rubyonrails.org/security.html): `The solution to this is including a security token in non-GET requests` – christianvuerings Apr 12 '13 at 15:09
  • @denbuzze I don't see any reason not to enumerate them if you'd like. Our app uses `POST`, `DELETE` and `PATCH`. I'm not sure if sending the token over `GET` is a security issue or not, but if it's not, setting them with `common` is a quick way to cover all your bases. – Michelle Tilley Apr 12 '13 at 16:21
  • @Brandon Tilley: sorry, I have to unmark your answer because HungYuHei's reply is more ready to practical use in my situation. If Stackoverflow allows, I would mark your replies both as best answers, because they complement each other. – Paul Apr 16 '13 at 12:29
  • How to access `csrfToken` without using jquery `$('meta[name=csrf-token]').attr('content')`? Since `$cookies` which carries the `XSRF-TOKEN` isn't available in the `config` block, and updating `headers` in `run` does no good. – red-devil Sep 10 '14 at 20:38
  • I'm having trouble with the last one. `$httpProvider.defaults.headers.delete['X-CSRF-Token'] = csrfToken` throws this error: `$injector:modulerr] Failed to instantiate module AngulaRails due to: undefined is not an object (evaluating '$httpProvider.defaults.headers["delete"]['X-CSRF-Token'] = csrfToken')` – ctilley79 Mar 02 '15 at 17:16
29

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'
Community
  • 1
  • 1
jsanders
  • 599
  • 5
  • 7
  • any idea how you should configure your application controller and other csrf/forgery-related settings, to use angular_rails_csrf correctly? – Ben Wheeler Apr 17 '15 at 21:03
  • At the time of this comment the `angular_rails_csrf` gem doesn't work with Rails 5. However, configuring Angular request headers with the value from the CSRF meta tag works! – bideowego Sep 23 '16 at 20:33
  • There is a new release of the gem, which supports Rails 5. – jsanders Dec 07 '16 at 15:02
4

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')
Anton Orel
  • 576
  • 7
  • 8
1

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.

Blaine Hatab
  • 1,626
  • 17
  • 24
  • 7
    this will cause issues if you are trying to use rails 'sessions' since it will be set to nil if it fails the forgery test, which would be always, since you're not sending the csrf-token from the client side. – hajpoj Jun 24 '14 at 23:28
  • But if you're not using Rails sessions all is well; thank you! I've been struggling to find the cleanest solution to this. – Morgan Oct 02 '14 at 21:14
1

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
Community
  • 1
  • 1
PaulL
  • 6,650
  • 3
  • 35
  • 39
1

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.

Ruby Racer
  • 5,690
  • 1
  • 26
  • 43
0
 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!