wake-up-neo.com

Rails, Devise-Authentifizierung, CSRF-Problem

Ich mache eine einseitige Anwendung mit Rails. Beim Anmelden und Abmelden werden Devise-Controller mit ajax aufgerufen. Das Problem, das ich bekomme, ist, dass, wenn ich 1) sich anmelde, 2) sich abmelden und dann wieder anmelden, nicht funktioniert. 

Ich denke, es hängt mit dem CSRF-Token zusammen, das zurückgesetzt wird, wenn ich mich abmelde (obwohl es nicht afaik sein sollte). Da es sich um eine einzige Seite handelt, wird das alte CSRF-Token in einer xhr-Anforderung gesendet, wodurch die Sitzung zurückgesetzt wird.

Genauer gesagt ist dies der Workflow:

  1. Einloggen 
  2. Ausloggen
  3. Anmeldung (erfolgreich 201. Druckt jedoch WARNING: Can't verify CSRF token authenticity in Serverprotokollen)
  4. Eine nachfolgende Ajax-Anforderung schlägt fehl (401) und ist nicht autorisiert
  5. Aktualisieren Sie die Website (zu diesem Zeitpunkt ändert sich die CSRF-Datei im Seitenkopf in etwas anderes).
  6. Ich kann mich anmelden, es funktioniert, bis ich mich abmelden und wieder anmelden möchte.

Alle Hinweise sehr geschätzt! Lassen Sie mich wissen, ob ich weitere Details hinzufügen kann.

36
vrepsys

Jimbo hat großartige Arbeit geleistet und das "Warum" hinter dem Thema erklärt, auf das Sie stoßen. Es gibt zwei Ansätze, um das Problem zu lösen:

  1. (Wie von Jimbo empfohlen) Überschreiben Sie Devise :: SessionsController, um das neue csrf-token zurückzugeben:

    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
    

    Und erstellen Sie einen Erfolgs-Handler für Ihre sign_out-Anforderung auf der Clientseite (wahrscheinlich sind einige Anpassungen erforderlich, z.

    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");
      });
    }
    

    Dies setzt auch voraus, dass Sie das CSRF-Token automatisch in alle AJAX -Anfragen mit folgendem Inhalt einfügen:

    $(document).ajaxSend(function (e, xhr, options) {
      xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
    });
    
  2. Viel einfacher, wenn es für Ihre Anwendung geeignet ist, können Sie einfach Devise::SessionsController überschreiben und die Tokenprüfung mit skip_before_filter :verify_authenticity_token überschreiben.

36
jredburn

Ich bin gerade auch auf dieses Problem gestoßen. Hier ist viel los. 

TL; DR - Der Grund für den Fehler ist, dass das CSRF-Token mit Ihrer Serversitzung verknüpft ist (Sie haben eine Serversitzung, unabhängig davon, ob Sie angemeldet oder abgemeldet sind). Das CSRF-Token ist bei jedem Laden der Seite im DOM Ihrer Seite enthalten. Beim Abmelden wird Ihre Sitzung zurückgesetzt und verfügt über kein CSRF-Token. Normalerweise führt eine Abmeldung zu einer anderen Seite/Aktion, wodurch Sie ein neues CSRF-Token erhalten. Da Sie jedoch ajax verwenden, müssen Sie dies manuell tun.

  • Sie müssen die Devise SessionController :: destroy-Methode überschreiben, um Ihr neues CSRF-Token zurückzugeben. 
  • Dann müssen Sie auf der Clientseite einen Erfolgsbehandler für Ihr Logout XMLHttpRequest festlegen. In diesem Handler müssen Sie dieses neue CSRF-Token aus der Antwort entnehmen und in Ihrem Dom festlegen: $('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>) 

Ausführlichere Erklärung Wahrscheinlich haben Sie protect_from_forgery in Ihrer ApplicationController.rb-Datei festgelegt, von der alle anderen Controller erben (dies ist ziemlich häufig, denke ich). protect_from_forgery führt CSRF-Prüfungen für alle Nicht-GET-HTML/Javascript-Anforderungen durch. Da Devise Login ein POST ist, führt es eine CSRF-Prüfung durch. Wenn eine CSRF-Prüfung fehlschlägt, wird die aktuelle Sitzung des Benutzers gelöscht, d. H., Der Benutzer wird abgemeldet, da der Server von einem Angriff ausgeht (was das richtige/gewünschte Verhalten ist).

Vorausgesetzt, Sie starten in einem abgemeldeten Status, führen ein neues Laden der Seite durch und laden die Seite niemals erneut:

  1. Beim Rendern der Seite: fügt der Server das mit Ihrer Serversitzung verknüpfte CSRF-Token in die Seite ein. Sie können dieses Token anzeigen, indem Sie Folgendes über eine JavaScript-Konsole in Ihrem Browser$('meta[name="csrf-token"]').attr('content') ausführen.

  2. Sie melden sich dann über ein XMLHttpRequest an: Ihr CSRF-Token bleibt an dieser Stelle unverändert, sodass das CSRF-Token in Ihrer Sitzung immer noch demjenigen entspricht, das in die Seite eingefügt wurde. Hinter den Kulissen, auf der Clientseite, lauscht jquery-ujs automatisch auf Xhrs und setzt automatisch einen 'X-CSRF-Token' -Header mit dem Wert von $('meta[name="csrf-token"]').attr('content') (Denken Sie daran, dass dies der CSRF-Token war, der vom Server in Schritt 1 gesetzt wurde. . Der Server vergleicht den im Header gesetzten Token anhand von jquery-ujs und des in den Sitzungsinformationen gespeicherten Token. Sie stimmen überein, damit die Anforderung erfolgreich ist.

  3. Sie melden sich dann über ein XMLHttpRequest ab: Dadurch wird die Sitzung zurückgesetzt. Sie erhalten eine neue Sitzung ohne CSRF-Token. 

  4. Sie melden sich dann erneut über ein XMLHttpRequest an: jquery-ujs zieht das CSRF-Token aus dem Wert von $('meta[name="csrf-token"]').attr('content'). Dieser Wert ist immer noch IhrALTESCSRF-Token. Dieses alte Token wird verwendet, um das 'X-CSRF-Token' festzulegen. Der Server vergleicht diesen Header-Wert mit einem neuen CSRF-Token, das er Ihrer Sitzung hinzufügt. Dieser Unterschied führt dazu, dass protect_form_forgery fehlschlägt, wodurch der WARNING: Can't verify CSRF token authenticity ausgelöst wird und Ihre Sitzung zurückgesetzt wird, wodurch der Benutzer abgemeldet wird.

  5. Sie erstellen dann ein weiteres XMLHttpRequest, das einen angemeldeten Benutzer erfordert: Die aktuelle Sitzung hat keinen angemeldeten Benutzer, so dass devise eine 401 zurückgibt.

Update: 8/14 Die Devise-Abmeldung gibt Ihnen kein neues CSRF-Token. Die Umleitung, die normalerweise nach einer Abmeldung erfolgt, gibt Ihnen ein neues CSRF-Token.

31
plainjimbo

Das ist mein 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

Und auf der Kundenseite:

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

Dadurch werden Ihre CSRF-Meta-Tags jedes Mal aktualisiert, wenn Sie X-CSRF-Token oder X-CSRF-Param-Header über eine Ajax-Anforderung zurückgeben.

8
Sija

Meine Antwort entlehnt mich sowohl bei @Jimbo als auch bei @Sija. Ich verwende jedoch die unter Rails CSRF Protection + Angular.js vorgeschlagene devise/anglejs-Konvention: protect_from_forgery lässt mich bei POST und ausloggen Ich habe etwas auf meinem blog ausgearbeitet, als ich das ursprünglich gemacht habe. Auf dem Anwendungscontroller gibt es eine Methode, Cookies für csrf zu setzen:

after_filter  :set_csrf_cookie_for_ng

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

Ich verwende also @ Sijas Format, verwende aber den Code dieser früheren SO -Lösung und gebe mir:

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

Der Vollständigkeit halber habe ich einige Minuten gebraucht, um das Problem zu lösen. Ich stelle auch fest, dass Sie Ihre config/routes.rb ändern müssen, um zu erklären, dass Sie den Sessions-Controller überschrieben haben. So etwas wie:

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

Dies war auch Teil einer umfangreichen CSRF-Bereinigung, die ich für meine Anwendung durchgeführt habe, was für andere interessant sein könnte. Der Blogeintrag ist hier , die anderen Änderungen beinhalten:

Rettung von ActionController :: InvalidAuthenticityToken, was bedeutet, dass sich die Anwendung von sich selbst löst, wenn sich die Dinge nicht mehr synchronisieren, und der Benutzer muss keine Cookies löschen. In Rails ist es meines Erachtens üblich, dass Ihr Anwendungscontroller standardmäßig verwendet wird:

protect_from_forgery with: :exception

In dieser Situation benötigen Sie dann:

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

Ich hatte auch etwas Trauer mit den Rennbedingungen und einigen Interaktionen mit dem Timeoutable-Modul in Devise, das ich im Blogbeitrag weiter kommentiert habe. Kurz gesagt, sollten Sie die Verwendung von active_record_store anstelle von cookie_store in Betracht ziehen und vorsichtig sein, wenn Sie parallel ausgeben Anforderungen in der Nähe der Aktionen sign_in und sign_out.

8
PaulL

Nachdem ich nach der Quelle von Warden gesucht hatte, fiel mir auf, dass die Einstellung von sign_out_all_scopes auf false die gesamte Sitzung von Warden abbricht, sodass das CSRF-Token zwischen den Abmeldungen beibehalten wird.

Verwandte Diskussionen zum Devise-Ausgabe-Tacker: https://github.com/plataformatec/devise/issues/2200

5
Lucas

Ich habe das gerade in meine layout-Datei eingefügt und es hat funktioniert

    <%= 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
r15

Prüfen Sie, ob Sie dies in Ihre application.js-Datei aufgenommen haben

// = Jquery erforderlich 

// = Jquery_ujs erforderlich

Der Grund dafür ist jquery-Rails gem, der standardmäßig das CSRF-Token für alle Ajax-Anforderungen automatisch setzt

0
pdpMathi

In meinem Fall musste ich nach dem Einloggen des Benutzers das Menü des Benutzers neu zeichnen. Das hat funktioniert, aber ich habe bei jeder Anfrage an den Server in demselben Abschnitt CSRF-Authentizitätsfehler erhalten (natürlich ohne Aktualisierung der Seite). Obige Lösungen funktionierten nicht, da ich eine js-Ansicht rendern musste. 

Was ich getan habe, ist dies mit Devise:

app/controller/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

Danach machte ich eine Anfrage an die Controller-Aktion #, die das Menü neu zeichnet. Und im Javascript habe ich das X-CSRF-Param und das X-CSRF-Token geändert:

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 %>');

Ich hoffe, es ist nützlich für jemanden, der sich in derselben Situation befindet. :)

0
JGutierrezC

Meine Situation war noch einfacher. In meinem Fall wollte ich nur Folgendes tun: Wenn eine Person auf einem Bildschirm mit einem Formular sitzt und das Sitzungszeitlimit abgelaufen ist (Timeout für Sitzung festlegen), würde Devise sie normalerweise zurückspringen lassen, wenn sie zu diesem Zeitpunkt auf Senden klicken zum Anmeldebildschirm. Nun, das wollte ich nicht, weil sie alle Formulardaten verlieren. Ich verwende JavaScript, um das Formular "Submit" abzufangen. Ajax ruft einen Controller auf, der feststellt, ob der Benutzer nicht mehr angemeldet ist. In diesem Fall richte ich ein Formular ein, in dem er sein Kennwort erneut eingibt, und authentifiziere es erneut (bypass_sign_in in einem Controller). mit einem Ajax-Anruf. Dann kann das ursprüngliche Formular weiter gesendet werden.

Hat einwandfrei funktioniert, bis ich protect_from_forgery hinzugefügt habe.

Dank der obigen Antworten war alles, was ich wirklich brauchte, in meinem Controller, wo ich den Benutzer wieder anmeldete (das bypass_sign_in). Ich habe gerade eine Instanzvariable auf das neue CSRF-Token gesetzt:

@new_csrf_token = form_authenticity_token

und dann in der gerenderten .js.erb (da dies wieder ein XHR-Aufruf war):

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

Voila. Meine Formularseite, die nicht aktualisiert wurde und daher nicht mehr mit dem alten Token funktioniert, enthält jetzt das neue Token aus der neuen Sitzung, die ich durch die Anmeldung meines Benutzers erhalten habe.

0
Jason Perrone