Adding an Authentication System

When a web client makes requests to a web server, the server's default behavior is to treat each request in isolation from those around it. If the client makes three requests in a row, for example, there is no default sequencing information in those requests to tie them together: to all intents and purposes, they may as well come from different clients. This makes it very difficult to provide continuity between different responses: for example, if the client is filling a shopping basket, how does the server tell that the three requests relate to a single basket, rather than three separate ones?

This is where cookies come in. A cookie originates on the server and is sent to the client: the server is effectively saying, "I don't know who you are, but if you send this cookie back to me with your next request, I can keep track of you." Each time a client makes a request to a server it scans its stored cookies (typically held in text files) and sends back any that originated from that server. Effectively, the client is saying "Here's that cookie you sent me, which proves who I am. This request relates back to the other one I made a few seconds ago."

Cookies can store any type of textual information (including serialized objects), but their most common use is as an identifier for a session: a sequence of requests from a single client. On the first request from a client, the server responds by setting a session ID cookie on the client and reserving a "scratch pad" for data relating to the session, which could be a file on the server's file system or some space in a database table. As the client subsequently interacts with the server (e.g. adding items to a shopping basket), the server writes that information onto the scratch pad. It knows which pad to use, as each request from the client carries the identifying session ID. The data stored on the server is a part of the client session; but the only data passing between the client and the server is an identifier for that session: a pointer to the scratch pad.

This is important, because it is how most web applications implement authentication. The basic pattern is as follows:

  1. The client requests a protected page in the application.
  2. The server sends a session identifier (a cookie) to the client. This cookie should be sent back to the server with each subsequent request from the client. Plus, as the client requested a protected page, but hasn't logged in yet, the server redirects the client to the login page.
  3. The client receives the redirection to the login form, as well as the Set-Cookie instruction from the server.
  4. The client makes a new request for the login page, sending the session identifier cookie with it.
  5. The server responds by sending an HTML form asking for the client's login credentials.
  6. The client fills in the username and password fields and submits them back to the server. The session identifier cookie is again sent with the request.
  7. The server tries to find a record in the user database with a username and password matching those submitted by the client:

    a. If a record exists, the server puts a mark in the client session to show that they have logged in successfully, and redirects the client to the protected page they requested.

    b. If the record doesn't exist, the server shows the login form again and denies access to the protected page.

This is the pattern we're going to follow for Intranet's authentication system. Before we do that, though, we'll have a brief look at how sessions and cookies are implemented in Rails.

Cookies and Sessions in Rails

Session handling is turned on by default in Rails. This means that the session identifier cookies are already being transmitted automatically between the web browser and the Intranet application, even though we haven't done anything yet.

To see this happening, you'll need some kind of tool that can interrogate request and response headers (e.g. LiveHTTPHeaders in Firefox, from http://livehttpheaders.mozdev.org/). As an example, when the URL http://localhost:3000/people was requested from Intranet, the following Set-Cookie header came back in the response:

Set-Cookie: _Intranet_session_id=69289c7593407d1a5cadefd3de09a8e7; path=/

Notice that this is a cookie which expires when the browser closes (there's no explicit expiry date set). Also, note that the cookie is called _Intranet_session_id. The name of this cookie comes from a setting in the ApplicationController class (in app/controllers/application.rb):

class ApplicationController < ActionController::Base
session :session_key => '_Intranet_session_id'

# ... other methods ...
end

Each application has its own :session_key setting, so that cookies for an application don't get mixed with cookies from unrelated applications on the same domain. You can change this name if you wish, but for our purposes we can leave it as it is.

On subsequent requests, the client sends back the session ID cookie:

Cookie: _Intranet_session_id=69289c7593407d1a5cadefd3de09a8e7

The Rails application can store data relating to this session via the session method on controllers. For example, if we wanted to store the date and the time when someone's session started, we might do this in the ApplicationController class:

class ApplicationController < ActionController::Base
before_filter :track_login_time
# Set the :login_time in the session if not already set
protected
def track_login_time
session[:login_time] ||= Time.now

end
end

Session data relating to the client is stored as a hash; the session method exposes this hash and enables the application to store values in it or retrieve values from it. If you want to destroy a session at any time, use:

reset_session

Similarly, we can set and retrieve cookies using the cookies method on a controller. For example, we might do something like record a user's font size preference in a cookie, which expires in two years' time:

def set_preference
cookies[:font_size] = {:value => 'small',
:expires => 2.years.from_now}
end

Note that the hash can also contain other standard cookie options, such as :secure, :path, and :domain: see the documentation for the ActionController::Cookies module for more details.

The preceding code sends this header in the response:

Set-Cookie: font_size=small; path=/; expires=Fri, 20 Feb 2009 08:03:27 GMT

Each time the client comes back to the application, providing the cookie hasn't expired, the preference is sent as a request header:

Cookie: font_size=small

And Rails can pick up the font size preference from the cookies inside a controller:

font_size = cookies[:font_size]

Building the Authentication System

Now that we've seen how to manage sessions in Rails, we are ready to start putting together the components required for the authentication system. These are:

  1. A User model, which stores usernames and passwords in the users table in the database.
  2. An index action to display the login form. To keep things simple, we'll add a dedicated login_controller.rb to manage this and other login-related actions.
  3. A check_credentials action, to check submitted credentials and either redirect the user back to the protected page they requested (if they logged in successfully), or show the login form.
  4. A logout action to reset the user's session and log them out. This is also useful during testing.
  5. A before_filter on our controllers to authenticate the user before permitting them to perform certain actions. In the case of our application, we want to protect any action that can modify the database, i.e. create, update, and delete. The employees' action on CompaniesController is a trickier one, as it both displays and allows editing of employees: in this case, it makes sense to disable the editing buttons where the user isn't logged in, rather than deny access to the action altogether.

The User Model

First generate the User model from the command line (inside RAILS_ROOT):

$ ruby script/generate model User

Edit db/migrate/004_create_users.rb to set up the columns for the users table:

class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.column :username, :string, :null => false
t.column :passwd, :string, :null => false
t.column :last_login, :datetime

end
end
def self.down
drop_table :users
end
end

Note the column name passwd (instead of password) as "password" is a reserved word in MySQL, and can create problems when you're working with the MySQL console.

Apply the migration to the database:

$ rake db:migrate

A few simple modifications to the User class itself will also improve security and reliability. Firstly, some validation to make sure a username only exists once within the users table; secondly, validation to ensure a user has both a username and a password; and finally, application of a one-way digest to passwords stored in the database.

The first two of these measures are self-explanatory. The final measure, using a digest of the password, means that passwords are stored in a form that prevents them from being read from the database directly: instead of being stored as plain text, they will be stored as an SHA1 digest. SHA1 is a hash algorithm that converts a text string into a longer, random-looking string; retrieving the original string from the digested version of the string is nigh-on impossible. For example, for the plain text 'elliot', the SHA1 digest is:

dee6300e151043f915cc24dbc1409935bc4ae592

The advantage of this approach is in protecting passwords from people who might read the users table in the database (e.g. administrators, or people who've stolen it). Even with access to the table, the user's password is not visible.

Here's how the User class is implemented (in app/models/user.rb) to perform the required validations and to implement digested passwords:

require 'digest/sha1'
class User < ActiveRecord::Base
validates_presence_of :username
validates_uniqueness_of :username
validates_presence_of :passwd
def passwd=(pwd)
write_attribute('passwd', Digest::SHA1.hexdigest(pwd))
end
end

The unusual parts of this is the require 'digest/sha1' statement at the top, which pulls in the SHA1 digest library, and the passwd= method. This method overrides the default mutator method for the passwd field, as supplied by ActiveRecord. Instead, the custom method intercepts the passwd attribute before it reaches the database, applying an SHA1 digest to it as it is set.

While some kind of management interface for users would make life easier in the long term, the console provides an acceptable temporary measure for adding users. Add at least one for testing purposes:

$ ruby script/console
Loading development environment.
>> u = User.new :username => 'elliot', :passwd => 'police73'
=> #<User:0xb711f848 @attributes={"passwd"=>"bdbc07452121cfe2f35ff510b291c09b2418d2db", "username"=>"elliot"}, @new_record=true>
>> u.save
=> true

Note

Another alternative would be to add a migration to create the default system users, or maybe write a batch script to add them from a tab-separated file.

It's also possible to use something other than the database to store user credentials, e.g. an LDAP server, and interface with this from your Rails application.

Displaying the Login Form

The next component is the controller for managing the login process. Generate it first:

$ ruby script/generate controller login index logout

We passed both the name of the controller (login) and the names of the actions we want to define for it (index, logout). This adds empty method definitions for the actions to app/controllers/login_controller.rb, and creates an empty view template for each too.

The index action will just display a simple login form, which we define by modifying app/views/login/index.rhtml:

<h2>Please login to continue</h2>
<% form_tag :action => 'check_credentials' do -%>
<p><%= label :login, 'Username' %></p>
<p><%= text_field 'login', 'username' %></p>
<p><%= label :login, 'Password' %></p>
<p><%= password_field 'login', 'passwd' %></p>
<p><%= submit_tag "Submit" %></p>
<% end -%>

Note that the action attribute for the form is set to the check_credentials action on the LoginController: we'll write that next.

Checking Submitted Credentials

The check_credentials action will retrieve the username and password from the submitted form and use them to look up a record in the users table with matching username and password. If a record exists, the credentials submitted correctly identify a user, and they can be logged in; if not, access is denied.

As the action will be needed to perform a database lookup, we'll need some code to perform the query. This code should be a part of the model. (Recall that the model implements the business logic of the application. Controllers should be lightweight, with as little involvement in the database as possible, so we don't want the database query code in the LoginController.) So the first step is to write a method for the User model, which can look up a user by his/her username and (hashed) password:

class User < ActiveRecord::Base
# ... other methods, validation etc.
def self.authenticate(username, passwd)
hashed_passwd = Digest::SHA1.hexdigest(passwd)
user = self.find_by_username_and_passwd(username, hashed_passwd)
return user
end
end

The authenticate method takes a username and password, hashes the password, then uses a dynamic finder (see: Finding Records Using Attribute-Based Finders in Chapter 4) to search for a matching record. The return value is a User instance if one was retrieved, or nil if the finder fails to retrieve a record. We can write a unit test to check if the method works correctly (in test/unit/user_test.rb):

require File.dirname(__FILE__) + '/../test_helper'
class UserTest < Test::Unit::TestCase
def setup
User.new(:username => 'elliot', :passwd => 'police73').save
end
def test_authenticate
assert User.authenticate('elliot', 'police73')
assert !(User.authenticate('elliot', 'bilbo'))
assert !(User.authenticate('frank', 'police73'))
end
end

and run it with:

$ rake test:units

Now that this method is in place, we can add a first version of the check_credentials action:

class LoginController < ApplicationController
def index
end
def logout
end
def check_credentials
username = params[:login][:username]
passwd = params[:login][:passwd]
user = User.authenticate(username, passwd)
redirect_to_index "Logged in OK? " + (!(user.nil?)).to_s
end

end

For now, this action just redirects to the login page, displaying Logged in OK? true if the login was successful, or false otherwise. Try it by going to http://localhost:3000/login and entering some valid/invalid credentials, making sure that the controller responds correctly.

To start transforming this simple outline into a full-fledged authentication system, we'll do several things:

  1. Set a session variable called :logged_in to true when the user successfully logs in. Otherwise, set it to false.
  2. When a user logs in successfully, store their User object in the session. This is useful for personalization. We'll also set the date and time of the login in their user record.
  3. When the user requests a protected page, store its URL in the session as :destination before redirecting them to the login page. If the user successfully logs in, redirect them back to that original page; if they fail to login, redirect back to the login form again.

Here's the implementation:

class LoginController < ApplicationController
def index
end
def check_credentials
username = params[:login][:username]
passwd = params[:login][:passwd]
# Default state is NOT to be logged in.
session[:logged_in] = false
user = User.authenticate(username, passwd)
unless user.nil?
# Set a marker in the session to show user is logged in.
session[:logged_in] = true
# Set a login success notice.
flash[:notice] = "You have logged in successfully"
# Store the login date and time.
user.last_login = Time.now
user.save
# Store the user in the session.
session[:user] = user
# Set the destination to the protected page originally
# requested, or to the list of people if coming in fresh.
destination = session[:destination] ||
{:controller => 'people'}
else
# Redirect back to the login form.
destination = {:controller => 'login'}
# Set a login failure notice.
flash[:notice] = "Your username and/or password were not recognised"
end
redirect_to destination
end
end

Logging Out

The logout action on the LoginController is simple; it resets the user's session and redirects them back to the home page for the application:

class LoginController < ApplicationController
# ... other actions
def logout
reset_session
flash[:notice] = "You have logged out"
redirect_to :controller => 'people'
end
end

With these components in place, you can now test the login process. Log in at http://localhost:3000/login; you should be redirected to the people list. Now logout at http://localhost:3000/logout; you should see a You have logged out message.

It's hard to tell whether you are logged in or logged out at the moment, but a small addition to the menu (in app/views/layouts/application.rhtml) can help here:

<div id="menu">
...
</ul>
<p>
<% if session[:logged_in] -%>
Logged in as <strong><%= session[:user].username %></strong>;
<%= link_to 'Logout', :controller => 'login', :action => 'logout' %>
<% else -%>
<%= link_to 'Login', :controller => 'login' %>
<% end -%>
</p>

</div>

This displays the user's username and a logout link if they are already logged in; otherwise, it shows a login link.

Protecting Actions

The final step is to protect some actions on controllers to prevent access by users who aren't logged in. Firstly, write a private method called authorize on the ApplicationController class, which checks whether a user has the :logged_in variable set to true in their session:

  • If they haven't, the URL of the current page is stored in the session (under the :destination key, as used in the check_credentials action) and the user is immediately redirected to the login form.
  • If they have, no action is taken, and the rest of the page can be displayed.

Here's the method (in, app/controllers/application.rb):

class ApplicationController < ActionController::Base
# ... other methods ...
protected
def authorize
unless (true == session[:logged_in])
session[:destination] = request.request_uri
redirect_to :controller => 'login'
return false
end
end
end

Note that the method explicitly returns false: this ensures that the filter chain halts at this point, no other filters are executed, and the user is immediately redirected.

To apply this method, we'll use a before_filter assigned to the actions we want to protect. For example, we can put it inside the PeopleController to protect any destructive actions:

class PeopleController < ApplicationController
before_filter :authorize, :except => [:index, :search, :show]

# ... other before_filter settings ...
# ... other methods ...
end

Note that authorize is the first before_filter (they are applied in the same order as listed within the class definition). We use an :except option rather than :only so that the default is to authorize every action. If we add new actions, this ensures that the default is to protect them; if we want them to be public; we have to explicitly expose them.

To test this, log out and browse around the index, search, and show pages for people. You should be able to see them fine. Now, try to edit a person: you should be redirected to the login form. Once you log in, you should be redirected back to the original edit page you requested and be able to modify the person's details.

You can set a similar filter on the CompaniesController to protect that too:

class CompaniesController < ApplicationController
before_filter :authorize, :except => [:index]

# ... other before_filter settings ...
# ... other methods ...
end

Finally, we need to protect AddressesController. Recall that this just uses the scaffold, so we need to enable the scaffold actions that list all addresses or show a single record: index and show, respectively:

class AddressesController < ApplicationController
before_filter :authorize, :except => [:index, :show]

scaffold :address
end
..................Content has been hidden....................

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