System Test Active Directory Authentication in Ruby

I recently added support for authenticating users against an Active Directory server from a Ruby on Rails web application. I came across a few Ruby libraries for connecting to Active Directory, but in the end my needs were met with the net-ldap gem, the environment_configurable gem and a few lines of code:


class ActiveDirectory
  include EnvironmentConfigurable
  configure_with "config/active_directory.yml"

  def self.valid_credentials?(login, password)
    ldap = Net::LDAP.new(
      :host => config.host, # active_directory.atomicobject.com
      :port => config.port, # 389
      :base => config.base, # dc=atomicobject,dc=com
      :auth => {
        :method => :simple,
        :username => config.server_username, # cn=server,dc=atomicobject,dc=com
        :password => config.server_password  # server password
      }
    )
    filter = Net::LDAP::Filter.eq('sAMAccountName', login)
    if ldap.bind_as(:filter => filter, :password => password)
      true
    else
      Rails.logger.error("Active Directory validation failed for '#{login}': #{ldap.get_operation_result.message}")
      false
    end
  end
end

I wanted to test the Active Directory integration from our Cucumber test suite, but did not want to have to rely on a real Active Directory server being configured and available in order to run the tests. Searching turned up this 2006 article about returning ActiveRecord models in response to LDAP queries using the ldap-activerecord-gateway, which is built on the ruby-ldapserver library.

I only needed a small portion of what the ldap-activerecord-gateway provided so I instead put together some very targeted Cucumber helpers using just ruby-ldapserver and knowledge gleaned from the ldap-activerecord-gateway source.

First a module that provides helpers for Cucumber steps to start the server, stop the server, and to set valid username/password combinations:


require 'ldap/server'
module TestLdapServer

  def start_ldap_server
    logger = Logger.new(File.join(Rails.root, "log", "ldap.log"))
    logger.level = Logger::DEBUG
    logger.datetime_format = "%H:%M:%S"

    @ldap_credentials = {"cn=server,dc=atomicobject,dc=com" => "server password"}
    @ldap_server = LDAP::Server.new({
      :port             => 3890,
      :bindaddr         => '0.0.0.0',
      :nodelay          => true,
      :listen           => 1,
      :namingContexts   => ["dc=atomicobject,dc=com"],
      :operation_class  => LdapOperation,
      :operation_args   => [@ldap_credentials, logger]
    })

    # This spawns a new thread, so tests can keep running in this thread
    @ldap_server.run_tcpserver
  end

  def stop_ldap_server
    if @ldap_server
      @ldap_server.stop
      @ldap_server = nil
    end
  end

  def set_ldap_login(login, password)
    @ldap_credentials[login] = password
  end
end

World(TestLdapServer)

Next a subclass of LDAP::Server::Operation that will handle incoming requests. To support the requests needed to authenticate against an Active Directory server both the simple_bind and search methods need to be implemented.


class LdapOperation < LDAP::Server::Operation
  def initialize(connection, message_id, valid_credentials, logger)
    super(connection, message_id)
    @logger = logger
    @valid_credentials = valid_credentials
  end

  def simple_bind(version, dn, password)
    @logger.info "Got a simple_bind version: #{version.inspect}, dn: #{dn.inspect}, password #{password.inspect}"
    if version != 3
      @logger.info "Incorrect simple_bind version: #{version.inspect}"
      raise LDAP::ResultError::ProtocolError, "version 3 only"
    end

    @logger.debug "Compare expected password #{password} with #{@valid_credentials[dn]}"
    if @valid_credentials[dn] == password
      @logger.debug "#{password} is a match"
    else
      @logger.debug "Bad password '#{password}' for #{dn}"
      raise LDAP::ResultError::InvalidCredentials, "Bad credentials"
    end
  end

  def search(basedn, scope, deref, filter)
    @logger.debug "Got search. basedn: #{basedn.inspect}, scope: #{scope.inspect}, deref: #{deref.inspect}, filter: #{filter.inspect}"

    full_dn = "#{filter[1]}=#{filter.last},#{basedn}"
    unless filter[0] == :eq &#38;&#38; @valid_credentials.has_key?(full_dn)
      @logger.debug "Unexpected search '#{basedn.inspect}' #{filter.inspect}"
      raise LDAP::ResultError::UnwillingToPerform, "Invalid"
    end

    account_name = filter.last
    ret = {
      "objectclass"     => ["top", "person", "organizationalPerson", "user"],
      "sAMAccountName"  => [filter.last],
      "sn"              => ["fake_sn"],
      "givenName"       => ["fake_givenName"],
      "cn"              => ["fake_cn"],
    }
    send_SearchResultEntry(full_dn, ret)
  end
end

I only want the test LDAP server to be started for Cucumber scenarios that need it so I set up a Before and After block with a tag. Only those scenarios/features tagged with @active_directory will have the test server available.


Before('@active_directory') do
  start_ldap_server
end
After('@active_directory') do
  stop_ldap_server
end

Now just call set_ldap_login from a Cucumber step before the application tries to authenticate against the server and your code will be working against a real(ish) LDAP server.

Conversation
  • Jason Lewis says:

    Interesting. I recently had to authenticate a Rails app against AD, but went about it a little differently. Our enterprise auth team frowns on LDAP, but recently approved Likewise. As we’re running on Linux, and Likewise ties PAM authentication into AD, I updated the rpam gem to use Ruby 1.9.2 and used the authlogic-pam plugin to tie into PAM, and thus AD. It’s been working like a champ. Nice to see there are other options for achieving this, though!

  • Comments are closed.