AD vs Ruby vs Me

So, I was tasked to create a authentication system for one of the apps that I wrote a while back at work. I had written the application in Ruby on Rails, and we use Microsoft’s Active Directory for network authentication. Taking pity on the users of my application, I decided to do some research and see if I could bump my authentication vs the AD LDAP server. This is what I came up with…

First things first – we need to add the secret sauce to the model which will allow use to connect to a LDAP server. We want to add the following function to the model that handles your users. We also need to add a require 'ldap' to the model, before the class definition. You also want to define AD_DOMAIN_NAME to be what ever the domain that your users will be logging into.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  attr_accessor :password
  
  def self.login(login, password, host)
    begin
      if password.nil? || password == "" || login.nil? || login == ""
        return false
      else
        conn = LDAP::Conn.new(host, 389)
        conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3 )
        fulllogin = AD_DOMAIN_NAME + login
        conn.bind(fulllogin, password)
        if conn.err == 0
          conn.unbind
          return true
        else
          conn.unbind
          return false
        end
      end
      
    rescue
      return false
    end
  
  end

I also added the following to allow me to create the basic user skeleton with out the validations running on it.

1
2
3
  def build
      self.save(false)
  end

I did not want the users passwords to be recorded in the log files, so we need to add this to the application controller


   filter_parameter_logging :password

Now, we need to add some code to the controller which controls your users and their authentication. I use the following

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  def authenticate
   temp = params[:login]
   if User.login(temp[:userid], temp[:password], LDAP_SERVER)
     if @user = User.find_by_userid(temp[:userid].downcase)
       @user.last_login = Time.now
       @user.save
       session[:id] = @user.id
       flash[:notice] = 'Login Successful! <br/> Welcome back, ' + @user.firstname
       session[:timeout] = Time.now
       redirect_to :controller => 'tickets', :action => 'new'
     else
       @newuser = User.new
       @newuser.userid = temp[:userid].downcase
       @newuser.last_login = Time.now
       session[:timeout] = Time.now
       if @newuser.build
         session[:id] = @newuser.id
         flash[:notice] = 'Please update your information'
         redirect_to :action => "myaccount"
       else
         flash[:warning] = 'Error saving your account information'
         reset_session()
         redirect_to :controller => 'users', :action => 'index'
      end
    end
   else
     flash[:warning] = 'Login Failed! Please verify your user ID and password and try again'
     redirect_to :action => 'login'
  end 
 end

What does this mess do? Let’s look at it.

First we assume that we have a form already hooked up to this function. When we call this, we assume that we have been passed 2 values, userid and password. We assume that we have defined LDAP_SERVER = "your_server" some where else in the controller.

So, first things, we pass the user name and password to the model and attempt to authenticate. We check to assure that none of the fields are blank. The line if password.nil? || password == "" || login.nil? || login == "" takes care of this, and we pass a false value back to the calling function if any of it is true.

Next, we need to set up the connection to the LDAP server conn = LDAP::Conn.new(host, 389) and conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3 ) takes care of this. Assuming that everything works, we now have a good connection to the LDAP server. You can test this assumption with conn.bound? (for example, from the console).

We need to pass the user name and password to the connection and see if we get a positive result from the resultant credentials check. fulllogin = AD_DOMAIN_NAME + login formats the ad domain name and user name correctly, and then conn.bind(fulllogin, password) sends the credentials off to the LDAP server. It will pause, and then return the LDAP object.

This is all well and good, but did we log in? if conn.err == 0 checks the error code returned when we tried to authenticate. If we have anything other then 0, we had problems, so we toss a false back up to the calling function. If we get a 0 back, then we toss a true back up to calling function and press on.

Let’s assume that we get a true back in the controller. So, the user has provided AD a good user name and password. We might have some information that we want to cache locally in the application for performance reasons. For example, I cache the users email address, first name and last name. I made a design choice to keep my applications interaction with AD as little as possible, so I do not pull that information from it.

If the controller can find a user record with the same user name as the user name which was provided, we press on. If not, we pop the user over to their ‘My Account’ page and force them to fill out a form which captures the data that we need.

We then save that data to the local database. Auto populating the database with information from AD is left to the gentle reader.

Leave a Reply