Class: OmniAuth::Strategies::LDAP
- Inherits:
-
Object
- Object
- OmniAuth::Strategies::LDAP
- 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.
Constant Summary collapse
- OMNIAUTH_GTE_V2 =
Whether the loaded OmniAuth version is >= 2.0.0; used to set default request methods.
Gem::Version.new(OmniAuth::VERSION) >= Gem::Version.new("2.0.0")
- InvalidCredentialsError =
Raised when credentials are invalid or the user cannot be authenticated.
Class.new(StandardError)
Class Method Summary collapse
-
.map_user(mapper, object) ⇒ Hash[String, untyped]
Map a user object (Net::LDAP::Entry-like) into a Hash for the auth info.
Instance Method Summary collapse
-
#callback_phase ⇒ Object
The callback_phase may call super (untyped) or return a failure symbol.
-
#directory_lookup(adaptor, username) ⇒ Object
Perform a directory lookup for a given username; returns an Entry or nil.
- #extra { ... } ⇒ void
-
#filter(adaptor, username_override = nil) ⇒ Object
Accepts an adaptor and returns a Net::LDAP::Filter or similar Optional second argument allows overriding the username (used for header-based SSO).
-
#header_auth_env_key ⇒ String
Rack env key selected by the explicit header auth source option.
-
#header_username ⇒ String?
Extract username from a trusted header when enabled.
- #info { ... } ⇒ void
-
#log_header_auth_warning ⇒ void
Warn operators that trusted header auth delegates authentication to the upstream gateway.
-
#mapping ⇒ Hash<String, String|Array|Hash>
Default mapping for converting LDAP attributes to OmniAuth
infokeys. -
#missing_credentials? ⇒ Boolean
Determine if the request is missing required credentials.
-
#request_phase ⇒ Rack::Response, ...
The request_phase either returns a Rack-compatible response or the form response.
-
#title ⇒ String
Default title shown on the login form.
- #uid { ... } ⇒ void
-
#validate_header_auth_configuration! ⇒ void
Validate trusted header auth before reading the configured identity key.
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
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_phase ⇒ Object
The callback_phase may call super (untyped) or return a failure symbol
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
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.
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)
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_key ⇒ String
Rack env key selected by the explicit header auth source option.
365 366 367 368 369 370 |
# File 'lib/omniauth/strategies/ldap.rb', line 365 def header_auth_env_key name = [:header_name] || "REMOTE_USER" return name if ([:header_auth_source] || :env).to_sym == :env "HTTP_#{name.upcase.tr("-", "_")}" end |
#header_username ⇒ String?
Extract username from a trusted header when enabled
334 335 336 337 338 339 340 341 |
# File 'lib/omniauth/strategies/ldap.rb', line 334 def header_username return unless [:header_auth] raw = request.env[header_auth_env_key] return if raw.nil? || raw.to_s.strip.empty? [:name_proc].call(raw.to_s) end |
#info { ... } ⇒ void
This method returns an undefined value.
36 |
# File 'sig/omniauth/strategies/ldap.rbs', line 36
def info: () { () -> Hash[untyped, untyped] } -> void
|
#log_header_auth_warning ⇒ void
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 |
#mapping ⇒ Hash<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
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.
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_phase ⇒ Rack::Response, ...
The request_phase either returns a Rack-compatible response or the form response.
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: [:title] || "LDAP Authentication", url: callback_url) f.text_field("Login", "username") f.password_field("Password", "password") f.("Sign In") f.to_response end |
#title ⇒ String
Default title shown on the login form.
80 |
# File 'lib/omniauth/strategies/ldap.rb', line 80 option :title, "LDAP Authentication" |
#uid { ... } ⇒ void
This method returns an undefined value.
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.
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 [:header_auth] log_header_auth_warning source = ([: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 [:header_auth_require_tls] && !request.ssl? raise ArgumentError, "header_auth requires TLS unless header_auth_require_tls is disabled" end end |