NOTE: This information is mostly outdated, and most links are dead

The login system of this site is based on Rick’s acts_as_authenticated (as people who read some of my previous articles are probably aware of), but it offers no “Remember me” functionality. Translated, when you close your browser, or leave your browser untouched for long enough, you are no longer logged in. This can get tedious, as you can probably understand :)

So I decided to implement this functionality. This is what I looked at, and what I decided to implement.

EDIT: As I explain in this blog entry, the code described in this article is now a part of acts_as_authenticated So unless you have a site that is already using acts_as_authenticated just make sure you got the latest version, and you are all set :)

Sessions, sessions, sessions

acts_as_authenticated uses sessions to remember the user. One way to get rails to remember" the user is to “extend the time a session is kept. Of course, the downside of this is that all sessions become “persistent”, it’s not easy to expire the “remember” state for selected users (manually or scripted), and a host of other consequences.

Hence, I looked elsewhere…

Cookies, lets look at the jar…

Sessions are kept on the server, and a cookie is set in the users browser with a session identifier. This way rails knows what session to load. This implies that if someone else can get a hold of your session id, he can steal your session.

Another way is to create a unique string of yourself, and set a user cookie with said string. Then if you load a page and no user is logged in have your app search for the user with that string, and load a new session for that user. This has some caveats as well, but we’ll address those later.

I ran across this article on how to implement a cookie based “remember me” functionality. It is for Login Engine (BTW, I am not fond of rails engines, but that’s another story ^^), but we’ll “translate” it for acts_as_authenticated.

The login controller

app/controllers/account_controller.rb excrept:

class AccountController < ApplicationController
  def login
    return unless request.post?
    self.current_user = User.authenticate(params[:login], params[:password])
    if current_user
      if params[:remember_me] == "1"
        self.current_user.remember_me
        cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires }
      end
      redirect_back_or_default(:controller => '/account', :action => 'index')
      flash[:notice] = "Logged in successfully"
    end
  end

  def logout
    self.current_user.forget_me if current_user
    self.current_user = nil
    cookies.delete :auth_token
    flash[:notice] = "You have been logged out."
    redirect_back_or_default(:controller => '/account', :action => 'index')
  end
end

The application controller

app/controllers/application.rb excrept:

class ApplicationController < ActionController::Base
  before_filter :login_from_cookie
  protected
  def login_from_cookie
    return unless cookies[:auth_token] && current_user.nil?
    user = User.find_by_remember_token(cookies[:auth_token])
    if user && !user.remember_token_expires.nil? && Time.now < user.remember_token_expires
      user.remember_me
      self.current_user = user
      cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires }
      flash[:notice] = "Logged in successfully"
    end
  end
end

The model

app/models/user.rb excrept:

class User < ActiveRecord::Base
  def remember_me
    self.remember_token_expires = 2.weeks.from_now
    self.remember_token = Digest::SHA1.hexdigest("#{salt}--#{self.email}--#{self.remember_token_expires}")
    self.password = ""  # This bypasses password encryption, thus leaving password intact
    self.save_with_validation(false)
  end

  def forget_me
    self.remember_token_expires = nil
    self.remember_token = nil
    self.password = ""  # This bypasses password encryption, thus leaving password intact
    self.save_with_validation(false)
  end
end

The migration

add_column :users, :remember_token, :string
add_column :users, :remember_token_expires, :datetime