Rieles, autenticación de ideario, problema de CSRF

Estoy haciendo una aplicación de página singe usando Rails. Al iniciar y cerrar sesión, los controladores ideados se invocan con ajax. El problema que tengo es que cuando yo 1) inicio sesión 2) cierro sesión y luego, iniciar sesión de nuevo no funciona.

Creo que está relacionado con el token CSRF que se restablece cuando cierro la sesión (aunque no debería afaik) y dado que se trata de una sola página, el token viejo CSRF se envía en solicitud xhr, lo que restablece la sesión.

Para ser más concreto, este es el flujo de trabajo:

  1. Registrarse
  2. desconectar
  3. Iniciar sesión (exitoso 201. Sin embargo imprime WARNING: Can't verify CSRF token authenticity en los registros del servidor)
  4. La solicitud subsiguiente de ajax falla 401 no autorizada
  5. Actualice el sitio web (en este punto, CSRF en el encabezado de la página cambia a otra cosa)
  6. Puedo iniciar sesión, funciona, hasta que bash desconectarme y volver a ingresar.

¡Alguna pista muy apreciada! Avíseme si puedo agregar más detalles.

Jimbo hizo un trabajo increíble explicando el “por qué” detrás del problema que te encuentras. Hay dos enfoques que puede tomar para resolver el problema:

  1. (Según lo recomendado por Jimbo) Anular a Devise :: SessionsController para devolver el nuevo 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 

    Y cree un controlador de éxito para su solicitud de inicio de sesión en el lado del cliente (probablemente necesite algunos ajustes basados ​​en su configuración, por ejemplo, GET frente a 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"); }); } 

    Esto también supone que está incluyendo el token CSRF automáticamente con todas las solicitudes AJAX con algo como esto:

     $(document).ajaxSend(function (e, xhr, options) { xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token")); }); 
  2. Mucho más simplemente, si es apropiado para su aplicación, simplemente puede anular el Devise::SessionsController y anular la verificación del token con skip_before_filter :verify_authenticity_token .

Me acabo de encontrar con este problema también. Están sucediendo muchas cosas aquí.

TL; DR : la razón de la falla es que el token CSRF está asociado con la sesión de su servidor (tiene una sesión de servidor si está conectado o desconectado). El token CSRF está incluido en el DOM de tu página en cada carga de página. Al desconectarse, su sesión se restablece y no tiene token csrf. Normalmente, un cierre de sesión redirige a una página / acción diferente, que le proporciona un nuevo token CSRF, pero como está utilizando ajax, debe hacerlo manualmente.

  • Debe anular el método Devise SessionController :: destroy para devolver su nuevo token CSRF.
  • Luego, del lado del cliente, debe establecer un controlador de éxito para su logout XMLHttpRequest. En ese controlador, debes tomar este nuevo token CSRF de la respuesta y configurarlo en tu dom: $('meta[name="csrf-token"]').attr('content', )

Explicación más detallada Lo más probable es que protect_from_forgery creado protect_from_forgery en tu archivo ApplicationController.rb del que todos tus otros controladores heredan (esto es bastante común, creo). protect_from_forgery realiza comprobaciones CSRF en todas las solicitudes HTML / Javascript no GET. Dado que Devise Login es una POST, realiza una verificación CSRF. Si falla una comprobación CSRF, la sesión actual del usuario se borra, es decir, desconecta al usuario, porque el servidor asume que es un ataque (que es el comportamiento correcto / deseado).

Entonces, suponiendo que está comenzando en un estado de sesión cerrada, realiza una carga de página nueva y nunca vuelve a cargar la página:

  1. Al renderizar la página: el servidor inserta el token CSRF asociado con la sesión de su servidor en la página. Puede ver este token ejecutando lo siguiente desde una consola de JavaScript en su navegador $('meta[name="csrf-token"]').attr('content') .

  2. A continuación, inicie sesión a través de XMLHttpRequest: su token CSRF permanece sin cambios en este punto, por lo que el token CSRF en su sesión aún coincide con el que se insertó en la página. Detrás de escena, en el lado del cliente, jquery-ujs está escuchando xhr’s y configurando un encabezado ‘X-CSRF-Token’ con el valor de $('meta[name="csrf-token"]').attr('content') automáticamente (recuerde que este fue el token CSRF establecido en el paso 1 por el servidor). El servidor compara el conjunto de tokens en el encabezado por jquery-ujs y el que está almacenado en la información de su sesión y coinciden para que la solicitud tenga éxito.

  3. A continuación, cierre la sesión a través de XMLHttpRequest: esta sesión de restablecimiento, le da una nueva sesión sin un token CSRF.

  4. A continuación , inicie sesión nuevamente mediante XMLHttpRequest: jquery-ujs extrae el token CSRF del valor de $('meta[name="csrf-token"]').attr('content') . Este valor sigue siendo su token de CSRF ANTERIOR. Toma este token antiguo y lo usa para establecer el ‘X-CSRF-Token’. El servidor compara este valor de encabezado con un nuevo token CSRF que agrega a su sesión, que es diferente. Esta diferencia hace que protect_form_forgery falle, lo que arroja la WARNING: Can't verify CSRF token authenticity y restablece su sesión, que registra al usuario.

  5. A continuación, realiza otra XMLHttpRequest que requiere un usuario conectado: la sesión actual no tiene un usuario conectado, por lo que el dispositivo devuelve un 401.

Actualización: 8/14 El cierre de sesión de idee no le da un token CSRF nuevo, el redireccionamiento que normalmente ocurre después de un cierre de sesión le da un nuevo token csrf.

Mi respuesta toma mucho prestado de @Jimbo y @Sija, sin embargo, estoy usando la convención de idea / angularjs sugerida en Rails CSRF Protection + Angular.js: protect_from_forgery me hace desconectarme de POST , y elaboré un poco en mi blog cuando originalmente hizo esto. Esto tiene un método en el controlador de la aplicación para establecer cookies para 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 

Así que estoy usando el formato de @Sija, pero usando el código de esa solución SO anterior, dándome:

 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 

Para completar, dado que me tomó un par de minutos resolverlo, también noté la necesidad de modificar su config / routes.rb para declarar que ha anulado el controlador de sesiones. Algo como:

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

Esto también fue parte de una gran limpieza de CSRF que hice en mi aplicación, que podría ser interesante para otros. La publicación del blog está aquí , los otros cambios incluyen:

Rescatando de ActionController :: InvalidAuthenticityToken, lo que significa que si las cosas se desincronizan, la aplicación se arreglará a sí misma, en lugar de que el usuario tenga que borrar las cookies. Tal como están las cosas en los Rails, creo que su controlador de aplicaciones estará predeterminado con:

 protect_from_forgery with: :exception 

En esa situación, entonces necesitas:

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

También he tenido un poco de dolor con las condiciones de carrera y algunas interacciones con el módulo de horario en Devise, que he comentado más adelante en la publicación del blog; en resumen, debes considerar usar active_record_store en lugar de cookie_store, y ten cuidado con la publicación paralela solicitudes cercanas a las acciones sign_in y sign_out.

Esta es mi opinión:

 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 

Y en el lado del cliente:

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

Lo cual mantendrá sus meta tags CSRF actualizadas cada vez que devuelva X-CSRF-Token o X-CSRF-Param header a través de una solicitud ajax.

Después de indagar sobre la fuente de Warden, noté que al configurar sign_out_all_scopes en false detiene a Warden para que no sign_out_all_scopes toda la sesión, por lo que el token CSRF se conserva entre los cierres de sesión.

Discusión relacionada sobre el atacante de problemas de Devise: https://github.com/plataformatec/devise/issues/2200

Acabo de agregar esto en mi archivo de diseño y funcionó

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

Compruebe si ha incluido esto en su archivo application.js

// = requiere jquery

// = requiere jquery_ujs

La razón es la gem jquery-rails que establece automáticamente el token CSRF en todas las solicitudes de Ajax por defecto, necesita esas dos

En mi caso, después de iniciar sesión con el usuario, tuve que volver a dibujar el menú del usuario. Eso funcionó, pero obtuve errores de autenticidad CSRF en cada solicitud al servidor, en esa misma sección (sin refrescar la página, por supuesto). Las soluciones anteriores no funcionaban porque necesitaba renderizar una vista js.

Lo que hice es esto, usando 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 

Después de eso hice una solicitud a la acción del controlador # que redibuja el menú. Y en el javascript, modifiqué X-CSRF-Param y 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 %>'); 

Espero que sea útil para alguien en la misma situación js 🙂

en respuesta a un comentario de @ sixty4bit; si encuentras este error:

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

reemplazar

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

con

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