Rails, Devise authentication, problem z CSRF

Robię aplikację na jednej stronie używając Rails. Podczas logowania i wychodzenia Kontrolery Devise są wywoływane przy użyciu ajax. Problem, który dostaję, polega na tym, że gdy 1) Zaloguj się 2) Wyloguj się, a następnie zaloguj się ponownie nie działa.

Myślę, że jest to związane z tokenem CSRF, który jest resetowany, gdy się wyloguję (choć nie powinien być afaik), a ponieważ jest to pojedyncza strona, Stary token CSRF jest wysyłany w żądaniu xhr, co resetuje sesję.

Aby być bardziej konkretnym to jest workflow:

  1. Zaloguj się
  2. Wyloguj się
  3. Zaloguj się (201. Jednak drukuje WARNING: Can't verify CSRF token authenticity w logach serwera)
  4. kolejne żądanie ajax nie powiodło się 401 nieautoryzowanych
  5. odśwież stronę (w tym momencie CSRF w nagłówku strony zmienia się na coś innego)
  6. mogę się zalogować, to działa, dopóki nie spróbuję się wylogować i ponownie się zalogować.

Wszelkie wskazówki bardzo mile widziane! Daj mi znać, jeśli Mogę dodać więcej szczegółów.

Author: vrepsys, 2012-08-07

9 answers

Jimbo zrobił świetną robotę wyjaśniając "dlaczego" za problemem, na który wpadasz. Istnieją dwa sposoby rozwiązania problemu:]}

  1. (zgodnie z zaleceniem Jimbo) Override Devise:: SessionsController zwróci nowy token csrf:

    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
    

    I utworzyć obsługę sukcesu dla Twojego żądania sign_out po stronie klienta (prawdopodobnie wymaga pewnych poprawek w oparciu o Twoją konfigurację, np. 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");
      });
    }
    

    Zakłada się również, że dołączasz token CSRF automatycznie ze wszystkimi żądaniami AJAX z czymś takim:

    $(document).ajaxSend(function (e, xhr, options) {
      xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
    });
    
  2. Znacznie prościej, jeśli jest to odpowiednie dla Twojej aplikacji, możesz po prostu nadpisać Devise::SessionsController i nadpisać sprawdzenie tokena za pomocą skip_before_filter :verify_authenticity_token.

 36
Author: jredburn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-27 14:45:37

Właśnie napotkałem ten problem. Dużo się tu dzieje.

TL; DR - powodem niepowodzenia jest to, że token CSRF jest powiązany z Twoją sesją serwera(masz sesję serwera niezależnie od tego, czy jesteś zalogowany, czy wylogowany). Token CSRF jest zawarty w DOM Twojej strony przy każdym załadowaniu strony. Po wylogowaniu sesja jest resetowana i nie ma tokenu csrf. Zwykle wylogowanie przekierowuje do innej strony / akcji, co daje nowy token CSRF, ale od używasz ajax, musisz to zrobić ręcznie.

  • musisz nadpisać metodę Devise SessionController::destroy, aby zwrócić nowy token CSRF.
  • następnie po stronie klienta musisz ustawić obsługę sukcesu dla swojego wylogowania XMLHttpRequest. W tej funkcji obsługi musisz pobrać ten nowy token CSRF z odpowiedzi i ustawić go w swoim dom: $('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)

Bardziej szczegółowe wyjaśnienie najprawdopodobniej masz protect_from_forgery ustawione w kontrolerze aplikacji.plik rb z którego dziedziczą wszystkie inne kontrolery (to chyba dość powszechne). protect_from_forgery sprawdza CSRF wszystkich żądań HTML / Javascript, które nie są pobierane. Ponieważ Devise Login jest postem, wykonuje sprawdzenie CSRF. Jeśli sprawdzenie CSRF nie powiedzie się, bieżąca sesja użytkownika zostanie wyczyszczona, tzn. wyloguje użytkownika, ponieważ serwer zakłada, że jest to atak (który jest prawidłowym/pożądanym zachowaniem).

Więc zakładając, że zaczynasz w stanie wylogowanym, ładujesz świeżą stronę i nigdy nie ładujesz jej ponownie znowu:

  1. Podczas renderowania strony: serwer wstawia do strony Token CSRF skojarzony z sesją serwera. Możesz wyświetlić ten token, uruchamiając następujące z konsoli javascript w przeglądarce$('meta[name="csrf-token"]').attr('content').

  2. Następnie logujesz się za pomocą XMLHttpRequest: Twój Token CSRF pozostaje niezmieniony w tym momencie, więc Token CSRF w twojej sesji nadal pasuje do tego, który został wstawiony na stronę. Za kulisami, po stronie klienta, jquery-ujs nasłuchuje xhr i automatycznie ustawia dla Ciebie nagłówek' X-CSRF-Token ' o wartości $('meta[name="csrf-token"]').attr('content') (pamiętaj, że był to Token CSRF ustawiony w kroku 1 przez sever). Serwer porównuje Token ustawiony w nagłówku przez jQuery-ujs i ten, który jest przechowywany w informacji o sesji i pasują tak, aby żądanie powiodło się.

  3. Następnie wylogowujesz się za pomocą XMLHttpRequest: to resetuje sesję, daje nową sesję bez tokenu CSRF.

  4. Następnie logujesz się ponownie przez XMLHttpRequest: jquery-ujs pobiera token CSRF z wartości $('meta[name="csrf-token"]').attr('content'). Ta wartość jest nadal Twoim starym tokenem CSRF. Pobiera ten stary token i używa go do Ustawienia 'X-CSRF-Token'. Serwer porównuje tę wartość nagłówka z nowym tokenem CSRF, który dodaje do sesji, co jest inne. Ta różnica powoduje awarię protect_form_forgery, która wyrzuca WARNING: Can't verify CSRF token authenticity i resetuje sesję, która loguje użytkownika Wynocha.

  5. Następnie wykonujesz kolejny XMLHttpRequest, który wymaga zalogowanego użytkownika: bieżąca sesja nie ma zalogowanego użytkownika, więc devise zwraca 401.

Aktualizacja: 8/14 Devise wylogowanie nie daje nowego tokena CSRF, przekierowanie, które zwykle dzieje się po wylogowaniu daje nowy token csrf.

 31
Author: plainjimbo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-14 20:22:30

Moja odpowiedź mocno zapożycza się zarówno z @ Jimbo, jak i @Sija, jednak używam konwencji devise / angularjs sugerowanej na Rails CSRF Protection + Angular.js: protect_from_forgery zmusza mnie do wylogowania się na POST , i trochę rozwinął się na moim blog kiedy to zrobiłem. Jest to metoda na kontrolerze aplikacji do ustawiania plików cookie dla csrf:

after_filter  :set_csrf_cookie_for_ng

def set_csrf_cookie_for_ng
  cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end

Więc używam formatu @ Sija, ale używam kodu z tego wcześniejszego więc rozwiązanie daje mi:

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

  protected
  def set_csrf_headers
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?  
  end
end

Dla kompletność, ponieważ zajęło mi to kilka minut, aby go wypracować, zauważam również potrzebę modyfikacji konfiguracji / trasy.RB, aby zadeklarować, że nadpisałeś kontroler sesji. Coś w stylu:

devise_for :users, :controllers => {sessions: 'sessions'}

Było to również częścią dużego czyszczenia CSRF, które zrobiłem w mojej aplikacji, co może być interesujące dla innych. Wpis na blogu jest tutaj , pozostałe zmiany to:

Ratowanie z ActionController:: InvalidAuthenticityToken, czyli jeśli coś wyjdzie synchronizacji aplikacja naprawi się sama, a nie użytkownik musi wyczyścić pliki cookie. Tak jak w rails myślę, że twój kontroler aplikacji będzie domyślnie ustawiony na:

protect_from_forgery with: :exception

W takiej sytuacji potrzebujesz:

rescue_from ActionController::InvalidAuthenticityToken do |exception|
  cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  render :error => 'invalid token', {:status => :unprocessable_entity}
end

Miałem też trochę smutku z warunkami wyścigowymi i niektóre interakcje z modułem timeoutable w Devise, które skomentowałem dalej w poście na blogu - w skrócie powinieneś rozważyć użycie active_record_store zamiast cookie_store, i być uważaj na wysyłanie równoległych żądań w pobliżu akcji sign_in i sign_out.

 8
Author: PaulL,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:33

To jest moje ujęcie:

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

I po stronie klienta:

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

Który będzie aktualizował meta tagi CSRF za każdym razem, gdy zwrócisz X-CSRF-Token lub X-CSRF-Param nagłówek za pomocą żądania ajax.

 8
Author: Sija,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-22 15:23:10

Po przekopaniu się do źródła Wardena zauważyłem, że ustawienie sign_out_all_scopes na false uniemożliwia wardenowi wyczyszczenie całej sesji, więc token CSRF jest zachowany między wylogowaniami.

Powiązane dyskusje na temat Devise issue tacker: https://github.com/plataformatec/devise/issues/2200

 5
Author: Lucas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-01-05 20:35:35

Właśnie dodałem to w moim pliku layoutu i zadziałało

    <%= csrf_meta_tag %>

    <%= javascript_tag do %>
      jQuery(document).ajaxSend(function(e, xhr, options) {
       var token = jQuery("meta[name='csrf-token']").attr("content");
        xhr.setRequestHeader("X-CSRF-Token", token);
      });
    <% end %>
 1
Author: r15,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-18 11:47:49

Sprawdź, czy uwzględniłeś to w swojej aplikacji.plik js

//= require jquery

/ / = require jquery_ujs

Powodem jest jQuery-rails gem, który automatycznie ustawia token CSRF na wszystkich żądaniach Ajax, wymaga tych dwóch

 0
Author: pdpMathi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-07 12:13:55

W moim przypadku, po zalogowaniu się użytkownika, musiałem przerysować menu użytkownika. Zadziałało, ale przy każdym zgłoszeniu do serwera, w tej samej sekcji (oczywiście bez odświeżania strony) dostaję błędy autentyczności CSRF. Powyższe rozwiązania nie działały, ponieważ musiałem renderować widok js.

Zrobiłem to, używając 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

Po tym wystosowałem prośbę do kontrolera # action, który przerysował menu. Oraz w javascript, zmodyfikowałem X-CSRF-param i 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 %>');

Mam nadzieję, że przyda się komuś w tej samej sytuacji js :)

 0
Author: JGutierrezC,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-15 05:22:05

W odpowiedzi na komentarz @ sixty4bit; jeśli napotkasz ten błąd:

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

Zastąp

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

Z

response.headers['X-CSRF-Param'] = request_forgery_protection_token.to_s
 -1
Author: Egbert Veenstra,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-02-27 13:54:51