web-dev-qa-db-fra.com

Rails, concevoir l'authentification, problème CSRF

Je fais une application de page simple en utilisant Rails. Lors de la connexion et de la déconnexion, les contrôleurs Devise sont appelés à l'aide de ajax. Le problème que je ressens, c'est que lorsque 1) je me connecte 2) que je me déconnecte puis que je me reconnecter ne fonctionne pas. 

Je pense que cela est lié au jeton CSRF qui est réinitialisé lorsque je me déconnecte (bien que cela ne devrait pas être perdu) et puisqu'il s'agit d'une page, l'ancien jeton CSRF est envoyé dans une requête xhr, réinitialisant ainsi la session.

Pour être plus concret, voici le workflow:

  1. Se connecter 
  2. Déconnexion
  3. Connectez-vous (réussi 201. Cependant, imprime WARNING: Can't verify CSRF token authenticity dans les journaux du serveur)
  4. La requête ajax ultérieure échoue 401 non autorisée
  5. Actualiser le site Web (à ce stade, CSRF dans l'en-tête de la page devient autre chose)
  6. Je peux me connecter, cela fonctionne, jusqu'à ce que j'essaie de me déconnecter et de me connecter à nouveau.

Tous les indices sont très appréciés! Faites-moi savoir si je peux ajouter plus de détails.

36
vrepsys

Jimbo a fait un travail remarquable en expliquant le "pourquoi" derrière le problème que vous rencontrez. Il existe deux approches pour résoudre le problème:

  1. (Comme recommandé par Jimbo) Remplacez Devise :: SessionsController pour renvoyer le nouveau jeton 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
    

    Et créez un gestionnaire de réussite pour votre demande sign_out côté client (vous avez probablement besoin de quelques ajustements en fonction de votre configuration, par exemple, 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");
      });
    }
    

    Cela suppose également que vous incluez automatiquement le jeton CSRF à toutes les demandes AJAX avec quelque chose comme ceci:

    $(document).ajaxSend(function (e, xhr, options) {
      xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
    });
    
  2. Beaucoup plus simplement, si cela convient à votre application, vous pouvez simplement remplacer le Devise::SessionsController et remplacer la vérification du jeton par skip_before_filter :verify_authenticity_token.

36
jredburn

Je viens de rencontrer ce problème aussi. Il se passe beaucoup de choses ici. 

TL; DR - La raison de l'échec est que le jeton CSRF est associé à votre session serveur (vous avez une session serveur, que vous soyez connecté ou déconnecté). Le jeton CSRF est inclus dans le DOM de votre page à chaque chargement de page. Lors de la déconnexion, votre session est réinitialisée et ne comporte pas de jeton csrf. Normalement, une déconnexion redirige vers une page/action différente, ce qui vous donne un nouveau jeton CSRF, mais puisque vous utilisez ajax, vous devez le faire manuellement.

  • Vous devez remplacer la méthode Devise SessionController :: destroy pour renvoyer votre nouveau jeton CSRF. 
  • Ensuite, côté client, vous devez définir un gestionnaire de réussite pour votre déconnexion XMLHttpRequest. Dans ce gestionnaire, vous devez extraire ce nouveau jeton CSRF de la réponse et le définir dans votre domaine: $('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>) 

Explication plus détaillée Vous avez probablement protect_from_forgery défini dans votre fichier ApplicationController.rb à partir duquel tous vos autres contrôleurs héritent (c'est assez commun à mon avis). protect_from_forgery effectue des contrôles CSRF sur toutes les requêtes HTML/Javascript non-GET. Comme Devise Login est un POST, il effectue un contrôle CSRF. Si une vérification CSRF échoue, la session en cours de l’utilisateur est effacée, c’est-à-dire qu’elle est déconnectée, car le serveur suppose qu’il s’agit d’une attaque (ce qui est le comportement correct/souhaité).

Donc, en supposant que vous démarriez dans un état déconnecté, vous chargez une nouvelle page et ne rechargez jamais la page:

  1. Lors du rendu de la page: le serveur insère le jeton CSRF associé à votre session serveur dans la page. Vous pouvez afficher ce jeton en exécutant ce qui suit à partir d'une console javascript dans votre navigateur$('meta[name="csrf-token"]').attr('content').

  2. Vous vous connectez ensuite via XMLHttpRequest: Votre jeton CSRF reste inchangé à ce stade, de sorte que le jeton CSRF de votre session correspond toujours à celui qui a été inséré dans la page. Dans les coulisses, côté client, jquery-ujs écoute les xhr et définit un en-tête 'X-CSRF-Token' avec la valeur $('meta[name="csrf-token"]').attr('content') pour vous automatiquement (rappelez-vous que c'était le jeton CSRF défini à l'étape 1 par le serveur) . Le serveur compare le jeton défini dans l'en-tête par jquery-ujs et celui qui est stocké dans les informations de votre session. Ils correspondent afin que la demande aboutisse.

  3. Vous vous déconnectez ensuite via XMLHttpRequest: Ceci réinitialise la session, vous donne une nouvelle session sans jeton CSRF. 

  4. Vous vous reconnectez ensuite via XMLHttpRequest: jquery-ujs extrait le jeton CSRF de la valeur $('meta[name="csrf-token"]').attr('content'). Cette valeur est toujours votre jetonOLDCSRF. Il prend ce vieux jeton et l'utilise pour définir le 'X-CSRF-Token'. Le serveur compare cette valeur d'en-tête avec un nouveau jeton CSRF ajouté à votre session, ce qui est différent. Cette différence entraîne l'échec de protect_form_forgery, qui lève le WARNING: Can't verify CSRF token authenticity et réinitialise votre session, ce qui déconnecte l'utilisateur.

  5. Vous créez ensuite un autre XMLHttpRequest nécessitant un utilisateur connecté: La session en cours n'a pas d'utilisateur connecté, alors le système renvoie un 401.

Mise à jour: 8/14 Devise logout ne vous donne pas un nouveau jeton CSRF, la redirection qui se produit normalement après une déconnexion vous donne un nouveau jeton CSRF.

31
plainjimbo

Ceci est ma prise:

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

Et du côté du client:

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

Ce qui gardera vos balises méta CSRF à jour chaque fois que vous renvoyez l'en-tête X-CSRF-Token ou X-CSRF-Param via une requête ajax.

8
Sija

Ma réponse emprunte énormément à @Jimbo et à @Sija, mais j'utilise la convention de schéma/angularjs suggérée dans Rails CSRF Protection + Angular.js: protect_from_forgery m'oblige à me déconnecter de POST , et élaboré un peu sur mon blog quand je l’ai fait à l’origine. Cela a une méthode sur le contrôleur d'application pour définir des cookies pour 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

J'utilise donc le format de @ Sija, mais j'utilise le code de la solution SO précédente pour me donner:

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

Pour être complet, comme il m’a fallu quelques minutes pour le résoudre, j’ai également noté la nécessité de modifier votre fichier config/routes.rb pour déclarer que vous avez remplacé le contrôleur de sessions. Quelque chose comme:

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

Cela faisait également partie d'un grand nettoyage CSRF que j'ai effectué sur mon application, ce qui pourrait être intéressant pour d'autres. Le blog est ici , les autres changements incluent:

Récupération d'ActionController :: InvalidAuthenticityToken, ce qui signifie que si les choses ne se synchronisent pas, l'application se corrigera d'elle-même, au lieu que l'utilisateur ne supprime les cookies. Dans l'état actuel des choses dans Rails, je pense que votre contrôleur d'application sera configuré par défaut avec:

protect_from_forgery with: :exception

Dans cette situation, vous avez alors besoin de:

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

J'ai également eu des problèmes avec les conditions de course et certaines interactions avec le module de Timeoutable dans Devise, que j'ai commenté plus loin dans l'article de blog. En bref, vous devriez envisager d'utiliser active_record_store plutôt que cookie_store, et faites attention à la publication parallèle. requêtes proches des actions sign_in et sign_out.

8
PaulL

Après avoir fouillé sur la source Warden, j'ai remarqué que le fait de définir sign_out_all_scopes sur false empêche Warden d'effacer toute la session, de sorte que le jeton CSRF est préservé entre les fermetures de session.

Discussion connexe sur le point problématique Devise Issue: https://github.com/plataformatec/devise/issues/2200

5
Lucas

Je viens d'ajouter ceci dans mon fichier layout et cela a fonctionné

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

Vérifiez si vous avez inclus cela dans votre fichier application.js

// = nécessite jquery 

// = nécessite jquery_ujs

La raison en est jquery-Rails gem, qui définit automatiquement le jeton CSRF sur toutes les requêtes Ajax par défaut.

0
pdpMathi

Dans mon cas, après la connexion de l'utilisateur, je devais redessiner le menu de l'utilisateur. Cela a fonctionné, mais des erreurs d'authenticité CSRF se sont produites lors de chaque demande adressée au serveur, dans la même section (sans rafraîchir la page, bien sûr). Les solutions ci-dessus ne fonctionnaient pas car j'avais besoin de rendre une vue en js. 

Voici ce que j'ai fait en utilisant 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

Après cela, j'ai fait une demande au contrôleur # action qui redessine le menu. Et dans le javascript, j'ai modifié le X-CSRF-Param et le 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 %>');

J'espère que c'est utile pour quelqu'un dans la même situation que js :)

0
JGutierrezC