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
|