Rails, Devise authentication, CSRF issue

前端 未结 10 1076
不思量自难忘°
不思量自难忘° 2020-11-28 20:52

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) sig

相关标签:
10条回答
  • 2020-11-28 21:39

    in reply to a comment of @sixty4bit; if you run into this error:

    Unexpected error while processing request: undefined method each for :authenticity_token:Symbol` 
    

    replace

    response.headers['X-CSRF-Param'] = request_forgery_protection_token
    

    with

    response.headers['X-CSRF-Param'] = request_forgery_protection_token.to_s
    
    0 讨论(0)
  • 2020-11-28 21:40

    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.

    0 讨论(0)
  • 2020-11-28 21:43

    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.

    0 讨论(0)
  • 2020-11-28 21:52

    In my case, after login the user in, i needed to redraw the user's menu. That worked, but i got CSRF authenticity errors on every request to the server, in that same section (without refreshing the page, of course). Above solutions wasn't working since i needed to render a js view.

    What i did is this, using Devise:

    app/controllers/sessions_controller.rb

       class SessionsController < Devise::SessionsController
          respond_to :json
    
          # GET /resource/sign_in
          def new
            self.resource = resource_class.new(sign_in_params)
            clean_up_passwords(resource)
            yield resource if block_given?
            if request.format.json?
              markup = render_to_string :template => "devise/sessions/popup_login", :layout => false
              render :json => { :data => markup }.to_json
            else
              respond_with(resource, serialize_options(resource))
            end
          end
    
          # POST /resource/sign_in
          def create
            if request.format.json?
              self.resource = warden.authenticate(auth_options)
              if resource.nil?
                return render json: {status: 'error', message: 'invalid username or password'}
              end
              sign_in(resource_name, resource)
              render json: {status: 'success', message: '¡User authenticated!'}
            else
              self.resource = warden.authenticate!(auth_options)
              set_flash_message(:notice, :signed_in)
              sign_in(resource_name, resource)
              yield resource if block_given?
              respond_with resource, location: after_sign_in_path_for(resource)
            end
          end
    
        end
    

    After that i made a request to the controller#action that redraw the menu. And in the javascript, i modified the X-CSRF-Param and X-CSRF-Token:

    app/views/utilities/redraw_user_menu.js.erb

      $('.js-user-menu').html('');
      $('.js-user-menu').append('<%= escape_javascript(render partial: 'shared/user_name_and_icon') %>');
      $('meta[name="csrf-param"]').attr('content', '<%= request_forgery_protection_token.to_s %>');
      $('meta[name="csrf-token"]').attr('content', '<%= form_authenticity_token %>');
    

    I hope it's useful for someone on the same js situation :)

    0 讨论(0)
提交回复
热议问题