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:
WARNING: Can't verify CSRF token authenticity
dans les journaux du serveur)Tous les indices sont très appréciés! Faites-moi savoir si je peux ajouter plus de détails.
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:
(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"));
});
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
.
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.
$('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:
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')
.
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.
Vous vous déconnectez ensuite via XMLHttpRequest: Ceci réinitialise la session, vous donne une nouvelle session sans jeton CSRF.
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.
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.
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.
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.
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
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 %>
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.
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 :)