Dynamic Session Time Management with Devise

Devise comes with 2 options for session management: rememberable and timeoutable.

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 request
  • session_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