Search code examples
ruby-on-railsajaxauthenticationdevisecsrf

Rails, Devise authentication, CSRF issue


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.


Solution

  • Jimbo did an awesome job explaining the "why" behind the issue you're running into. There are two approaches you can take to resolve the issue:

    1. (As recommended by Jimbo) Override Devise::SessionsController to return the new csrf-token:

      class SessionsController < Devise::SessionsController
        def destroy # Assumes only JSON requests
          signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
          render :json => {
              'csrfParam' => request_forgery_protection_token,
              'csrfToken' => form_authenticity_token
          }
        end
      end
      

      And create a success handler for your sign_out request on the client side (likely needs some tweaks based on your setup, e.g. GET vs DELETE):

      signOut: function() {
        var params = {
          dataType: "json",
          type: "GET",
          url: this.urlRoot + "/sign_out.json"
        };
        var self = this;
        return $.ajax(params).done(function(data) {
          self.set("csrf-token", data.csrfToken);
          self.unset("user");
        });
      }
      

      This also assumes you're including the CSRF token automatically with all AJAX requests with something like this:

      $(document).ajaxSend(function (e, xhr, options) {
        xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
      });
      
    2. Much more simply, if it is appropriate for your application, you can simply override the Devise::SessionsController and override the token check with skip_before_filter :verify_authenticity_token.