Basic User Authentication in Rails

This article walks through creating a basic authentication system in rails.

Update: If you liked this article Digg it!

The question of user authentication comes up regularly on the rails mailing list and there are several articles and discussions around the web on whether it should be part of the rails framework. Its not included in rails and it is not likely to be.

The reason given is that its difficult to generalize it to suit everybody. When I started using rails I tried out several different plugins and generators for user authentication. Each time I ended up spending more time installing, figuring out and adapting the code than I would have spent if I wrote it from scratch. Each project I’ve worked on has had different authentication requirements so now I write the authentication code from scratch every time.

The advantages to writing your own are:

  • You will fully understand it.
  • It will match exactly the needs of your application.
  • It will be easier for you to adapt it.
  • In many cases its actually quicker than using a plugin.
  • You have to write your own tests.

If you’re not interested in rolling your own, check these out (even if you don’t use them, reading through the code for each one is a good way to learn different approaches to doing rails authentication):

So in this article I will walk through creating a simple user authentication system with rails. Hopefully this tutorial will be useful to others who are starting to learn rails and need to do some basic authentication.

Its worth taking a moment to discuss how I wrote the code. The sequence for this article is to help explain what’s going on, but its not really the sequence of how I wrote the code. When writing the code I sketched out the views on paper and used this to figure out what methods I would need in the model and controller. Then I grabbed some tests from login generator and login engine and added some more tests based on the functionality I wanted. Then I implemented the model followed by the controller, one test at a time. When all the tests passed, I coded up the views. This was a bit of an epiphany for me as before I started using rails and for a while after I started to use it I rarely used unit tests. Now they’re an integral part of my coding process. For the purposes of this tutorial I’m not going to pay much attention to the tests other than to list them but I have commented them so I would strongly encourage you to go through them.

Lets start by thinking about our views. This gives us an idea of the functionality that we want. We can draw the pages that our app will have. It can be helpful at this stage to draw the views using pen and paper. By forcing ourselves to draw the interface now we force ourselves to think about the functionality we actually need. To signup the user will need to supply a username, a password and confirm their password. When logging in they will provide a username and password. If a user forgets their password they can have it emailed to their account - so we need to get their email when they signup too. When the user changes their password they just need to supply the new password and confirm it.

Users can signup, login and logout. Users can also change their password and have a password emailed to themselves if they forget it. Access to actions can be restricted depending on whether a user is logged in or not. So from thinking about our views we need to store for each user a username, password and email address. Now lets move on to creating the model.

> ruby script/generate model User
exists  app/models/

exists  test/unit/
exists  test/fixtures/
create  app/models/user.rb
create  test/unit/user_test.rb
create  test/fixtures/users.yml
create  db/migrate
create  db/migrate/001_create_users.rb

This generates the user model and test stubs and creates our initial migration. Open your user migration. It has already been created in db/migrate/001_create_users.rb

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.column :login, :string
      t.column :hashed_password, :string
      t.column :email, :string
      t.column :salt, :string
      t.column :created_at, :datetime
    end    
  end

  def self.down
    drop_table :users
  end
end

This code defines our database table that will store users. login is the user’s username and email is their email address - we will use this to send them a new password if they forget it. The other thing we need to store is their password so that we can authenticate them and log them into the system.

Its not a good idea to store the password in cleartext in the database. Instead we will store a hashed version of the password. This is stored in the hashed_password field. Before comparing the password to the one stored in the database we encrypt it and then check if the encrypt of the entered password matches the one stored in the database. We will use the SHA1 algorithm to encrypt the password.

Note: By default all your post parameters including cleartext passwords will appear in the rails production log. If you dont want this to happen you should take steps to avoid it. E.g. Filter Logged Params Plugin

The other database column is salt. Adding a salt to the hashed password make it more difficult to break the password. The salt is a random string that is generated and stored for each user. We then add this salt to the password before encrypting it. Every user has a different salt, but we store each user’s salt in the database so that we can authenticate the user’s password.

Running the migration will create the database

> rake db:migrate           
== CreateUsers: migrating =====================================================
-- create_table("users")
   -> 0.6538s
== CreateUsers: migrated (0.6554s) ============================================

and running

> rake db:test:clone 

sets up the test database.

The model

We need to decide what functionality goes in the model and what goes into the controllers. Rails is based on the MVC pattern and the idea that the business logic goes into the model. This is sometimes confused with the 3-tier architecture where the models are just used to interface with the database and the business logic appears in the controllers. The rails way seems to be to put as much logic as possible into the models. To think of it another way, you should be able to run your application through all its important processes by calling methods on model objects at the console. More concretely core logic such as authentication and sending a new password should be in the model, not the controller.

require 'digest/sha1'

class User < ActiveRecord::Base
  validates_length_of :login, :within => 3..40
  validates_length_of :password, :within => 5..40
  validates_presence_of :login, :email, :password, :password_confirmation, :salt
  validates_uniqueness_of :login, :email
  validates_confirmation_of :password
  validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :message => "Invalid email"  

We start out by defining validations for the user model. password and login must be within pre-defined length. login and email must be unique. email must match a certain format. validates_confirmation_of ensures that password must be confirmed using password_confirmation. login, email, password, password_confirmation and salt must all be present. When creating a new user or saving an existing one, all these conditions must be met or the save will fail.

Looking back at the database definition we see that we didn’t have a password or password confirmation field. We had hashed_password. We will store the hashed password in the database but we will create variables to hold the raw text version of password and password_confirmation. These values will not be stored in the database but storing them as variables enables us to take advantages of the rails validation methods.

  attr_protected :id, :salt

We make the id and salt attributes protected. This makes sure that users can’t set them by sending a post request - you have to update them in the model. For example if you extended this model to include a roles field that specified if a user was an admin or normal user it would be important to specify that field as protected. Any field that you don’t want to be updatable from your web forms should be protected.

We set the salt for the user to a random string if it hasn’t already been set. This happens the first time the users password is set. When we set the users password it calls the protected random_string method to generates a random string of digits and numbers of a pre-defined length.

 def self.random_string(len)
   #generate a random password consisting of strings and digits
   chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
   newpass = ""
   1.upto(len) { |i| newpass << chars[rand(chars.size-1)] }
   return newpass
 end

We now need to make sure that the password that is stored in the database is encrypted. We create an instance method password=. This method gets called whenever a password is assigned to a user e.g. u.password=”secret”.

def password=(pass)
  @password=pass
  self.salt = User.random_string(10) if !self.salt?
  self.hashed_password = User.encrypt(@password, self.salt)
end

This sets the variable @password to the text value of password and stores the hashed version in the database. The password is hashed using the protected encrypt class method. This generates a hash from the password and the users salt. The salt is set to a random value if it hasn’t already been set. (You could set the salt to a different value every time you change the password if you wanted).

 def self.encrypt(pass, salt)
   Digest::SHA1.hexdigest(pass+salt)
 end

So that takes care of encrypting the password. The user model also needs to be able to authenticate a user. The class method authenticate returns a user if they’re hashed password matches the one stored for that user in the database.

def self.authenticate(login, pass)
  u=find(:first, :conditions=>["login = ?", login])
  return nil if u.nil?
  return u if User.encrypt(pass, u.salt)==u.hashed_password
  nil
end  

First we find the user that corresponds to the login. If we didn’t find the login authentication fails. We then compute the users hashed password using the supplied password and the users salt. Authentication is successful if these values match.

The last piece of functionality for the user is the ability to send them a new password if they forget their password. We can’t send them their existing password because we don’t store it - we only store a salted hash of it. Instead we generate a new random password, change the users password to the new random password and email this new password to the user (we will describe the Notifications mailer method later).

def send_new_password
  new_pass = User.random_string(10)
  self.password = self.password_confirmation = new_pass
  self.save
  Notifications.deliver_forgot_password(self.email, self.login, new_pass)
end

Here is a full listing of the model:

require 'digest/sha1'

class User < ActiveRecord::Base
  validates_length_of :login, :within => 3..40
  validates_length_of :password, :within => 5..40
  validates_presence_of :login, :email, :password, :password_confirmation, :salt
  validates_uniqueness_of :login, :email
  validates_confirmation_of :password
  validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :message => "Invalid email"  

  attr_protected :id, :salt

  attr_accessor :password, :password_confirmation

  def self.authenticate(login, pass)
    u=find(:first, :conditions=>["login = ?", login])
    return nil if u.nil?
    return u if User.encrypt(pass, u.salt)==u.hashed_password
    nil
  end  

  def password=(pass)
    @password=pass
    self.salt = User.random_string(10) if !self.salt?
    self.hashed_password = User.encrypt(@password, self.salt)
  end

  def send_new_password
    new_pass = User.random_string(10)
    self.password = self.password_confirmation = new_pass
    self.save
    Notifications.deliver_forgot_password(self.email, self.login, new_pass)
  end

  protected

  def self.encrypt(pass, salt)
    Digest::SHA1.hexdigest(pass+salt)
  end

  def self.random_string(len)
    #generat a random password consisting of strings and digits
    chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
    newpass = ""
    1.upto(len) { |i| newpass << chars[rand(chars.size-1)] }
    return newpass
  end

end

Testing the model

With an authentication system the behaviour that you want your code to exhibit and the behaviour you don’t want it to exhibit are very well defined. Writing tests can be a good way of specifying the behaviour before writing the code. Write unit tests that specify how the model will behave. Write functional tests that specify how the controller will behave. Once these tests pass, you can be fairly confident that your model and controller are working as you expect. More importantly you can be confident that it still works in the future if you make changes to it by running the tests again and adding new ones. In other cases you can develop your code and tests side by side.

I’ve been harping on about tests so here are the unit tests for the user model. These tests go in the file test/unit/user_test.rb

To run the tests we need some fixtures. Our fixtures are in test/fixtures/users.yml

bob:
  id: 1000001
  login: bob
  salt: 1000
  email: bob@mcbob.com
  hashed_password: 77a0d943cdbace52716a9ef9fae12e45e2788d39 # test

existingbob:
  id: 1000002
  salt: 1000
  login: existingbob
  email: exbob@mcbob.com
  hashed_password: 77a0d943cdbace52716a9ef9fae12e45e2788d39 # test

longbob:
  id: 1000003
  login: longbob
  email: lbob@mcbob.com
  hashed_password: 00728d3362c26746ec25963f71be022b152237a9 # longtest
  salt: 1000

Here is a listing of tests for the model:

require File.dirname(__FILE__) + '/../test_helper'

class UserTest < Test::Unit::TestCase
  self.use_instantiated_fixtures  = true
  fixtures :users

  def test_auth 
    #check that we can login we a valid user 
    assert_equal  @bob, User.authenticate("bob", "test")    
    #wrong username
    assert_nil    User.authenticate("nonbob", "test")
    #wrong password
    assert_nil    User.authenticate("bob", "wrongpass")
    #wrong login and pass
    assert_nil    User.authenticate("nonbob", "wrongpass")
  end


  def test_passwordchange
    # check success
    assert_equal @longbob, User.authenticate("longbob", "longtest")
    #change password
    @longbob.password = @longbob.password_confirmation = "nonbobpasswd"
    assert @longbob.save
    #new password works
    assert_equal @longbob, User.authenticate("longbob", "nonbobpasswd")
    #old pasword doesn't work anymore
    assert_nil   User.authenticate("longbob", "longtest")
    #change back again
    @longbob.password = @longbob.password_confirmation = "longtest"
    assert @longbob.save
    assert_equal @longbob, User.authenticate("longbob", "longtest")
    assert_nil   User.authenticate("longbob", "nonbobpasswd")
  end

  def test_disallowed_passwords
    #check thaat we can't create a user with any of the disallowed paswords
    u = User.new    
    u.login = "nonbob"
    u.email = "nonbob@mcbob.com"
    #too short
    u.password = u.password_confirmation = "tiny" 
    assert !u.save     
    assert u.errors.invalid?('password')
    #too long
    u.password = u.password_confirmation = "hugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehuge"
    assert !u.save     
    assert u.errors.invalid?('password')
    #empty
    u.password = u.password_confirmation = ""
    assert !u.save    
    assert u.errors.invalid?('password')
    #ok
    u.password = u.password_confirmation = "bobs_secure_password"
    assert u.save     
    assert u.errors.empty? 
  end

  def test_bad_logins
    #check we cant create a user with an invalid username
    u = User.new  
    u.password = u.password_confirmation = "bobs_secure_password"
    u.email = "okbob@mcbob.com"
    #too short
    u.login = "x"
    assert !u.save     
    assert u.errors.invalid?('login')
    #too long
    u.login = "hugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhug"
    assert !u.save     
    assert u.errors.invalid?('login')
    #empty
    u.login = ""
    assert !u.save
    assert u.errors.invalid?('login')
    #ok
    u.login = "okbob"
    assert u.save  
    assert u.errors.empty?
    #no email
    u.email=nil   
    assert !u.save     
    assert u.errors.invalid?('email')
    #invalid email
    u.email='notavalidemail'   
    assert !u.save     
    assert u.errors.invalid?('email')
    #ok
    u.email="validbob@mcbob.com"
    assert u.save  
    assert u.errors.empty?
  end


  def test_collision
    #check can't create new user with existing username
    u = User.new
    u.login = "existingbob"
    u.password = u.password_confirmation = "bobs_secure_password"
    assert !u.save
  end


  def test_create
    #check create works and we can authenticate after creation
    u = User.new
    u.login      = "nonexistingbob"
    u.password = u.password_confirmation = "bobs_secure_password"
    u.email="nonexistingbob@mcbob.com"  
    assert_not_nil u.salt
    assert u.save
    assert_equal 10, u.salt.length
    assert_equal u, User.authenticate(u.login, u.password)

    u = User.new(:login => "newbob", :password => "newpassword", :password_confirmation => "newpassword", :email => "newbob@mcbob.com" )
    assert_not_nil u.salt
    assert_not_nil u.password
    assert_not_nil u.hashed_password
    assert u.save 
    assert_equal u, User.authenticate(u.login, u.password)

  end

  def test_send_new_password
    #check user authenticates
    assert_equal  @bob, User.authenticate("bob", "test")    
    #send new password
    sent = @bob.send_new_password
    assert_not_nil sent
    #old password no longer workd
    assert_nil User.authenticate("bob", "test")
    #email sent...
    assert_equal "Your password is ...", sent.subject
    #... to bob
    assert_equal @bob.email, sent.to[0]
    assert_match Regexp.new("Your username is bob."), sent.body
    #can authenticate with the new password
    new_pass = $1 if Regexp.new("Your new password is (\\w+).") =~ sent.body 
    assert_not_nil new_pass
    assert_equal  @bob, User.authenticate("bob", new_pass)    
  end

  def test_rand_str
    new_pass = User.random_string(10)
    assert_not_nil new_pass
    assert_equal 10, new_pass.length
  end

  def test_sha1
    u=User.new
    u.login      = "nonexistingbob"
    u.email="nonexistingbob@mcbob.com"  
    u.salt="1000"
    u.password = u.password_confirmation = "bobs_secure_password"
    assert u.save   
    assert_equal 'b1d27036d59f9499d403f90e0bcf43281adaa844', u.hashed_password
    assert_equal 'b1d27036d59f9499d403f90e0bcf43281adaa844', User.encrypt("bobs_secure_password", "1000")
  end

  def test_protected_attributes
    #check attributes are protected
    u = User.new(:id=>999999, :salt=>"I-want-to-set-my-salt", :login => "badbob", :password => "newpassword", :password_confirmation => "newpassword", :email => "badbob@mcbob.com" )
    assert u.save
    assert_not_equal 999999, u.id
    assert_not_equal "I-want-to-set-my-salt", u.salt

    u.update_attributes(:id=>999999, :salt=>"I-want-to-set-my-salt", :login => "verybadbob")
    assert u.save
    assert_not_equal 999999, u.id
    assert_not_equal "I-want-to-set-my-salt", u.salt
    assert_equal "verybadbob", u.login
  end
end

Running these tests gives

> ruby test/unit/user_test.rb 
Loaded suite test/unit/user_test
Started
..........
Finished in 4.767613 seconds.

10 tests, 63 assertions, 0 failures, 0 errors

The controller


> ruby script/generate controller User signup login logout delete edit forgot_password
exists  app/controllers/
exists  app/helpers/
create  app/views/user
exists  test/functional/
create  app/controllers/user_controller.rb
create  test/functional/user_controller_test.rb
create  app/helpers/user_helper.rb
create  app/views/user/signup.rhtml
create  app/views/user/login.rhtml
create  app/views/user/logout.rhtml
create  app/views/user/delete.rhtml
create  app/views/user/edit.rhtml
create  app/views/user/forgot_password.rhtml

Since the model contained the business logic, the controller methods are a bit easier. Here is the code for the controller.

class UserController < ApplicationController

  before_filter :login_required, :only=>['welcome', 'change_password', 'hidden']

  def signup
    @user = User.new(@params[:user])
    if request.post?  
      if @user.save
        session[:user] = User.authenticate(@user.login, @user.password)
        flash[:message] = "Signup successful"
        redirect_to :action => "welcome"          
      else
        flash[:warning] = "Signup unsuccessful"
      end
    end
  end

  def login
    if request.post?
      if session[:user] = User.authenticate(params[:user][:login], params[:user][:password])
        flash[:message]  = "Login successful"
        redirect_to_stored
      else
        flash[:warning] = "Login unsuccessful"
      end
    end
  end

  def logout
    session[:user] = nil
    flash[:message] = 'Logged out'
    redirect_to :action => 'login'
  end

  def forgot_password
    if request.post?
      u= User.find_by_email(params[:user][:email])
      if u and u.send_new_password
        flash[:message]  = "A new password has been sent by email."
        redirect_to :action=>'login'
      else
        flash[:warning]  = "Couldn't send password"
      end
    end
  end

  def change_password
    @user=session[:user]
    if request.post?
      @user.update_attributes(:password=>params[:user][:password], :password_confirmation => params[:user][:password_confirmation])
      if @user.save
        flash[:message]="Password Changed"
      end
    end
  end

  def welcome
  end
  def hidden
  end
end

The code for the controller is pretty straight-forward as all the tricky business logic is in the model. In each controller method, it is just a matter of deciding which action to direct to next depending on what input it gets.

For example the signup action creates a new user using the parameters it receives. It it is a post request (the form was submitted) it tries to save the new user. If the save operation was successful the user is authenticated and redirected to the welcome screen. If we fail to save the user (e.g. if validation fails) we add a warning to the flash and the page renders again.

The login action attempts to authenticate the user using the given parameters. If successful it redirects them to a page stored in the session or a default.

The forgot password action finds a user using the email address provided as a parameter and the tries to send them a new password. If successful it redirect them to the login action.

There are also some methods that we would like to be available to all controllers in the application. These are stored in app/controllers/application.rb. These are

class ApplicationController < ActionController::Base

  def login_required
    if session[:user]
      return true
    end
    flash[:warning]='Please login to continue'
    session[:return_to]=request.request_uri
    redirect_to :controller => "user", :action => "login"
    return false 
  end

  def current_user
    session[:user]
  end

  def redirect_to_stored
    if return_to = session[:return_to]
      session[:return_to]=nil
      redirect_to_url(return_to)
    else
      redirect_to :controller=>'user', :action=>'welcome'
    end
  end
end

login_required is a filter that allows us to control access to actions. In the user controller we have three actions, welcome, hidden and forgot_password that can only be accessed by logged in users. The line

before_filter :login_required, :only=>['welcome', 'change_password', 'hidden']

ensures that the login_required method is run before the hidden and welcome actions. Processing of these actions only continues if this filter returns true. The login_required method returns true if session[:user] is set i.e. if the user is logged in. Otherwise it stores the page to return to in the session and redirects to the login page. The redirect_to_stored method is used to redirect to a page stored in the session (It redirects to the url stored in the variable session[:return_to]).

current_user is a convenience method for accessing the currently-logged-in user. Using this to get the user in our application instead of accessing session[:user] directly will allow us to change this in the future. For example instead of storing the entire user object in the session we might change our implementation to store only the users id and retrieve the user object from the database with a before_filter (see the extensions at the end for more on this).

Testing the controller

Here are the controller tests. These specify how each controller method should behave.

require File.dirname(__FILE__) + '/../test_helper'
require 'user_controller'

# Re-raise errors caught by the controller.
class UserController; def rescue_action(e) raise e end; end

class UserControllerTest < Test::Unit::TestCase

  self.use_instantiated_fixtures  = true

  fixtures :users

  def setup
    @controller = UserController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
    @request.host = "localhost"
  end



  def test_auth_bob
    #check we can login
    post :login, :user=> { :login => "bob", :password => "test" }
    assert_session_has :user
    assert_equal @bob, session[:user]
    assert_response :redirect
    assert_redirected_to :action=>'welcome'
  end

  def test_signup
    #check we can signup and then login
    post :signup, :user => { :login => "newbob", :password => "newpassword", :password_confirmation => "newpassword", :email => "newbob@mcbob.com" }
    assert_response :redirect
    assert_not_nil session[:user]
    assert_session_has :user
    assert_redirected_to :action=>'welcome'
  end

  def test_bad_signup
    #check we can't signup without all required fields
    post :signup, :user => { :login => "newbob", :password => "newpassword", :password_confirmation => "wrong" , :email => "newbob@mcbob.com"}
    assert_response :success
    assert_invalid_column_on_record "user", "password"
    assert_template "user/signup"
    assert_nil session[:user]

    post :signup, :user => { :login => "yo", :password => "newpassword", :password_confirmation => "newpassword" , :email => "newbob@mcbob.com"}
    assert_response :success
    assert_invalid_column_on_record "user", "login"
    assert_template "user/signup"
    assert_nil session[:user]

    post :signup, :user => { :login => "yo", :password => "newpassword", :password_confirmation => "wrong" , :email => "newbob@mcbob.com"}
    assert_response :success
    assert_invalid_column_on_record "user", ["login", "password"]
    assert_template "user/signup"
    assert_nil session[:user]
  end

  def test_invalid_login
    #can't login with incorrect password
    post :login, :user=> { :login => "bob", :password => "not_correct" }
    assert_response :success
    assert_session_has_no :user
    assert flash[:warning]
    assert_template "user/login"
  end

  def test_login_logoff
    #login
    post :login, :user=>{ :login => "bob", :password => "test"}
    assert_response :redirect
    assert_session_has :user
    #then logoff
    get :logout
    assert_response :redirect
    assert_session_has_no :user
    assert_redirected_to :action=>'login'
  end

  def test_forgot_password
    #we can login
    post :login, :user=>{ :login => "bob", :password => "test"}
    assert_response :redirect
    assert_session_has :user
    #logout
    get :logout
    assert_response :redirect
    assert_session_has_no :user
    #enter an email that doesn't exist
    post :forgot_password, :user => {:email=>"notauser@doesntexist.com"}
    assert_response :success
    assert_session_has_no :user
    assert_template "user/forgot_password"
    assert flash[:warning]
    #enter bobs email
    post :forgot_password, :user => {:email=>"exbob@mcbob.com"}   
    assert_response :redirect
    assert flash[:message]
    assert_redirected_to :action=>'login'
  end

  def test_login_required
    #can't access welcome if not logged in
    get :welcome
    assert flash[:warning]
    assert_response :redirect
    assert_redirected_to :action=>'login'
    #login
    post :login, :user=>{ :login => "bob", :password => "test"}
    assert_response :redirect
    assert_session_has :user
    #can access it now
    get :welcome
    assert_response :success
    assert flash.empty?
    assert_template "user/welcome"
  end

  def test_change_password
    #can login
    post :login, :user=>{ :login => "bob", :password => "test"}
    assert_response :redirect
    assert_session_has :user
    #try to change password
    #passwords dont match
    post :change_password, :user=>{ :password => "newpass", :password_confirmation => "newpassdoesntmatch"}
    assert_response :success
    assert_invalid_column_on_record "user", "password"
    #empty password
    post :change_password, :user=>{ :password => "", :password_confirmation => ""}
    assert_response :success
    assert_invalid_column_on_record "user", "password"
    #success - password changed
    post :change_password, :user=>{ :password => "newpass", :password_confirmation => "newpass"}
    assert_response :success
    assert flash[:message]
    assert_template "user/change_password"
    #logout
    get :logout
    assert_response :redirect
    assert_session_has_no :user
    #old password no longer works
    post :login, :user=> { :login => "bob", :password => "test" }
    assert_response :success
    assert_session_has_no :user
    assert flash[:warning]
    assert_template "user/login"
    #new password works
    post :login, :user=>{ :login => "bob", :password => "newpass"}
    assert_response :redirect
    assert_session_has :user
  end

  def test_return_to
    #cant access hidden without being logged in
    get :hidden
    assert flash[:warning]
    assert_response :redirect
    assert_redirected_to :action=>'login'
    assert_session_has :return_to
    #login
    post :login, :user=>{ :login => "bob", :password => "test"}
    assert_response :redirect
    #redirected to hidden instead of default welcome
    assert_redirected_to 'user/hidden'
    assert_session_has_no :return_to
    assert_session_has :user
    assert flash[:message]
    #logout and login again
    get :logout
    assert_session_has_no :user
    post :login, :user=>{ :login => "bob", :password => "test"}
    assert_response :redirect
    #this time we were redirected to welcome
    assert_redirected_to :action=>'welcome'
  end
end

Running these tests gives:

> ruby test/functional/user_controller_test.rb 
Loaded suite test/functional/user_controller_test
Started
.........
Finished in 3.526899 seconds.

9 tests, 97 assertions, 0 failures, 0 errors

The Mailer

We need a mailer to send the forgotten password emails.

> ruby script/generate mailer Notifications forgot_password
      exists  app/models/
      create  app/views/notifications
      exists  test/unit/
      create  test/fixtures/notifications
      create  app/models/notifications.rb
      create  test/unit/notifications_test.rb
      create  app/views/notifications/forgot_password.rhtml
      create  test/fixtures/notifications/forgot_password

We configure the mailer in the environment configuration file. In this case we configure it to use an smtp server.

ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.server_settings = {
  :address => "mail.mydomain.com",
  :port => 25,
  :domain => "mydomain.com",
  :user_name => "MyUsername",
  :password => "MyPassword",
  :authentication => :login
}

We referred to the Mailer function earlier. We saw that our model called a method Notifications.deliver_forgot_password(self.email, self.login, new_pass) to send an email with a new password to a user. When rails sees deliver_forgot_password it will look for a method called forgot_password in the mailer to setup the email. This is in the file app/models/notifications.rb. We set the subject and the recipient of the email. We also pass it the users username and password by setting values in the @body variable. These values are then available as local variables in the view for this email.

class Notifications < ActionMailer::Base
  def forgot_password(to, login, pass, sent_at = Time.now)
    @subject    = "Your password is ..."
    @body['login']=login
    @body['pass']=pass
    @recipients = to
    @from       = 'support@yourdomain.com'
    @sent_on    = sent_at
    @headers    = {}
  end
end

The view used to create the email is stored in app/views/notifications/forgot_password.rhtml

_____________
Your username is <%= @login %>. Your new password is <%= @pass %>. Please login and change it to something more memorable.

-------------

This uses the variables from the body hash to construct the email.

The views

All thats left is to have a look at our views. The views just use the rails form helpers to construct the forms we need to send the appropriate parameters to each controller method and display messaged and results. These views just use the text_field and password_field methods. For these, the first argument is the model name and the second argument is the field name followed by optional arguments such as :size or :value. So <%= text_field "user", "login", :size => 20 %> indicates that the field is for the login field of the user model.

Here are the views.

app/views/user/signup.rhtml

<%= start_form_tag :action=> "signup" %>

<%= error_messages_for 'user' %><br/>

<label for="user_login">Username</label><br/>
<%= text_field "user", "login", :size => 20 %><br/>

<label for="user_password">Password</label><br/>
<%= password_field "user", "password", :size => 20 %><br/>

<label for="user_password_confirmation">Password Confirmation</label><br/>
<%= password_field "user", "password_confirmation", :size => 20 %><br/>

<label for="user_email">Email</label><br/>
<%= text_field "user", "email",  :size => 20 %><br/>

<%= submit_tag "Signup" %>

<%= end_form_tag %>

app/views/user/login.rhtml

<%= start_form_tag :action=> "login" %>
    <h3>Login</h3> 

  <label for="user_login">Login:</label><br/>
  <%= text_field "user", "login", :size => 20 %><br/>

  <label for="user_password">Password:</label><br/>
  <%= password_field "user", "password", :size => 20 %><br/>

<%= submit_tag "Submit" %>

    <%= link_to 'Register', :action => 'signup' %> |
    <%= link_to 'Forgot my password', :action => 'forgot_password' %>     

<%= end_form_tag %>

app/views/user/forgot_password.rhtml

<%= start_form_tag :action=>'forgot_password'%>

<h3>Forgotten password</h3>

Email: <%= text_field "user","email" %><br/>

<%= submit_tag 'Email password' %>

<%= end_form_tag %>

app/views/user/change_password.rhtml

<%= error_messages_for 'user' %>

<h3>Change password</h3>

<%= start_form_tag :action => 'change_password' %>

<label for="user_password">Choose password:</label><br/>
<%= password_field "user", "password", :size => 20, :value=>"" %><br/>

<label for="user_password_confirmation">Confirm password:</label><br/>
<%= password_field "user", "password_confirmation", :size => 20, :value=>"" %><br/>

<%= submit_tag "Submit" %>

<%= end_form_tag %>

Security

Since this is an authentication system we should pay attention to security. Here are some things to pay attention to:

Protect attributes. Protect any attributes that you don’t want to be updated by the user.

Guard against sql injection. Sql injection is where a malicious user passes sql as a parameter to your application in an attempt to get the sql to run on your database. Rails model methods such as create and update_attributes guard against this. When you passing parameters to your database always contruct your queries like this:

find(:all, :conditions=>["created_on=? and user_id=?",date, uid])

This will take care of quoting uid and date. Never use the ruby string substitution #{date} to construct queries.

Make sure you check ids against users. If you are controlling access to items based on the user, make sure all your find methods include the user_id in the query. E.g. if you have a multi-user blog with an edit method takes the id of the post to edit as a parameter. Dont just use the id to find the post - use the user_id in the query too:

def edit
  @item=Blog.find(:first, :conditions=>["user_id=? and id=?", current_user.id, params[:id]])
  ...
end

See here for more about securing rails application.

Enhancements

This is a pretty basic authentication system but it could be useful for a lot of simple web apps. But your web app probably needs something slightly different. Some ways you might extend it are:

  • Store user_id instead of user. We stored the entire user object in the session. If your user has many more attributes than this model or has lots of child objects you could end up with a lot of stuff in the session. In this case you could change it to store the users id in the session and use a before_filter to send a query to the database to get the user.

  • Captcha and email validation. This doesn’t use any validation of users identity. You might want to make it a bit harder for spammers to automate account creation. You could add a captcha to the registration page. Check out rubyforge for some captcha libraries. Alternatively you could require users to validate using email. This would mean adding a field to the database with a validation key. Put a random unique hash in this field when the user is created. Don’t allow a user to login unless this field is null. Add a controller method and view to validate users using this hash key and email them a link to this with their own key as a parameter when they signup.

  • Roles / admin user. This approach only controls access based on whether a user is logged in or not. You might need more fine grained access control. You could add a role field to the user table that describes the users role. Then add a before filter the the application controller for each role. E.g. admin_required only returns true if the user is an administrator.

  • Creating a generator. If you find yourself using the same code over and over in different applications you could write a generator to automatically generate your basic authentication and start customizing it from there. Update: see my follow tutorial on creating a generator.

Update - Getting the code

The code from this article is now available as a generator.

Update - deprecation warnings with Rails 1.2

The code from this tutorial will generate some deprecation warnings when run with Rails 1.2. The updated version of the generator has some minor changes to the code to avoid getting deprecation warnings.

Update - Filtered Parameter logging

As of rails 1.1.6 you can filter form data that you don’t want saved in the log, such as passwords or credit card numbers. Adding filter_parameter_logging "password" to ApplicationController will prevent any field that matches /password/ from being logged.

Good and informative. Do appreciate the effort

Your CSS needs some work. I can barely see this page on a 1400x1050 monitor!

If the font's too small, just use Ctrl + '+' to increase it, assuming you're using Firefox. What's so hard about that?

This is a very well done tuturial. I like how you've included the functional test as well, and I think you're spot on with the idea of rolling your own.

Great job!

nice tute!

I have a system very similar to what you've described. My problem is - how do you update the property of a user without updating the password? Let's say they just want to change their last name - if we don't pass a password, it'll throw an error because of the validates_confirmation_of constraint on the password. If you re-pass the same password, it will re-hash it, changing the password. Is there a solution to that?

Thanks!

You could try something like this (example from the mephisto source):

In the user model:


validates_presence_of     :password,                   :if => :password_required?
validates_presence_of     :password_confirmation,      :if => :password_required?
validates_length_of       :password, :within => 5..40, :if => :password_required?
validates_confirmation_of :password,                   :if => :password_required?

protected
def password_required?
  crypted_password.nil? || !password.blank?
end

I had Problems @ crypted_password.nil? saying that to be unknown method .... any clues ?

crypted_password should be hashed_password.
other than this edit - this implementation works perfectly.

Maybe this is a newbie question but should this be self.hashed_password instead of just hashed_password which should be treated as a local variable?

You can add a method like this:


def skip_password_validation
@password = '-' * 20
@password_confirmation = '-' * 20
end

and in your controller, something like:


if params[:system_user][:password].blank?
@newuser.skip_password_validation
else
@newuser.password = params[:system_user][:password]
@newuser.password_confirmation = params[:system_user][:password_confirmation]
end

It basically just fills the password/password_confirmation instance variable with something that passes the validation without updating the fields salt and hashed_password.

use .update_attribute method instead of .save

what i used for Conditional validation is the :on option, that is added to the above code for the user model of:

protected
def password_required?
!password.blank?
end

changed with the :on (e.g. update user details)

validates_presence_of :password,:password_confirmation, :on => :update, :if => :password_required?
validates_length_of :password, :within => 5..20, :on => :update, :if => :password_required?
validates_confirmation_of :password, :on => :update, :if => :password_required?

and then changed the original validates (e.g. registration) to:

validates_length_of :password, :on => :create, :within => 5..20
validates_confirmation_of :password, :on => :create

Thanks for writing this up.

Um... Suppose that the model knew of the crypted_password and the salt but not of the password and the confirmation_password. Suppose we left the password and confirmation password to the controller. This seems to handle the problem raised by Tom, although this approach states that how one confirms a password is a user interface issue, not a data model issue.

Great authentication tutorial, could you please post how to include roles and email validation.

Also... Transaction boundaries. You haven't specified transaction boundaries. This suggests that in the "forgot password" routine, the user record could be loaded into memory from the database. Some piece of code somewhere else in the system could then modify the user record in the database. When the forgot password routine is executed, it will overwrite the user record with stale data from the session.

Also... In the signup routine, you wrote "@params[:user]" and in the login routine its more like "params[:user][:password]". Why does the at-sign appear in the signup routine?

Also... Error handling. This code doesn't look like it does a real precise job of handling errors. The final generation of the error string should be done in the view -- the error may need to be translated into other languages and the Model probably doesn't want to worry about that. Additionally, a variety of different kinds of underlying errors look like they could all collapse into a single error.

I mention this stuff because when I write this kind of thing in C, I am very careful with transaction boundaries, precise error messages, and logging of operations to monitor problems that the web pages may be having. I'm trying to figure out how one does these sorts of things with Ruby and Rails.

I wrote @params out of habit. @params is deprecated but still works. params gives the same result but is the new recommended way of accessing parameters.

If you add an integer column lock_version to your table Activerecord will throw an exception if you try to save an object to the database that has been modified by another part of the system.

Found the solution in test_return_to
replace
assert_redirected_to :action=>'hidden'
with
assert_redirected_to @controller.url_for( :action => 'hidden' )

it's magic ! it works !

yves

Did you mean to replace

assert_redirected_to 'user/hidden'

...rather than...

assert_redirected_to :action=>'hidden'

(The article may have changed)

In either case, thanks for the tip.

re: aidan's reply to Tom's question about updating things other than the password:

I like the :if qualifications on updating the password, but they miss an important point. What about the case when a user is trying to change their password, but, say, hits the submit button before entering anything. This would update the password to "" since the validation hooks would not be fired.

My question is this:
Should there be a check for an empty password in the change_password method of UserController? Coming from a Java background, this seems logical, but it has two drawbacks:
1. Validation is now in two places
2. ActionControllers don't have a validate method, so there would have to be some sort of call like
{
@user.errors.add("password", "Please enter your new password")
@user.validate
}

but that seems icky, since the ActionController already calls validate in a different place.

The "better" option would be to validate that something was entered within/before the password=(pass) method of User, but I can't seem to figure out how to add validation that's conditional on what method is called in the ActiveRecord (model).

Thoughts?

In addition to adding Aidan's mods from the Mephisto code (comment Submitted by aidan on Thu, 2006-06-15 10:30) and changing crypted_password to hashed_password, I added a check into the change_password method of the user_controller to look to see if the password field was blank, if so then add an error and redirect back to the change_password action


def change_password
    @user = session[:user]
    
    if request.post?

      # throws an error if the password is blank
      # other errors are caught by existing validation
      if params[:user][:password].blank?
        @user.errors.add 'password', 'field is empty'
        redirect_to :controller => 'user', :action => 'change_password'
      else 
        @user.update_attributes(:password => params[:user][:password], :password_confirmation=> params[:user][:password_confirmation])
        if @user.save
          flash[:message] = "Password changed"
          redirect_to :controller => 'user', :action => 'edit'
        else
          flash[:warning] = "Password not changed"
          redirect_to :controller => 'user', :action => 'change_password'
        end
      end
    end
  end

It might not be the cleanest solution but at least now I can update other user attributes without the @user.save failing because it doesnt have password and password_confirmation attributes.

I think this is a poor design, you don't want to be carrying around the user's password in their session. Opens your application up to too many security risks.

I believe I have come up with a much elegant solution to this problem. I have simply added an "if not blank" to the method that creates the hash. (this is in addition to the :if => :password_required? method suggested above).


def password=(pwd)
    @password = pwd
    if !@password.blank?
        create_new_salt
        self.hashed_password = User.encrypted_password(self.password, self.salt)
    end
end

I get errors when I run your tests that I'm not sure how to handle. Maybe I'm missing some depedencies?


>ruby test/unit/user_test.rb
Loaded suite test/unit/user_test
Started
........E.
Finished in 0.453 seconds.

1) Error:
test_send_new_password(UserTest):
NameError: uninitialized constant Notifications

1) Error:
test_change_password(UserControllerTest):

2) Error:
test_forgot_password(UserControllerTest):
NameError: uninitialized constant Notifications

Looks like you're missing a couple of files. You have to create the mailer (Notifications) as described above before the users tests will pass.

Hey, great walkthrough. but one question. you mention a wrapper function
def current_user
session[:user]
end

however, how can i assign to current_user to maintain the extendability? i have tried current_user=(value) methods, and it appears that the a simple var current_user var is being created and assigned to as well... any insights for the lowly ruby newb?

You need to use "self.current_user= value" to force the method to be called rather than using a local variable.

The functions in the application.rb file are very useful, but how does one use them in the views?

For example, if I want to show a link to a user only if logged in, this doesn't work:

<% if current_user -%>
<a href="/admin">Admin Functions</a>
<% end -%>

because current_user is not available to the view.

Just add:

helper_method :current_user after the ApplicationController#current_user definition:

def current_user
session[:user]
end
helper_method :current_user

I did this to create a logged_in? method (as in acts_as_authenticated) that is available to the views and the controllers:

def logged_in?
return session[:user] ? true : false
end
helper_method :logged_in?

Great tutorial, but one question:

If the a user fires a request with POST variables (which requires login), the redirect_to_stored method don't seem to restore the POST form after the user had logged in. Any workarounds?

Two things:

- How about cookie'ing the user so that if the session expires they still get logged in? In a secure way, not just cookie'ing their user id. Perhaps cookie'ing a salted hash of their login, then doing lookups that way if it exists.

- Salt seems pointless. IIRC, the salt is supposed to be stored separately from the passwords (e.g. not even in the same database, but perhaps in a file or somewhere). And if a cracker gets access to the database they'll almost certainly be able to access whatever salt value is used.

The salt is supposed to be public.

Its purpose is to prevent dictionary attacks. It makes the storage requirements for a lookup-table prohibitive, and, being unique, also prevents attacking the entire user base in parallel.

Hmm.. "Public" wasn't quite the right word.

Anyways, my point is that the salt is there to protect the users password, not your site. If someone can read the salts your server is already compromised..

I've used the authentication tutorial from Chad Fowler's Rails Recipes book before, but this was perfect!

I do have further questions regarding adding an admin, or possibly other types of users. If you're familiar with Fowler's book, can you describe how to tie into his authorization functions, or provide more of an example of how to complete the example you give above. Simply calling the before_filter :admin_required doesn't work--what does the definition of admin_required look like?

thanks Jimmy for pointing Chad's Book
i was looking for Roles and how to set them up

thanks aidan for this informative tutorial

At which point does is confirmation password compared the the regular password field? I couldn't see this anywhere.

Thanks for the article.

In the user model:

validates_confirmation_of :password

Have you ever used ActiveRBAC ? if do what do you think of it ?

About the unit_test part?
Where can I change the default database setting?
I tried to add
adapter: sqlserver
database: xxxx
username: sa
password: xxxx
host: .\SQLEXPRESS
mode: DBI:ADO
provider: SQLNCLI

in the users_test.yml.
But it seems not working.
Anyone using SQL server other with this
application?

Hi all !
Nice tutorial !
Just one thing ...
It would be a bad idea to store the entire user in the session. Just the id is enough ...

So, I think login should be :

def login
    if request.post?
       user = User.authenticate(params[:user][:login], params[:user])
      if user
        session[:user] = user.id
        flash[:message]  = "Login successful"
        redirect_to_stored
      else
        flash[:warning] = "Login unsuccessful"
      end
    end
  end


That way you only store the user id in the session, and if you want more info about this is user later one then you simply find it using session[:user].id

Hope that helps ...

If you decide to store the user_id in the session instead of the user, you could modify the current_user method retrieve the user based on the id stored in the session. Something like this (I haven't tested this):

def current_user
  @current_user ||= ((session[:user] && User.find_by_id(session[:user])) || nil)
end

This is the first tutorial on authentication that was straight forward and actually worked for me the first time...thanks!

I'm hoping someone can help me capture the user name in the user model? Actually, my end goal is to have my other model called link.rb which runs my MySQL query, be able to access the user name so I can add it to the condition of the DB query.

Here is what I have which only works if I hardcode user_name with an actual user's name in the DB:

class Link < ActiveRecord::Base
def self.return_links
@data = find(:all, :conditions
=> "username = 'user_name'")
end

I tried puts'ing the current username out in user.rb with this statement but it keeps coming up blank

puts "#{session[:user].login}" and every variation there of with no luck.

Any help here would be a Christmas gift :-)
Thanks,
John

Everything is working Great! Thanks for this tutorial.

My question is: How do I get the current user variable into my other model called link.rb? I need it in link.rb so I can search the DB by whomever is logged in.
Here is my code:

class Link < ActiveRecord::Base
def self.return_links
@data = find(:all, :conditions => "username = #{CURRENT USER NAME HERE}")
end
end

Thanks!

Thanks for the write-up, this was very helpful.
One comment: In the "change_password" method you are calling both @user.update_attributes and then @user.save, where in fact the first call already does the update for you. Looking at the log you can see that its actually updating twice in the DB. The code should be changed to


def change_password
    @user=session[:user]
    if request.post?
      if @user.update_attributes(:password=>params[:user][:password], :password_confirmation => params[:user][:password_confirmation])
        flash[:message]="Password Changed"
      end
    end
  end

in test_collision, you need to set the email of the user, otherwise it will fail to save because the email is nil instead of because of the uniqueness.


  def test_collision
    #check can't create new user with existing username
    u = User.new
    u.login = "existingbob"
    u.password = u.password_confirmation = "bobs_secure_password"
    u.email = "bob@mcbob.org"
    assert !u.save
  end

Aidan,

Thanks for the article - you thought about approaching O'Reilly and writing for them online?

Paul Browne - Technology in Plain English

I'm getting an error when I try to use the Forgot Password thing. (ArgumentError (wrong number of arguments (3 for 1))

It shows up in the controller here:

u = User.find_by_email([params[:user][:email]])
if u and u.send_new_password

Specifically with the .send_new_password, from the model:

Notifications.deliver_forgot_password(self.email, self.login, new_pass).

Is there a problem somewhere between the model's three arguments (self.email, self.login, new_pass) and the controller? Did anyone else have this problem?

Yeah, Dave, I did have this problem. Change the line in notification_test.rb to:


assert_equal @expected.encoded, Notifications.create_forgot_password('to', 'login', 'pass', @expected.date).encoded

and all should be well!

Hi Aidan,

Thanks for doing this -- it's great! For those of us in the 1.2 world, I've put together a tar file holding the code (in a Rails project with nothing else) with all the Deprecation warnings banished. It's here if anyone wants it (just add an 'http://'):

www.chrissterritt.com/AidanLoginRails2.tar

Thanks for that Chris - what changes did you have to make for rails 1.2 compatibility?

Hi Aidan,

The main things were deprecation warnings for some of the assert variants. Tracking down what to do instead was challenging sometimes :-).

Cheers!

I’ve updated the generator for this so that the code it generates doesn’t give deprecation warnings.

Thanks for the great tutorial. I am havng a bit of trouble getting the mailer to work. Has anyone else gotten this error?


2) Error:
test_forgot_password(UserControllerTest):
Errno::EBADF: Bad file descriptor - connect(2)

This prevents e-mails from being sent, thereby locking me out of my account as I have no access to the new password. Any clues?

When I logged out. This message "Logged out" is not displaying as it is redirecting to login page. How can i see that ?

Just change your layout template to display flash[:message] and flash[:warning]

E.g.

<% for name in [:warning, :message] %>
  <% if flash[name] %>
    <%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
  <% end %>
<% end %>

minor typo inverses the meaning of your sentence:

"This was a bit of an epiphany for me as before I started using rails and for a while after I started to use it I rarely used unit tests. Not they’re an integral part of my coding process."

I think you meant "Now they're an integral part..."

You might like to replace the "random_string" method with:


def self.random_string(len)
open("/dev/random").read(len)
end

Hi :-)

Thanks for this - just used it. Worked perfect!

Hi, would someone be able to quantify the whole storing-user-data-in-the-session thing. I am confused about where to draw the line between user id storage and whole user object storage.

Am I right in thinking the session data resides on the users web browser?

If for example, you wanted (in addition to the above fields) to have just a Name and Admin status available to the app is that enough to warrant the replacement of session[:user] with session[:userid] and do a database query every time you wanted to information about or credentials of a user?

What is the downside of storing *large* amounts of information in the session object and what constitutes *large* today?

If session data is stored in the browser rather than on the server, should the number of users accessing the application effect the decision about how much data to store in the session before relieving it with database queries?

Thanks!

Nick