Building Authentication with acts_as_authenticated

Problem

You want to have portions of your application restricted to authorized users. You've looked into complete authentication systems, such as the Salted Login Generator, but have found that it won't meet your needs. You just want a foundation for an authentication system that allows you to develop the specifics of how it ties into your application.

Solution

Use the acts_as_authenticated plug-in and then build on the model and methods it provides to complete your authentication system. Start by installing the plug-in into your application.

$ ruby script/plugin source http://svn.techno-weenie.net/projects/plugins
$ ruby script/plugin install acts_as_authenticated

Your application has a reporting section to which you want to restrict access. The reports table is set up with the following schema:

db/schema.rb:

ActiveRecord::Schema.define() do
 create_table "reports", :force => true do |t|
 t.column "title", :string
 t.column "summary", :text
 t.column "details", :text
 end end

To initialize a basic authentication system, run the authenticated generator provided by the plug-in, passing it a model name and a controller name. The following command sets up a User model and an Account controller, and creates a database migration:

$ ruby 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
 exists db/migrate
 create db/migrate/002_create_users.rb

Apply the migration to your database with rake:

$ rake db:migrate

At the top of the account_controller.rb file, you'll see a line with include AuthenticationSystem. Move this line to your Application controller:

app/controllers/application.rb:

class ApplicationController < ActionController::Base
 include AuthenticatedSystem
end

To apply basic authentication to the actions of a controller, add a before filter on the controller class definition, passing it :login_required:

app/controllers/report_controller.rb:

class ReportController < ApplicationController
 before_filter :login_required
 def index
 end 
end

You can modify your layout to provide users the option to log out. The logout link is visible only to logged in users. This file is also a good place to display flash notices generated by the authentication actions.

app/views/layouts/application.rhtml:

<html>
 <head> 
 <title>Rails Demo</title>
 </head> 
 <body> 
 <% if logged_in? %>
 <%= link_to 'logout', :controller => 'account', :action => 'logout' %>
 <% end %>
 <p ><%= flash[:notice] %></p>
 <%= @content_for_layout %>
 </body> 
</html>

To add descriptive messages to failure events, such as invalid login attempts or sign-up validation errors, add the following flash assignments to the Account Controller.

app/controllers/account_controller.rb:

class AccountController < ApplicationController
 def index
 redirect_to(:action => 'signup') unless logged_in? or User.count > 0
 end
 def login
 return unless request.post?
 self.current_user = User.authenticate(params[:login], params[:password])
 if current_user
 redirect_back_or_default(:controller => '/report', :action => 'index')
 flash[:notice] = "Logged in successfully"
 else
 flash[:notice] = "Invalid Login/Password!"
 end
 end
 def signup
 @user = User.new(params[:user])
 return unless request.post?
 if @user.save
 redirect_back_or_default(:controller => '/report', :action => 'index')
 flash[:notice] = "Thanks for signing up!"
 else
 flash[:notice] = @user.errors.full_messages.join("<br />")
 end
 end
 def logout
 self.current_user = nil
 flash[:notice] = "You have been logged out."
 redirect_back_or_default(:controller => '/account', :action => 'login')
 end end

Discussion

When you restart your application, attempts to view the reports page will be redirected to the default login form created by the authenticated generator. The generator also creates a basic sign-up form that the login page links to. The following method keeps track of the initial URL; it is used for redirection once users authenticate.

def (default)
 session[:return_to] ? redirect_to_url(session[:return_to]) \
 : redirect_to(default)
 session[:return_to] = nil end 

shows the default sign-up and login form provided by the plug-in.

Figure 14-2. An authentication system with options to sign up, log in, and log out

The implementation details provided by acts_as_authenticated are deliberately minimalistic, for the same reasons that Rails does not provide an authentication system: there are many different ways to do authentication, and the authentication method you choose has serious implications on the design of the rest of your application. Authentication is not an area in which being prescriptive is very helpful.

See Also