I\'m doing a singe-page application using Rails. When signing in and out Devise controllers are invoked using ajax. The problem I\'m getting is that when I 1) sign in 2) sig
in reply to a comment of @sixty4bit; if you run into this error:
Unexpected error while processing request: undefined method each for :authenticity_token:Symbol`
replace
response.headers['X-CSRF-Param'] = request_forgery_protection_token
with
response.headers['X-CSRF-Param'] = request_forgery_protection_token.to_s
Jimbo did an awesome job explaining the "why" behind the issue you're running into. There are two approaches you can take to resolve the issue:
(As recommended by Jimbo) Override Devise::SessionsController to return the new 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
And create a success handler for your sign_out request on the client side (likely needs some tweaks based on your setup, e.g. 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");
});
}
This also assumes you're including the CSRF token automatically with all AJAX requests with something like this:
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
});
Much more simply, if it is appropriate for your application, you can simply override the Devise::SessionsController
and override the token check with skip_before_filter :verify_authenticity_token
.
My situation was even simpler. In my case, all I wanted to do was this: if a person is sitting on a screen with a form, and their session times out (Devise timeoutable session timeout), normally if they hit Submit at that point, Devise would bounce them back to the login screen. Well, I didn't want that, because they lose all their form data. I use JavaScript to catch the form submit, Ajax call a controller which determines if the user is no longer signed in, and if that's the case I put up a form where they retype their password, and I reauthenticate them (bypass_sign_in in a controller) using an Ajax call. Then the original form submit is allowed to continue.
Was working perfectly until I added protect_from_forgery.
So, thanks to the above answers all I needed really was in my controller where I sign the user back in (the bypass_sign_in) I just set an instance variable to the new CSRF token:
@new_csrf_token = form_authenticity_token
and then in the .js.erb that was rendered (since again, this was an XHR call):
$('meta[name="csrf-token"]').attr('content', '<%= @new_csrf_token %>');
$('input[type="hidden"][name="authenticity_token"]').val('<%= @new_csrf_token %>');
Voila. My form page, which was not refreshed and therefore was stuck with the old token, now has the new token from the new session I got from signing in my user.
In my case, after login the user in, i needed to redraw the user's menu. That worked, but i got CSRF authenticity errors on every request to the server, in that same section (without refreshing the page, of course). Above solutions wasn't working since i needed to render a js view.
What i did is this, using 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
After that i made a request to the controller#action that redraw the menu. And in the javascript, i modified the X-CSRF-Param and 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 %>');
I hope it's useful for someone on the same js situation :)