Finally a chance to reference a ruby experience from the workplace! The last couple of day's I've been developing a rails based intranet webapplication that would require some form of authentication. Considering my application will be at work I thought it would be great to minimize the number of passwords my co-workers had to remember. What better way to do that than to authenticate via ActiveDirectory!
I had a lot to consider, so the first thing to do was determine what all the requirements were.
1) I need to have a User object for relationships within rails.
Page's belong to Users - so I can't just map everything to the ActiveDirectory db because then I can't build those relationships (since AD don't seem to have Primary Keys being an OO db and all.)
I'm also a fan of acts_as_authenticated plugin, so I would like to keep all of it's functionality. So where did I begin?
I had to decide how to handle users. Simple enough, first the user would log in. I would lookup his username in the Active Directory and create a new ActiveDirectory::User class from it (activedirectory library).
With that I will authenticate the User with the libraries authenticate() method. If it authenticated I would take the password I was given and extract the username, and email from the ActiveDirectory::User object and create a new User object from it.
Next I would use acts_as_authenticated's built in functions to save my new User object's password and other information into my local database. I will then authenticate locally using acts_as_authenticated 's built in methods, and pass the rest off to that plugin.
But why would I want to do all of this? Well it all simply stemmed from a few concerns I had about this setup:
What if the user attempts to login and Active Directory is down?
- well if I have already copied the user's data into the local database, then I'll simply authenticate locally instead.
What if the user changes his information in Active Directory?
- everytime the user successfully authenticates I will synch up the local database, to keep it current.
Will I use ActiveDirectory for Permissions, or will I handle those locally?
- After much thought I prefered to handle it locally. Mainly because I wouldn't want to keep hitting ActiveDirectory throughout the application's use. Plus it seems like a better idea to keep from losing relationships, or having to rely on ActiveDirectory for the application to function. This way AD could go away for ever and I could still log people in locally and handle everything from within the application. Active Directory authentication is just the prefered method, not the only method!
Without further ado - let me provide you the code that I used to make it all happen. First my User.rb Model:
# Authenticates a user by their login name and unencrypted
#password. Returns the user or nil.
def self.authenticate(login,password)
begin
u = ActiveDirectory::User.find(login)
rescue => er
# User wasn't found so we'll attempt to authenticate locally
logger.warn("Failed: AD Authentication for #{login} ERROR: #{er}")
return self.authenticate_locally(login,password)
end
# Authenticate User
begin
u.authenticate(password)
# Sync Active Directory User with Local User
begin
if self.sync_activedirectory_account(u,password)
return self.authenticate_locally(login,password)
else
return nil
end
end
rescue => er
logger.warn("Authentication Failed \n ERROR: #{er}")
end
end
def self.sync_activedirectory_account(active_directory_user,password)
# Copy data over to the local database
# if there is already a local user copy over the information
u = active_directory_user
if User.find_by_login(u.username)
logger.info("Updating User Information")
user = User.find_by_login(u.username)
user.password = password
user.password_confirmation = password
user.email = u.email
begin
user.save!
return true
rescue => er
logger.warn("ERROR: Attemping to Update Local User Information Failed \n #{er}")
end
else
user = User.new(:login => u.username.to_s, :password => password.to_s, :password_confirmation => password.to_s, :email => u.email.to_s )
begin
user.save!
logger.info("#{u.username} User Created")
return true
rescue => er
logger.warn("Could not create new user #{u.username} - ERROR: #{er}")
return false
end
end
end
def self.authenticate_locally(login, password)
logger.info("Authenticating Locally...")
u = find_by_login(login) # need to get the salt
# modified for better logging
if u && u.authenticated?(password)
logger.info("Locally Authenticated")
return u
else
return nil
end
end
You will also need to add the following to your Environment.rb to make it all work:
ENVIROMENT.RB
--------------------
# This will have to placed in your /config/enviroment.rb file
# you may need to replace 'require' with require_gem if you installed activedirectory as a gem.
# I personally prefer to place the active_directory.rb and the /active_directory directory in my /lib folder
# to make sure that it doesn't go anywhere.
-------------------------------------------
require "active_directory"
ActiveDirectory::Base.server_settings = {
:host => "ADSERVER.DOMAIN.COM",
:username => "DOMAINADMINACCOUNT",
:password => "PASS",
:domain => "DOMAIN.COM",
:base_dn => "DC=domain, DC=com"
}

Charles said:
Kudos to my friend Jim for allowing me to edit and post his write up on the site!
Giving back to the community - always a good thing =)
2006-08-17 20:26:43 UTC