Skip to content

Commit

Permalink
Merge pull request #226 from satoryu/define_auth_adapters
Browse files Browse the repository at this point in the history
Define auth adapters
  • Loading branch information
jch committed Oct 13, 2015
2 parents 4157684 + 8be5224 commit 4667a08
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 121 deletions.
6 changes: 6 additions & 0 deletions lib/net/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ class LDAP
require 'net/ldap/connection'
require 'net/ldap/version'
require 'net/ldap/error'
require 'net/ldap/auth_adapter'
require 'net/ldap/auth_adapter/simple'
require 'net/ldap/auth_adapter/sasl'

Net::LDAP::AuthAdapter.register([:simple, :anon, :anonymous], Net::LDAP::AuthAdapter::Simple)
Net::LDAP::AuthAdapter.register(:sasl, Net::LDAP::AuthAdapter::Sasl)

# == Quick-start for the Impatient
# === Quick Example of a user-authentication against an LDAP directory:
Expand Down
29 changes: 29 additions & 0 deletions lib/net/ldap/auth_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module Net
class LDAP
class AuthAdapter
def self.register(names, adapter)
names = Array(names)
@adapters ||= {}
names.each do |name|
@adapters[name] = adapter
end
end

def self.[](name)
a = @adapters[name]
if a.nil?
raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (#{name})"
end
return a
end

def initialize(conn)
@connection = conn
end

def bind
raise "bind method must be overwritten"
end
end
end
end
40 changes: 40 additions & 0 deletions lib/net/ldap/auth_adapter/gss_spnego.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'net/ldap/auth_adapter'
require 'net/ldap/auth_adapter/sasl'

module Net
class LDAP
module AuthAdapers
#--
# PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
# Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
# integrate it without introducing an external dependency.
#
# This authentication method is accessed by calling #bind with a :method
# parameter of :gss_spnego. It requires :username and :password
# attributes, just like the :simple authentication method. It performs a
# GSS-SPNEGO authentication with the server, which is presumed to be a
# Microsoft Active Directory.
#++
class GSS_SPNEGO < Net::LDAP::AuthAdapter
def bind(auth)
require 'ntlm'

user, psw = [auth[:username] || auth[:dn], auth[:password]]
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)

nego = proc { |challenge|
t2_msg = NTLM::Message.parse(challenge)
t3_msg = t2_msg.response({ :user => user, :password => psw },
{ :ntlmv2 => true })
t3_msg.serialize
}

Net::LDAP::AuthAdapter::Sasl.new(@connection).
bind(:method => :sasl, :mechanism => "GSS-SPNEGO",
:initial_credential => NTLM::Message::Type1.new.serialize,
:challenge_response => nego)
end
end
end
end
end
60 changes: 60 additions & 0 deletions lib/net/ldap/auth_adapter/sasl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
require 'net/ldap/auth_adapter'

module Net
class LDAP
class AuthAdapter
class Sasl < Net::LDAP::AuthAdapter
#--
# Required parameters: :mechanism, :initial_credential and
# :challenge_response
#
# Mechanism is a string value that will be passed in the SASL-packet's
# "mechanism" field.
#
# Initial credential is most likely a string. It's passed in the initial
# BindRequest that goes to the server. In some protocols, it may be empty.
#
# Challenge-response is a Ruby proc that takes a single parameter and
# returns an object that will typically be a string. The
# challenge-response block is called when the server returns a
# BindResponse with a result code of 14 (saslBindInProgress). The
# challenge-response block receives a parameter containing the data
# returned by the server in the saslServerCreds field of the LDAP
# BindResponse packet. The challenge-response block may be called multiple
# times during the course of a SASL authentication, and each time it must
# return a value that will be passed back to the server as the credential
# data in the next BindRequest packet.
#++
def bind(auth)
mech, cred, chall = auth[:mechanism], auth[:initial_credential],
auth[:challenge_response]
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (mech && cred && chall)

message_id = @connection.next_msgid

n = 0
loop {
sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
request = [
Net::LDAP::Connection::LdapVersion.to_ber, "".to_ber, sasl
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)

@connection.send(:write, request, nil, message_id)
pdu = @connection.queued_read(message_id)

if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
raise Net::LDAP::NoBindResultError, "no bind result"
end

return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress
raise Net::LDAP::SASLChallengeOverflowError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)

cred = chall.call(pdu.result_server_sasl_creds)
}

raise Net::LDAP::SASLChallengeOverflowError, "why are we here?"
end
end
end
end
end
34 changes: 34 additions & 0 deletions lib/net/ldap/auth_adapter/simple.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require 'net/ldap/auth_adapter'

module Net
class LDAP
class AuthAdapter
class Simple < AuthAdapter
def bind(auth)
user, psw = if auth[:method] == :simple
[auth[:username] || auth[:dn], auth[:password]]
else
["", ""]
end

raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)

message_id = @connection.next_msgid
request = [
Net::LDAP::Connection::LdapVersion.to_ber, user.to_ber,
psw.to_ber_contextspecific(0)
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)

@connection.send(:write, request, nil, message_id)
pdu = @connection.queued_read(message_id)

if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
raise Net::LDAP::NoBindResultError, "no bind result"
end

pdu
end
end
end
end
end
123 changes: 2 additions & 121 deletions lib/net/ldap/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -250,130 +250,11 @@ def next_msgid
def bind(auth)
instrument "bind.net_ldap_connection" do |payload|
payload[:method] = meth = auth[:method]
if [:simple, :anonymous, :anon].include?(meth)
bind_simple auth
elsif meth == :sasl
bind_sasl(auth)
elsif meth == :gss_spnego
bind_gss_spnego(auth)
else
raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (#{meth})"
end
end
end

#--
# Implements a simple user/psw authentication. Accessed by calling #bind
# with a method of :simple or :anonymous.
#++
def bind_simple(auth)
user, psw = if auth[:method] == :simple
[auth[:username] || auth[:dn], auth[:password]]
else
["", ""]
end

raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)

message_id = next_msgid
request = [
LdapVersion.to_ber, user.to_ber,
psw.to_ber_contextspecific(0)
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)

write(request, nil, message_id)
pdu = queued_read(message_id)

if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
raise Net::LDAP::NoBindResultError, "no bind result"
adapter = Net::LDAP::AuthAdapter[meth]
adapter.new(self).bind(auth)
end

pdu
end

#--
# Required parameters: :mechanism, :initial_credential and
# :challenge_response
#
# Mechanism is a string value that will be passed in the SASL-packet's
# "mechanism" field.
#
# Initial credential is most likely a string. It's passed in the initial
# BindRequest that goes to the server. In some protocols, it may be empty.
#
# Challenge-response is a Ruby proc that takes a single parameter and
# returns an object that will typically be a string. The
# challenge-response block is called when the server returns a
# BindResponse with a result code of 14 (saslBindInProgress). The
# challenge-response block receives a parameter containing the data
# returned by the server in the saslServerCreds field of the LDAP
# BindResponse packet. The challenge-response block may be called multiple
# times during the course of a SASL authentication, and each time it must
# return a value that will be passed back to the server as the credential
# data in the next BindRequest packet.
#++
def bind_sasl(auth)
mech, cred, chall = auth[:mechanism], auth[:initial_credential],
auth[:challenge_response]
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (mech && cred && chall)

message_id = next_msgid

n = 0
loop {
sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
request = [
LdapVersion.to_ber, "".to_ber, sasl
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)

write(request, nil, message_id)
pdu = queued_read(message_id)

if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
raise Net::LDAP::NoBindResultError, "no bind result"
end

return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress
raise Net::LDAP::SASLChallengeOverflowError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)

cred = chall.call(pdu.result_server_sasl_creds)
}

raise Net::LDAP::SASLChallengeOverflowError, "why are we here?"
end
private :bind_sasl

#--
# PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
# Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
# integrate it without introducing an external dependency.
#
# This authentication method is accessed by calling #bind with a :method
# parameter of :gss_spnego. It requires :username and :password
# attributes, just like the :simple authentication method. It performs a
# GSS-SPNEGO authentication with the server, which is presumed to be a
# Microsoft Active Directory.
#++
def bind_gss_spnego(auth)
require 'ntlm'

user, psw = [auth[:username] || auth[:dn], auth[:password]]
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)

nego = proc { |challenge|
t2_msg = NTLM::Message.parse(challenge)
t3_msg = t2_msg.response({ :user => user, :password => psw },
{ :ntlmv2 => true })
t3_msg.serialize
}

bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO",
:initial_credential => NTLM::Message::Type1.new.serialize,
:challenge_response => nego)
end
private :bind_gss_spnego


#--
# Allow the caller to specify a sort control
#
Expand Down
11 changes: 11 additions & 0 deletions test/test_auth_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'test_helper'

class TestAuthAdapter < Test::Unit::TestCase
def test_undefined_auth_adapter
flexmock(TCPSocket).should_receive(:new).ordered.with('ldap.example.com', 379).once.and_return(nil)
conn = Net::LDAP::Connection.new(host: 'ldap.example.com', port: 379)
assert_raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (foo)" do
conn.bind(method: :foo)
end
end
end

0 comments on commit 4667a08

Please sign in to comment.