Thoroughly supported, inconspicuously documented, authlogic lets you manage multiple user sessions -perfect if you want to allow users to login to your application under separate accounts, or as in the following case, if you want to build an administrative backend secure from potential session hijacks.
While everything you need to know can be found by reviewing the authlogic source, it’s not readily clear how to put all the elements together, nor does there seem to be much community code available which approaches the problem of security in a Rails admin section with authlogic features.
Fortunately, it’s quite simple. The key to making multiple sessions work can be found in the id.rb file which contains the module Authlogic::Session::Id. This additional field lets your user’s session model uniquely identify itself, such that you can create controller filters that require certain types of sessions. In this example I’ll be creating a namespaced administrative backend with its own login controller for ‘admin sessions’.
I’ll only be focusing on the administrative code. To compare this against a basic authlogic scenario with regular user authentication, check out the authlogic_example project on github.
I’ll start with a basic routes file, setting up the admin namespace and what we can imagine are the dashboard/home pages for users and admins, although we’ll just be using them here as reference for when unauthorized users are redirected.
routes.rb
ActionController::Routing::Routes.draw do |map|
. . .
map.namespace :admin do |admin|
admin.resource :admin_session,:only => [:new, :create, :destroy]
admin.root :controller => 'dashboards', :action => "show"
end
map.resource :user_session, :only => [:new, :create, :destroy]
map.root :controller => "home", :action => "index"
. . .
end
Next we’ll need to modify the acts_as_authentic model to tell it that we’re using multiple types of sessions by setting the Authlogic:: ActsAsAuthentic:: SessionMaintenance:: Config option called session_ids. Further details can be found in the session_maintenance.rb file, but here’s the summary of session_ids method:
# As you may know, authlogic sessions can be separate by id (See Authlogic::Session::Base#id). You can
# specify here what session ids you want auto maintained. By default it is the main session, which has
# an id of nil.
We’ll just be using the default session store and an additional admin session, which are passed to session_ids as an array. We’ll also assume that users have a role attribute we can use to check their admin credentials.
user.rb
class User < ActiveRecord::Base
acts_as_authentic do |c|
c.session_ids = [nil,:admin]
end
. . .
validates_inclusion_of :role, :in => %w(member admin)
. . .
end
For the administrative backend we’ll use a base controller class that all of our admin controllers can inherit from. This class will house the customary authlogic helper methods for the admin user and session, as well as the filter methods used to validate requests. The require_admin method is where the validation occurs, expecting a user with a session and the “admin” role. This is essentially identical to code within the authlogic_example project .
base_admin_controller.rb
class Admin::BaseAdminController < ApplicationController
include ApplicationHelper, Admin::AdminHelper
layout 'admin'
helper_method :current_admin_session, :current_admin
before_filter :require_admin
private
def current_admin_session
return @current_admin_session if defined?(@current_admin_session)
@current_admin_session = UserSession.find(:admin)
end
def current_admin
return @current_admin if defined?(@current_admin)
@current_admin = current_admin_session && current_admin_session.record
end
def require_admin
unless current_admin && ["admin"].include?(current_admin.role)
flash[:notice] = "You must be logged in to access this page"
redirect_to new_admin_login_url
return false
end
end
end
The AdminSessionsController class is also quite similar to a standard authlogic UserSessionsController, with some additional filtering done to key the session in question to administrative users.
Specifically, the user’s session model is assigned the session_id symbol that we’ll use to track admin authentications. The key is precisely how authlogic separates sessions, so in order to add yet another type of session you’d simply set a new session key to use throughout your application.
admin_sessions_controller.rb
class Admin::AdminSessionsController < Admin::BaseAdminController
skip_before_filter :require_admin
before_filter :prepare_model, :except => [:destroy]
def new
end
def create
if @admin_session.save
flash[:notice] = "Login successful"
redirect_to admin_root_url
else
flash[:error] = "Invalid login"
render :action => :new
end
end
def destroy
current_admin_session.destroy
redirect_to new_admin_login_url
end
private
def prepare_model
params[:user_session] ||= {}
@admin_session = UserSession.new(params[:user_session])
@admin_session.id =:admin end
End
The view for logging into the admin backend is no different than a normal user session form. Change the fields according to your model attributes
new.html.erb
<% form_for @admin_session, :url => admin_login_path do |f| %> <%= f.error_messages %> <div> <%= f.label :email %> <%= f.text_field :email %> </div> <div> <%= f.label :password %> <%= f.password_field :password %> </div> <%= f.submit "Login" %> <% end %>
Finally, a couple of quick functional tests to get started and sanity check our work.
admin_sessions_controller_test.rb
class Admin::AdminSessionsControllerTest < ActionController::TestCase
def test_should_post_to_create
post :create, :user_session => { :email => users(:admin).email, :password => "12345" }
assert_equal users(:cmd), assigns(:admin_session).user, "user record should be assigned to instance variable"
assert_equal users(:cmd), UserSession.find(session_key).record, "user record should be instantiated for session"
assert_not_equal UserSession.find(session_key), UserSession.find, "admin and member user logins should create different sessions"
assert_match /login successful/i, flash[:notice]
end
def test_should_destroy_session
post :create, :user_session => { :email => users(:admin).email, :password => "12345" }
delete :destroy
assert_nil UserSession.find
end
end
And that’s it – the key points of getting authlogic to authenticate user sessions in a nicely ordered fashion and the minimal monkey code to hook it all together.
