From 454f1c897d2210a5822de1d942b6266fed4473ff Mon Sep 17 00:00:00 2001 From: Ping Yu Date: Fri, 1 Oct 2010 20:48:13 -0500 Subject: [PATCH] adding ldap authentication for oa-enterprise gem --- .gitignore | 3 +- oa-enterprise/lib/omniauth/enterprise.rb | 1 + oa-enterprise/lib/omniauth/strategies/ldap.rb | 58 ++++ .../lib/omniauth/strategies/ldap/adaptor.rb | 286 ++++++++++++++++++ oa-enterprise/oa-enterprise.gemspec | 4 +- omniauth/lib/omniauth.rb | 2 + 6 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 oa-enterprise/lib/omniauth/strategies/ldap.rb create mode 100644 oa-enterprise/lib/omniauth/strategies/ldap/adaptor.rb diff --git a/.gitignore b/.gitignore index d48d757..808ae2a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,8 @@ coverage rdoc pkg tmp - +oa-live ## PROJECT::SPECIFIC *.gem .bundle +.project diff --git a/oa-enterprise/lib/omniauth/enterprise.rb b/oa-enterprise/lib/omniauth/enterprise.rb index 531cc55..2e66858 100644 --- a/oa-enterprise/lib/omniauth/enterprise.rb +++ b/oa-enterprise/lib/omniauth/enterprise.rb @@ -3,5 +3,6 @@ require 'omniauth/core' module OmniAuth module Strategies autoload :CAS, 'omniauth/strategies/cas' + autoload :LDAP, 'omniauth/strategies/ldap' end end diff --git a/oa-enterprise/lib/omniauth/strategies/ldap.rb b/oa-enterprise/lib/omniauth/strategies/ldap.rb new file mode 100644 index 0000000..afe27ef --- /dev/null +++ b/oa-enterprise/lib/omniauth/strategies/ldap.rb @@ -0,0 +1,58 @@ +require 'omniauth/enterprise' +require 'net/ldap' + +module OmniAuth + module Strategies + class LDAP + include OmniAuth::Strategy + + autoload :Adaptor, 'omniauth/strategies/ldap/adaptor' + + def initialize(app, title, options = {}) + super(app, options.delete(:name) || :ldap) + @title = title + @adaptor = OmniAuth::Strategies::LDAP::Adaptor.new(options) + end + + protected + + def request_phase + if env['REQUEST_METHOD'] == 'GET' + get_credentials + else + perform + end + end + + def get_credentials + OmniAuth::Form.build(@title) do + text_field 'Login', 'username' + password_field 'Password', 'password' + end.to_response + end + def perform + begin + @adaptor.bind(:bind_dn => request.POST['username'], :password => request.POST['password']) + rescue + fail!(:invalid_credentials) + end + request.POST['auth'] = auth_hash + @env['REQUEST_METHOD'] = 'GET' + @env['PATH_INFO'] = "#{OmniAuth.config.path_prefix}/#{name}/callback" + + @app.call(@env) + end + + def callback_phase + fail!(:invalid_request) + end + + def auth_hash + OmniAuth::Utils.deep_merge(super, { + 'uid' => request.POST['username'] + }) + end + + end + end +end \ No newline at end of file diff --git a/oa-enterprise/lib/omniauth/strategies/ldap/adaptor.rb b/oa-enterprise/lib/omniauth/strategies/ldap/adaptor.rb new file mode 100644 index 0000000..12d1068 --- /dev/null +++ b/oa-enterprise/lib/omniauth/strategies/ldap/adaptor.rb @@ -0,0 +1,286 @@ +require 'rack' +require 'net/ldap' +require 'net/ntlm' +require 'uri' +module OmniAuth + module Strategies + class LDAP + class Adaptor + class LdapError < StandardError; end + class ConfigurationError < StandardError; end + class AuthenticationError < StandardError; end + class ConnectionError < StandardError; end + VALID_ADAPTER_CONFIGURATION_KEYS = [:host, :port, :method, :bind_dn, :password, + :try_sasl, :sasl_mechanisms, :sasl_quiet] + MUST_HAVE_KEYS = [:host, :port, :method] + METHOD = { + :ssl => :simple_tls, + :tls => :start_tls, + :plain => nil + } + attr_reader :bind_dn + + def initialize(configuration={}) + @connection = nil + @disconnected = false + @bound = false + @configuration = configuration.dup + @logger = @configuration.delete(:logger) + message = [] + MUST_HAVE_KEYS.each do |name| + message << name if configuration[name].nil? + end + raise ArgumentError.new(message.join(",") +" MUST be provided") unless message.empty? + VALID_ADAPTER_CONFIGURATION_KEYS.each do |name| + instance_variable_set("@#{name}", configuration[name]) + end + end + + def connect(options={}) + host = options[:host] || @host + method = options[:method] || @method || :plain + port = options[:port] || @port || ensure_port(method) + method = ensure_method(method) + @disconnected = false + @bound = false + @bind_tried = false + config = { + :host => host, + :port => port, + } + config[:encryption] = {:method => method} if method + @connection, @uri, @with_start_tls = + begin + uri = construct_uri(host, port, method == :simple_tls) + with_start_tls = method == :start_tls + puts ({:uri => uri, :with_start_tls => with_start_tls}).inspect + [Net::LDAP::Connection.new(config), uri, with_start_tls] + rescue Net::LDAP::LdapError + raise ConnectionError, $!.message + end + end + + def unbind(options={}) + @connection.close # Net::LDAP doesn't implement unbind. + end + + def bind(options={}) + connect(options) unless connecting? + begin + @bind_tried = true + + bind_dn = (options[:bind_dn] || @bind_dn).to_s + try_sasl = options.has_key?(:try_sasl) ? options[:try_sasl] : @try_sasl + + # Rough bind loop: + # Attempt 1: SASL if available + # Attempt 2: SIMPLE with credentials if password block + # Attempt 3: SIMPLE ANONYMOUS if 1 and 2 fail (or pwblock returns '') + if try_sasl and sasl_bind(bind_dn, options) + puts "bind with sasl" + elsif simple_bind(bind_dn, options) + puts "bind with simple" + else + message = yield if block_given? + message ||= ('All authentication methods for %s exhausted.') % target + raise AuthenticationError, message + end + + @bound = true + rescue Net::LDAP::LdapError + raise AuthenticationError, $!.message + end + end + + def disconnect!(options={}) + unbind(options) + @connection = @uri = @with_start_tls = nil + @disconnected = true + end + + def rebind(options={}) + unbind(options) if bound? + connect(options) + end + + def connecting? + !@connection.nil? and !@disconnected + end + + def bound? + connecting? and @bound + end + + + private + def execute(method, *args, &block) + result = @connection.send(method, *args, &block) + message = nil + if result.is_a?(Hash) + message = result[:errorMessage] + result = result[:resultCode] + end + unless result.zero? + message = [Net::LDAP.result2string(result), message].compact.join(": ") + raise LdapError, message + end + end + + def ensure_port(method) + if method == :ssl + URI::LDAPS::DEFAULT_PORT + else + URI::LDAP::DEFAULT_PORT + end + end + + def prepare_connection(options) + end + + + def need_credential_sasl_mechanism?(mechanism) + not %(GSSAPI EXTERNAL ANONYMOUS).include?(mechanism) + end + + def ensure_method(method) + method ||= "plain" + normalized_method = method.to_s.downcase.to_sym + return METHOD[normalized_method] if METHOD.has_key?(normalized_method) + + available_methods = METHOD.keys.collect {|m| m.inspect}.join(", ") + format = "%s is not one of the available connect methods: %s" + raise ConfigurationError, format % [method.inspect, available_methods] + end + + def sasl_bind(bind_dn, options={}) + if options.has_key?(:sasl_quiet) + sasl_quiet = options[:sasl_quiet] + else + sasl_quiet = @sasl_quiet + end + + sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms + begin + sasl_mechanisms.each do |mechanism| + normalized_mechanism = mechanism.downcase.gsub(/-/, '_') + sasl_bind_setup = "sasl_bind_setup_#{normalized_mechanism}" + next unless respond_to?(sasl_bind_setup, true) + initial_credential, challenge_response = + send(sasl_bind_setup, bind_dn, options) + args = { + :method => :sasl, + :initial_credential => initial_credential, + :mechanism => mechanism, + :challenge_response => challenge_response, + } + info = { + :name => "bind: SASL", :dn => bind_dn, :mechanism => mechanism, + } + puts info.inspect + return true if execute(:bind, args) + end + rescue Exception => e + puts e.message + false + end + false + end + + def parse_sasl_digest_md5_credential(cred) + params = {} + cred.scan(/(\w+)=(\"?)(.+?)\2(?:,|$)/) do |name, sep, value| + params[name] = value + end + params + end + CHARS = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + def generate_client_nonce(size=32) + nonce = "" + size.times do |i| + nonce << CHARS[rand(CHARS.size)] + end + nonce + end + def sasl_bind_setup_digest_md5(bind_dn, options) + initial_credential = "" + nonce_count = 1 + challenge_response = Proc.new do |cred| + params = parse_sasl_digest_md5_credential(cred) + qops = params["qop"].split(/,/) + unless qops.include?("auth") + raise ActiveLdap::AuthenticationError, + _("unsupported qops: %s") % qops.inspect + end + qop = "auth" + server = @connection.instance_variable_get("@conn").addr[2] + realm = params['realm'] + uri = "ldap/#{server}" + nc = "%08x" % nonce_count + nonce = params["nonce"] + cnonce = generate_client_nonce + requests = { + :username => bind_dn.inspect, + :realm => realm.inspect, + :nonce => nonce.inspect, + :cnonce => cnonce.inspect, + :nc => nc, + :qop => qop, + :maxbuf => "65536", + "digest-uri" => uri.inspect, + } + a1 = "#{bind_dn}:#{realm}:#{@password}" + a1 = "#{Digest::MD5.digest(a1)}:#{nonce}:#{cnonce}" + ha1 = Digest::MD5.hexdigest(a1) + a2 = "AUTHENTICATE:#{uri}" + ha2 = Digest::MD5.hexdigest(a2) + response = "#{ha1}:#{nonce}:#{nc}:#{cnonce}:#{qop}:#{ha2}" + requests["response"] = Digest::MD5.hexdigest(response) + nonce_count += 1 + requests.collect do |key, value| + "#{key}=#{value}" + end.join(",") + end + [initial_credential, challenge_response] + end + def sasl_bind_setup_gss_spnego(bind_dn, options) + puts options.inspect + user,psw = [bind_dn, @password] + raise LdapError.new( "invalid binding information" ) unless (user && psw) + + nego = proc {|challenge| + t2_msg = Net::NTLM::Message.parse( challenge ) + user, domain = user.split('\\').reverse + t2_msg.target_name = Net::NTLM::encode_utf16le(domain) if domain + t3_msg = t2_msg.response( {:user => user, :password => psw}, {:ntlmv2 => true} ) + t3_msg.serialize + } + [Net::NTLM::Message::Type1.new.serialize, nego] + end + + def simple_bind(bind_dn, options={}) + args = { + :method => :simple, + :username => bind_dn, + :password => @password, + } + execute(:bind, args) + true + end + + def construct_uri(host, port, ssl) + protocol = ssl ? "ldaps" : "ldap" + URI.parse("#{protocol}://#{host}:#{port}").to_s + end + + def target + return nil if @uri.nil? + if @with_start_tls + "#{@uri}(StartTLS)" + else + @uri + end + end + end + end + end +end \ No newline at end of file diff --git a/oa-enterprise/oa-enterprise.gemspec b/oa-enterprise/oa-enterprise.gemspec index dd98825..5521c6e 100644 --- a/oa-enterprise/oa-enterprise.gemspec +++ b/oa-enterprise/oa-enterprise.gemspec @@ -9,12 +9,14 @@ Gem::Specification.new do |gem| gem.description = %Q{Enterprise strategies for OmniAuth.} gem.email = "james.a.rosen@gmail.com" gem.homepage = "http://github.com/intridea/omniauth" - gem.authors = ["James A. Rosen"] + gem.authors = ["James A. Rosen", "Ping Yu"] gem.files = Dir.glob("{lib}/**/*") + %w(README.rdoc LICENSE.rdoc CHANGELOG.rdoc) gem.add_dependency 'oa-core', version gem.add_dependency 'nokogiri', '~> 1.4.2' + gem.add_dependency 'net-ldap', '~> 0.1.1' + gem.add_dependency 'rubyntlm', '~> 0.1.1' eval File.read(File.join(File.dirname(__FILE__), '../development_dependencies.rb')) end diff --git a/omniauth/lib/omniauth.rb b/omniauth/lib/omniauth.rb index f14240b..ad1f3aa 100644 --- a/omniauth/lib/omniauth.rb +++ b/omniauth/lib/omniauth.rb @@ -2,3 +2,5 @@ require 'omniauth/core' require 'omniauth/basic' require 'omniauth/oauth' require 'omniauth/openid' +require 'omniauth/enterprise' +