Today, I pushed a branch to my fork of authlogic_example: with-activeldap.

This branch shows a way of implementing pass-through authentication to an LDAP server using ActiveLdap and Authlogic, with just some small changes to the User and UserSession models.


First, we’ll need to bring in the net-ldap and activeldap gems. We edit config/environment.rb to include the following two lines:

config.gem "net-ldap", :lib => false, :version => '>=0.0.5'
config.gem "activeldap", :lib => "active_ldap", :version => '1.0.9'

If you use sudo gem install net-ldap, you’ll get 0.0.4. Here, I’ve built the 0.0.5 gem from the GitHub repo. This is because there are some bug fixes in the GitHub master that aren’t in the RubyForge gem. In the with-activeldap branch, the two gems are vendored.

Now, we create config/ldap.yml to configure ActiveLdap’s connection to our LDAP server. Here’s mine:

development:
  host: 127.0.0.1
  base: dc=dev,dc=Asuka,dc=local
  bind_dn: cn=Manager,dc=dev,dc=Asuka,dc=local
  password: secret

test:
  host: 127.0.0.1
  base: dc=test,dc=Asuka,dc=local
  bind_dn: cn=Manager,dc=Asuka,dc=local
  password: secret

production:
  host: 127.0.0.1
  base: dc=prod,dc=Asuka,dc=local
  bind_dn: cn=Manager,dc=Asuka,dc=local
  password: secret

This tells ActiveLdap the server/port to connect to, what the base entry for our LDAP objects is, and what user to bind as for operations on the server. Now, we create a LdapUser class to represent user entries in the LDAP:

class LdapUser < ActiveLdap::Base
  ldap_mapping :dn_attribute => "uid",
    :scope => :sub,
    :prefix => "o=users"
end

This defines an LDAP user as being an entry from the o=users organization, where all of the entries are distinguished by the uid attribute. Now, we should be able to use the console to list all of our LDAP users like so:

 >> LdapUser.all

Now, we’ll add some methods to the User model that allow us to look up users by login in the LDAP and create entries in the database if they don’t already exist. We’ll also need a method for forwarding the credentials provided on the login form to the LDAP and see if they are valid:

class User < ActiveRecord::Base
  acts_as_authentic do |c|
    # Don't validate password, since that will be held in the LDAP
    c.validate_password_field = false
  end
 
  def ldap_entry
    LdapUser.find(self.login)
  end
 
  # Tries to find a User first by looking into the database and then by
  # creating a User if there's an LDAP entry for the given login
  def self.find_or_create_from_ldap(login)
    find_by_login(login) || create_from_ldap_if_valid(login)
  end
 
  # Creates a User record in the database if there is an entry in the LDAP
  # with the given login
  def self.create_from_ldap_if_valid(login)
    begin
      User.create(:login => login) if LdapUser.find(login)
    rescue ActiveLdap::EntryNotFound
      nil # Don't do anything since we can't find an entry
    end
  end
 
  protected
    # Authenticates the user against the LDAP.
    def valid_ldap_credentials?(password_plaintext)
      ldap_entry.bind(password_plaintext)
      ldap_entry.remove_connection
      true
    rescue ActiveLdap::AuthenticationError, ActiveLdap::LdapError::UnwillingToPerform
      false
    end
end

With this, there is no longer a need for the crypted_password and password_salt columns in the users table, so if those columns exist, you’ll have to write a migration to remove them (or at least allow NULL values for them). Now, we modify the UserSession to use our custom methods for looking up users and validating their credentials:

class UserSession < Authlogic::Session::Base
  find_by_login_method :find_or_create_from_ldap
  verify_password_method :valid_ldap_credentials?
end

With that, we should now be able to log in as a user by providing her uid as the login and the password. LDAP pass-through authentication achieved! There is a downside though: ActiveLdap is not particularly efficient with its queries but this can be mitigated by storing the user’s LDAP entry (the LdapUser object) in the User instance when it is first looked up.

def ldap_entry
  @ldap_entry ||= LdapUser.find(self.login)
end

In a future post, I will extend this further by modeling LDAP groups and bringing in declarative_authorization to implement role-based access control based on LDAP group membership.