Rails, Devise authentication, CSRF issue

Viewed 26519

I'm doing a singe-page application using Rails. When signing in and out Devise controllers are invoked using ajax. The problem I'm getting is that when I 1) sign in 2) sign out then signing in again doesn't work.

I think it's related to CSRF token which gets reset when I sign out (though it shouldn't afaik) and since it's single page, the old CSRF token is being sent in xhr request thus resetting the session.

To be more concrete this is the workflow:

  1. Sign in
  2. Sign out
  3. Sign in (successful 201. However prints WARNING: Can't verify CSRF token authenticity in server logs)
  4. Subsequent ajax request fails 401 unauthorised
  5. Refresh the website (at this point, CSRF in the page header changes to something else)
  6. I can sign in, it works, until I try to sign out and in again.

Any clues very much appreciated! Let me know if I can add any more details.

10 Answers

This is my take:

class SessionsController < Devise::SessionsController
  after_filter :set_csrf_headers, only: [:create, :destroy]
  respond_to :json

  protected
  def set_csrf_headers
    if request.xhr?
      response.headers['X-CSRF-Param'] = request_forgery_protection_token
      response.headers['X-CSRF-Token'] = form_authenticity_token
    end
  end
end

And on the client side:

$(document).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);
  }
});

Which will keep your CSRF meta tags updated every time you return X-CSRF-Token or X-CSRF-Param header via ajax request.

My situation was even simpler. In my case, all I wanted to do was this: if a person is sitting on a screen with a form, and their session times out (Devise timeoutable session timeout), normally if they hit Submit at that point, Devise would bounce them back to the login screen. Well, I didn't want that, because they lose all their form data. I use JavaScript to catch the form submit, Ajax call a controller which determines if the user is no longer signed in, and if that's the case I put up a form where they retype their password, and I reauthenticate them (bypass_sign_in in a controller) using an Ajax call. Then the original form submit is allowed to continue.

Was working perfectly until I added protect_from_forgery.

So, thanks to the above answers all I needed really was in my controller where I sign the user back in (the bypass_sign_in) I just set an instance variable to the new CSRF token:

@new_csrf_token = form_authenticity_token

and then in the .js.erb that was rendered (since again, this was an XHR call):

$('meta[name="csrf-token"]').attr('content', '<%= @new_csrf_token %>');
$('input[type="hidden"][name="authenticity_token"]').val('<%= @new_csrf_token %>');

Voila. My form page, which was not refreshed and therefore was stuck with the old token, now has the new token from the new session I got from signing in my user.

Related