Dynamic Session Time Management with Devise
Devise comes with 2 options for session management:
rememberablestores the time when the user was signed in in the database and compares it with current time - if the difference is more than config's
remember_forsetting the user will be signed out.
timeoutablefirst 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's
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.
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_atis updated on each request
session_valid_forneeds 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