Simple Windows Active Directory LDAP Authentication with Rails
Posted by Stijn Pint on Jun 17, 2008
In this short tutorial I’ll describe an easy way to make your Rails application even more enterprise-ready ;-)
For our latest project, our customer asked to change the authentication module, they wanted to be able to use their Active Directory credentials to enter the application.
So, we have an existing rails application :
- running in production (Windows environment)
- using the restful_authentication plugin for the login system.
What do we need :
- User should be able to keep using the old login system (at least temporarily, until all users are ‘upgraded’)
- User should be able to login with his LDAP credentitals
I didn’t want to use a full-blown ActiveLdap implementation so I decided to customize the plugin generated code of the restful_authentication plugin. The application is still on Rails 1.2.3 and it uses an older version of restful_authentication. So if you’re going to try this yourself, there might be some differences with the code snippets below… but I’m sure you’ll figure it out :-).
Ok, let’s get started:
Since there are existing users in the system, I needed to connect their LDAP account to their application account.
Add ‘ldap_account’ field to the users-table
You can let the users fill this in (through ‘my profile’ page), let the admin do it, or do it manually yourself. Once it is filled in, the user should be able to use it to log into the application.
class AddLdapAccountToUsers < ActiveRecord::Migration
def self.up
add_column :users, :ldap_account, :string
end
def self.down
remove_column :users, :ldap_account
end
end
Install the LDAP libraries on your system
You’ll need the ruby-ldap libraries installed on your system, for linux you can find them here: http://sourceforge.net/projects/ruby-ldap/
You need to build this to use in a Windows environment. For your convenience we ‘ve packaged the linux and windows versions as a gem, get them here (but use at your own risk, we don’t provide support on this ;-) :
- ruby-ldap-0.9.7-i386-mswin32.gem (md5sum: 00d04a489df12f13c60b409d47e3202e)
- ruby-ldap-0.9.7-x86-linux.gem (md5sum: 158aab7b28e6411c2d020f3aa050d1f1)
LDAP connection configuration
I found some useful information in the following article: http://blog.craz8.com/articles/2007/02/28/rails-and-ldap-gotchas/
And ofcourse in the ruby-ldap docs: http://ruby-ldap.sourceforge.net/rdoc/
Create a file ldap_conn.rb in the lib-folder :
require 'ldap'
def ldap_authenticated?(login,password)
return false if password.blank?
ldap_host = '10.10.10.10' #LDAP server IP
ldap_port = 389
begin
ldap_conn = LDAP::Conn.new(ldap_host, ldap_port)
ldap_conn.set_option( LDAP::LDAP_OPT_PROTOCOL_VERSION, 3 )
ldap_conn.bind( login, password )
true
rescue
return false
end
end
LDAP authentication methods
- require ‘ldapp_conn’ in user.rb
- add a method ‘ldap_autenticate’ in user.rb :
# restful_authentication method
# Authenticates a user by their login name and unencrypted password. Returns the user or nil.
def self.authenticate(login, password)
u = find_by_login(login) # need to get the salt
u && u.authenticated?(password) ? u : nil
end
def self.ldap_authenticate(login, password)
# Allow 'classic' login method
if find_by_login(login)
return User.authenticate(login, password)
elsif u = find_by_ldap_account(login.upcase)
return (ldap_authenticated?(login,password) ? u : nil)
else
return nil
end
end
- call the new method from the account_controller login-action (or sessions_controller/create in the new plugin version)
def login
return unless request.post?
self.current_user = User.ldap_authenticate(params[:login], params[:password])
if logged_in?
if params[:remember_me] == "1"
self.current_user.remember_me
cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
# store in cookie the module where the user logs in.
cookies[:module] = {:value => params[:select][:module], :expires => self.current_user.remember_token_expires_at}
end
flash[:notice] = "Logged in successfully"
else
flash[:error] = "Wrong login credentials !"
end
end
Case sensitvity issue
I ran into a small problem regarding case sensitivity: the Active Directory authentication is not case sensitive, but the User.find_by_ldap_account(‘account’) is.
I solved this by making sure the ldap_account field in the users table was always saved as uppercase in the DB, and by doing the user lookup like this: find_by_ldap_account(login.upcase) (ldap_authenticate method)
Secure authentication (SSL)
It should be straightforward to do this LDAP authentication through SSL (so the login and password are not sent in clear text over the network). Instead of the method LDAP::Conn.new(...) you can use LDAP::SSLConn.new(...). However, I haven’t been able yet to get it to work, and I’m guessing it has something to do with the network settings/permissions.
Extra: Active Directory search
To execute a simple search you could do something like this:
ldap_conn.bind( 'login', 'pass' ) do |conn|
base = 'cn=yourname,OU=Users,OU=bla,OU=bla,dc=yourdomain,dc=com'
results = conn.search2(base, LDAP::LDAP_SCOPE_SUBTREE, '(cn=name*)')
results.each { |entry| puts entry['cn'] }
end
The ‘cn=name*’ part is your search parameter.
You’ll need to customize the ‘base’-string according to your environment of course.