Using Filters for Authentication

Problem

You want to authenticate users before they're allowed to use certain areas of your application; you wish to redirect unauthenticated users to a login page. Furthermore, you want to remember the page that the user requested and, if authentication succeeds, redirect them to that page once they've authenticated. Finally, once a user has logged in, you want to remember his credentials and let him move around the site without having to re-authenticate.

Solution

Implement an authentication system, and apply it to selected controller actions using before_filter.

First, create a user database to store user account information and login credentials. Always store passwords as hashed strings in your database, in case your server is compromised.

db/migrate/001_create_users.rb:

class CreateUsers < ActiveRecord::Migration
 def self.up
 create_table :users do |t|
 t.column :first_name, :string
 t.column :last_name, :string
 t.column :username, :string
 t.column :hashed_password, :string
 end
 User.create :first_name => 'Rob',
 :last_name => 'Orisni',
 :username => 'rorsini',
 :hashed_password => 
 '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8'
 end
 def self.down
 drop_table :users
 end end

In your ApplicationController, define an authenticate method that checks if a user is logged in and stores the URL of the page the user initially requested:

app/controllers/application.rb:

# Filters added to this controller will be run for all controllers in the
# application.
# Likewise, all the methods added will be available for all controllers.
class ApplicationController < ActionController::Base
 def authenticate
 if session['user'].nil?
 session['initial_uri'] = request.request_uri
 redirect_to :controller => "users", :action => "login" 
 end
 end end

To make sure the authenticate method is invoked, pass the symbol :authenticate to before_filter in each controller that gives access to pages requiring authentication. Here's how to make sure that users are authenticated before they can access anything governed by the ArticlesController or the BooksController:

app/controllers/articles_controller.rb:

class ArticlesController < ApplicationController
 before_filter :authenticate
 def admin
 end end

app/controllers/books_controller.rb:

class BooksController < ApplicationController
 before_filter :authenticate
 def admin
 end end

Now, create a login form template to collect user credentials:

app/views/users/login.rhtml:

<% if flash['notice'] %>
 <p ><%= flash['notice'] %></p>
<% end %>
<% form_tag :action => 'verify' do %>
 <p><label for="user_username">Username</label>;
 <%= text_field 'user', 'username' %></p> 
 <p><label for="user_hashed_password">Password</label>;
 <%= password_field 'user', 'hashed_password' %></p> 
 <%= submit_tag "Login" %>
<% end %>

The User sController defines login, verify, and logout methods to handle the authentication of new users:

app/controllers/users_controller.rb:

class UsersController < ApplicationController
 def login
 end
 def verify
 hash_pass = Digest::SHA1.hexdigest(params[:user][:hashed_password])[0..39]
 user = User.find(:first,:conditions => 
 ["username = ? and hashed_password = ?", 
 params[:user][:username], hash_pass ])
 if user
 session['user'] = user
 redirect_to session['initial_uri']
 else 
 flash['notice'] = "Bad username/password!" 
 redirect_to :controller => "users", :action => "login" 
 end 
 end
 def logout
 reset_session
 # Redirect users to Books#admin, which in turn sends them to 
 # Users#login, with a refering url of Books#admin:
 redirect_to :controller => "books", :action => "admin" 
 end end

Next, provide a mechanism for users to log themselves out if they're not comfortable letting their session time out on its own. Create a "logout" link with a named route using logout_url:

app/views/articles/admin.rhtml:

<h1>Articles Admin</h1>
<%= link_to "logout", :logout_url %>

app/views/books/admin.rhtml:

<h1>Books Admin</h1>
<%= link_to "logout", :logout_url %>

Finally, define the "logout " named route with its URL mapping:

config/routes.rb:

ActionController::Routing::Routes.draw do |map|
 map.logout '/logout', :controller => "users", :action => "logout" 
 # Install the default route as the lowest priority.
 map.connect ':controller/:action/:id'
end

Discussion

Adding authentication to a site is one of the most common tasks in web development. Almost any site that does anything meaningful requires some level of security, or at least a way to differentiate between site visitors.

The Rails before_filter lends itself perfectly to the task of access control by invoking an authentication method just before controller actions are executed. Code that is declared as a filter with before_filter has access to all the same objects as the controller, including the request and response objects, and the params and session hashes.

The solution places the authenticate filter in the Book and Article controllers. Every request to either controller first executes the code in authenticate. This code checks for the existence of a user object in the session hash, under the key of user. If that session key is empty, the URL of the request is stored in its own session key, and the request is redirected to the login method of the User controller.

The login form submits the username and password to the Login controller, which looks for a match in the database. If a user is found with that username and a matching hashed password, the request is redirected to the URL that was stored in the session earlier.

When the user wishes to log out, the logout action of the User controller calls reset_session, clearing out all the objects stored in the session. The user is then redirected to the login screen.

See Also