Friday, August 26, 2005

Forget Me Not

For TaskTHIS, I’ve implemented a simple authentication cache. The “Remember me…” checkbox on the login form. I’m going to present to you, good or bad, how I decided to design it.

After looking at how it’s done in other technologies and how secure (or actually insecure) it is, I decided to try something a little different. My goals were to 1) avoid setting cookies with any personal information, and 2) make it perishable. Meaning, I wanted the cookie value itself to expire.

High Level

 Gallery Data Thumbnails 3 Psss82-1

When “Remember Me” is selected on the login screen the application creates a unique token and saves it into a cookie. The token is called a remembrall. The remembrall is essentially an SHA hash of the user email and the exact time the remembrall was created. In addition, the application also generates an expiration date for the remembrall. So it’s only a valid token for a short while, satisfying my second goal.

It may appear that I have already broken my first goal, I have used the user’s email address in the token. Looking back, I probably should have used the USER record’s ID (usually just an integer that’s unique within the system) as the information that’s unique to the user. However, since it’s hashing the email and timestamp, I believe it satisfactorily meets the first goal.

At this point, we need a way to recognize the remembrall on pages that are protected, and automatically log the user into the system if the remembrall is still ‘fresh’. To accomplish this, TaskTHIS uses a filter that runs before every page is loaded. The filter checks to see if there’s a remembrall set and if it’s valid. The flow looks a little something like this:

TaskTHIS Autologin Process

  1. Is there a remembrall cookie set? Yes…
  2. Is there a currently logged in user? No…
  3. Is there a user in the system with this remembrall? Yes…
  4. Has the remembrall expired? No…
  5. Log the user in!

Whew, that was a mouthful.

Less High Level

So let’s look at some code, eh?

The initial user authentication was created with the excellent login_generator. What follows build upon that foundation.

We need to modify the User so that the remembrall can be saved. So, in Migration format, the user gets:

add_column :users, :remembrall, :string, :length=>40
add_column :users, :remembrall_expires, :datetime

One of the results of having an automatic login process is that the login procedure can be called from two different places. Each with a different destination after the successful login. What does that mean? It means I needed to abstract the code for logging in a user. For TaskTHIS, I put that into the AutoLogin module. I wouldn’t really recommend handling it this way, in the future TaskTHIS won’t. I think it should really be a protected method in your ApplicationController.

In the login process, TaskTHIS keeps track of the last time a user logs in. Also, each user’s settings are stored as a YAML string. The login code needs to handle all of this, the code looks like this:

def handle_login( user )
  @session[:user] = user
  @session[:user_prefs] = YAML::load(user.prefs)
  user.last_login = Time.now
  user.save
end

The following is the filtering code used with :before_filter in the ApplicationController:

def login_from_cookie
  user = User.find_by_remembrall( cookies[:remembrall] ) if cookies[:remembrall] and @session[:user].nil?
  if user and active_remembrall? user
    handle_login user
  end
end

It’s using a helper method in the same module for testing the remembrall timestamp:

def active_remembrall?( user )
  return Time.now < user.remembrall_expires unless user.remembrall_expires.nil?
  false
end

From here, it’s just a matter of hooking it all up. In the Controller::login, we’ll need to make sure it calls our custom handle_login method. For clarity, here’s the entire login method:

def login
  case @request.method
    when :post
      if user = User.authenticate(@params[:user_login], @params[:user_password])
        handle_login( user )
        if @params[:remember_me]
          create_remembrall( current_user )
        end
        flash['notice']  = “Login successful”
        redirect_back_or_default :action => “welcome”
      else
        flash.now['notice']  = “Login unsuccessful”
        @login = @params[:user_login]
      end
  end
end

You’ll notice that it uses some things we haven’t discussed: currentuser and createremembrall. They can be found in the lib/auto_login.rb file.

Now, in our ApplicationController, we’ll need to add the call for the :before_filter:

class ApplicationController < ActionController::Base

  include LoginSystem
  include AutoLogin

  before_filter :login_from_cookie

  # the handle_login method and other stuff here...

end

So that, in a nutshell, is how TaskTHIS handles cached user authentication. I’ve not shown all of the code. For example, I didn’t show the code that actually creates the remembrall. For all of that, you can download the code and have a look for yourself.

Let me know what you think or what you would have done differently.

Technorati Tags:

6 comments:

  1. Yeah I can't get this to work at all cause you please provide complete code for this to work 100% correctly cause the link you have on this page for the code does not work!

    ReplyDelete
  2. Andrew, I don't know ifi you read the beginning, but he said that he used Login_Generator for the initial code!

    ReplyDelete
  3. Forget Me Not at M@ McCray...
    For TaskTHIS, Ive implemented a simple authentication cache. The Remember me checkbox on the login form. Im going to present to you, good or bad, how I decided to design it....

    ReplyDelete
  4. I'm sure you know this, but the code sections of the site are no longer highlighted / readable.
    Switched blogging software I take it =)

    ReplyDelete
  5. [Shanti Braford:4] Yep, you guessed it. I changed away from WordPress.

    ReplyDelete
  6. here’s a well-thought-out article about persistent logins and best practices: http://fishbowl.pastiche.org/2004/01/19/..http://fishbowl.pastiche.org/2004/01/19/persistent_login_cookie_best_practice

    ReplyDelete