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 && @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.
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!