omniauth-ldap 2.0.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,13 @@
1
- #this code borrowed pieces from activeldap and net-ldap
1
+ # frozen_string_literal: true
2
+
3
+ # this code borrowed pieces from activeldap and net-ldap
4
+
5
+ # External Gems
6
+ require "net/ldap"
7
+ require "net/ntlm"
8
+ require "rack"
9
+ require "sasl"
2
10
 
3
- require 'rack'
4
- require 'net/ldap'
5
- require 'net/ntlm'
6
- require 'sasl'
7
- require 'kconv'
8
11
  module OmniAuth
9
12
  module LDAP
10
13
  class Adaptor
@@ -13,31 +16,68 @@ module OmniAuth
13
16
  class AuthenticationError < StandardError; end
14
17
  class ConnectionError < StandardError; end
15
18
 
16
- VALID_ADAPTER_CONFIGURATION_KEYS = [:host, :port, :method, :bind_dn, :password, :try_sasl, :sasl_mechanisms, :uid, :base, :allow_anonymous, :filter]
19
+ VALID_ADAPTER_CONFIGURATION_KEYS = [
20
+ :hosts,
21
+ :host,
22
+ :port,
23
+ :encryption,
24
+ :disable_verify_certificates,
25
+ :bind_dn,
26
+ :password,
27
+ :try_sasl,
28
+ :sasl_mechanisms,
29
+ :uid,
30
+ :base,
31
+ :allow_anonymous,
32
+ :filter,
33
+ :tls_options,
34
+ :password_policy,
35
+ # Timeouts
36
+ :connect_timeout,
37
+ :read_timeout,
38
+
39
+ # Deprecated
40
+ :method,
41
+ :ca_file,
42
+ :ssl_version,
43
+ ]
17
44
 
18
45
  # A list of needed keys. Possible alternatives are specified using sub-lists.
19
- MUST_HAVE_KEYS = [:host, :port, :method, [:uid, :filter], :base]
46
+ MUST_HAVE_KEYS = [
47
+ :base,
48
+ [:encryption, :method], # :method is deprecated
49
+ [:hosts, :host],
50
+ [:hosts, :port],
51
+ [:uid, :filter],
52
+ ]
53
+
54
+ ENCRYPTION_METHOD = {
55
+ simple_tls: :simple_tls,
56
+ start_tls: :start_tls,
57
+ plain: nil,
20
58
 
21
- METHOD = {
22
- :ssl => :simple_tls,
23
- :tls => :start_tls,
24
- :plain => nil,
59
+ # Deprecated. This mapping aimed to be user-friendly, but only caused
60
+ # confusion. Better to pass through the actual `Net::LDAP` encryption type.
61
+ ssl: :simple_tls,
62
+ tls: :start_tls,
25
63
  }
26
64
 
27
65
  attr_accessor :bind_dn, :password
28
- attr_reader :connection, :uid, :base, :auth, :filter
29
- def self.validate(configuration={})
66
+ attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response
67
+
68
+ def self.validate(configuration = {})
30
69
  message = []
31
70
  MUST_HAVE_KEYS.each do |names|
32
71
  names = [names].flatten
33
- missing_keys = names.select{|name| configuration[name].nil?}
72
+ missing_keys = names.select { |name| configuration[name].nil? }
34
73
  if missing_keys == names
35
- message << names.join(' or ')
74
+ message << names.join(" or ")
36
75
  end
37
76
  end
38
- raise ArgumentError.new(message.join(",") +" MUST be provided") unless message.empty?
77
+ raise ArgumentError.new(message.join(",") + " MUST be provided") unless message.empty?
39
78
  end
40
- def initialize(configuration={})
79
+
80
+ def initialize(configuration = {})
41
81
  Adaptor.validate(configuration)
42
82
  @configuration = configuration.dup
43
83
  @configuration[:allow_anonymous] ||= false
@@ -45,23 +85,44 @@ module OmniAuth
45
85
  VALID_ADAPTER_CONFIGURATION_KEYS.each do |name|
46
86
  instance_variable_set("@#{name}", @configuration[name])
47
87
  end
48
- method = ensure_method(@method)
49
88
  config = {
50
- :host => @host,
51
- :port => @port,
52
- :base => @base
89
+ base: @base,
90
+ hosts: @hosts,
91
+ host: @host,
92
+ port: @port,
93
+ encryption: encryption_options,
53
94
  }
54
- @bind_method = @try_sasl ? :sasl : (@allow_anonymous||!@bind_dn||!@password ? :anonymous : :simple)
55
-
95
+ # Remove passing timeouts here to avoid issues on older net-ldap versions.
96
+ # We'll set them after initialization if the connection responds to writers.
97
+ @bind_method = if @try_sasl
98
+ :sasl
99
+ else
100
+ ((@allow_anonymous || !@bind_dn || !@password) ? :anonymous : :simple)
101
+ end
56
102
 
57
- @auth = sasl_auths({:username => @bind_dn, :password => @password}).first if @bind_method == :sasl
58
- @auth ||= { :method => @bind_method,
59
- :username => @bind_dn,
60
- :password => @password
61
- }
103
+ @auth = sasl_auths({username: @bind_dn, password: @password}).first if @bind_method == :sasl
104
+ @auth ||= {
105
+ method: @bind_method,
106
+ username: @bind_dn,
107
+ password: @password,
108
+ }
62
109
  config[:auth] = @auth
63
110
  @connection = Net::LDAP.new(config)
64
- @connection.encryption(method)
111
+ # Apply optional timeout settings if supported by the installed net-ldap version
112
+ if !@connect_timeout.nil?
113
+ if @connection.respond_to?(:connect_timeout=)
114
+ @connection.connect_timeout = @connect_timeout
115
+ else
116
+ @connection.instance_variable_set(:@connect_timeout, @connect_timeout)
117
+ end
118
+ end
119
+ if !@read_timeout.nil?
120
+ if @connection.respond_to?(:read_timeout=)
121
+ @connection.read_timeout = @read_timeout
122
+ else
123
+ @connection.instance_variable_set(:@read_timeout, @read_timeout)
124
+ end
125
+ end
65
126
  end
66
127
 
67
128
  #:base => "dc=yourcompany, dc=com",
@@ -69,17 +130,47 @@ module OmniAuth
69
130
  # :password => psw
70
131
  def bind_as(args = {})
71
132
  result = false
133
+ @last_operation_result = nil
134
+ @last_password_policy_response = nil
72
135
  @connection.open do |me|
73
- rs = me.search args
74
- if rs and rs.first and dn = rs.first.dn
75
- password = args[:password]
76
- method = args[:method] || @method
77
- password = password.call if password.respond_to?(:call)
78
- if method == 'sasl'
79
- result = rs.first if me.bind(sasl_auths({:username => dn, :password => password}).first)
80
- else
81
- result = rs.first if me.bind(:method => :simple, :username => dn,
82
- :password => password)
136
+ rs = me.search(args)
137
+ raise ConnectionError.new("LDAP search operation failed") unless rs
138
+
139
+ if rs && rs.first
140
+ dn = rs.first.dn
141
+ if dn
142
+ password = args[:password]
143
+ password = password.call if password.respond_to?(:call)
144
+
145
+ bind_args = if @bind_method == :sasl
146
+ sasl_auths({username: dn, password: password}).first
147
+ else
148
+ {
149
+ method: :simple,
150
+ username: dn,
151
+ password: password,
152
+ }
153
+ end
154
+
155
+ # Optionally request LDAP Password Policy control (RFC Draft - de facto standard)
156
+ if @password_policy
157
+ # Always request by OID using a simple hash; avoids depending on gem-specific control classes
158
+ control = {oid: "1.3.6.1.4.1.42.2.27.8.5.1", criticality: true, value: nil}
159
+ if bind_args.is_a?(Hash)
160
+ bind_args = bind_args.merge({controls: [control]})
161
+ else
162
+ # Some Net::LDAP versions allow passing a block for SASL only; ensure we still can add controls if hash
163
+ # When not a Hash, we can't merge; rely on server default behavior.
164
+ end
165
+ end
166
+
167
+ begin
168
+ success = bind_args ? me.bind(bind_args) : me.bind
169
+ ensure
170
+ capture_password_policy(me)
171
+ end
172
+
173
+ result = rs.first if success
83
174
  end
84
175
  end
85
176
  end
@@ -87,29 +178,64 @@ module OmniAuth
87
178
  end
88
179
 
89
180
  private
90
- def ensure_method(method)
91
- method ||= "plain"
92
- normalized_method = method.to_s.downcase.to_sym
93
- return METHOD[normalized_method] if METHOD.has_key?(normalized_method)
94
181
 
95
- available_methods = METHOD.keys.collect {|m| m.inspect}.join(", ")
182
+ def encryption_options
183
+ translated_method = translate_method
184
+ return unless translated_method
185
+
186
+ {
187
+ method: translated_method,
188
+ tls_options: tls_options(translated_method),
189
+ }
190
+ end
191
+
192
+ def translate_method
193
+ method = @encryption || @method
194
+ method ||= "plain"
195
+ normalized_method = method.to_s.downcase.to_sym
196
+
197
+ unless ENCRYPTION_METHOD.has_key?(normalized_method)
198
+ available_methods = ENCRYPTION_METHOD.keys.collect { |m| m.inspect }.join(", ")
96
199
  format = "%s is not one of the available connect methods: %s"
97
200
  raise ConfigurationError, format % [method.inspect, available_methods]
201
+ end
202
+
203
+ ENCRYPTION_METHOD[normalized_method]
98
204
  end
99
205
 
100
- def sasl_auths(options={})
206
+ def tls_options(translated_method)
207
+ return {} if translated_method.nil? # (plain)
208
+
209
+ options = default_options
210
+
211
+ if @tls_options
212
+ # Prevent blank config values from overwriting SSL defaults
213
+ configured_options = sanitize_hash_values(@tls_options)
214
+ configured_options = symbolize_hash_keys(configured_options)
215
+
216
+ options.merge!(configured_options)
217
+ end
218
+
219
+ # Retain backward compatibility until deprecated configs are removed.
220
+ options[:ca_file] = @ca_file if @ca_file
221
+ options[:ssl_version] = @ssl_version if @ssl_version
222
+
223
+ options
224
+ end
225
+
226
+ def sasl_auths(options = {})
101
227
  auths = []
102
228
  sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms
103
229
  sasl_mechanisms.each do |mechanism|
104
- normalized_mechanism = mechanism.downcase.gsub(/-/, '_')
230
+ normalized_mechanism = mechanism.downcase.tr("-", "_")
105
231
  sasl_bind_setup = "sasl_bind_setup_#{normalized_mechanism}"
106
232
  next unless respond_to?(sasl_bind_setup, true)
107
233
  initial_credential, challenge_response = send(sasl_bind_setup, options)
108
234
  auths << {
109
- :method => :sasl,
110
- :initial_credential => initial_credential,
111
- :mechanism => mechanism,
112
- :challenge_response => challenge_response
235
+ method: :sasl,
236
+ initial_credential: initial_credential,
237
+ mechanism: mechanism,
238
+ challenge_response: challenge_response,
113
239
  }
114
240
  end
115
241
  auths
@@ -118,8 +244,8 @@ module OmniAuth
118
244
  def sasl_bind_setup_digest_md5(options)
119
245
  bind_dn = options[:username]
120
246
  initial_credential = ""
121
- challenge_response = Proc.new do |cred|
122
- pref = SASL::Preferences.new :digest_uri => "ldap/#{@host}", :username => bind_dn, :has_password? => true, :password => options[:password]
247
+ challenge_response = proc do |cred|
248
+ pref = SASL::Preferences.new(digest_uri: "ldap/#{@host}", username: bind_dn, has_password?: true, password: options[:password])
123
249
  sasl = SASL.new("DIGEST-MD5", pref)
124
250
  response = sasl.receive("challenge", cred)
125
251
  response[1]
@@ -130,18 +256,74 @@ module OmniAuth
130
256
  def sasl_bind_setup_gss_spnego(options)
131
257
  bind_dn = options[:username]
132
258
  psw = options[:password]
133
- raise LdapError.new( "invalid binding information" ) unless (bind_dn && psw)
259
+ raise LdapError.new("invalid binding information") unless bind_dn && psw
134
260
 
135
- nego = proc {|challenge|
136
- t2_msg = Net::NTLM::Message.parse( challenge )
137
- bind_dn, domain = bind_dn.split('\\').reverse
138
- t2_msg.target_name = Net::NTLM::encode_utf16le(domain) if domain
139
- t3_msg = t2_msg.response( {:user => bind_dn, :password => psw}, {:ntlmv2 => true} )
261
+ nego = proc { |challenge|
262
+ t2_msg = Net::NTLM::Message.parse(challenge)
263
+ bind_dn, domain = bind_dn.split("\\").reverse
264
+ t2_msg.target_name = Net::NTLM.encode_utf16le(domain) if domain
265
+ t3_msg = t2_msg.response({user: bind_dn, password: psw}, {ntlmv2: true})
140
266
  t3_msg.serialize
141
267
  }
142
268
  [Net::NTLM::Message::Type1.new.serialize, nego]
143
269
  end
144
270
 
271
+ private
272
+
273
+ def default_options
274
+ if @disable_verify_certificates
275
+ # It is important to explicitly set verify_mode for two reasons:
276
+ # 1. The behavior of OpenSSL is undefined when verify_mode is not set.
277
+ # 2. The net-ldap gem implementation verifies the certificate hostname
278
+ # unless verify_mode is set to VERIFY_NONE.
279
+ {verify_mode: OpenSSL::SSL::VERIFY_NONE}
280
+ else
281
+ OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.dup
282
+ end
283
+ end
284
+
285
+ # Removes keys that have blank values
286
+ #
287
+ # This gem may not always be in the context of Rails so we
288
+ # do this rather than `.blank?`.
289
+ def sanitize_hash_values(hash)
290
+ hash.delete_if do |_, value|
291
+ value.nil? ||
292
+ (value.is_a?(String) && value !~ /\S/)
293
+ end
294
+ end
295
+
296
+ def symbolize_hash_keys(hash)
297
+ hash.each_with_object({}) do |(key, value), result|
298
+ result[key.to_sym] = value
299
+ end
300
+ end
301
+
302
+ # Capture the operation result and extract any Password Policy response control if present.
303
+ def capture_password_policy(conn)
304
+ return unless @password_policy
305
+ return unless conn.respond_to?(:get_operation_result)
306
+
307
+ begin
308
+ @last_operation_result = conn.get_operation_result
309
+ controls = if @last_operation_result && @last_operation_result.respond_to?(:controls)
310
+ @last_operation_result.controls || []
311
+ else
312
+ []
313
+ end
314
+ if controls.any?
315
+ # Find Password Policy response control by OID
316
+ ppolicy_oid = "1.3.6.1.4.1.42.2.27.8.5.1"
317
+ ctrl = controls.find do |c|
318
+ (c.respond_to?(:oid) && c.oid == ppolicy_oid) || (c.is_a?(Hash) && c[:oid] == ppolicy_oid)
319
+ end
320
+ @last_password_policy_response = ctrl if ctrl
321
+ end
322
+ rescue StandardError
323
+ # Swallow errors to keep authentication flow unaffected when server or gem doesn't support controls
324
+ @last_password_policy_response = nil
325
+ end
326
+ end
145
327
  end
146
328
  end
147
329
  end
@@ -1,5 +1,8 @@
1
1
  module OmniAuth
2
2
  module LDAP
3
- VERSION = "2.0.0"
3
+ module Version
4
+ VERSION = "2.3.2"
5
+ end
6
+ VERSION = Version::VERSION # Make VERSION available in traditional way
4
7
  end
5
8
  end
data/lib/omniauth-ldap.rb CHANGED
@@ -1,4 +1,9 @@
1
+ require "version_gem"
2
+
1
3
  require "omniauth-ldap/version"
2
4
  require "omniauth-ldap/adaptor"
3
- require 'omniauth/strategies/ldap'
5
+ require "omniauth/strategies/ldap"
4
6
 
7
+ OmniAuth::LDAP::Version.class_eval do
8
+ extend VersionGem::Basic
9
+ end
@@ -0,0 +1,72 @@
1
+ module OmniAuth
2
+ module LDAP
3
+ class Adaptor
4
+ class LdapError < ::StandardError
5
+ end
6
+
7
+ class ConfigurationError < ::StandardError
8
+ end
9
+
10
+ class AuthenticationError < ::StandardError
11
+ end
12
+
13
+ class ConnectionError < ::StandardError
14
+ end
15
+
16
+ VALID_ADAPTER_CONFIGURATION_KEYS: Array[Symbol]
17
+ MUST_HAVE_KEYS: Array[untyped]
18
+ ENCRYPTION_METHOD: Hash[Symbol, Symbol?]
19
+
20
+ attr_accessor bind_dn: String?
21
+ attr_accessor password: String?
22
+
23
+ # Net::LDAP is provided by the net-ldap gem; we reference it here for clarity.
24
+ attr_reader connection: Net::LDAP
25
+ attr_reader uid: String?
26
+ attr_reader base: String?
27
+ # auth is the hash passed to Net::LDAP#auth or similar
28
+ attr_reader auth: Hash[Symbol, untyped]
29
+ # filter is an LDAP filter string when configured
30
+ attr_reader filter: String?
31
+ # optional: request password policy control and capture response
32
+ attr_reader password_policy: bool?
33
+ attr_reader last_operation_result: untyped
34
+ attr_reader last_password_policy_response: untyped
35
+
36
+ # Validate that required keys exist in the configuration
37
+ def self.validate: (?Hash[Symbol, untyped]) -> void
38
+ def initialize: (?Hash[Symbol, untyped]) -> void
39
+
40
+ # Perform a search and optionally bind; returns the matched entry or false
41
+ def bind_as: (?Hash[Symbol, untyped]) -> (Net::LDAP::Entry? | false)
42
+
43
+ private
44
+
45
+ # Returns encryption settings hash or nil
46
+ def encryption_options: () -> Hash[Symbol, untyped]?
47
+ # Translate configured method/encryption into Net::LDAP symbol
48
+ def translate_method: () -> Symbol?
49
+ # Returns a Net::LDAP encryption default options hash
50
+ def default_options: () -> Hash[Symbol, untyped]
51
+ # Sanitize provided TLS options
52
+ def sanitize_hash_values: (Hash[untyped, untyped]) -> Hash[untyped, untyped]
53
+ # Symbolize option keys
54
+ def symbolize_hash_keys: (Hash[untyped, untyped]) -> Hash[Symbol, untyped]
55
+ # Capture password policy control from last operation
56
+ def capture_password_policy: (Net::LDAP) -> void
57
+
58
+ # Returns an array of SASL auth hashes
59
+ def sasl_auths: (?Hash[Symbol, untyped]) -> Array[Hash[Symbol, untyped]]
60
+
61
+ # Returns initial credential and a proc that accepts a challenge and returns the response
62
+ def sasl_bind_setup_digest_md5: (?Hash[Symbol, untyped]) -> Array[untyped]
63
+ def sasl_bind_setup_gss_spnego: (?Hash[Symbol, untyped]) -> Array[untyped]
64
+
65
+ @try_sasl: bool?
66
+ @allow_anonymous: bool?
67
+ @tls_options: Hash[untyped, untyped]?
68
+ @sasl_mechanisms: Array[String]?
69
+ @disable_verify_certificates: bool?
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,11 @@
1
+ module OmniAuth
2
+ module LDAP
3
+ module Version
4
+ VERSION: String
5
+ end
6
+
7
+ # Traditional constant alias
8
+ VERSION: String
9
+ end
10
+ end
11
+
@@ -0,0 +1,35 @@
1
+ module OmniAuth
2
+ module Strategies
3
+ class LDAP
4
+ OMNIAUTH_GTE_V2: bool
5
+
6
+ # The request_phase either returns a Rack-compatible response or the form response.
7
+ def request_phase: () -> (Rack::Response | Array[untyped] | String)
8
+
9
+ # The callback_phase may call super (untyped) or return a failure symbol
10
+ def callback_phase: () -> untyped
11
+
12
+ # Accepts an adaptor and returns a Net::LDAP::Filter or similar
13
+ # Optional second argument allows overriding the username (used for header-based SSO)
14
+ def filter: (OmniAuth::LDAP::Adaptor) -> Net::LDAP::Filter
15
+ | (OmniAuth::LDAP::Adaptor, String?) -> Net::LDAP::Filter
16
+
17
+ # Map a user object (Net::LDAP::Entry-like) into a Hash for the auth info
18
+ def self.map_user: (Hash[String, untyped], untyped) -> Hash[String, untyped]
19
+
20
+ def missing_credentials?: () -> bool
21
+
22
+ # Extract username from a trusted header when enabled
23
+ def header_username: () -> (String | nil)
24
+
25
+ # Perform a directory lookup for a given username; returns an Entry or nil
26
+ def directory_lookup: (OmniAuth::LDAP::Adaptor, String) -> untyped
27
+
28
+ def uid: () { () -> String } -> void
29
+
30
+ def info: () { () -> Hash[untyped, untyped] } -> void
31
+
32
+ def extra: () { () -> Hash[Symbol, untyped] } -> void
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,10 @@
1
+ # Top-level signature file for the gem. Detailed signatures live in the files below:
2
+ # - sig/omniauth/ldap/version.rbs
3
+ # - sig/omniauth/ldap/adaptor.rbs
4
+ # - sig/omniauth/strategies/ldap.rbs
5
+ # This file is intentionally minimal to avoid duplicating declarations.
6
+
7
+ module OmniAuth
8
+ module LDAP
9
+ end
10
+ end
@@ -0,0 +1,35 @@
1
+ # Minimal stubs for net-ldap types used by the gem
2
+ module Net
3
+ class LDAP
4
+ def initialize: (Hash[Symbol, untyped]) -> void
5
+ def open: () { (self) -> untyped } -> untyped
6
+ def search: (?Hash[Symbol, untyped]) -> Array[Net::LDAP::Entry]
7
+ def bind: (?Hash[Symbol, untyped]) -> bool
8
+ def get_operation_result: () -> Net::LDAP::PDU
9
+ end
10
+
11
+ class LDAP::Entry
12
+ def dn: () -> String
13
+ end
14
+
15
+ class LDAP::Filter
16
+ def self.construct: (String) -> Net::LDAP::Filter
17
+ def self.eq: (String, String) -> Net::LDAP::Filter
18
+ end
19
+
20
+ class LDAP::Control
21
+ def initialize: (String, bool, untyped) -> void
22
+ def oid: () -> String
23
+ end
24
+
25
+ module LDAP::Controls
26
+ class PasswordPolicy
27
+ def initialize: () -> void
28
+ def oid: () -> String
29
+ end
30
+ end
31
+
32
+ class LDAP::PDU
33
+ def controls: () -> Array[untyped]
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ # Minimal stubs for net-ntlm types used by the gem
2
+ module Net
3
+ module NTLM
4
+ class Message
5
+ def self.parse: (untyped) -> Net::NTLM::Message
6
+ def response: (?Hash[Symbol, untyped], ?Hash[Symbol, untyped]) -> Net::NTLM::Message
7
+ # writer used by adaptor to set target name when a domain is present
8
+ def target_name=: (String) -> String
9
+ end
10
+
11
+ class Message::Type1
12
+ def serialize: () -> String
13
+ end
14
+
15
+ def self.encode_utf16le: (String) -> String
16
+ end
17
+ end
data/sig/rbs/sasl.rbs ADDED
@@ -0,0 +1,12 @@
1
+ # Minimal stubs for SASL bindings used in tests
2
+ module SASL
3
+ class Preferences
4
+ def initialize: (?Hash[Symbol, untyped]) -> void
5
+ end
6
+
7
+ class SASL
8
+ def initialize: (String, SASL::Preferences) -> void
9
+ def receive: (String, untyped) -> [untyped, untyped]
10
+ end
11
+ end
12
+
data.tar.gz.sig ADDED
Binary file