2012-08-07 6 views
34

Sto facendo un'applicazione di una singola pagina utilizzando Rails. Quando si effettua l'accesso e l'uscita, i controller Devise vengono richiamati usando ajax. Il problema che sto ottenendo è che quando 1) accedi 2) esci, quindi l'accesso di nuovo non funziona.Rails, Devise authentication, CSRF issue

Penso sia correlato al token CSRF che viene reimpostato quando si disconnette (anche se non dovrebbe essere afaik) e poiché si tratta di una singola pagina, il vecchio token CSRF viene inviato nella richiesta xhr reimpostando quindi la sessione.

Per essere più concreto questo è il flusso di lavoro:

  1. Accedi
  2. Esci
  3. Accedi (buon fine 201. Tuttavia stampa WARNING: Can't verify CSRF token authenticity nei log dei server)
  4. successiva richiesta Ajax non riesce 401 non autorizzato
  5. Aggiorna il sito Web (a questo punto, CSRF nell'intestazione della pagina cambia in qualcos'altro)
  6. Posso accedere, funziona, finché non provo ad uscire e rientrare.

Qualsiasi indizio molto apprezzato! Fammi sapere se posso aggiungere ulteriori dettagli.

risposta

35

Jimbo ha fatto un lavoro fantastico spiegando il "perché" del problema che stai incontrando. Ci sono due approcci si possono adottare per risolvere il problema:

  1. (Come raccomandato dal Jimbo) override Devise :: SessionsController per tornare al nuovo 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 
    

    E creare un gestore di successo per la richiesta sign_out sul lato client (probabilmente ha bisogno di alcune modifiche in base alla configurazione, ad esempio 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"); 
        }); 
    } 
    

    Ciò presuppone anche che stai compreso il CSRF Token automaticamente con tutte le richieste AJAX con qualcosa di simile:

    $(document).ajaxSend(function (e, xhr, options) { 
        xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token")); 
    }); 
    
  2. molto più semplicemente, se è appropriato per l'applicazione, è sufficiente eseguire l'override del Devise::SessionsController e sovrascrivere il controllo token con skip_before_filter :verify_authenticity_token.

+1

qualcuno sa se questo problema è stato sollevato con gli sviluppatori di Devise? – IanWhalen

+1

# 2 non funziona per me perché sto ottenendo l'errore di autenticità CSRF dopo l'accesso tramite Devise/Ajax con qualsiasi richiesta POST che faccio successivamente. Sono anche incerto su come eseguire il rendering dei nuovi token csrf perché sto già facendo il rendering di un modello come passaggio finale nella mia azione ': create'. Ho fatto una domanda al riguardo qui (http://stackoverflow.com/questions/26640326/after-devise-sign-in-via-ajax-any-post-request-results-in-csrf-authenticity-err) e lo apprezzerei davvero se avessi un momento per dare un'occhiata a – sixty4bit

+1

@ sixty4bit. Sembra che tu abbia lo stesso problema di te, ma la tua domanda è stata cancellata. L'hai mai capito? –

0

Verificare se hai inserito questo nel vostro file di application.js

//= require jquery

//= require jquery_ujs

Il motivo è jquery-rails gemma che imposta automaticamente il token CSRF su tutte le richieste Ajax di default, ha bisogno di quei due

+0

Sì, ce l'ho. Ho controllato le richieste con gli strumenti di sviluppo, hanno tutti CSRF. – vrepsys

+0

Controlla se stai ricevendo lo stesso token CSRF o token diversi – pdpMathi

+0

Questa soluzione funziona per me. Thanks & Saluti, – Icicle

31

Ho appena incontrato anche questo problema. C'è molto da fare qui.

TL; DR - Il motivo dell'errore è che il token CSRF è associato alla sessione del server (se si è connessi o disconnessi è disponibile una sessione del server). Il token CSRF è incluso nel DOM della pagina su ogni caricamento della pagina. Al logout, la sessione viene ripristinata e non ha token csrf. Normalmente, un logout reindirizza a una pagina/azione diversa, che ti dà un nuovo token CSRF, ma dal momento che stai usando un jax, devi farlo manualmente.

  • È necessario eseguire l'override del metodo Devise SessionController :: destroy per restituire il nuovo token CSRF.
  • Quindi sul lato client è necessario impostare un gestore di successo per il logout XMLHttpRequest. In tale gestore è necessario prendere questo nuovo token CSRF dalla risposta e impostarlo nel vostro dom: $('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)

spiegazione più dettagliata molto probabilmente avete ottenuto protect_from_forgery set nel file ApplicationController.rb da cui tutti gli altri controller ereditano (questo è piuttosto comune, credo). protect_from_forgery esegue controlli CSRF su tutte le richieste HTML/Javascript non GET. Poiché Devise Login è un POST, esegue un controllo CSRF. Se un controllo CSRF fallisce, la sessione corrente dell'utente viene cancellata, ovvero disconnette l'utente, poiché il server presume che si tratti di un attacco (che è il comportamento corretto/desiderato).

Quindi supponendo che si sta iniziando in uno stato disconnesso, si fa un caricamento della pagina fresca, e mai ricaricare nuovamente la pagina:

  1. sul rendering della pagina: il server inserisce il token CSRF associato alla sessione del server nella pagina. Puoi visualizzare questo token eseguendo quanto segue da una console javascript nel tuo browser $('meta[name="csrf-token"]').attr('content').

  2. È quindi accedere tramite una XMLHttpRequest: tuo CSRF token rimane invariato a questo punto così il CSRF token nella sessione corrisponde ancora quello che è stato inserito nella pagina. Dietro le quinte, sul lato client, jquery-ujs sta ascoltando xhr e sta impostando automaticamente un'intestazione 'X-CSRF-Token' con il valore di $('meta[name="csrf-token"]').attr('content') (ricorda che questo era il token CSRF impostato nel passaggio 1 dal sever) . Il server confronta il token impostato nell'intestazione da jquery-ujs e quello che è memorizzato nelle informazioni di sessione e corrispondono in modo tale che la richiesta abbia esito positivo.

  3. Esegui il logout tramite XMLHttpRequest: Questa sessione di reimpostazione, ti dà una nuova sessione senza un token CSRF.

  4. È quindi accedere di nuovo tramite una XMLHttpRequest: jquery-UJS tira il token CSRF dal valore di $('meta[name="csrf-token"]').attr('content'). Questo valore è ancora il tuo token CSRF OLD. Prende questo vecchio token e lo usa per impostare il 'X-CSRF-Token'. Il server confronta questo valore di intestazione con un nuovo token CSRF che aggiunge alla sessione, che è diverso. Questa differenza fa sì che protect_form_forgery non riesca, che getta il WARNING: Can't verify CSRF token authenticity e reimposta la sessione, che registra l'utente.

  5. È poi fare un altro XMLHttpRequest che richiede un utente collegato: La sessione corrente non dispone di un utente collegato in modo da elaborare un rendimento 401.

Aggiornamento: 8/14 Devise logout non ti dà un nuovo token CSRF, il reindirizzamento che normalmente avviene dopo un logout ti dà un nuovo token csrf.

+0

Una volta capito come implementare i due passaggi in TL, DR posterò il codice. – plainjimbo

+0

Il valore CSRF del DOM non è una buona soluzione. Da Angular Doc: [$ http] (http://docs.angularjs.org/api/ng.$http) > Poiché solo il codice JavaScript che viene eseguito sul tuo dominio può leggere il cookie, il tuo server può essere sicuro che l'XHR proveniva da JavaScript in esecuzione sul tuo dominio. L'intestazione non verrà impostata per le richieste tra domini. – chakming

+1

Sto avendo lo stesso problema ma nel mio flusso di lavoro non mi disconnetto mai. Non riesco a completare le azioni POST dopo aver effettuato l'accesso tramite XHR. Quindi il mio flusso è l'accesso dal nuovo modulo di widget tramite xhr (nessun avviso csrf nella console) -> prova a inviare il nuovo modulo di widget (avviso csrf nella console) -> disconnesso e reindirizzato al modulo di accesso regolare. Secondo la console, 'params [: authenticity_token]' è lo stesso durante la richiesta xhr di accesso e la richiesta html #create. Qualche aiuto qui ?? – sixty4bit

5

Dopo aver scavato sulla sorgente Warden, ho notato che l'impostazione sign_out_all_scopes-false ferma Warden da cancellare l'intera sessione, in modo che il token CSRF è conservata tra le uscite dei segni.

spiegazioni dettagliate sulla Devise problema Tacker: https://github.com/plataformatec/devise/issues/2200

+0

Questa risposta ha risolto molti dei miei problemi. Grazie Lucas. – Matt

7

Questo è il mio introito:

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 

E sul lato 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); 
    } 
}); 

che manterrà i vostri CSRF meta tag aggiornati ogni tempo si restituisce X-CSRF-Token o X-CSRF-Param intestazione tramite richiesta Ajax.

+0

Grazie, ho dovuto usare l'interpolazione delle stringhe quando ho chiamato 'request_forgery_protection_token' e' form_authenticity_token' dal mio controller mentre ricevevo un errore sul non poter chiamare 'split' su': authenticity_token: String' ... Ma questa tecnica funziona molto bene. Grazie. – stephenmurdoch

+2

Questa è la risposta giusta, anche se mi chiedo delle implicazioni sulla sicurezza della rigenerazione di un nuovo token crsf. Dovresti aggiungere se self.request.format.symbol ==: json per esempio o per qualsiasi mimo per il quale non stai reindirizzando – Gepsens

+1

Ho aggiornato la risposta in base al tuo suggerimento. Buona cattura, grazie! – Sija

8

La mia risposta prende in prestito pesantemente sia da @Jimbo sia da @Sija, tuttavia sto usando la convenzione di devise/angularjs suggerita a Rails CSRF Protection + Angular.js: protect_from_forgery makes me to log out on POST e ho elaborato un po 'sul mio blog quando inizialmente l'ho fatto. Questo ha un metodo sul controller applicazione per impostare i cookie per 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 

Così sto utilizzando @ formato di Sija, ma usando il codice da questa soluzione in precedenza SO, dandomi:

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 

Per completezza, poiché mi ci sono voluti un paio di minuti per risolverlo, noto anche la necessità di modificare il file config/routes.rb per dichiarare che hai sovrascritto il controller delle sessioni. Qualcosa di simile:

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

Questo è stato anche parte di una grande pulizia CSRF che ho fatto sulla mia domanda, che potrebbe essere interessante per gli altri. Il blog post is here, le altre modifiche includono:

Salvataggio da ActionController :: InvalidAuthenticityToken, il che significa che se le cose non vengono sincronizzate, l'applicazione si risolverà da sola, piuttosto che l'utente che ha bisogno di cancellare i cookie. Come stanno le cose in rotaie Credo che il controller applicazione sarà in default con:

protect_from_forgery with: :exception 

In tale situazione, è quindi necessario:

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

Ho anche avuto qualche dolore con condizioni di gara e alcune interazioni con il modulo timeoutable in Devise, che ho commentato ulteriormente nel post del blog: in breve, dovresti prendere in considerazione l'utilizzo di active_record_store piuttosto che cookie_store e prestare attenzione all'emissione di richieste parallele vicino a sign_in e sign_out actions.

1

ho solo aggiunto questo nel mio file di layout e ha funzionato

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

in risposta ad un commento di @ sixty4bit; se si esegue in questo errore:

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

sostituire

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

con

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

Questo potrebbe rispondere alla domanda nel commento, ma non dovrebbe pertanto essere una risposta a questa domanda. Ti suggerirei di creare una nuova domanda che si colleghi a questa domanda e di rispondere autonomamente con questa risposta. È quindi possibile aggiungere anche un commento a questa domanda che si collega a quello appena creato – Bowdzone

0

Nel mio caso, dopo il login l'utente, avevo bisogno di ridisegnare menù dell'utente. Ha funzionato, ma ho ricevuto errori di autenticità CSRF su ogni richiesta al server, nella stessa sezione (senza aggiornare la pagina, ovviamente). Le soluzioni di cui sopra non funzionavano poiché avevo bisogno di rendere una vista js.

quello che ho fatto è questo, 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 

Dopo che ho fatto una richiesta al controller # azione che ridisegna il menu. E in javascript, ho modificato il X-CSRF-Param e 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 %>'); 

Spero che sia utile per qualcuno in stessa situazione js :)