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 endIn 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 endAs 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 endIf 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:
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.
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.
Post a Comment