#this code borrowed pieces from activeldap and net-ldap require 'rack' require 'net/ldap' require 'net/ntlm' require 'sasl' require 'kconv' module OmniAuth module LDAP class Adaptor class LdapError < StandardError; end class ConfigurationError < StandardError; end class AuthenticationError < StandardError; end class ConnectionError < StandardError; end VALID_ADAPTER_CONFIGURATION_KEYS = [ :hosts, :host, :port, :encryption, :disable_verify_certificates, :bind_dn, :password, :try_sasl, :sasl_mechanisms, :uid, :base, :allow_anonymous, :filter, :tls_options, # Deprecated :method, :ca_file, :ssl_version ] # A list of needed keys. Possible alternatives are specified using sub-lists. MUST_HAVE_KEYS = [ :base, [:encryption, :method], # :method is deprecated [:hosts, :host], [:hosts, :port], [:uid, :filter] ] ENCRYPTION_METHOD = { :simple_tls => :simple_tls, :start_tls => :start_tls, :plain => nil, # Deprecated. This mapping aimed to be user-friendly, but only caused # confusion. Better to pass-through the actual `Net::LDAP` encryption type. :ssl => :simple_tls, :tls => :start_tls, } attr_accessor :bind_dn, :password attr_reader :connection, :uid, :base, :auth, :filter def self.validate(configuration={}) message = [] MUST_HAVE_KEYS.each do |names| names = [names].flatten missing_keys = names.select{|name| configuration[name].nil?} if missing_keys == names message << names.join(' or ') end end raise ArgumentError.new(message.join(",") +" MUST be provided") unless message.empty? end def initialize(configuration={}) Adaptor.validate(configuration) @configuration = configuration.dup @configuration[:allow_anonymous] ||= false @logger = @configuration.delete(:logger) VALID_ADAPTER_CONFIGURATION_KEYS.each do |name| instance_variable_set("@#{name}", @configuration[name]) end config = { base: @base, hosts: @hosts, host: @host, port: @port, encryption: encryption_options } @bind_method = @try_sasl ? :sasl : (@allow_anonymous||!@bind_dn||!@password ? :anonymous : :simple) @auth = sasl_auths({:username => @bind_dn, :password => @password}).first if @bind_method == :sasl @auth ||= { :method => @bind_method, :username => @bind_dn, :password => @password } config[:auth] = @auth @connection = Net::LDAP.new(config) end #:base => "dc=yourcompany, dc=com", # :filter => "(mail=#{user})", # :password => psw def bind_as(args = {}) result = false @connection.open do |me| rs = me.search args if rs and rs.first and dn = rs.first.dn password = args[:password] method = args[:method] || @method password = password.call if password.respond_to?(:call) if method == 'sasl' result = rs.first if me.bind(sasl_auths({:username => dn, :password => password}).first) else result = rs.first if me.bind(:method => :simple, :username => dn, :password => password) end end end result end private def encryption_options translated_method = translate_method return nil unless translated_method { method: translated_method, tls_options: tls_options(translated_method) } end def translate_method method = @encryption || @method method ||= "plain" normalized_method = method.to_s.downcase.to_sym unless ENCRYPTION_METHOD.has_key?(normalized_method) available_methods = ENCRYPTION_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 ENCRYPTION_METHOD[normalized_method] end def tls_options(translated_method) return {} if translated_method == nil # (plain) options = default_options if @tls_options # Prevent blank config values from overwriting SSL defaults configured_options = sanitize_hash_values(@tls_options) configured_options = symbolize_hash_keys(configured_options) options.merge!(configured_options) end # Retain backward compatibility until deprecated configs are removed. options[:ca_file] = @ca_file if @ca_file options[:ssl_version] = @ssl_version if @ssl_version options end def sasl_auths(options={}) auths = [] sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms 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, options) auths << { :method => :sasl, :initial_credential => initial_credential, :mechanism => mechanism, :challenge_response => challenge_response } end auths end def sasl_bind_setup_digest_md5(options) bind_dn = options[:username] initial_credential = "" challenge_response = Proc.new do |cred| pref = SASL::Preferences.new :digest_uri => "ldap/#{@host}", :username => bind_dn, :has_password? => true, :password => options[:password] sasl = SASL.new("DIGEST-MD5", pref) response = sasl.receive("challenge", cred) response[1] end [initial_credential, challenge_response] end def sasl_bind_setup_gss_spnego(options) bind_dn = options[:username] psw = options[:password] raise LdapError.new( "invalid binding information" ) unless (bind_dn && psw) nego = proc {|challenge| t2_msg = Net::NTLM::Message.parse( challenge ) bind_dn, domain = bind_dn.split('\\').reverse t2_msg.target_name = Net::NTLM::encode_utf16le(domain) if domain t3_msg = t2_msg.response( {:user => bind_dn, :password => psw}, {:ntlmv2 => true} ) t3_msg.serialize } [Net::NTLM::Message::Type1.new.serialize, nego] end private def default_options if @disable_verify_certificates # It is important to explicitly set verify_mode for two reasons: # 1. The behavior of OpenSSL is undefined when verify_mode is not set. # 2. The net-ldap gem implementation verifies the certificate hostname # unless verify_mode is set to VERIFY_NONE. { verify_mode: OpenSSL::SSL::VERIFY_NONE } else OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.dup end end # Removes keys that have blank values # # This gem may not always be in the context of Rails so we # do this rather than `.blank?`. def sanitize_hash_values(hash) hash.delete_if do |_, value| value.nil? || (value.is_a?(String) && value !~ /\S/) end end def symbolize_hash_keys(hash) hash.keys.each do |key| hash[key.to_sym] = hash[key] end hash end end end end