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:
Set-Cookie
instruction from the server.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.
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]
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:
User
model, which stores usernames and passwords in the users
table in the database. 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. 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. logout
action to reset the user's session and log them out. This is also useful during testing. 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.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
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.
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.
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:
:logged_in
to true
when the user successfully logs in. Otherwise, set it to false
. :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
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.
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:
:destination
key, as used in the check_credentials
action) and the user is immediately redirected to the login form.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
18.227.134.154