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.