Class: OmniAuth::Strategies::LDAP

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

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, Object>

Map LDAP attributes from the directory entry into a simple Hash used for the OmniAuth ‘info` hash according to the provided `mapper`.

The mapper supports three types of values:

  • String: a single attribute name. The method will call the attribute reader (downcased symbol) on the ‘object` and take the first value.

  • Array: iterate values and pick the first attribute that exists on the object.

  • Hash: a mapping of a pattern string to an array of attribute-name lists where each ‘%<n>` placeholder in the pattern will be substituted by the first available attribute from the corresponding list.

Parameters:

  • mapper (Hash)

    mapping configuration (see option :mapping)

  • object (#respond_to?, #[])

    directory entry (commonly a Net::LDAP::Entry or similar)

Returns:

  • (Hash<String, Object>)

    the mapped user info hash



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/omniauth/strategies/ldap.rb', line 251

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

Callback phase: Authenticate user or perform header-based lookup

This method executes on the callback URL and implements the main authentication logic. There are two primary paths:

  • Header-based lookup: when ‘options` is enabled and a header value is present, we perform a read-only directory lookup for the user and, if found, map attributes and finish.

  • Password bind: when username/password are provided we attempt a bind as the user using the adaptor.

Errors raised by the LDAP adaptor are captured and turned into OmniAuth failures.

Returns:

  • (Object)

    result of calling ‘super` from the OmniAuth::Strategy chain

Raises:



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
# File 'lib/omniauth/strategies/ldap.rb', line 161

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

  return fail!(:invalid_request_method) unless valid_request_method?

  # 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
       = entry
      @user_info = self.class.map_user(@options[:mapping], )
      return super
    rescue => e
      return fail!(:ldap_error, e)
    end
  end

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

    unless 
      # 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], )
    super
  rescue => e
    fail!(:ldap_error, e)
  end
end

#filter(adaptor, username_override = nil) ⇒ Net::LDAP::Filter

Build an LDAP filter for searching/binding the user.

If the adaptor has a custom ‘filter` option set it will be used (with interpolation of `%username`). Otherwise a simple equality filter for the configured uid attribute is used.

Parameters:

  • adaptor (OmniAuth::LDAP::Adaptor)

    the adaptor used to build connection/filters

  • username_override (String, nil) (defaults to: nil)

    optional username to build the filter for (defaults to request username)

Returns:

  • (Net::LDAP::Filter)

    the constructed filter object



210
211
212
213
214
215
216
217
218
# File 'lib/omniauth/strategies/ldap.rb', line 210

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

#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 %<n> 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",
}

#request_phaseArray

Request phase: Render the login form or redirect to callback for header-auth or direct POSTed credentials

This will behave differently depending on OmniAuth version and request method:

  • For OmniAuth >= 2.0 a GET to /auth/:provider should return 404 (so we return a 404 for GET requests).

  • If header-based SSO is enabled and a trusted header is present we immediately redirect to the callback.

  • If credentials are POSTed directly to /auth/:provider we redirect to the callback so the test helpers that populate ‘env` can operate on the callback request.

Returns:

  • (Array)

    A Rack response triple from the login form or redirect.



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/omniauth/strategies/ldap.rb', line 120

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

  # 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"