Ruby on Rails
ActiveDirectory

Here’s a real fast way to get up and running with Active Directory authentication. What we’ll end up with is an application that ‘Authenticates’ only, I do not cover authorisation. We’ll actually create the user in the database though, so applying roles should be a trivial exercise. Futher down, you’ll also be able to change passwords in Active Directory.

Firstly grab the ‘Depot’ application source code, you can find it on the agile rails book web page. Once you have it up and running the application has a basic authentication module for users stored in a database. We’ll amend this to give a basic LDAP authentication and pull a simple trick to get Active Directory working.

If you have ruby, rails, gem and MySql installed you can enter the Depot folder and go. Copy the depot_r application from the archive and cd into its directory.

$> rake db:sessions:clear
$> mysqladmin -u root create depot_development
$> rake db:migrate

If you are using passwords on MySQL then add -p to the mysql command above and also remember to adjust the ./config/database.yml as necessary.

$> ruby script/server
=> Booting WEBrick...
=> Rails application started on <a href="http://0.0.0.0">http://0.0.0.0</a>:3000
=> Ctrl-C to shutdown server; call with --help for options
[2006-12-05 16:51:12] INFO  WEBrick 1.3.1
[2006-12-05 16:51:12] INFO  ruby 1.8.5 (2006-08-25) [i386-mswin32]
[2006-12-05 16:51:12] INFO  WEBrick::HTTPServer#start: pid=1048 port=3000

Assuming you now have the depot app up running you will need some kind of LDAP library. I suggest the pure ruby one :-

gem install ruby-net-ldap
Successfully installed ruby-net-ldap-0.0.4
Installing ri documentation for ruby-net-ldap-0.0.4...
Installing RDoc documentation for ruby-net-ldap-0.0.4...

Next we need to modify the file app/models/user.rb

Add to the top of the file


require ‘net/ldap’

Then replace the existing authenticate method


def self.authenticate(username,password)
ldap_con = initialize_ldap_con(‘WORKGROUP\username’,’password’)
treebase = “DC=mycompany,DC=topleveldomain”
user_filter = Net::LDAP::Filter.eq( “sAMAccountName”, username )
op_filter = Net::LDAP::Filter.eq( “objectClass”, “organizationalPerson” )
dn = String.new
ldap_con.search( :base => treebase, :filter => op_filter & user_filter, :attributes=> ’dn’) do |entry|
dn = entry.dn
end
login_succeeded = false
unless dn.empty?
ldap_con = initialize_ldap_con(dn,password)
login_succeeded = true if ldap_con.bind
end
login_succeeded
end

This is shamelessly stolen from the NET::LDAP documentation with a couple of minor mods. What it does is:-

  1. Logs onto your AD server using a domain account and password.
  2. Looks up the username passed into the method. You can cheat here to use any thing unique that works for you. For instance you may want to provide a drop down list of domains to your users and append it to the username. Invest in an LDAP browser to view what records are actually stored in your directory, or ask an admin!
  3. After obtaining a DN using the username that was provided in the previous step, actually try to login to the domain server with this DN and the password supplied to the method.

If we can log on, we are authenticated, however, this is different to how the original depot application worked. We are now returning true or false, instead of a user. There is also a little utility method missing.

Still in app/models/user.rb add this method to the bottom of the class, somewhere after the ‘private’ qualifier


def self.initialize_ldap_con(user_name, password)
Net::LDAP.new( {:host => ‘ldap.int.clsa.com’, :port => 389, :auth => { :method => :simple, :username => user_name, :password => password }} )
end

And a change to the login method in app/controllers/login_controller.rb


def login
session[:user_id] = nil
if request.post?
if User.authenticate(params[:name], params[:password])
user = User.find_by_name(params[:name])
if !user
user = User.new
user.name = params[:name]
user.password = params[:password]
user.password_confirmation = params[:password]
user.save
end
session[:user_id] = user.id
redirect_to(:action => "index")
else
flash[:notice] = “Invalid user/password combination”
end
end
end

Finally we have a solution that allows any AD user to log into the application and stores the user in the database. I’ve purposefully not changed too much so that you can implement a more typical arrangement whereby most users come from LDAP but you can still fallback to database authentication as needed. The depot app is simple in that once you’re in, you can access everything, however having the LDAP user uniquely identified as an object in your domain (i.e. an instance of a User object), you can quite simply implement roles and a full authorisation model from here.

You can even use the above code to search on a few other fields giving you a little LDAP browser that you can integrate into your app where necessary. For instance, looking up email addresses, phone numbers and all the while maintaining a nice simple decoupling via your own User objects.

If you would like your users to be able to change their AD password from your Rails applications, the first thing you have to do is setup your domain controller to accept LDAP connections over SSL. Microsoft does not allow password changes over an insecure connection. This page on Microsoft’s site describes how to do so, however if you do not already have a secure certificate or do not plan on purchasing one, see here for instructions to generate your own self-signed certificate for free. Net::LDAP doesn’t actually check the certificate to see if its trusted or not, so as long as the domain controller accepts it and enables SSL, you should be good.

Because you are now connecting over SSL, port 636 should be used rather than port 389. You will also have to let Net::LDAP know by setting the encryption to :simple_tls.

There are two ways of changing passwords over LDAP. The first is authenticating as an admin and replacing (resetting) a user’s password. The second, which this article will describe, allows a user to change their own password, provided they have their old password

The following code, given a username, the old and new passwords, the server location, and the base DN (ex: “dc=microsoft,dc=com”) will authenticate with Active Directory, encode the passwords, and change the passwords remotely over SSL. The last line, which is commented out can be used to determine the success of this operation (0 means success – see this table for information on the other possible error codes)


username = “JoeSmith”
password = “oldpass”
newpassword = “newpass”
server_location = “server1.microsoft.com”
base_dn = “dc=microsoft,dc=com”

  1. require ‘rubygems’ (required if using outside of rails)
    require ‘net/ldap’
    ldap = Net::LDAP.new
    ldap.port = 636 #must be 636 for SSL
    ldap.host = server_location
    ldap.encryption :simple_tls
    ldap.auth username,password
    ldap.bind
    newPass = “"
    newpassword = "\”" + newpassword + “\”"
    newpassword.length.times{|i|
    newPass+= “#{newpassword[i..i]}\000”
    } # encoding password in format Microsoft wants
    oldPass = “"
    password = "\”" + password + “\”"
    password.length.times{|i|
    oldPass+= “#{password[i..i]}\000”
    } # encoding password in format Microsoft wants
    dn = ldap.search(:filter=> Net::LDAP::Filter.eq(“samaccountname”,username), :base=>base_dn)0.dn #finding location of the user, as the change password operation requires the full path to the user
    ops = [[:delete, :unicodePwd, oldPass],[:add, :unicodePwd, newPass]] #Microsoft requires that in one operation, you delete the old password then add the new one.
    ldap.modify :dn => dn, :operations => ops
    #ldap.get_operation_result.code (to see error if there is one)