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:

  1. class AccountController < ApplicationController
  2. def login
  3. return unless request.post?
  4. self.current_user = User.authenticate(params[:login], params[:password])
  5. if current_user
  6. if params[:remember_me] == "1"
  7. self.current_user.remember_me
  8. cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires }
  9. end
  10. redirect_back_or_default(:controller => '/account', :action => 'index')
  11. flash[:notice] = "Logged in successfully"
  12. end
  13. end
  14. def logout
  15. self.current_user.forget_me if current_user
  16. self.current_user = nil
  17. cookies.delete :auth_token
  18. flash[:notice] = "You have been logged out."
  19. redirect_back_or_default(:controller => '/account', :action => 'index')
  20. end
  21. end

The application controller

app/controllers/application.rb excrept:

  1. class ApplicationController < ActionController::Base
  2. before_filter :login_from_cookie
  3. protected
  4. def login_from_cookie
  5. return unless cookies[:auth_token] && current_user.nil?
  6. user = User.find_by_remember_token(cookies[:auth_token])
  7. if user && !user.remember_token_expires.nil? && Time.now < user.remember_token_expires
  8. user.remember_me
  9. self.current_user = user
  10. cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires }
  11. flash[:notice] = "Logged in successfully"
  12. end
  13. end
  14. end

The model

app/models/user.rb excrept:

  1. class User < ActiveRecord::Base
  2. def remember_me
  3. self.remember_token_expires = 2.weeks.from_now
  4. self.remember_token = Digest::SHA1.hexdigest("#{salt}--#{self.email}--#{self.remember_token_expires}")
  5. self.password = "" # This bypasses password encryption, thus leaving password intact
  6. self.save_with_validation(false)
  7. end
  8. def forget_me
  9. self.remember_token_expires = nil
  10. self.remember_token = nil
  11. self.password = "" # This bypasses password encryption, thus leaving password intact
  12. self.save_with_validation(false)
  13. end
  14. end

The migration

  1. add_column :users, :remember_token, :string
  2. add_column :users, :remember_token_expires, :datetime