on Mar 8th, 2006How to persist Rails sessions via cookies using the Login Engine plugin

I absolutely can't stand having to log in to web applications on every visit. If I manage to remember what login I used, I'm not going to remember the password. It's just one more unnecessary step that I don't want to take.

If you're building a Ruby on Rails application that requires authentication, you're probably going to want to use the Login Engine plugin. There's no point in rewriting something that's been written thousands of times before. The problem is that by default you will have to log in again every time you close your browser window. You don't want to modify the actual engine code because then you're going to have upgrade problems when they release a new version. I'll explain how I solved this problem.

The first thing you will want to do is create your own user.rb in /app/models/. Your code should look like this:

RUBY:
  1. require 'digest/sha1'
  2. class User <ActiveRecord::Base
  3.   include LoginEngine::AuthenticatedUser
  4.  
  5.   def generate_login_token
  6.     Digest::SHA1.hexdigest("#{self.id}-#{self.salt}-#{self.salted_password}-#{Time.now.to_i}")[0..39]
  7.   end
  8. end

This generate_login_token method will generate a unique login_token that will be stored in a cookie. We don't have a login_token field in our users table yet though, so let's add that now.

To add the login_token column create a new migration via the command line, "ruby script/generate migration AddLoginToken". Your AddLoginToken migration class should look like this:

RUBY:
  1. class AddLoginToken <ActiveRecord::Migration
  2.   def self.up
  3.     add_column(:users, :login_token, :string, {:limit => 40})
  4.   end
  5.  
  6.   def self.down
  7.     remove_column(:users, :login_token)
  8.   end
  9. end

Run your migration and upgrade your database.

The next thing you will want to do is create your own user_controller.rb in /apps/controllers/ if you have not yet. Since the login engine has already implemented all the login* functionality in it's own user_controller.rb, what we want to do is add filter methods that will be executed after login and logout that will set the cookie, and delete the cookie.

RUBY:
  1. after_filter :set_login_cookie, :only => [:login]
  2. after_filter :delete_login_cookie, :only => [:logout]
  3.  
  4. protected
  5. def set_login_cookie
  6.   if user?
  7.     login_token = current_user.generate_login_token
  8.     current_user.update_attribute(:login_token, login_token)
  9.     cookies[:login] = { :value => login_token, :expires => Time.now.next_year}
  10.   end
  11. end
  12.  
  13. def delete_login_cookie
  14.   cookies.delete :login
  15. end

The final step is adding a block of code that will check on every request if a user is logged in. If no one is logged in, it checks if the cookie exists. If the cookie exists, it will load the user from the database and stuff it into session. Edit your application.rb and add the following code:

RUBY:
  1. prepend_before_filter :refresh_user_from_cookie
  2.  
  3. protected
  4. def refresh_user_from_cookie
  5.   return if user?
  6.   if !cookies[:login].nil?
  7.     user = User.find_by_login_token(cookies[:login])
  8.     if !user.nil?
  9.       session[:user] = user
  10.     else
  11.       logger.warning "user not found by login token. #{cookies[:login]}"
  12.     end
  13.   end
  14. end

That's all there is to it.

6 Responses to “How to persist Rails sessions via cookies using the Login Engine plugin”

  1. Hemanton 01 Jun 2006 at 2:41 am

    Awesome entry man…
    I was looking for this stuff since morning or something.

  2. Justinon 01 Aug 2006 at 3:52 pm

    Nice job. It works great! FYI, your 2 calls to after_filter have the wrong names (set_cookie vs. set_login_cookie)

    I modified my version to check if the user checked a “Remember Me?” checkbox before setting the cookie.

    thanks for posting this.

  3. Justinon 01 Aug 2006 at 3:57 pm

    Oh, 1 more thing if you’re using login engine. I found that the engine doesn’t clear out the password and confirmation (after someone does a change password) so if you happen to be printing debug info, you just might see it. the “falsify_password” method of loginengine doesn’t do enough to clear it.

    i added the following to user.rb:

    after_save :clear_password_fields

    protected
    def clear_password_fields
    self.password = nil
    self.password_confirmation = nil
    true
    end

  4. mehdion 25 Aug 2006 at 6:50 pm

    Thanks for the code.
    I got this error though:

    NoMethodError (undefined method `warning’ for #):
    /app/controllers/application.rb:109:in `refresh_user_from_cookie’

    My guess was I got this error because I was still logged in through a different computer. For now I removed the warning code from refresh_user_from_cookie to prevent it from breaking my code.

    Is the a way to allow persistent login from different machines though?

  5. mehdion 25 Aug 2006 at 9:28 pm

    I guess I was wrong. Your code works even when you login from multiple browsers.

  6. DWFrankon 11 Nov 2006 at 2:42 am

    I’m trying to adapt this code for a set of apps single-sign-on. One database, multiple rails apps.

    I’ve tried changing the login cookie to use the domain instead of the app+domain as follows:

    cookies[:login] = { :value => login_token, :domain => ‘.mysite.com’, :expires => Time.now.next_year}

    The cookie write appears to work, but reading back cookies[:login] does not. I’m not sure if this is a problem with my code or this code.

    thoughts anyone?

Trackback URI | Comments RSS

Leave a Reply