Devise work out of box for maintaining user session. But it fails to handle redirect in case of ajax request.
Problem Description:
- User logged in to Rails Application using Devise Auth flow.
- Go to fill a form but left for some work and comes back in 15 minutes(divise timeout configured for 15 minutes).
- Try to submit the form through ajax call and you show the loder to indicate form got submitted.
- The loader keep rotating as the request intercepted in between as unauthorized(401 error in console).
Here Devise did its job and unauthorized the user. The only thing it failed to do is redirect user to login page. If you reload the page you can see that user redirected to login page. So our goal is to exhibit same behavior in ajax request also.
Solution:
The only thing we have to handle is that, make Devise to redirect to login page if user session got expired when we make Ajax call. I looked for the way Devise handling the redirect and got idea from this issue thread on devise. The redirect logic of devise in case of unauthentication is written in the file failure_app.rb .
The redirect logic is written as below in the file
def redirect_url
if warden_message == :timeout
flash[:timedout] = true if is_flashing_format?
path = if request.get?
attempted_path
else
request.referrer
end
path || scope_url
else
scope_url
end
end
If you see it do not handle redirect for ajax request i,e of format .js.
The beauty of Ruby is that we can open any class and add our custom behavior. So lets customize the above class. Create a file lib/custom_failure_app.rb and add below line to it.
class CustomFailureApp < Devise::FailureApp
def redirect_url
if request.xhr?
send(:"new_#{scope}_session_path", :format => :js)
else
super
end
end
end
So we just told Devise that, if xhr request, redirect to login page with format js.
Now we have to configure Devise to use our CustomFailure APP – config/initializer/devise.rb
config.http_authenticatable = false
config.http_authenticatable_on_xhr = false
config.navigational_formats = ["*/*", :html, :js]
config.warden do |manager|
manager.failure_app = CustomFailureApp
end
So next time whenever ajax request is made on session timeout user will be redirected to users/sign_in.js path.
create view/devise/sessions/new.js.erb file and add below line:
window.location = "/"
At this point I was expecting that, things will start working now. But it is close but not working as expected. Below is the log:
Completed 401 Unauthorized in 31ms (ActiveRecord: 18.2ms)
Started GET “/users/sign_in.js” for 127.0.0.1 at 2018-07-02 21:26:22 +0530
Processing by Customized::SessionsController#new as JS
Completed 406 Not Acceptable in 3ms (ActiveRecord: 0.0ms)
ActionController::UnknownFormat (ActionController::UnknownFormat):
So, the desired behavior is there, on ajax session expire user got redirected to users/sign_in.js . The error here is because new action of Devise session controller is defined as below:
# GET /resource/sign_in
def new
self.resource = resource_class.new(sign_in_params)
clean_up_passwords(resource)
yield resource if block_given?
respond_with(resource, serialize_options(resource))
end
It is using respond_with which is not handling .js format. So I overrided it as below:
1 – Changed default devise routes
devise_for :users, controllers: { sessions: 'customized/sessions' }
2 – Overrided the new method in controllers/customized/sessions.rb .
class Customized::SessionsController < Devise::SessionsController
# GET /resource/sign_in
def new
self.resource = resource_class.new(sign_in_params)
clean_up_passwords(resource)
yield resource if block_given?
respond_to do |format|
format.js
format.html
end
end
end
Now everything working fine. If session got expired while ajax request and user got redirected to login page.