Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't verify CSRF token authenticity Rails 4 Ajax even when header is set

I'm really having trouble with this, and in this instance, I neither want to skip the verify_authenticity_token filter, nor change to protect_from_forgery with: :null_session.

In my request method, I am setting a header with the csrf token as follows:

var token = document.querySelector("meta[name='csrf-token']").content;
xhr.setRequestHeader("X-CSRF-Token", token);

And by inserting a breakpoint in my controller like so:

def verify_authenticity_token
  binding.pry
  super
end

I have verified that the header is set:

[1] pry(#<MyController>)> request.headers
=> #<ActionDispatch::Http::Headers:0x007fb227cbf490
 @env=
  {"CONTENT_LENGTH"=>"202",
   .
   .
   .
   # omitted headers
   .
   .
   .
   "HTTP_X_CSRF_TOKEN"=>"the-correct-token-from-meta-tag",
   .
   .
   .
  }

I have also tried passing the token as a param with the key authenticity_token (as is done with Rails forms), and set the X-CSRF-Param tag to match (from meta[name="csrf-param"]).

Yet I am still getting:

Can't verify CSRF token authenticity
Completed 422 Unprocessable Entity in 14638ms

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken

Anyone seen this before? Any thoughts on what might cause this?

Thanks in advance!

EDIT:

Following discussion in the comments of marflar's answer, it looks like the token has expired when the request is made (tested by comparing to form_authenticity_token). This is confusing me further, as the token set in <%= csrf_meta_tags %> is expired when the next request comes in. Any thoughts?

EDIT2: Following marflar's advice below, I added the following after_filter to my app controller:

def set_csrf_headers
  response.headers['X-CSRF-Param'] = request_forgery_protection_token.to_s
  response.headers['X-CSRF-Token'] = form_authenticity_token
end

And I updated xhr.onload in my request method as follows:

namespace.request = = function (type, url, opts, callback) {

// code omitted

  xhr.onload = function () {
    setCSRFHeaders(xhr);
    var res = {data: JSON.parse(xhr.response), status: xhr.status};
    return callback.call(xhr, null, res);
  };

// code omitted

}

function setCSRFHeaders ( xhr ) {
  var csrf_param = xhr.getResponseHeader('X-CSRF-Param');
  var csrf_token = xhr.getResponseHeader('X-CSRF-Token');

  if (csrf_param) {
    document.querySelector("meta[name='csrf-param']").content = csrf_param;
  }
  if (csrf_token) {
    document.querySelector("meta[name='csrf-token']").content = csrf_token;
  }
}

I verified that the response headers, and then the meta tags are getting reset properly, however, by the time the next request comes in, this new token is expired again. Thoughts?

like image 227
mattmattmatt Avatar asked Oct 18 '14 21:10

mattmattmatt


2 Answers

I have the same problem. I inspected Rails source code and concluded next:

  • authenticity_token not expired by itself, so no need to update it after each ajax request to server
  • don't need to send both params[:authenticity_token] and header['x-csrf-token'], just one of them, rails will check params first, than header
  • on page refresh, authenticity_token will be different, but it doesn't matter, because it's generated each time with one time pad and real csrf token (on the server) is time independent
  • real csrf token saved in session[:_csrf_token]

As you can see token is kept in session and my problem was that my session was expired after 24h (probably user stays on page for day without refresh)

If user is logged in by cookie or some other token params, anyway he gets new session and with it new CSRF token will be generated and any request with old authenticity_token will be invalid.

So, the main problem is with session, it's expired or reset.

like image 170
Attenzione Avatar answered Sep 23 '22 05:09

Attenzione


My guess is that Rails might expect the token to be in the HTML, not the header. Can you try that? Hope it helps.

Actually I think you might be using a stale CRSF token because you're copying it from your template.

I normally set it like so in my controller action:

response.headers['X-CSRF-Param'] = "#{request_forgery_protection_token}"
response.headers['X-CSRF-Token'] = "#{form_authenticity_token}"

Does the token in your page match the one returned by calling form_authenticity_token?

UPDATE

In response to your comment (quoted below):

I just checked and you are right about it being a stale token, which unfortunately leaves me even more confused. The meta tags with the CSRF data are set on the initial page load, at which time they match form_authenticity_token, yet the token is stale by the time the first ajax request is made. So it won't matter whether I set them in the HTML or as headers, as this would occur at the same time, and thus run into the same issue with the token expiring before the next request is made. Thanks for your help so far -- any ideas here?

I ran into this sort of problem when implementing AJAX login. I found I was unable to make POST requests of any kind after logging in, and that I needed the following code to refresh my token:

var update_csrf_token_and_param_after_ajax_login = function() {
  $(document).on("ajaxComplete", function(event, xhr, settings) {
    var csrf_param = xhr.getResponseHeader('X-CSRF-Param');
    var csrf_token = xhr.getResponseHeader('X-CSRF-Token');

    if (csrf_param) {
      $('meta[name="csrf-param"]').attr('content', csrf_param);
    }
    if (csrf_token) {
      $('meta[name="csrf-token"]').attr('content', csrf_token);
    }
  });
}

I think you probably just need to write a fresh token into your page before doing the POST...

like image 1
stephenmurdoch Avatar answered Sep 22 '22 05:09

stephenmurdoch