Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 888
vrepsys Avatar asked Aug 07 '12 12:08

vrepsys


People also ask

How does Rails prevent CSRF?

Briefly, Cross-Site Request Forgery (CSRF) is an attack that allows a malicious user to spoof legitimate requests to your server, masquerading as an authenticated user. Rails protects against this kind of attack by generating unique tokens and validating their authenticity with each submission.

How does Rails verify CSRF token?

CSRF protection when plain vanilla Rails form is used On the server, Rails retrieves the token using params[:authenticity_token]. Rails checks if the token has been tampered with and if everything is fine then that request proceeds.

Why CSRF token is not working?

Invalid or missing CSRF token This error message means that your browser couldn't create a secure cookie, or couldn't access that cookie to authorize your login. This can be caused by ad- or script-blocking plugins, but also by the browser itself if it's not allowed to set cookies.

What does Protect_from_forgery do in Rails?

Rails includes a built-in mechanism for preventing CSRF, protect_from_forgery , which is included by default in the application_controller. rb controller when generating new applications. This protect_from_forgery method leverages magic to ensure that your application is protected from hackers!


2 Answers

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.

like image 186
jredburn Avatar answered Sep 19 '22 15:09

jredburn


I've just run into this problem as well. There's a lot going on here.

TL;DR - The reason for the failure is that the CSRF token is associated with your server session (you've got a server session whether you're logged in or logged out). The CSRF token is included in the DOM your page on every page load. On logout, your session is reset and has no csrf token. Normally, a logout redirects to a different page/action, which gives you a new CSRF token, but since you're using ajax, you need to do this manually.

  • You need to override the Devise SessionController::destroy method to return your new CSRF token.
  • Then on the client side you need to set a success handler for your logout XMLHttpRequest. In that handler you need to take this new CSRF token from the response and set it in your dom: $('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)

More Detailed Explanation You've most likely got protect_from_forgery set in your ApplicationController.rb file from which all of your other controllers inherit (this is pretty common I think). protect_from_forgery performs CSRF checks on all non-GET HTML/Javascript requests. Since Devise Login is a POST, it performs a CSRF Check. If a CSRF Check fails then the user's current session is cleared, i.e., logs the user out, because the server assumes it's an attack (which is the correct/desired behavior).

So assuming you're starting in a logged out state, you do a fresh page load, and never reload the page again:

  1. On rendering the page: the server inserts the CSRF Token associated with your server session into the page. You can view this token by running the following from a javascript console in your browser$('meta[name="csrf-token"]').attr('content').

  2. You then Sign In via an XMLHttpRequest: Your CSRF Token remains unchanged at this point so the CSRF Token in your Session still matches the one that was inserted into the page. Behind the scenes, on the client side, jquery-ujs is listening for xhr's and setting a 'X-CSRF-Token' header with the value of $('meta[name="csrf-token"]').attr('content') for you automatically (remember this was the CSRF Token set in step 1 by the sever). The server compares the Token set in the header by jquery-ujs and the one that is stored in your session information and they match so the request succeeds.

  3. You then Log Out via an XMLHttpRequest: This resets session, gives you a new session without a CSRF Token.

  4. You then Sign In again via an XMLHttpRequest: jquery-ujs pulls the CSRF token from the value of $('meta[name="csrf-token"]').attr('content'). This value is still your OLD CSRF token. It takes this old token and uses it to set the 'X-CSRF-Token'. The server compares this header value with a new CSRF token that it adds to your session, which is different. This difference causes the protect_form_forgery to fail, which throws the WARNING: Can't verify CSRF token authenticity and resets your session, which logs the user out.

  5. You then make another XMLHttpRequest that requires a logged in user: The current session doesn't have a logged in user so devise returns a 401.

Update: 8/14 Devise logout does not give you a new CSRF token, the redirect that normally happens after a logout gives you a new csrf token.

like image 40
plainjimbo Avatar answered Sep 19 '22 15:09

plainjimbo