Chapter 14. Login and Authentication

 

“Thanks goodness, there’s only about a billion of these because DHH doesn’t think auth/auth belongs in the core.”

 
 --Comment at http://del.icio.us/revgeorge/authentication

I bet every web app you’ve ever worked on has needed some form of user security, and some people assume it makes sense to include some sort of standard authentication functionality in a “kitchen-sink” framework such as Rails.

However, it turns out that user security is one of those areas of application design that usually involves a bit more business logic than anyone realizes upfront.

David has clearly stated his opinions on the matter, to help us understand why Rails does not include any sort of standard authentication mechanism:

Context beats consistency. Reuse only works well when the particular instances are so similar that you’re willing to trade the small differences for the increased productivity. That’s often the case for infrastructure, such as Rails, but rarely the case for business logic, such as authentication and modules and components in general.

For better or worse, we need to either write our own authentication code or look outside of Rails core for a suitable solution. It’s not too difficult to write your own authentication code, to the extent that it isn’t really that difficult to write anything in Rails. But why reinvent the wheel? That’s not the Rails way!

As alluded to in the chapter quote, we have many different options out there to choose from. It seems that since authentication is one of the first features you add to a new application, it is also one of the first projects undertaken by many an aspiring plugin writer.

A multitude of options can be a good thing, but I contend that in this particular case it is not. A Google search for “rails authentication” turns up over 5 million results! Looking through the first page of results alone I can count at least ten different approaches to tackling this problem—and what the heck is a salted password generator?

No need to worry or be confused. It turns out that Rails pros agree that the two best authentication plugins are written by Rails core team member Rick Olson, a.k.a. techno weenie[1]. In this chapter, we take an in-depth look at both of them.

Acts as Authenticated

Acts as Authenticated is described by Rick as “a simple authentication generator plugin.” It allows you to easily add form-based authentication to your application. It also provides a standard API for accessing information such as whether a user is logged in or authorized as an admin, as well as accessing the User object itself.

Installation and Setup

Install acts_as_authenticated as a plugin by invoking script/plugin install acts_as_authenticated. First you generate skeleton code for authentication using a Rails generator included in the plugin, and then you customize the basic implementation so that it behaves according to the needs of your particular application.

The code generator is invoked using script/generate and takes a couple of parameters: one for model and one for controller name. We’ll invoke the generator, specifying user and account as our desired names for our authentication model and controller.

$ script/generate authenticated user account
      exists  app/models/
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/account
      exists  test/functional/
      exists  test/unit/
      create  app/models/user.rb
      create  app/controllers/account_controller.rb
      create  lib/authenticated_system.rb
      create  lib/authenticated_test_helper.rb
      create  test/functional/account_controller_test.rb
      create  app/helpers/account_helper.rb
      create  test/unit/user_test.rb
      create  test/fixtures/users.yml
      create  app/views/account/index.rhtml
      create  app/views/account/login.rhtml
      create  app/views/account/signup.rhtml
      create  db/migrate
      create  db/migrate/001_create_users.rb

Somewhat similar to the way scaffolding code is generated, we see that a number of files for a model and controller, migration, and associated tests are created. I’ll walk you through use of the plugin and point out what we can learn from it with regard to our own application code.

The User Model

First let’s take a look at the migration that was automatically created by the generator: db/migrate/001_create_users.rb.

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table "users", :force => true do |t|
      t.column :login,                     :string
      t.column :email,                     :string
      t.column :crypted_password,          :string, :limit => 40
      t.column :salt,                      :string, :limit => 40
      t.column :created_at,                :datetime
      t.column :updated_at,                :datetime
      t.column :remember_token,            :string
      t.column :remember_token_expires_at, :datetime
    end
  end

  def self.down
    drop_table "users"
  end
end

The standard columns should look pretty much like ones you’d associate with a User model. We’ll cover the meaning of crypted_password and salt a little later on in the chapter. If you wanted additional columns, such as first and last names, just add them to this migration.

Now we’ll open app/models/user.rb and see what our shiny new User model looks like in Listing 14.1.

Example 14.1. The User Model Generated by Acts As Authenticated

require 'digest/sha1'

class User < ActiveRecord::Base
  # Virtual attribute for the unencrypted password
  attr_accessor :password

  validates_presence_of     :login, :email
  validates_presence_of     :password,
                            :if => :password_required?
  validates_presence_of     :password_confirmation,
                            :if => :password_required?
  validates_length_of       :password, :within => 4..40,
                            :if => :password_required?
  validates_confirmation_of :password,
                            :if => :password_required?
  validates_length_of       :login,    :within => 3..40
  validates_length_of       :email,    :within => 3..100
  validates_uniqueness_of   :login, :email, :case_sensitive => false

  before_save :encrypt_password

  # Authenticates a user by their login name and unencrypted password,
  # returning the user or nil.
  def self.authenticate(login, password)
    u = find_by_login(login) # need to get the salt
    u && u.authenticated?(password) ? u : nil
  end

  # Encrypts some data with the salt.
  def self.encrypt(password, salt)
    Digest::SHA1.hexdigest("—#{salt}--#{password}--")
  end

  # Encrypts the password with the user salt
  def encrypt(password)
    self.class.encrypt(password, salt)
  end

  def authenticated?(password)
    crypted_password == encrypt(password)
  end

  def remember_token?
    remember_token_expires_at &&
   (Time.now.utc < remember_token_expires_at)
  end

  # These create and unset the fields required
  # for remembering users between browser closes
  def remember_me
    self.remember_token_expires_at =
      2.weeks.from_now.utc
    self.remember_token =
      encrypt("#{email}--#{remember_token_expires_at}")
    save(false)
  end

  def forget_me
    self.remember_token_expires_at = nil
    self.remember_token            = nil
    save(false)
  end

  protected
    def encrypt_password
      return if password.blank?
      self.salt =
        Digest::SHA1.hexdigest("--#{Time.now}--#{login}--") if
new_record?
      self.crypted_password = encrypt(password)
    end

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

Whoa! That’s certainly a lot more code than we’re used to seeing in a Rails-generated class. Let’s analyze the User model to see what we can learn.

Non-Database Attributes

Sometimes it makes sense to add non-database attributes to your ActiveRecord models. They’re added using attr_* macros, which you should know about from practically every Ruby language primer in existence.

At the very top of User, notice that an attribute has been specified for :password. Does that seem a little weird? Doesn’t the user’s password need to be kept in the database?

It might make a little more sense if we consider that non-database attributes are often used in ActiveRecord models to hold transient data. The password exists in plaintext only during a request, when it is being submitted from an HTML form. Before saving, the plaintext password string needs to be encrypted, by the encrypt_password method.

def encrypt_password
  return if password.blank?
  if new_record?
    self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--")
  end
  self.crypted_password = encrypt(password)
end

Notice that the password property is referenced in the call to password.blank? and encrypt(password). Since non-database attributes exist outside the knowledge of ActiveRecord, when does the password attribute get set? Explicitly?

The fact that we have unit test coverage means we have a great way of seeing exactly where our password attribute is set. My gut says that most of the time these extra attributes are set via ActiveRecord constructor methods, but by using the User unit test I can prove it to you quite vividly.

What unit test? The plugin generated one. Before changing anything, let’s run rake test to make sure we have passing tests to begin with.

$ rake
(in /Users/obie/time_and_expense)
/opt/local/bin/ruby -Ilib:test
"/opt/local/lib/ruby/gems/1.8/gems/rake-
0.7.1/lib/rake/rake_test_loader.rb" "test/unit/user_test.rb"
Loaded suite /opt/local/lib/ruby/gems/1.8/gems/rake-
0.7.1/lib/rake/rake_test_loader

Started
..........
Finished in 0.312914 seconds.

10 tests, 17 assertions, 0 failures, 0 errors

/opt/local/bin/ruby -Ilib:test
"/opt/local/lib/ruby/gems/1.8/gems/rake-
0.7.1/lib/rake/rake_test_loader.rb"
"test/functional/account_controller_test.rb"
Loaded suite /opt/local/lib/ruby/gems/1.8/gems/rake-
0.7.1/lib/rake/rake_test_loader

Started
..............
Finished in 0.479761 seconds.

14 tests, 26 assertions, 0 failures, 0 errors
/opt/local/bin/ruby -Ilib:test
"/opt/local/lib/ruby/gems/1.8/gems/rake-
0.7.1/lib/rake/rake_test_loader.rb"

Green bar—all tests passed! That means it’s safe to do some experimentation. Remember we were wondering about how that password attribute is used. What will break if we change attribute_accessor to attribute_reader, thereby making password read-only?

class User < ActiveRecord::Base
  # Non-database attribute for the unencrypted password
  attr_reader :password

When we run the test suite again, it turns out that a whole bunch of tests broke, effectively pointing to every place in the codebase where the value of password is set. All of them are indeed ActiveRecord constructors.

For example, one of the errors occurs in the test for the signup method of AccountController.

4) Error:
test_should_require_pwd_confirmation_on_signup(AccountControllerTest):
NoMethodError: undefined method `password=' for #<User:0x2a45068>
    active_record/base.rb:1842:in  `method_missing'
    active_record/base.rb:1657:in `attributes='
    active_record/base.rb:1656:in `attributes='
    active_record/base.rb:1490:in `initialize_without_callbacks'
    active_record/callbacks.rb:225:in `initialize'
    app/controllers/account_controller.rb:23:in `signup'

A quick look at the signup method confirms that the password attribute is getting passed into User’s constructor, bundled into the user parameters hash that is generated when the signup form is posted submitted from the view.

def signup
  @user = User.new(params[:user])
  return unless request.post?
  ...
end

Non-database attributes are significant because they can be leveraged to mask implementation choices from the view. In this case, it would present a security risk to expose the salt and crypted_password properties to the view.

Validations

Turning our attention back to the User model, we see that Rick has provided sensible defaults for password validation. Of course, you have the freedom to modify these to your heart’s content to match your own requirements and purpose.

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

Those validation rules should only execute under one condition, if the crypted_password attribute is blank and the password attribute is not. Without this condition in place, every update to the user model would reset the password.

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

The before_save Callback

As covered in Chapter 2, “Working with Controllers,” callbacks allow you to point at a method that should be invoked at a certain stage in the life cycle of an ActiveRecord object, like this:

before_save :encrypt_password

Getting back to the User model walk-through, notice that there is a need to encrypt the user’s password before saving a new user record to the database. The :encrypt_password symbol points to the protected method of the same name closer to the bottom of the class:

def encrypt_password
  return if password.blank?
  if new_record?
    self.salt = Digest::SHA1.hexdigest("--#{Time.now}--#{login}--")
  end
self.crypted_password = encrypt(password)
end

This says: “First of all, if the password is blank, don’t try to encrypt it. Otherwise, calculate and capture the salt and crypted_password attributes for saving.”

The salt column stores a one-time hashing key, which helps to make our authentication system more secure than if a system-wide constant were used.

The authenticate Method

Moving along, the two-line authenticate method serves as a good example of a class method for your application code. It is a generic bit of class logic, not associated with any particular instance. However, its implementation might be somewhat dense and cryptic unless you know Ruby really well. I’ll try to dissect it for you.

# Authenticates a user by their login name and unencrypted password,
# returning the user or nil.
def self.authenticate(login, password)
  u = find_by_login(login) # need to get the salt
  u && u.authenticated?(password) ? u : nil
end

The first line attempts to find a user record using the login value supplied. If the record does not exist, the finder will return nil, which will be assigned to u, causing the && expression on line 2 to return false.

However, if a record user is found, then it is time to check the password. Rick’s comment succinctly says: “need to get the salt,” because if we were not using a unique salt value per user, then authentication could be done by simply querying the database for username and the crypted password. Assuming that call to find_by_login returned a user instance, the ternary expression on line 2 will invoke authenticated? to determine whether to return the user instance or nil.

The remember_token

You know how a lot of web applications have a little check box under their login and password fields so that you don’t have to authenticate manually every time? That is accomplished via a shared secret in the form of the remember_token. Whenever the remember_me method of User is invoked, an encrypted string is created to be stored as a cookie on the user’s web browser. Rick’s default implementation lasts two weeks before expiring, but you can change it to meet your need.

The forget_me method simply clears the attributes.

def remember_me
  self.remember_token_expires_at = 2.weeks.from_now.utc
  self.remember_token =
      encrypt("#{email}--#{remember_token_expires_at}")
  save(false)
end

def forget_me
  self.remember_token_expires_at = nil
  self.remember_token            = nil
  save(false)
end

Note

Here’s a bit of Rails trivia about save with a Boolean argument, as seen in those last two methods. It means save without running validations. I call this a bit of trivia because the Rails API docs don’t mention that save takes a Boolean argument. In fact that’s because the normal implementation doesn’t—until you specify validations on your model—meaning that the method signature of save changes dynamically at runtime.

The remember_token? method checks to see whether we have an unexpired token to check. Notice that by default it works with UTC, not local time.

def remember_token?
  remember_token_expires_at && (Time.now.utc <
remember_token_expires_at)
end

That does it for our walk-through of the acts_as_authenticated plugin-generated User model. Of course you can add additional application code to User to represent your own user-related business logic.

The Account Controller

Now, let’s open up app/controllers/account_controller.rb and examine the actions that were generated for us by the plugin, in Listing 14.2.

Example 14.2. The AccountController Class

class AccountController < ApplicationController

  # Be sure to include AuthenticationSystem in Application Controller
  # instead of here
  include AuthenticatedSystem

  # If you want "remember me" functionality, add this before_filter
  # to Application Controller
  before_filter :login_from_cookie

  # say something nice, you goof!  something sweet.
  def index
    redirect_to(:action => 'signup') unless logged_in? || User.count > 0
  end

  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_at }
      end

      redirect_back_or_default(:controller => '/account',
                               :action => 'index')

flash[:notice] = "Logged in successfully"
    end
  end

  def signup
    @user = User.new(params[:user])
    return unless request.post?
    @user.save!
    self.current_user = @user
    redirect_back_or_default(:controller => '/account',
                             :action => 'index')
flash[:notice] = "Thanks for signing up!"
  rescue ActiveRecord::RecordInvalid
    render :action => 'signup'
  end

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

Note the important instructions given in the comments near the top of the file. There are a couple of lines of code there that we should move to our application controller.

class ApplicationController < ActionController::Base
  include AuthenticatedSystem

First of all, we should include the AuthenticatedSystem module in our ApplicationController so that its methods are available to all the controllers in our system. The AuthenticatedSystem module gives us reader and writer methods in our controllers for current_user (as stored in the session). It also gives us a very useful logged_in? method. We can use these methods in the header of our application layout, for example, to display login and logout links based on whether the user is logged in or not:

<div id="login_message">
 <% if logged_in? -%>
  <%= "Logged in as <strong>#{current_user.name}</strong>" %> |
  <%= link_to "Logout", :controller => 'account', :action => 'logout' %>
 <% else -%>
  <%= link_to "Logout", :controller => 'account', :action => 'login' %>
|
  <%= link_to "Signup", :controller => 'account', :action => 'signup' %>
 <% end -%>
</div>

This brings up a very interesting and potentially puzzling question: How is it that you can access current_user and logged_in? from the view? Controller methods (which these are, via the AuthenticatedSystem module). They should only be available from the view if they’re declared as helper methods. Taking a quick look at application.rb, we notice that there isn’t a call to helper_method in sight.

The answer lies near the middle of authenticated_system.rb in the self.included method:

# Inclusion hook to make #current_user and #logged_in?
# available as ActionView helper methods.
def self.included(base)
  base.send :helper_method, :current_user, :logged_in?
end

Aha! Personally, I don’t like putting included hooks anywhere except the top of the modules because it can cause a bit of confusion if you don’t see them right away.

To explain, what is happening here is that when AuthenticatedSystem is included into another class, this hook will be invoked in the class context of the object doing the inclusion, and thus the helper_method will happen exactly as if it had been hard-coded into the original class. A bit of metaprogramming magic? No, just proper use of Ruby’s modules functionality and very much part of the Rails way.

Login from Cookie

Second, there’s the optional filter that enables logging in using a browser cookie, which gives us the so-called “Remember me” functionality. For now, let’s assume that we do intend to let users stay logged in to our application, so we move before_filter :login_from_cookie to ApplicationController also. Let’s take a peek under the covers at what the login_from_cookie method of AuthenticatedSystem actually does:

# When called with before_filter :login_from_cookie will check for an
# :auth_token cookie and log the user back in if apropriate
def login_from_cookie
  return unless cookies[:auth_token] && !logged_in?
  user = User.find_by_remember_token(cookies[:auth_token])
  if user && user.remember_token?
    user.remember_me
    self.current_user = user
    cookies[:auth_token] = {
      :value => self.current_user.remember_token,
      :expires => self.current_user.remember_token_expires_at
    }
    flash[:notice] = "Logged in successfully"
  end
end

This code reads fairly well, but let’s do a quick walk-through as another example of idiomatic Ruby and Rails code, as well as usage of the cookies object.

The first line of the method demonstrates proper use of Ruby’s optional return keyword, bailing out unless there is an :auth_token cookie and we’re not already logged in. It’s important to not burn too many cycles before bailing out, since this line will get executed during each request in the system.

Next step is to use the :auth_token from the cookie to look up the user from the database. The way that the if clause is structured is a very common idiom. The && will short-circuit and return false if user is nil, which prevents the NoMethodError from occurring if remember_token was invoked on nil. You’ll see this particular idiom over and over again in Rails code and you should learn to use it in your own coding.

If the condition passes, we call remember_me on the user object, and set it as the current user (which is effectively what accomplishes the “login”). Finally, updated cookie values are set and a “Logged in successfully” message is placed in flash storage for display to the user.

The Current User

My Java-addled brain has occasionally found lines such as self.current_user = user confusing. “Why on earth would we be setting the current user as an instance variable on the controller!?!?” The thing is that the implementation of the current_user reader and writer methods are more than just pedestrian getters and setters! They actually have quite a bit of logic in them, albeit written in somewhat terse Ruby code.

# Accesses the current user from the session.
def current_user
  @current_user ||=
  (session[:user] && User.find_by_id(session[:user])) || :false
end

# Store the given user in the session.
def current_user=(new_user)
  session[:user]=
    (new_user.nil? || new_user.is_a?(Symbol)) ? nil : new_user.id
@current_user = new_user
end

Your own implementation of GuestUser will vary depending on the needs of your application. You might be inclined to mimic the interface of a real User object, except with empty attributes.

A different approach would be to give your GuestUser a method_missing callback that raises a LoginRequiredError to be rescued by the authentication system. The idea is to automatically prompt for a login before access to a given resource can continue, instead of having to code the particular case explicitly or even worse, cause a server error.

Logging In During Testing

Rick also gives us a Ruby module named AuthenticatedTestHelper that we can mix into TestUnit in our test_helper.rb file so that its methods are always available in our test cases.

class Test::Unit::TestCase
  include AuthenticatedTestHelper

The most important method in that module is login_as, which you can call from a setup method or a test case to establish a session. Pass it a User object to log in; or (this is where your user fixtures come in pretty handy), login_as knows to reference the user fixtures (test/fixtures/users.yml) when you pass it a symbol instead:

def setup
  login_as(:quentin) # quentin was added by acts_as_authenticated

AuthenticatedTestHelper also gives you a method named authorize_as that simulates HTTP basic authentication, instead of simply assigning the user you identify as the current_user for the session. You can use authorize_as to test controller actions that will be serving web services and other users that will authenticate via HTTP, instead of logging in via a form.

Finally, the assert_requires_login and assert_http_authentication_required test helper methods take blocks and allow you to verify that given controller actions actually force the user to log in or use basic authentication.

Conclusion

Almost every Rails application needs some sort of login and access control functionality. That’s why it’s so convenient to learn how to use the Acts as Authenticated plugin, the main subject of this chapter. In addition to the plugin’s code generator and user class, we also looked at the account controller it generates, how to log in from a cookie, and how to access the current user from the rest of our application code.

References

1.

Rick’s website is http://techno-weenie.net/.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.134.151