Monday, May 2, 2011

Session Timeouts on Rails

Session Timeouts are a perfect example of the conflict between usability and security. From a security standpoint you don't want to leave a user logged in for an extended time period, because someone else may get access to their browser and then act as that person. As an example I have a a Facebook friend who frequently has posts made in her name by friends who get a hold of her phone. On the other hand, as a user, it is really frustrating when I have to keep logging back into a site.

In general, be only as secure as you need to be. I am ok with my bank logging me out after a short period of inactivity. However, I am willing to risk the occasional prank post on Facebook to not have to keep logging back in. As long as the social sites require a re-entering of a password before allowing a password change or any other personal information changes, they are secure enough from my point of view.

Session Expiration Tasks
If you want to expire sessions, you need to do a couple of things:
  • Keep track of the time of the user's last action
  • Show the timeout on the client side, by having a javascript function running on the browser which will load the login page after the session has timed out
  • Enforce the timeout on the server side by having the server redirect to a login page if a different page is requested after a timeout
  • Make sure the redirect also happens on Ajax updates

Rail Implementation
For purposes of this code, I am assuming that you have a login system like shown in Michael Hartl's "Ruby on Rails Tutorial".

Tracking Timeout
 Following the example I found at http://snippets.dzone.com/posts/show/7400 I made my ApplicationController look like:
class ApplicationController < ActionController::Base
  protect_from_forgery
  before_filter :session_expiry
  before_filter :update_activity_time

  include SessionsHelper

  def session_expiry
    get_session_time_left
    unless @session_time_left > 0
      sign_out
      deny_access 'Your session has timed out. Please log back in.'
    end
  end

  def update_activity_time
    session[:expires_at] = 30.minutes.from_now
  end

  private

  def get_session_time_left
    expire_time = session[:expires_at] || Time.now
    @session_time_left = (expire_time - Time.now).to_i
  end

end
In this code we store an expiration time in the session scope, and we make sure the session hasn't expired before performing any actions. update_activity_time accomplishes the goal of tracking the user's last action and session_expiry enforces the timeout.

Support Ajax Calls
A complication comes up if your web application has Ajax calls. If an Ajax request comes in after the session times out you want to do a full page redirect to the login page, not just a partial update. To accomplish this I modified the deny_access method in the SessionsHelper module to look like:
def deny_access(msg = nil)
  msg ||= "Please sign in to access this page."
  flash[:notice] ||= msg
  respond_to do |format|
    format.html {
      store_location
      redirect_to signin_url
    }
    format.js {
      store_location request.referer
      render 'sessions/redirect_to_login', :layout=>false
    }
  end
end
As you can see, on an Ajax call we store the referrer, i.e. the page that the user was on when they made the Ajax call, and then render a javascript fragment. The file 'views/sessions/redirect_to_login.js.haml' just consists of the one line of javascript:
window.location.replace( '#{escape_javascript signin_url}' );
which tells the browser to redirect to the login page.

Client Side Timeout
It can be a frustrating user experience to perform an action on a page, only to find out that you have timed out. Since we've already decided that we will timeout the user, we can't prevent that behavior, but we can at least let the user know they have timed out by automatically redirecting them to the login page when they time out. To accomplish this we add the following javascript lines to our public/javascripts/application.js file
function checkSessionTimeout(url) {
 $.getScript(url);
}
function setSessionTimeout(url, seconds) {
 setTimeout("checkSessionTimeout(\'" + url + "\')", seconds*1000 + 15);
}
checkSessionTimeout is just a method which makes an Ajax call (using jQuery) to a URL and executes the returned javascript. setSessionTimeout sets a javascript timeout to call checkSessionTimeout after the specified delay.

To call these functions we add the following to our views/layout/application.html.haml
-if @session_time_left
  :javascript
    $(function() {
      setSessionTimeout('#{check_session_alive_sessions_path}', #{@session_time_left})
    });
Basically, if the @session_time_left variable is set, we use the jquery functionality to call the setSessionTimeout method when the page has been loaded. We pass the url that corresponds to the check_session_alive action on the sessions controller which looks like:
def check_session_alive

  get_session_time_left
  if @session_time_left > 0
    render 'sessions/check_session_alive', :layout=>false
  else
    sign_out
    deny_access 'Your session has timed out. Please log back in.'
  end
end
If the session has timed out, we just call deny_access which we showed above and already handles the redirect. If the session hasn't timed out, we render sessions/check_session_alive.js.haml which just calls setSessionTimeout again, with the new timeout value:
setSessionTimeout('#{check_session_alive_sessions_path}', #{@session_time_left})

One Catch
We don't want to enforce that the session is valid on the session actions like login, logout, and check_session_alive. To fix this we add the following to the top of our SessionsController:
class SessionsController < ApplicationController

  skip_before_filter :session_expiry
  skip_before_filter :update_activity_time

Conclusion
With that you have a bare bones session expiration system on your Rails app. I will leave it as an exercise to the reader to add extra features, like giving a user a 5 minute warning that they are about to be logged out.

2 comments:

railsguage48 said...

Wow .... just what I would like to do with my sessions.

I am using rails 3.2, ruby 1.9.3-p0, postgresql all on ubuntu 11.10.

I will try and see if I can implement this in my app. Just a quick note to thank you for posting this and for styling it as a tutorial.

Will you take a question or two?

Thanks again.

Michael Haddox-Schatz said...

When I worked on this tutorial it was with 3.0, but I would think it should work the same with 3.2. Feel free to ask me any questions, though I can't promise I'll know the answers.