Class: OmniAuth::LDAP::Adaptor

Inherits:
Object
  • Object
show all
Includes:
OmniAuth::LDAP::AUTH_SANITIZER::FilteredAttributes
Defined in:
lib/omniauth/ldap/adaptor.rb,
sig/omniauth/ldap/adaptor.rbs

Overview

Note:

Public API: Adaptor.validate, #initialize, #bind_as, and attr readers such as #connection, #uid

Adaptor encapsulates the behavior required to connect to an LDAP server and perform searches and binds. It maps user-provided configuration into a Net::LDAP connection and provides compatibility helpers for different net-ldap and SASL versions. The adaptor is intentionally defensive and provides a small, stable API used by the OmniAuth strategy.

Examples:

Initialize with minimal config

adaptor = OmniAuth::LDAP::Adaptor.new(base: 'dc=example,dc=com', host: 'ldap.example.com')

Defined Under Namespace

Classes: AuthenticationError, ConfigurationError, ConnectionError, LdapError

Constant Summary collapse

VALID_ADAPTER_CONFIGURATION_KEYS =

Valid configuration keys accepted by the adaptor. These correspond to the options supported by the gem and are used during initialization.

Returns:

  • (Array<Symbol>)
[
  :hosts,
  :host,
  :port,
  :encryption,
  :disable_verify_certificates,
  :bind_dn,
  :password,
  :try_sasl,
  :sasl_mechanisms,
  :uid,
  :base,
  :allow_anonymous,
  :filter,
  :tls_options,
  :password_policy,
  # Timeouts
  :connect_timeout,
  :read_timeout,
  :method
]
MUST_HAVE_KEYS =

Required configuration keys. This may include alternatives as sub-lists (e.g., [:hosts, :host] means either key is acceptable).

Returns:

  • (Array)
[
  :base,
  [:encryption, :method], # :method is deprecated
  [:hosts, :host],
  [:hosts, :port],
  [:uid, :filter]
]
ENCRYPTION_METHOD =

Supported encryption method mapping for configuration readability.

Returns:

  • (Hash<Symbol,Symbol,nil>)
{
  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
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(configuration = {}) ⇒ OmniAuth::LDAP::Adaptor

Create a new adaptor instance backed by a Net::LDAP connection.

The constructor does not immediately open a network connection but prepares the Net::LDAP instance according to the provided configuration. It also applies timeout settings where supported by the installed net-ldap version.

Parameters:

  • configuration (Hash) (defaults to: {})

    user-provided configuration options

  • (Hash[Symbol, untyped])

Raises:



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/omniauth/ldap/adaptor.rb', line 157

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
  }
  # Remove passing timeouts here to avoid issues on older net-ldap versions.
  # We'll set them after initialization if the connection responds to writers.
  @bind_method = if @try_sasl
    :sasl
  else
    ((@allow_anonymous || !@bind_dn || !@password) ? :anonymous : :simple)
  end

  @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)
  # Apply optional timeout settings if supported by the installed net-ldap version
  if !@connect_timeout.nil?
    if @connection.respond_to?(:connect_timeout=)
      @connection.connect_timeout = @connect_timeout
    else
      @connection.instance_variable_set(:@connect_timeout, @connect_timeout)
    end
  end
  if !@read_timeout.nil?
    if @connection.respond_to?(:read_timeout=)
      @connection.read_timeout = @read_timeout
    else
      @connection.instance_variable_set(:@read_timeout, @read_timeout)
    end
  end
end

Instance Attribute Details

#authHash[Symbol, untyped] (readonly)

auth is the hash passed to Net::LDAP#auth or similar

Returns:

  • (Hash[Symbol, untyped])


126
# File 'lib/omniauth/ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

#baseString (readonly)

The base DN for searches.

Returns:

  • (String)


126
# File 'lib/omniauth/ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

#bind_dnString?

The distinguished name used for binding when provided in configuration.

Parameters:

  • value (String, nil)

Returns:

  • (String, nil)


99
100
101
# File 'lib/omniauth/ldap/adaptor.rb', line 99

def bind_dn
  @bind_dn
end

#connectionNet::LDAP (readonly)

Net::LDAP is provided by the net-ldap gem; we reference it here for clarity.

Returns:



126
127
128
# File 'lib/omniauth/ldap/adaptor.rb', line 126

def connection
  @connection
end

#filterString? (readonly)

filter is an LDAP filter string when configured

Returns:

  • (String, nil)


126
# File 'lib/omniauth/ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

#last_operation_resultObject? (readonly)

Last operation result object returned by the ldap library (if any)

Returns:

  • (Object, nil)


126
# File 'lib/omniauth/ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

#last_password_policy_responseObject (readonly)

Read-only attributes exposing connection and configuration state.

Returns:

  • (Object)


126
# File 'lib/omniauth/ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

#passwordString?

The bind password (may be nil for anonymous binds)

Returns:

  • (String, nil)


99
# File 'lib/omniauth/ldap/adaptor.rb', line 99

attr_accessor :bind_dn, :password

#password_policyBoolean? (readonly)

optional: request password policy control and capture response

Returns:

  • (Boolean, nil)


126
# File 'lib/omniauth/ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

#uidString (readonly)

The user id attribute used for lookups (e.g., 'sAMAccountName')

Returns:

  • (String)


126
# File 'lib/omniauth/ldap/adaptor.rb', line 126

attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response

Class Method Details

.validate(configuration = {}) ⇒ void

This method returns an undefined value.

Validate that required keys exist in the configuration

Parameters:

  • (Hash[Symbol, untyped])


37
38
39
40
41
42
43
44
45
46
47
# File 'sig/omniauth/ldap/adaptor.rbs', line 37

def 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

Instance Method Details

#bind_as(args = {}) ⇒ Net::LDAP::Entry?, false

Perform a search and optionally bind; returns the matched entry or false

Parameters:

  • (Hash[Symbol, untyped])

Returns:



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/omniauth/ldap/adaptor.rb', line 218

def bind_as(args = {})
  result = false
  @last_operation_result = nil
  @last_password_policy_response = nil
  @connection.open do |me|
    rs = me.search(args)
    raise ConnectionError.new("LDAP search operation failed") unless rs

    if rs && rs.first
      dn = rs.first.dn
      if dn
        password = args[:password]
        password = password.call if password.respond_to?(:call)

        bind_args = if @bind_method == :sasl
          sasl_auths({username: dn, password: password}).first
        else
          {
            method: :simple,
            username: dn,
            password: password
          }
        end

        # Optionally request LDAP Password Policy control (RFC Draft - de facto standard)
        if @password_policy
          # Always request by OID using a simple hash; avoids depending on gem-specific control classes
          control = {oid: "1.3.6.1.4.1.42.2.27.8.5.1", criticality: true, value: nil}
          if bind_args.is_a?(Hash)
            bind_args = bind_args.merge({controls: [control]})
          else
            # Some Net::LDAP versions allow passing a block for SASL only; ensure we still can add controls if hash
            # When not a Hash, we can't merge; rely on server default behavior.
          end
        end

        begin
          success = bind_args ? me.bind(bind_args) : me.bind
        ensure
          capture_password_policy(me)
        end

        result = rs.first if success
      end
    end
  end
  result
end

#capture_password_policy(conn) ⇒ void

This method returns an undefined value.

Capture password policy control from last operation

Parameters:



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'lib/omniauth/ldap/adaptor.rb', line 421

def capture_password_policy(conn)
  return unless @password_policy
  return unless conn.respond_to?(:get_operation_result)

  begin
    @last_operation_result = conn.get_operation_result
    controls = if @last_operation_result && @last_operation_result.respond_to?(:controls)
      @last_operation_result.controls || []
    else
      []
    end
    if controls.any?
      # Find Password Policy response control by OID
      ppolicy_oid = "1.3.6.1.4.1.42.2.27.8.5.1"
      ctrl = controls.find do |c|
        (c.respond_to?(:oid) && c.oid == ppolicy_oid) || (c.is_a?(Hash) && c[:oid] == ppolicy_oid)
      end
      @last_password_policy_response = ctrl if ctrl
    end
  rescue
    # Swallow errors to keep authentication flow unaffected when server or gem doesn't support controls
    @last_password_policy_response = nil
  end
end

#default_optionsHash[Symbol, untyped]

Returns a Net::LDAP encryption default options hash

Returns:

  • (Hash[Symbol, untyped])


378
379
380
381
382
383
384
385
386
387
388
# File 'lib/omniauth/ldap/adaptor.rb', line 378

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

#encryption_optionsHash[Symbol, untyped]?

Returns encryption settings hash or nil

Returns:

  • (Hash[Symbol, untyped], nil)


272
273
274
275
276
277
278
279
280
# File 'lib/omniauth/ldap/adaptor.rb', line 272

def encryption_options
  translated_method = translate_method
  return unless translated_method

  {
    method: translated_method,
    tls_options: tls_options(translated_method)
  }
end

#sanitize_hash_values(hash) ⇒ Hash[untyped, untyped]

Sanitize provided TLS options

Parameters:

  • (Hash[untyped, untyped])

Returns:

  • (Hash[untyped, untyped])


397
398
399
400
401
402
# File 'lib/omniauth/ldap/adaptor.rb', line 397

def sanitize_hash_values(hash)
  hash.delete_if do |_, value|
    value.nil? ||
      (value.is_a?(String) && value !~ /\S/)
  end
end

#sasl_auths(options = {}) ⇒ Array[Hash[Symbol, untyped]]

Returns an array of SASL auth hashes

Parameters:

  • (Hash[Symbol, untyped])

Returns:

  • (Array[Hash[Symbol, untyped]])


324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/omniauth/ldap/adaptor.rb', line 324

def sasl_auths(options = {})
  auths = []
  sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms
  sasl_mechanisms.each do |mechanism|
    normalized_mechanism = mechanism.downcase.tr("-", "_")
    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

#sasl_bind_setup_digest_md5(options) ⇒ Array[untyped]

Returns initial credential and a proc that accepts a challenge and returns the response

Parameters:

  • (Hash[Symbol, untyped])

Returns:

  • (Array[untyped])


345
346
347
348
349
350
351
352
353
354
355
# File 'lib/omniauth/ldap/adaptor.rb', line 345

def sasl_bind_setup_digest_md5(options)
  bind_dn = options[:username]
  initial_credential = ""
  challenge_response = proc 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

#sasl_bind_setup_gss_spnego(options) ⇒ Array

Prepare SASL GSS-SPNEGO bind details

Parameters:

  • options (Hash)
  • (Hash[Symbol, untyped])

Returns:

  • (Array)

    initial Type1 message and a nego proc

Raises:



360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/omniauth/ldap/adaptor.rb', line 360

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

#symbolize_hash_keys(hash) ⇒ Hash[Symbol, untyped]

Symbolize option keys

Parameters:

  • (Hash[untyped, untyped])

Returns:

  • (Hash[Symbol, untyped])


408
409
410
411
412
# File 'lib/omniauth/ldap/adaptor.rb', line 408

def symbolize_hash_keys(hash)
  hash.each_with_object({}) do |(key, value), result|
    result[key.to_sym] = value
  end
end

#translate_methodSymbol?

Translate configured method/encryption into Net::LDAP symbol

Returns:

  • (Symbol, nil)


286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/omniauth/ldap/adaptor.rb', line 286

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