15

I am developing an Ionic mobile app to talk to my backend Rails server using a JSON API. I have read that AngularJS will automatically handle XSRF protection by sending a header X-XSRF-TOKEN on the POST request if the first GET request returns a cookie named XSRF-TOKEN

I have updated my Rails application_controller.rb to be as follows:

class ApplicationController < ActionController::Base
  protect_from_forgery
  after_filter :set_access_control_headers
  after_filter :set_csrf_cookie_for_ng

  def after_sign_in_path_for(resource)
    main_path
  end

  def after_sign_out_path_for(resource)
    login_path
  end

  ##
   # Sets headers to support AJAX Cross-Origin Resource Sharing.
   # This is only needed for testing within browser (i.e. mobile apps do not need it).
   ##
  def set_access_control_headers
    # hosts who can make AJAX requests
    headers['Access-Control-Allow-Origin'] = 'http://localhost:8100'
    headers['Access-Control-Request-Method'] = '*'
    headers['Access-Control-Allow-Headers'] = 'accept, content-type, x-xsrf-token'
    # allow clients to use cookies to track session state
    headers['Access-Control-Allow-Credentials'] = 'true'
  end

  ##
   # Sets a cookie containing an XSRF token. This should be returned by the
   # client as a header field named 'X-XSRF-TOKEN'
  def set_csrf_cookie_for_ng
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  end

protected

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

The AngularJS code is:

$http({
  method: 'POST',
  url: $scope.getBackendUrl() + '/reports.json',
  params: params,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
})

I have taken Wireshark dumps and can see that the Rails server is sending through the Cookie (and I can read this within AngularJS). However, AngularJS is not sending the header X-XSRF-TOKEN to my backend Rails server, causing an WARNING: Can't verify CSRF token authenticity.

I have read through a bunch of SO questions to no avail. E.g. XSRF headers not being set in AngularJS

I have added the CORS header stuff so that I could test from Chrome. Again, I can see the cookie come through from the server, but the header is not being sent back. However, the cookie is being sent back in the 'Cookie' header field.

Can anyone see what I am missing, or things I can try in order to solve this? I am currently parsing the Cookie header field in the request to pull out the token and check authenticity to get around the issue while testing.

def verified_request?
  # should just need to do the below, but for some reason AngularJS is not setting 'X-XSRF-TOKEN'
  #super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
  if(super)
    true
  else
    cookie = request.headers['Cookie'] || ""
    value = cookie.nil? ? "" : CGI.unescape( cookie.gsub(/.*XSRF-TOKEN=(.+);.*/, '\1') )
    form_authenticity_token == value
  end
end

Versions:

  • Rails 3.2.3
  • Ionic 1.1.6 (which bundles in AngularJS)
Community
  • 1
  • 1
al.
  • 1,296
  • 13
  • 14

3 Answers3

15

According to the documentation

$http: The header will not be set for cross-domain requests.

If you have to use CORS, you would also doing cross domain requests. Would still be able to do it yourself with an $httpInterceptor. You first read the cookie value, and then attach the header to the config before the request is fired.

function readCookie(name) {
    var nameEQ = name + "=";
    var ca = document.cookie.split(';');
    for(var i=0;i < ca.length;i++) {
        var c = ca[i];
        while (c.charAt(0)==' ') c = c.substring(1,c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
    }
    return null;
}
module.factory('XSRFInterceptor', function() {
    var XSRFInterceptor = {
        request: function(config) {
            var token = readCookie('XSRF-TOKEN');
            if (token) {
                config.headers['X-XSRF-TOKEN'] = token;
            }
            return config;
        }
    };
    return XSRFInterceptor;
}]);
module.config(['$httpProvider', function($httpProvider) {
    $httpProvider.interceptors.push('XSRFInterceptor');
}]);
Jeremy Wilken
  • 6,965
  • 22
  • 21
  • 1
    Thanks! I missed that line in the `$http` doco. That makes sense as to why I was not seeing the header come across from Angular. Much appreciated. – al. Aug 25 '14 at 02:25
11

Just for anyone who came here like me trying to figure out why Angular is not sending X-XSRF-TOKEN header:

In my case I did not pay attention that I'm sending XSRF-TOKEN cookie with HttpOnly flag.

Eugene
  • 3,280
  • 21
  • 17
  • 1
    That's was far from obvious to me and yet **it does get it working** for me. Thanks @eugene ! – rtome Oct 18 '17 at 14:58
  • Why might this not be a good idea? Wouldn't this open up access to cookies in the browser. Is it really worth the "convenience" afforded to us by angular's `$http` service at a cost of security? Or am I reading this wrong? – Kevin Friedheim Mar 20 '18 at 00:26
0

Configuring the httpProvider with

$httpProvider.defaults.xsrfCookieName = 'csrftoken'; $httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';

does not set X-CSRFToken header for cross-domain requests, while the requests to original domains are fine.

The Access-Control-Allow-... headers are set to allow CORS with cookies and X-CSRFToken header:

Access-Control-Allow-Headers: x-requested-with, content-type, accept, origin, authorization, x-csrftoken Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS Access-Control-Allow-Origin: origin_url Access-Control-Allow-Credentials:true

hackp0int
  • 4,052
  • 8
  • 59
  • 95
Charu Jain
  • 36
  • 6