Take me home

Dynamic session expiration time in Rails 3

Published January 16, 2010

Update: new syntax

cookies[:thing] = {:value => {:normal => "session stuff"}, :expires => 2.hours.from_now}

# Signed cookies are encrypted with the apps secret_token.
cookies.signed[:login] = {:value => @user.id, :expires => 1.day.from_now}

# These calls are equivalent.
cookies[:foo] = {:value => "bar", :expires => 20.years.from_now}
cookies.permanent[:foo] = "bar"

Following is the outdated and obsolete original version of this post.

I created a plugin for remember me checkboxes in Rails 3. This post explains how the plugin works.

Remember me

Dynamic session expiration is a fancy name for “Remember me” checkboxes on login forms. When unchecked, the session resets when the browser is closed. When checked, the session will last for a given time period (30 minutes, 2 weeks, 1 year, whatever), and when the user visits your site, the countdown resets.

This is how you set coookie expiration time in Rails 3:

env["rack.session.options"][:expire_after] = 10.minutes

This alone won’t do, though.

Cookie limitations

Setting :expire_after will set the expires attribute on the cookie. This causes the cookie to persist to the given date even when the browser is closed, while a normal cookie without expires will last as long as the browser is open.

Here’s our problem: When we change something in the session of our Rails app, the cookie needs to be updated. This is done by creating a entirely new cookie to replace the old one. The reason this is a problem is, as I learned recently, that browsers will only send the raw cookie data to our Rails app, leaving out the meta data such as the expires of the cookie.

In other words, when we change something in our session, we lose track of when the cookie was supposed to expire. There is no way for Rails to know what the original expires was, since the browser doesn’t send that information to our Rails app.

Workaround

The simple answer is to use my plugin. Here’s what my plugin does, under the hood.

When we log in, we set the expiration time in our session.

def create
  session[:remember_for] = 1.week
  # Perform regular login..
end

In our application controller, we create an after filter that will pass this session variable to our new cookie.

after_filter :persist_session
 
def persist_session
  if session[:remember_for]
    env["rack.session.options"][:expire_after] = session[:remember_for]
  end
end

When we want to log out, just have to delete the session key, and the session will return to normal.

def destroy
  session.delete(:remember_for)
end

The after filter in the application controller will run before Rails creates the new cookie. This means that out :remember_for from the old cookie will get passed on to the new cookie before it is replaced.

Win!

Full example, using the plugin

class SessionsController < ApplicationController
  def create
    session_expires_after = 1.day if params[:remember_me]
    
    user = User.authenticate(params[:username], params[:password])
    if user
      session[:user_id] = user.id
      # redirect_to ...
    else
      # ...
    end
  end

  def destroy
     session_expires_now # unsets the session key
     session.delete(:user_id)
     # redirect_to ...
  end
end

Questions or comments?

Feel free to contact me on Twitter, @augustl, or e-mail me at august@augustl.com.