Dynamic Session Time Management with Devise
Devise comes with 2 options for session management: rememberable
and timeoutable
.
rememberable
stores the time when the user was signed in in the database and compares it with current time - if the difference is more than config'sremember_for
setting the user will be signed out.timeoutable
first checks if rememberable is being used and not expired, then compares current time with last user's request time - the difference is then being compared with config'stimeout_in
setting.
What if you want to have multiple timeout_in
settings for different cases?
Well, there is an existing solution in devise wiki: How To: Add timeout_in value dynamically. The idea is to override timeout_in
method in the user model, if it's enough for your purposes you can stop reading here. In my case I wanted to have a different session time based on the url which user uses(long session for regular application sign in, short session for embedded app version), so the implementation should be independed from the user model. There is no built in solution for this case in devise, so I have built my own.
Solution
First of all, I checked warden manager in devise for timeoutable and removed this feature from the User
model to implement my own. Here is how it looks like:
Warden::Manager.after_set_user do |record, warden, options|
scope = options[:scope]
env = warden.request.env
if record && warden.authenticated?(scope)
last_request_at = warden.session(scope)["last_request_at"]
if last_request_at.is_a? Integer
last_request_at = Time.at(last_request_at).utc
elsif last_request_at.is_a? String
last_request_at = Time.parse(last_request_at)
end
proxy = Devise::Hooks::Proxy.new(warden)
session_valid_for = warden.env["rack.session"][:session_valid_for]
if session_valid_for.present? && Time.now.utc.to_i - last_request_at.to_i > session_valid_for.to_i
Devise.sign_out_all_scopes ? proxy.sign_out : proxy.sign_out(scope)
throw :warden, scope: scope, message: :timeout
end
unless env["devise.skip_trackable"]
warden.session(scope)["last_request_at"] = Time.now.utc.to_i
end
end
end
last_request_at
is updated on each requestsession_valid_for
needs to be set in a separate controller
class SessionsController < Devise::SessionsController
def create
super do
if params[:layout] == "embedded"
session[:session_valid_for] = SessionTimeManagerService.short
else
session[:session_valid_for] = SessionTimeManagerService.standard
end
end
end
end
To honor the single responsibility principle, I moved session time related logic into separate service, but of course the easiest way is to set it explicitly like
session[:session_valid_for] = 5.minutes / 1.hour
or using environment variables
session[:session_valid_for] = ENV["session_time_short"] / ENV["session_time_standard"]
One remaining issue is that existing signed users don't have session_valid_for
variable in their sessions, so they won't get signed out. The easy fix is to set it in the application conrtoller's before_filter
which is being executed before any call to any controller action:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :ensure_session_valid_for_set
def ensure_session_valid_for_set
if session[:session_valid_for].blank? && user_signed_in?
session[:session_valid_for] = SessionTimeManagerService.standard
end
end
end