Simplified Active Directory User Authentication

Comments

A central source for user authentication is a worthwhile thing for a company to have. Unfortunately, for much of the corporate world, this means Microsoft Active Directory. Hey, it's not all bad. At least Ruby applications can access Active Directory with the ruby-net-ldap gem. Still, code in a Rails application which has to deal with LDAP attributes can look downright foreign next to all of your nifty English-looking attribute names, and ActiveLdap, while very cool, is overkill for simple tasks. Sometimes you just want a simple way to get at a few key attributes about your users once they log in. Maybe you're looking to cache that info in a local database table so that you can enforce database integrity checks, for instance? Here's a single file you can just drop into your Rails (or plain old Ruby) project and use any way you see fit.

Authenticating is half the battle

In my case, most of my Rails projects end up having an ActiveRecord-based User model. I'm not looking at giving that up anytime soon -- pretty much every app I write maintains some sort of concept of "ownership" over tickets, notes, or something, and I like keeping that in the database for the convenience of has_many and belongs_to and foreign key constraints. Still, if I'm going to go through the hassle of authenticating against the corporate directory server anyway, there's no reason not to take a few more steps to make the thing more readily reusable by my coworkers, and make certain user attributes more readily available for database caching by exposing them using attribute names similar to what we tend to use in ActiveRecord.

And hey, while we're at it, some of the values stored in these attributes are just downright goofy in format. Let's provide a configurable way to override the values we get back from the Net::LDAP::Entry in our accessors.

Authentication. Nothing to see here.

OK, so we want this thing to behave much like one of the typical ActiveRecord-based user authentication schemes. This means we're going to want to call it like so:

    user = ActiveDirectoryUser.authenticate('username','password')

And then be able to call things like user.first_name, user.last_name, user.login, and so forth. So let's get the boring part out of the way first, the simple authentication. There are a bunch of tutorials about doing authentication against an LDAP directory, but unfortunately many of them stop after that. We'll build something a little bit more useful. But first, let's get the boring stuff out of the way:

    require 'net/ldap' # gem install ruby-net-ldap

    class ActiveDirectoryUser
      ### BEGIN CONFIGURATION ###
      SERVER = 'ad01.company.com'   # Active Directory server name or IP
      PORT = 389                    # Active Directory server port (default 389)
      BASE = 'DC=company,DC=com'    # Base to search from
      DOMAIN = 'company.com'        # For simplified user@domain format login
      ### END CONFIGURATION ###

      def self.authenticate(login, pass)
        return false if login.empty? or pass.empty?

        conn = Net::LDAP.new :host => SERVER,
                             :port => PORT,
                             :base => BASE,
                             :auth => { :username => "#{login}@#{DOMAIN}",
                                        :password => pass,
                                        :method => :simple }
        if conn.bind
          return true
        else
          return false
        end
      # If we don't rescue this, Net::LDAP is decidedly ungraceful about failing
      # to connect to the server. We'd prefer to say authentication failed.
      rescue Net::LDAP::LdapError => e
        return false
      end
    end

This is the basic authentication code you'll see most of the time. It just tries to bind using the supplied username and password, and if it succeeds, returns true. That's suitable if you just want a yes/no answer as to whether the person knows their password, but somewhat less useful if you intend to actually do anything that in anyway involves information about this user. Let's change a few lines:

        if conn.bind and user = conn.search(:filter => "sAMAccountName=#{login}").first
          return self.new(user)
        else
          return nil
        end

Nothing magic going on here. We're just pulling back the user's LDAP entry if we succeed in logging in. sAMAccountName is the Active Directory attribute that matches the user's login name, and Net::LDAP's search method returns results in an array, so by grabbing its first element, we'll get the Net::LDAP::Entry object instead (and conveniently return nil if we didn't find anything, since we don't want to log in if the user somehow authenticated but doesn't have a sAMAccountName). While we're at it, we'll also switch those other places where we were returning false to instead return nil. Nice! Now we're attempting to return a new instance of ActiveDirectoryUser instantiated with the LDAP entry!

Which, of course, doesn't work, because we haven't written an initialize method.

A more flexible initialize

OK. So we need to do something in the class's initialize method. At the very least, we might want to assign a few instance variables from values contained in the LDAP entry we sent over. Something like:

    def initialize(entry)
      @login = entry.samaccountname # case doesn't matter to Net::LDAP
      @first_name = entry.givenname # givenName is LDAPese for first name.
      @email = entry.mail # Holy crap, this attribute is logical!
    end

Then we could add some attr_readers and call it a day, right? Well, I guess. But a little bit of metaprogramming could make this thing much more easily configurable for new attributes. How about placing a hash or something up top, in that configuration section? Something with our desired attribute names as keys, and the goofy LDAP attributes as values. Then, we could use some Ruby magic to generate readers for anything we like.

Of course, if you've ever used an LDAP browser, you'll know there are single value and multi-value attributes. The difference is that the latter can have multiple occurrences in the same directory entry. For instance, a person can have only one givenName, so givenName is a single valued attribute. However, a person can belong to multiple groups, which is why memberOf is a multi-value attribute. Net::LDAP doesn't care one way or the other. It returns all attributes in an array.

We'd rather have things nice and predictable for the developer using our class, though -- let's only return an array for multi-value attributes. We'll handle single valued attributes in one configuration hash, and multi-valued in another. While we're at it, it would be really handy if we could also configure some simple preprocessing on these attributes if they aren't quite in the format we would like to work with. Let's see what we can do.

In the configuration section:

      # ATTR_SV is for single valued attributes only. Generated readers will
      # convert the value to a string before returning or calling your Proc.
      ATTR_SV = {
                  :login => :samaccountname,
                  :first_name => :givenname,
                  :last_name => :sn,
                  :email => :mail
                }


      # ATTR_MV is for multi-valued attributes. Generated readers will always 
      # return an array.
      ATTR_MV = {
                  :groups => [ :memberof,
                               # Get the simplified name of first-level groups.
                               # TODO: Handle escaped special characters
                               Proc.new {|g| g.sub(/.*?CN=(.*?),.*/, '\1')} ]
                }

You'll notice that we've decided to accomplish the preprocessing of attributes by using a Proc (optionally) on the value side of our hashes. We can check to see if a Proc was supplied in our initializer, and if so, pass the value to it, or use it as the block for an Array#collect, depending on what type of attribute we're dealing with.

A dash of metaprogramming

So now we have our handy configuration section all fleshed out. The only thing left to do is set up the method generation for all of our shiny new attributes:

      def initialize(entry)
        @entry = entry
        self.class.class_eval do
          generate_single_value_readers
          generate_multi_value_readers
        end
      end

      def self.generate_single_value_readers
        ATTR_SV.each_pair do |k, v|
          val, block = Array(v)
          define_method(k) do
            if @entry.attribute_names.include?(val)
              if block.is_a?(Proc)
                return block[@entry.send(val).to_s]
              else
                return @entry.send(val).to_s
              end
            else
              return ''
            end
          end
        end
      end

      def self.generate_multi_value_readers
        ATTR_MV.each_pair do |k, v|
          val, block = Array(v)
          define_method(k) do
            if @entry.attribute_names.include?(val)
              if block.is_a?(Proc)
                return @entry.send(val).collect(█)
              else
                return @entry.send(val)
              end
            else
              return []
            end
          end
        end
      end

So, rather than set a bunch of instance variables for the attributes we're interested in, we're just going to store the LDAP entry, and use our custom attribute readers to pluck what we want out of it, and do whatever processing we need done. If you're unfamiliar with it, the self.class.class_eval block in initialize is one of the bread and butter features of Ruby metaprogramming. The code inside this block gets executed as though it was in the scope of the class itself, rather than an instance, which lets us use Module#define_method to set up brand new methods based on our configuration hashes.

The code in the two generate_* methods isn't all that complicated. The only two things to note are that they are defined in the class's scope using self, and that we are casting the sent value to an array first, since we don't know if we'll get the optional Proc element or just a symbol. That way we can just do multiple assignment and let the block be nil if no Proc was supplied. If block is a Proc, then we go ahead and use it to format our output.

That about covers it. Here's the finished model, fully commented: ActiveDirectoryUser

comments powered by Disqus