Archive | February 2011

Authlogic and LDAP Part 2: Searching Active Directory

In Part 1 we configured authlogic to authenticate your password against Active Directory (AD).

But, you still have to manage users within the application and if we’re authenticating against AD we need some way of searching AD for new users. Here’s how to do it…

Modify your Ldap class so that it looks like this:

require 'ostruct'
require 'net/ldap'

class Ldap
  LDAP_DOMAIN = 'ad'
  LDAP_SERVER_IP = '10.193.168.52'
  LDAP_USERNAME = 'ldap_username'
  LDAP_PASSWORD = 'ldap_password'

  LDAP_TREEBASE = 'dc=ad,dc=your_domain,dc=net'
  LDAP_ATTRS = [
    'cn', 'samaccountname', 'displayname','telephonenumber', 'mail'
  ]
  LDAP_FILTERS = Net::LDAP::Filter.eq("objectcategory", "person")

  def self.valid?(username, password)
    init "#{LDAP_DOMAIN}\\#{username}", password
    @ldap.bind
  end

  def self.search(search_string)
    init LDAP_USERNAME, LDAP_PASSWORD
    results, os = [], nil
    filters = LDAP_FILTERS & 
      Net::LDAP::Filter.eq("cn", "*#{search_string}*")
    @ldap.search(
        :base => LDAP_TREEBASE, 
        :filter => filters, 
        :attributes => LDAP_ATTRS) do |entry|
      os = OpenStruct.new
      entry.each do |attribute, values|
        eval "os.#{attribute.to_s} = '#{values[0]}'"
      end
      results << os
    end
    results
  end

  protected

  def self.init(username, password)
    @ldap = Net::LDAP.new
    @ldap.host = LDAP_SERVER_IP
    @ldap.auth username, password
  end
end

We added a few constants which define the domain, the AD fields we want to return in our search and a filter so that we only return AD objects that are people. Bear in mind that your AD may be configured differently so these fields may need tweaking to fit your own scenario.

In the search method, we execute the search and create a new OpenStruct object for each search result. Then return an array on OpenStruct objects.

My Restful search controller looks like this:

class SearchesController < ApplicationController
  def create
    @searches = Ldap.search(params[:name])
    render :action => 'index'
  end
end

The index and new methods just render views so no need to explicitly define the controller actions.

Finally, my index view, which renders the search results, looks like this (in haml of course!):

- title "Searches"

- if @searches
  %table
    %thead
      %tr
        %th Name
        %th AD Name
        %th Display name
        %th Email
        %th Telephone
    %tbody
      - for search in @searches
        %tr
          %td= h search.name
          %td= h search.samaccountname
          %td= h search.displayname
          %td= h search.mail
          %td= h search.telephonenumber
- else
  No matches found.

%p= link_to "New Search", new_search_path

It’s now very easy for us to make sure we populate all our user fields with valid information from AD.

Hope this helps!

Authlogic and LDAP Part 1: Authentication

In the Rails, Authlogic and Single Sign On post, we setup a central authentication app to handle user authentication and sessions centrally.

Let’s move on from that…

All our applications are on a closed network which uses Active Directory (AD). Users first authenticate against AD to get to our login page. They again authenticate against our database to get into our application portal. This is cumbersome and impractical because users in most cases have to remember two passwords.

“We don’t mind logging in twice, but why can’t we just use our same credentials?” they cried…
“You can!” we replied.

All our users use their email address and password for AD authentication. We need to configure our application to use the same credentials for application authentication because our application uses a unique username for logging in – not email address.

First, add this line to your environment.rb file (your version may differ):

config.gem "net-ldap", :lib => false, :version => '>=0.0.5'

Modify the UserSession model by telling authlogic to use our own custom method for password verification:

class UserSession < Authlogic::Session::Base
  verify_password_method :valid_ldap_credentials?
end

Next, we need to modify the User model with 3 things:

1. Tell authlogic to use the email field as the username
2. Tell authlogic NOT to validate the password field against the database
3. Implement our custom password verification method

Your User model should look like this:

class User < ActiveRecord::Base
  acts_as_authentic do |config|
    config.login_field = :email
    config.validate_password_field = false
  end

  protected

  def valid_ldap_credentials?(password)
    Ldap.valid?(self.username, password)
  end
end

Easy enough.

Finally, we create a new class which will talk to AD. The valid? method will return true if the user credentials are valid.

require 'net/ldap'

class Ldap

  LDAP_DOMAIN = 'ad'
  LDAP_SERVER_IP = '10.193.168.52'
  LDAP_USERNAME = 'ldap_username'
  LDAP_PASSWORD = 'ldap_password'

  def self.valid?(username, password)
    init "#{LDAP_DOMAIN}\\#{username}", password
    @ldap.bind
  end

  protected

  def self.init(username, password)
    @ldap = Net::LDAP.new
    @ldap.host = LDAP_SERVER_IP
    @ldap.auth username, password
  end
end

Finally, you can remove the password fields from your users table using a simple migration.

What about security?

Ah, yes. One very important point is that as we are now validating users against AD by allowing them to enter their AD credentials in a web form, you should use SSL to make sure the details cannot be sniffed and that the form details are encrypted.

In Part 2, we’ll look at how to search for users in the Active Directory.

Rails, Authlogic and Single Sign On

This post is for Rails 2. For Rails 3 compliant code, see the first comment.

Introduction

We have a suite of integrated tools and we needed a single sign on solution. We looked at OpenID, OpenAuth and CAS but decided to roll our own so that we could manage all our user accounts, sessions, roles and permissions centrally. We are also behind a secure network (Active Directory) and the use of OpenID and OpenAuth was ruled out anyway…

What did we need?

1. A central app to manage user authentication and sessions
2. A satellite app which uses the central app to authenticate users

The Central App

Nothing special going on here. We have a Rails application which uses authlogic, and we have some basic CRUD screens setup to manage users, roles and permissions.

In your database.yml file define your database:

  development:
    adapter: sqlite3
    database: db/development.sqlite3
    pool: 5
    timeout: 5000

Make sure you are using the database to persist session information. To do this, add the following line to your config/environment.rb:

config.action_controller.session_store = :active_record_store

Then, create a sessions table in your database by running:

rake db:sessions:create

Finally, make a note of the session settings in your config/initializers/session_store.rb file. This is important because these will have to be the same across all the applications:

ActionController::Base.session = {
  :key         => '_session_key',
  :secret      => '_some_very_long_string'
}

The Satellite App

Any satellite app that is to use our central authentication app also needs to be using authlogic. However, there are a few tweaks we will make to redirect some of the magic to the central app.

In your config/initializers/session_store.rb file, make sure the session information matches that of your central app:

ActionController::Base.session = {
  :key         => '_session_key',
  :secret      => '_some_very_long_string'
}

We also need the following lines added to the config/initializers/session_store.rb file:

ActionController::Base.session_store = active_record_store
ActiveRecord::SessionStore::Session.establish_connection("sessions_#{RAILS_ENV}")

Modify your user.rb model to look like this:

class User < ActiveRecord::Base
  establish_connection "sessions_#{RAILS_ENV}"
  acts_as_authentic
end

Finally, define the databases in your database.yml file. You will need both the application database as well as the authentication database definitions:

development:
  adapter: sqlite3
  database: db/development.sqlite3
  pool: 5
  timeout: 5000
  
sessions_development:
  adapter: sqlite3
  database: ../auth_app/db/development.sqlite3
  pool: 5
  timeout: 5000

Helper methods

Finally, add the following helper methods to your ApplicationController models in both/all your applications:

helper_method :current_user

protected

def current_user_session
  return @current_user_session if defined?(@current_user_session)
  @current_user_session = UserSession.find
end

def current_user
  return @current_user if defined?(@current_user)
  @current_user = current_user_session && current_user_session.record
end

def require_user
  unless current_user
    flash[:error] = "You must be logged..."
    redirect_to root_path
    return false
  end
end

def require_no_user
  if current_user
    flash[:error] = "You cannot be logged in..."
    redirect_to root_path
    return false
  end
end

Viola! We now have a single sign on solution with shared sessions across multiple Rails applications. We also don’t use any cookies so this could easily work across multiple domains.

Summary

This is a simplified illustration of our solution. Obviously this is for applications within the same “portal”. This solution is not “secure”. If you plan on using something like this for production applications you should use HTTPS to secure your cookies.

I’m using SQLite here, but in production we use ODBC drivers, meaning we don’t have to have all the databases on the same server. Just a consideration to bear in mind.

Follow

Get every new post delivered to your Inbox.