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.