Class: OmniAuth::Strategies::LDAP

Inherits:
Object
  • Object
show all
Includes:
OmniAuth::Strategy
Defined in:
lib/omniauth/strategies/ldap.rb,
sig/omniauth/strategies/ldap.rbs

Overview

LDAP OmniAuth strategy

This class implements the OmniAuth::Strategy interface and performs LDAP authentication using an Adaptor object. It supports three primary flows:

  • Interactive login form (request_phase) where users POST username/password
  • Callback binding where the strategy attempts to bind as the user
  • Header-based SSO (trusted upstream) where a header identifies the user

The mapping from LDAP attributes to resulting info fields is configurable via the :mapping option. See map_user for the mapping algorithm.

See Also:

  • OmniAuth::Strategy

Constant Summary collapse

OMNIAUTH_GTE_V2 =

Whether the loaded OmniAuth version is >= 2.0.0; used to set default request methods.

Returns:

  • (Boolean)
Gem::Version.new(OmniAuth::VERSION) >= Gem::Version.new("2.0.0")
InvalidCredentialsError =

Raised when credentials are invalid or the user cannot be authenticated.

Examples:

raise InvalidCredentialsError, 'Invalid credentials'
Class.new(StandardError)

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.map_user(mapper, object) ⇒ Hash[String, untyped]

Map a user object (Net::LDAP::Entry-like) into a Hash for the auth info

Parameters:

  • (Hash[String, untyped])
  • (Object)

Returns:

  • (Hash[String, untyped])


263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/omniauth/strategies/ldap.rb', line 263

def map_user(mapper, object)
  user = {}
  mapper.each do |key, value|
    case value
    when String
      user[key] = object[value.downcase.to_sym].first if object.respond_to?(value.downcase.to_sym)
    when Array
      value.each do |v|
        if object.respond_to?(v.downcase.to_sym)
          user[key] = object[v.downcase.to_sym].first
          break
        end
      end
    when Hash
      value.map do |key1, value1|
        pattern = key1.dup
        value1.each_with_index do |v, i|
          part = ""
          v.collect(&:downcase).collect(&:to_sym).each do |v1|
            if object.respond_to?(v1)
              part = object[v1].first
              break
            end
          end
          pattern.gsub!("%#{i}", part || "")
        end
        user[key] = pattern
      end
    else
      # unknown mapping type; ignore
    end
  end
  user
end

Instance Method Details

#callback_phaseObject

The callback_phase may call super (untyped) or return a failure symbol

Returns:

  • (Object)


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
204
205
206
207
208
209
210
211
# File 'lib/omniauth/strategies/ldap.rb', line 167

def callback_phase
  @adaptor = OmniAuth::LDAP::Adaptor.new(@options)

  return fail!(:invalid_request_method) unless valid_request_method?

  begin
    validate_header_auth_configuration!
  rescue => e
    return fail!(:ldap_error, e)
  end

  # Header-based SSO (REMOTE_USER-style) path
  if (hu = header_username)
    begin
      entry = directory_lookup(@adaptor, hu)
      unless entry
        return fail!(:invalid_credentials, InvalidCredentialsError.new("User not found for header #{hu}"))
      end
      @ldap_user_info = entry
      @user_info = self.class.map_user(@options[:mapping], @ldap_user_info)
      return super
    rescue => e
      return fail!(:ldap_error, e)
    end
  end

  return fail!(:missing_credentials) if missing_credentials?
  begin
    @ldap_user_info = @adaptor.bind_as(filter: filter(@adaptor), size: 1, password: request_data["password"])

    unless @ldap_user_info
      # Attach password policy info to env if available (best-effort)
      attach_password_policy_env(@adaptor)
      return fail!(:invalid_credentials, InvalidCredentialsError.new("Invalid credentials for #{request_data["username"]}"))
    end

    # Optionally attach policy info even on success (e.g., timeBeforeExpiration)
    attach_password_policy_env(@adaptor)

    @user_info = self.class.map_user(@options[:mapping], @ldap_user_info)
    super
  rescue => e
    fail!(:ldap_error, e)
  end
end

#directory_lookup(adaptor, username) ⇒ Object

Perform a directory lookup for a given username; returns an Entry or nil

Parameters:

Returns:

  • (Object)


388
389
390
391
392
393
394
395
396
# File 'lib/omniauth/strategies/ldap.rb', line 388

def directory_lookup(adaptor, username)
  entry = nil
  search_filter = filter(adaptor, username)
  adaptor.connection.open do |conn|
    rs = conn.search(filter: search_filter, size: 1)
    entry = rs && rs.first
  end
  entry
end

#extra { ... } ⇒ void

This method returns an undefined value.

Yields:

Yield Returns:

  • (Hash[Symbol, untyped])


38
# File 'sig/omniauth/strategies/ldap.rbs', line 38

def extra: () { () -> Hash[Symbol, untyped] } -> void

#filter(arg0) ⇒ Net::LDAP::Filter #filter(arg0, arg1) ⇒ Net::LDAP::Filter

Accepts an adaptor and returns a Net::LDAP::Filter or similar Optional second argument allows overriding the username (used for header-based SSO)

Overloads:



222
223
224
225
226
227
228
229
230
# File 'lib/omniauth/strategies/ldap.rb', line 222

def filter(adaptor, username_override = nil)
  flt = adaptor.filter
  if flt && !flt.to_s.empty?
    username = Net::LDAP::Filter.escape(@options[:name_proc].call(username_override || request_data["username"]))
    Net::LDAP::Filter.construct(flt % {username: username})
  else
    Net::LDAP::Filter.equals(adaptor.uid, @options[:name_proc].call(username_override || request_data["username"]))
  end
end

#header_auth_env_keyString

Rack env key selected by the explicit header auth source option.

Returns:

  • (String)


365
366
367
368
369
370
# File 'lib/omniauth/strategies/ldap.rb', line 365

def header_auth_env_key
  name = options[:header_name] || "REMOTE_USER"
  return name if (options[:header_auth_source] || :env).to_sym == :env

  "HTTP_#{name.upcase.tr("-", "_")}"
end

#header_usernameString?

Extract username from a trusted header when enabled

Returns:

  • (String, nil)


334
335
336
337
338
339
340
341
# File 'lib/omniauth/strategies/ldap.rb', line 334

def header_username
  return unless options[:header_auth]

  raw = request.env[header_auth_env_key]
  return if raw.nil? || raw.to_s.strip.empty?

  options[:name_proc].call(raw.to_s)
end

#info { ... } ⇒ void

This method returns an undefined value.

Yields:

Yield Returns:

  • (Hash[untyped, untyped])


36
# File 'sig/omniauth/strategies/ldap.rbs', line 36

def info: () { () -> Hash[untyped, untyped] } -> void

#log_header_auth_warningvoid

This method returns an undefined value.

Warn operators that trusted header auth delegates authentication to the upstream gateway.



375
376
377
378
379
380
# File 'lib/omniauth/strategies/ldap.rb', line 375

def log_header_auth_warning
  logger = OmniAuth.config.respond_to?(:logger) ? OmniAuth.config.logger : nil
  return unless logger && logger.respond_to?(:warn)

  logger.warn("[omniauth-ldap] SECURITY WARNING: header_auth is enabled. This trusts upstream authentication completely; only enable it behind a trusted proxy that strips client-supplied identity headers.")
end

#mappingHash<String, String|Array|Hash>

Default mapping for converting LDAP attributes to OmniAuth info keys. Keys are the resulting info hash keys (strings). Values may be:

  • String: single LDAP attribute name
  • Array: list of attribute names in priority order
  • Hash: pattern mapping where pattern keys contain % placeholders that are substituted from a list of possible attribute names

Returns:

  • (Hash<String, String|Array|Hash>)


62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/omniauth/strategies/ldap.rb', line 62

option :mapping, {
  "name" => "cn",
  "first_name" => "givenName",
  "last_name" => "sn",
  "email" => ["mail", "email", "userPrincipalName"],
  "phone" => ["telephoneNumber", "homePhone", "facsimileTelephoneNumber"],
  "mobile" => ["mobile", "mobileTelephoneNumber"],
  "nickname" => ["uid", "userid", "sAMAccountName"],
  "title" => "title",
  "location" => {"%0, %1, %2, %3 %4" => [["address", "postalAddress", "homePostalAddress", "street", "streetAddress"], ["l"], ["st"], ["co"], ["postOfficeBox"]]},
  "uid" => "dn",
  "url" => ["wwwhomepage"],
  "image" => "jpegPhoto",
  "description" => "description"
}

#missing_credentials?Boolean

Determine if the request is missing required credentials.

Returns:

  • (Boolean)

    true when username or password are nil/empty



315
316
317
# File 'lib/omniauth/strategies/ldap.rb', line 315

def missing_credentials?
  request_data["username"].nil? || request_data["username"].empty? || request_data["password"].nil? || request_data["password"].empty?
end

#request_phaseRack::Response, ...

The request_phase either returns a Rack-compatible response or the form response.

Returns:

  • (Rack::Response, Array[untyped], String)


124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/omniauth/strategies/ldap.rb', line 124

def request_phase
  # OmniAuth >= 2.0 expects the request phase to be POST-only for /auth/:provider.
  # Some test environments (and OmniAuth itself) enforce this by returning 404 on GET.
  if OMNIAUTH_GTE_V2 && request.get?
    return Rack::Response.new("", 404, {"Content-Type" => "text/plain"}).finish
  end

  validate_header_auth_configuration!

  # Fast-path: if a trusted identity header is present, skip the login form
  # and jump to the callback where we will complete using directory lookup.
  if header_username
    return Rack::Response.new([], 302, "Location" => callback_url).finish
  end

  # If credentials were POSTed directly to /auth/:provider, redirect to the callback path.
  # This mirrors the behavior of many OmniAuth providers and allows test helpers (like
  # OmniAuth::Test::PhonySession) to populate `env['omniauth.auth']` on the callback request.
  if request.post? && request_data["username"].to_s != "" && request_data["password"].to_s != ""
    return Rack::Response.new([], 302, "Location" => callback_url).finish
  end

  OmniAuth::LDAP::Adaptor.validate(@options)
  f = OmniAuth::Form.new(title: options[:title] || "LDAP Authentication", url: callback_url)
  f.text_field("Login", "username")
  f.password_field("Password", "password")
  f.button("Sign In")
  f.to_response
end

#titleString

Default title shown on the login form.

Returns:

  • (String)


80
# File 'lib/omniauth/strategies/ldap.rb', line 80

option :title, "LDAP Authentication"

#uid { ... } ⇒ void

This method returns an undefined value.

Yields:

Yield Returns:

  • (String)


34
# File 'sig/omniauth/strategies/ldap.rbs', line 34

def uid: () { () -> String } -> void

#validate_header_auth_configuration!void

This method returns an undefined value.

Validate trusted header auth before reading the configured identity key.

Raises:

  • (ArgumentError)

    when the header auth options are unsafe or invalid



347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/omniauth/strategies/ldap.rb', line 347

def validate_header_auth_configuration!
  return unless options[:header_auth]

  log_header_auth_warning

  source = (options[:header_auth_source] || :env).to_sym
  unless [:env, :http_header].include?(source)
    raise ArgumentError, "header_auth_source must be :env or :http_header"
  end

  if options[:header_auth_require_tls] && !request.ssl?
    raise ArgumentError, "header_auth requires TLS unless header_auth_require_tls is disabled"
  end
end