Secure Rails Admin Backend With Authlogic and Multiple Sessions

January 26, 2010

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 &amp;&amp; current_admin_session.record
  end

  def require_admin
    unless current_admin &amp;&amp; ["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.

Tags: ,

Leave a Reply

Powered by WP Hashcash