-62
-63
-64
+66
+67
+68
- # File 'lib/omniauth-ldap/adaptor.rb', line 62
+ # File 'lib/omniauth-ldap/adaptor.rb', line 66
def auth
@auth
@@ -646,12 +768,12 @@
-62
-63
-64
+66
+67
+68
|
- # File 'lib/omniauth-ldap/adaptor.rb', line 62
+ # File 'lib/omniauth-ldap/adaptor.rb', line 66
def base
@base
@@ -688,12 +810,12 @@
-61
-62
-63
+65
+66
+67
|
- # File 'lib/omniauth-ldap/adaptor.rb', line 61
+ # File 'lib/omniauth-ldap/adaptor.rb', line 65
def bind_dn
@bind_dn
@@ -730,12 +852,12 @@
-62
-63
-64
+66
+67
+68
|
- # File 'lib/omniauth-ldap/adaptor.rb', line 62
+ # File 'lib/omniauth-ldap/adaptor.rb', line 66
def connection
@connection
@@ -772,12 +894,12 @@
-62
-63
-64
+66
+67
+68
|
- # File 'lib/omniauth-ldap/adaptor.rb', line 62
+ # File 'lib/omniauth-ldap/adaptor.rb', line 66
def filter
@filter
@@ -788,6 +910,90 @@
+
+
+
+
+ #last_operation_result ⇒ Object
+
+
+
+
+
+
+
+ Returns the value of attribute last_operation_result.
+
+
+
+
+
+
+
+
+
+
+
+
+
+66
+67
+68
+ |
+
+ # File 'lib/omniauth-ldap/adaptor.rb', line 66
+
+def last_operation_result
+ @last_operation_result
+end
+ |
+
+
+
+
+
+
+
+
+
+ #last_password_policy_response ⇒ Object
+
+
+
+
+
+
+
+ Returns the value of attribute last_password_policy_response.
+
+
+
+
+
+
+
+
+
+
+
+
+
+66
+67
+68
+ |
+
+ # File 'lib/omniauth-ldap/adaptor.rb', line 66
+
+def last_password_policy_response
+ @last_password_policy_response
+end
+ |
+
+
+
+
+
@@ -814,12 +1020,12 @@
-61
-62
-63
+65
+66
+67
|
- # File 'lib/omniauth-ldap/adaptor.rb', line 61
+ # File 'lib/omniauth-ldap/adaptor.rb', line 65
def password
@password
@@ -830,6 +1036,48 @@
+
+
+
+
+ #password_policy ⇒ Object
+
+
+
+
+
+
+
+ Returns the value of attribute password_policy.
+
+
+
+
+
+
+
+
+
+
+
+
+
+66
+67
+68
+ |
+
+ # File 'lib/omniauth-ldap/adaptor.rb', line 66
+
+def password_policy
+ @password_policy
+end
+ |
+
+
+
+
+
@@ -856,12 +1104,12 @@
-62
-63
-64
+66
+67
+68
|
- # File 'lib/omniauth-ldap/adaptor.rb', line 62
+ # File 'lib/omniauth-ldap/adaptor.rb', line 66
def uid
@uid
@@ -916,20 +1164,20 @@
-64
-65
-66
-67
68
69
70
71
72
73
-74
+74
+75
+76
+77
+78
|
- # File 'lib/omniauth-ldap/adaptor.rb', line 64
+ # File 'lib/omniauth-ldap/adaptor.rb', line 68
def self.validate(configuration = {})
message = []
@@ -980,47 +1228,97 @@
-110
-111
-112
-113
-114
-115
-116
-117
-118
-119
-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
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
|
- # File 'lib/omniauth-ldap/adaptor.rb', line 110
+ # File 'lib/omniauth-ldap/adaptor.rb', line 131
def bind_as(args = {})
result = false
+ @last_operation_result = nil
+ @last_password_policy_response = nil
@connection.open do |me|
rs = me.search(args)
- if rs and rs.first and dn = rs.first.dn
- password = args[:password]
- method = args[:method] || @method
- password = password.call if password.respond_to?(:call)
- if method == "sasl"
- result = rs.first if me.bind(sasl_auths({username: dn, password: password}).first)
- elsif me.bind(
- method: :simple,
- username: dn,
- password: password,
- )
- result = rs.first
+ 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
+
+ if @password_policy
+ 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
+ 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
@@ -1036,7 +1334,7 @@
diff --git a/docs/OmniAuth/LDAP/Adaptor/AuthenticationError.html b/docs/OmniAuth/LDAP/Adaptor/AuthenticationError.html
index 741dd00..353f275 100644
--- a/docs/OmniAuth/LDAP/Adaptor/AuthenticationError.html
+++ b/docs/OmniAuth/LDAP/Adaptor/AuthenticationError.html
@@ -114,7 +114,7 @@
diff --git a/docs/OmniAuth/LDAP/Adaptor/ConfigurationError.html b/docs/OmniAuth/LDAP/Adaptor/ConfigurationError.html
index 5b84f08..c065402 100644
--- a/docs/OmniAuth/LDAP/Adaptor/ConfigurationError.html
+++ b/docs/OmniAuth/LDAP/Adaptor/ConfigurationError.html
@@ -114,7 +114,7 @@
diff --git a/docs/OmniAuth/LDAP/Adaptor/ConnectionError.html b/docs/OmniAuth/LDAP/Adaptor/ConnectionError.html
index 532f5cc..d41a9bc 100644
--- a/docs/OmniAuth/LDAP/Adaptor/ConnectionError.html
+++ b/docs/OmniAuth/LDAP/Adaptor/ConnectionError.html
@@ -114,7 +114,7 @@
diff --git a/docs/OmniAuth/LDAP/Adaptor/LdapError.html b/docs/OmniAuth/LDAP/Adaptor/LdapError.html
index 9a907d5..94e94ca 100644
--- a/docs/OmniAuth/LDAP/Adaptor/LdapError.html
+++ b/docs/OmniAuth/LDAP/Adaptor/LdapError.html
@@ -114,7 +114,7 @@
diff --git a/docs/OmniAuth/LDAP/Version.html b/docs/OmniAuth/LDAP/Version.html
index c67a473..83f9fc2 100644
--- a/docs/OmniAuth/LDAP/Version.html
+++ b/docs/OmniAuth/LDAP/Version.html
@@ -111,7 +111,7 @@
diff --git a/docs/OmniAuth/Strategies.html b/docs/OmniAuth/Strategies.html
index c5db66f..dd54494 100644
--- a/docs/OmniAuth/Strategies.html
+++ b/docs/OmniAuth/Strategies.html
@@ -105,7 +105,7 @@ Defined Under Namespace
diff --git a/docs/OmniAuth/Strategies/LDAP.html b/docs/OmniAuth/Strategies/LDAP.html
index bd39ec2..7157854 100644
--- a/docs/OmniAuth/Strategies/LDAP.html
+++ b/docs/OmniAuth/Strategies/LDAP.html
@@ -123,25 +123,6 @@
Class.new(StandardError)
- CONFIG =
-
-
- {
- "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",
-}.freeze
-
@@ -285,14 +266,6 @@
-134
-135
-136
-137
-138
-139
-140
-141
142
143
144
@@ -318,10 +291,18 @@
164
165
166
-167
+167
+168
+169
+170
+171
+172
+173
+174
+175
|
- # File 'lib/omniauth/strategies/ldap.rb', line 134
+ # File 'lib/omniauth/strategies/ldap.rb', line 142
def map_user(mapper, object)
user = {}
@@ -383,9 +364,6 @@
-78
-79
-80
81
82
83
@@ -416,10 +394,18 @@
108
109
110
-111
+111
+112
+113
+114
+115
+116
+117
+118
+119
|
- # File 'lib/omniauth/strategies/ldap.rb', line 78
+ # File 'lib/omniauth/strategies/ldap.rb', line 81
def callback_phase
@adaptor = OmniAuth::LDAP::Adaptor.new(@options)
@@ -434,7 +420,7 @@
return fail!(:invalid_credentials, InvalidCredentialsError.new("User not found for header #{hu}"))
end
@ldap_user_info = entry
- @user_info = self.class.map_user(CONFIG, @ldap_user_info)
+ @user_info = self.class.map_user(@options[:mapping], @ldap_user_info)
return super
rescue => e
return fail!(:ldap_error, e)
@@ -443,13 +429,18 @@
return fail!(:missing_credentials) if missing_credentials?
begin
- @ldap_user_info = @adaptor.bind_as(filter: filter(@adaptor), size: 1, password: request.params["password"])
+ @ldap_user_info = @adaptor.bind_as(filter: filter(@adaptor), size: 1, password: request_data["password"])
unless @ldap_user_info
- return fail!(:invalid_credentials, InvalidCredentialsError.new("Invalid credentials for #{request.params["username"]}"))
+ attach_password_policy_env(@adaptor)
+ return fail!(:invalid_credentials, InvalidCredentialsError.new("Invalid credentials for #{request_data["username"]}"))
end
- @user_info = self.class.map_user(CONFIG, @ldap_user_info)
+ attach_password_policy_env(@adaptor)
+
+ @user_info = self.class.map_user(@options[:mapping], @ldap_user_info)
super
rescue => e
fail!(:ldap_error, e)
@@ -475,26 +466,26 @@
-113
-114
-115
-116
-117
-118
-119
-120
-121
+121
+122
+123
+124
+125
+126
+127
+128
+129
|
- # File 'lib/omniauth/strategies/ldap.rb', line 113
+ # File 'lib/omniauth/strategies/ldap.rb', line 121
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.params["username"]))
+ 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.params["username"]))
+ Net::LDAP::Filter.equals(adaptor.uid, @options[:name_proc].call(username_override || request_data["username"]))
end
end
|
@@ -517,9 +508,6 @@
-50
-51
-52
53
54
55
@@ -543,10 +531,13 @@
73
74
75
-76
+76
+77
+78
+79
- # File 'lib/omniauth/strategies/ldap.rb', line 50
+ # File 'lib/omniauth/strategies/ldap.rb', line 53
def request_phase
if request.post? && request_data["username"].to_s != "" && request_data["password"].to_s != ""
return Rack::Response.new([], 302, "Location" => callback_url).finish
end
@@ -585,7 +576,7 @@
diff --git a/docs/_index.html b/docs/_index.html
index 344bc7f..e8944cd 100644
--- a/docs/_index.html
+++ b/docs/_index.html
@@ -254,7 +254,7 @@ Namespace Listing A-Z
diff --git a/docs/file.CHANGELOG.html b/docs/file.CHANGELOG.html
index 68adc67..36c5361 100644
--- a/docs/file.CHANGELOG.html
+++ b/docs/file.CHANGELOG.html
@@ -81,10 +81,34 @@ Added
under a subdirectory
+ Password Policy for LDAP Directories
+
+ -
+
+
+
+ | password_policy: true |
+ false (default: false) |
+
+
+
+
+ - on authentication failure, if the server returns password policy controls, the info will be included in the failure message
+ - https://datatracker.ietf.org/doc/html/draft-behera-ldap-password-policy-11
+
+
Changed
+
+ - Make support for OmniAuth v1.2+ explicit
+
+ - Versions < 1.2 do not support SCRIPT_NAME properly, and may cause other issues
+
+
+
+
Deprecated
Removed
@@ -293,7 +317,7 @@
diff --git a/docs/file.CITATION.html b/docs/file.CITATION.html
index a0f652b..91e946d 100644
--- a/docs/file.CITATION.html
+++ b/docs/file.CITATION.html
@@ -82,7 +82,7 @@
diff --git a/docs/file.CODE_OF_CONDUCT.html b/docs/file.CODE_OF_CONDUCT.html
index 6e8b968..36775d6 100644
--- a/docs/file.CODE_OF_CONDUCT.html
+++ b/docs/file.CODE_OF_CONDUCT.html
@@ -191,7 +191,7 @@ Attribution
diff --git a/docs/file.CONTRIBUTING.html b/docs/file.CONTRIBUTING.html
index c25b68f..cc7c0b9 100644
--- a/docs/file.CONTRIBUTING.html
+++ b/docs/file.CONTRIBUTING.html
@@ -295,7 +295,7 @@ Manual process
diff --git a/docs/file.FUNDING.html b/docs/file.FUNDING.html
index fcef94f..33a9f14 100644
--- a/docs/file.FUNDING.html
+++ b/docs/file.FUNDING.html
@@ -104,7 +104,7 @@ Another Way to Support Open
diff --git a/docs/file.LICENSE.html b/docs/file.LICENSE.html
index 457a3ab..ab7bff6 100644
--- a/docs/file.LICENSE.html
+++ b/docs/file.LICENSE.html
@@ -60,7 +60,7 @@
MIT License
Copyright (c) 2025 Peter H. Boling, and omniauth-ldap contributors Copyright (c) 2011 by Ping Yu and Intridea, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/docs/file.README.html b/docs/file.README.html
index f747a0f..3728b3d 100644
--- a/docs/file.README.html
+++ b/docs/file.README.html
@@ -101,7 +101,7 @@
๐ OmniAuth LDAP
- 
+ 
if ci_badges.map(&:color).detect { it != "green"} โ๏ธ let me know, as I may have missed the discord notification.
@@ -125,9 +125,17 @@ ๐ป Synopsis
name_proc: proc { |name| name.gsub(/@.*$/, "") },
bind_dn: "default_bind_dn",
password: "password",
+ # Optional timeouts (seconds)
+ connect_timeout: 3,
+ read_timeout: 7,
tls_options: {
ssl_version: "TLSv1_2",
ciphers: ["AES-128-CBC", "AES-128-CBC-HMAC-SHA1", "AES-128-CBC-HMAC-SHA256"],
+ },
+ mapping: {
+ "name" => "cn;lang-en",
+ "email" => ["preferredEmail", "mail"],
+ "nickname" => ["uid", "userid", "sAMAccountName"],
}
# Or, alternatively:
# use OmniAuth::Strategies::LDAP, filter: '(&(uid=%{username})(memberOf=cn=myapp-users,ou=groups,dc=example,dc=com))'
@@ -345,8 +353,38 @@ Optional Options
:allow_anonymous - Whether to allow anonymous binding (default: false).
:logger - A logger instance for debugging (optional, for internal use).
+
+:password_policy - When true, the strategy will request the LDAP Password Policy response control (OID 1.3.6.1.4.1.42.2.27.8.5.1) during the user bind. If the server supports it, the adaptor exposes:
+
+ -
+
adaptor.last_operation_result โ the last Net::LDAP operation result object.
+ -
+
adaptor.last_password_policy_response โ the matching password policy response control (implementation-specific object). This can indicate conditions such as password expired, account locked, reset required, or grace logins remaining (per the draft RFC).
+
+
+
+:connect_timeout - Maximum time in seconds to wait when establishing the TCP connection to the LDAP server. Forwarded to Net::LDAP.
+
+:read_timeout - Maximum time in seconds to wait for reads during LDAP operations (search/bind). Forwarded to Net::LDAP.
+
+:mapping - Customize how LDAP attributes map to the returned auth.info hash. A sensible default mapping is built into the strategy and will be merged with your overrides. See lib/omniauth/strategies/ldap.rb for the default keys and behavior; values can be a String (single attribute), an Array (first present attribute wins), or a Hash (string pattern with placeholders like %0 combined from multiple attributes).
+Example enabling password policy:
+
+use OmniAuth::Builder do
+ provider :ldap,
+ host: "ldap.example.com",
+ base: "dc=example,dc=com",
+ uid: "uid",
+ bind_dn: "cn=search,dc=example,dc=com",
+ password: ENV["LDAP_SEARCH_PASSWORD"],
+ password_policy: true
+end
+
+
+Note: This is best-effort and compatible with a range of net-ldap versions. If your server supports the control, you can inspect the response via the adaptor instance during/after authentication (for example in a failure handler) to tailor error messages.
+
Auth Hash UID vs LDAP :uid (search attribute)
@@ -457,6 +495,65 @@ Rails (initializer) example
Then link users to /auth/ldap in your app (for example, in a Devise sign-in page).
+Use JSON Body
+
+This gem is compatible with JSON-encoded POST bodies as well as traditional form-encoded.
+
+
+ - Set header
Content-Type to application/json.
+ - Send a JSON object containing
username and password.
+ - Rails automatically exposes parsed JSON params via
env["action_dispatch.request.request_parameters"], which this strategy reads first. In non-Rails Rack apps, ensure you use a JSON parser middleware if you post raw JSON.
+
+
+Examples
+
+
+ -
+
curl (JSON):
+
+ curl -i \
+ -X POST \
+ -H 'Content-Type: application/json' \
+ -d '{"username":"alice","password":"secret"}' \
+ http://localhost:3000/auth/ldap
+
+
+ The request phase will redirect to /auth/ldap/callback when both fields are present.
+
+ -
+
curl (form-encoded, still supported):
+
+ curl -i \
+ -X POST \
+ -H 'Content-Type: application/x-www-form-urlencoded' \
+ --data-urlencode 'username=alice' \
+ --data-urlencode 'password=secret' \
+ http://localhost:3000/auth/ldap
+
+
+ -
+
Browser (JavaScript fetch):
+
+ fetch('/auth/ldap', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username: 'alice', password: 'secret' })
+}).then(res => {
+ if (res.redirected) {
+ window.location = res.url; // typically /auth/ldap/callback
+ }
+});
+
+
+
+
+Notes
+
+
+ - You can still initiate authentication by visiting
GET /auth/ldap to render the HTML form and then submitting it (form-encoded). JSON is an additional option, not a replacement.
+ - In the callback phase (
POST /auth/ldap/callback), the strategy reads JSON credentials the same way; Rails exposes them via action_dispatch.request.request_parameters and non-Rails apps should use a JSON parser middleware.
+
+
Using a custom filter
If you need to restrict authentication to a group or use a more complex lookup, pass :filter. Use %{username} โ it will be replaced with the processed username (after :name_proc).
@@ -624,7 +721,10 @@ Mounted under a subdirectory (
title: "Acme LDAP",
host: "ldap.acme.internal",
base: "dc=acme,dc=corp",
- uid: "uid"
+ uid: "uid",
+ bind_dn: "cn=search,dc=acme,dc=corp",
+ password: ENV["LDAP_SEARCH_PASSWORD"],
+ name_proc: proc { |n| n.split("@").first }
end
@@ -785,8 +885,6 @@ Code Coverage

-
-
๐ช Code of Conduct
Everyone interacting with this projectโs codebases, issue trackers,
@@ -868,6 +966,9 @@ ยฉ Copyright
, and omniauth-ldap contributors.
+ -
+ Copyright (C) 2014 David Benko
+
-
Copyright (c) 2011 by Ping Yu and Intridea, Inc.
@@ -888,7 +989,7 @@ ๐ค A request for help
To say โthanks!โ โ๏ธ Join the Discord or ๐๏ธ send money.
- ๐ ๐ 
+ ๐ ๐ 
Please give the project a star โญ โฅ.
@@ -897,7 +998,7 @@ Please give the project a star โญ โฅ
diff --git a/docs/file.REEK.html b/docs/file.REEK.html
index cbbef16..2365083 100644
--- a/docs/file.REEK.html
+++ b/docs/file.REEK.html
@@ -61,7 +61,7 @@
diff --git a/docs/file.RUBOCOP.html b/docs/file.RUBOCOP.html
index 535c4d0..d86061a 100644
--- a/docs/file.RUBOCOP.html
+++ b/docs/file.RUBOCOP.html
@@ -161,7 +161,7 @@ Benefits of rubocop_gradual
diff --git a/docs/file.SECURITY.html b/docs/file.SECURITY.html
index 282d6cd..1ee7bdb 100644
--- a/docs/file.SECURITY.html
+++ b/docs/file.SECURITY.html
@@ -91,7 +91,7 @@ Additional Support
diff --git a/docs/file.adaptor.html b/docs/file.adaptor.html
index ffab603..3dd8918 100644
--- a/docs/file.adaptor.html
+++ b/docs/file.adaptor.html
@@ -74,7 +74,7 @@
VALID_ADAPTER_CONFIGURATION_KEYS: Array[Symbol]
MUST_HAVE_KEYS: Array[untyped]
- METHOD: Hash[Symbol, Symbol?]
+ ENCRYPTION_METHOD: Hash[Symbol, Symbol?]
attr_accessor bind_dn: String?
attr_accessor password: String?
@@ -87,6 +87,10 @@
attr_reader auth: Hash[Symbol, untyped]
# filter is an LDAP filter string when configured
attr_reader filter: String?
+ # optional: request password policy control and capture response
+ attr_reader password_policy: bool?
+ attr_reader last_operation_result: untyped
+ attr_reader last_password_policy_response: untyped
# Validate that required keys exist in the configuration
def self.validate: (?Hash[Symbol, untyped]) -> void
@@ -97,23 +101,37 @@
private
- # Returns a Net::LDAP encryption symbol (e.g. :simple_tls, :start_tls) or nil
- def ensure_method: (untyped) -> Symbol?
+ # Returns encryption settings hash or nil
+ def encryption_options: () -> Hash[Symbol, untyped]?
+ # Translate configured method/encryption into Net::LDAP symbol
+ def translate_method: () -> Symbol?
+ # Returns a Net::LDAP encryption default options hash
+ def default_options: () -> Hash[Symbol, untyped]
+ # Sanitize provided TLS options
+ def sanitize_hash_values: (Hash[untyped, untyped]) -> Hash[untyped, untyped]
+ # Symbolize option keys
+ def symbolize_hash_keys: (Hash[untyped, untyped]) -> Hash[Symbol, untyped]
+ # Capture password policy control from last operation
+ def capture_password_policy: (Net::LDAP) -> void
# Returns an array of SASL auth hashes
def sasl_auths: (?Hash[Symbol, untyped]) -> Array[Hash[Symbol, untyped]]
- # Returns initial credential (string) and a proc that accepts a challenge and returns the response
- # Use Array[untyped] here to avoid tuple syntax issues in some linters; the runtime value
- # is commonly a two-element array [initial_credential, proc].
+ # Returns initial credential and a proc that accepts a challenge and returns the response
def sasl_bind_setup_digest_md5: (?Hash[Symbol, untyped]) -> Array[untyped]
def sasl_bind_setup_gss_spnego: (?Hash[Symbol, untyped]) -> Array[untyped]
+
+ @try_sasl: bool?
+ @allow_anonymous: bool?
+ @tls_options: Hash[untyped, untyped]?
+ @sasl_mechanisms: Array[String]?
+ @disable_verify_certificates: bool?
end end end
diff --git a/docs/file.ldap.html b/docs/file.ldap.html
index 80cb27a..d044ea7 100644
--- a/docs/file.ldap.html
+++ b/docs/file.ldap.html
@@ -62,10 +62,7 @@
class LDAP
OMNIAUTH_GTE_V2: bool
- # CONFIG is a read-only mapping of string keys to mapping definitions
- CONFIG: Hash[String, untyped]
-
- # The request_phase either returns a Rack-compatible response or the form response.
+ # The request_phase either returns a Rack-compatible response or the form response.
def request_phase: () -> (Rack::Response | Array[untyped] | String)
# The callback_phase may call super (untyped) or return a failure symbol
@@ -86,12 +83,18 @@
# Perform a directory lookup for a given username; returns an Entry or nil
def directory_lookup: (OmniAuth::LDAP::Adaptor, String) -> untyped
+
+ def uid: () { () -> String } -> void
+
+ def info: () { () -> Hash[untyped, untyped] } -> void
+
+ def extra: () { () -> Hash[Symbol, untyped] } -> void
end end end
diff --git a/docs/file.net-ldap.html b/docs/file.net-ldap.html
index d5781b4..9380b92 100644
--- a/docs/file.net-ldap.html
+++ b/docs/file.net-ldap.html
@@ -64,6 +64,7 @@
def open: () { (self) -> untyped } -> untyped
def search: (?Hash[Symbol, untyped]) -> Array[Net::LDAP::Entry]
def bind: (?Hash[Symbol, untyped]) -> bool
+ def get_operation_result: () -> Net::LDAP::PDU
end
class LDAP::Entry
@@ -73,12 +74,28 @@
class LDAP::Filter
def self.construct: (String) -> Net::LDAP::Filter
def self.eq: (String, String) -> Net::LDAP::Filter
+ end
+
+class LDAP::Control
+ def initialize: (String, bool, untyped) -> void
+ def oid: () -> String
+ end
+
+module LDAP::Controls
+ class PasswordPolicy
+ def initialize: () -> void
+ def oid: () -> String
+ end
+ end
+
+class LDAP::PDU
+ def controls: () -> Array[untyped]
end
end
diff --git a/docs/file.net-ntlm.html b/docs/file.net-ntlm.html
index beea2ad..b4d0821 100644
--- a/docs/file.net-ntlm.html
+++ b/docs/file.net-ntlm.html
@@ -63,6 +63,8 @@
class Message
def self.parse: (untyped) -> Net::NTLM::Message
def response: (?Hash[Symbol, untyped], ?Hash[Symbol, untyped]) -> Net::NTLM::Message
+ # writer used by adaptor to set target name when a domain is present
+ def target_name=: (String) -> String
end
class Message::Type1
@@ -74,7 +76,7 @@
diff --git a/docs/file.omniauth-ldap-2.3.1.gem.html b/docs/file.omniauth-ldap-2.3.1.gem.html
index 114d84a..7912f44 100644
--- a/docs/file.omniauth-ldap-2.3.1.gem.html
+++ b/docs/file.omniauth-ldap-2.3.1.gem.html
@@ -61,7 +61,7 @@
diff --git a/docs/file.omniauth-ldap.html b/docs/file.omniauth-ldap.html
index 3f250e0..2fd85c4 100644
--- a/docs/file.omniauth-ldap.html
+++ b/docs/file.omniauth-ldap.html
@@ -62,10 +62,15 @@ - sig/omniauth/ldap/version.rbs
- sig/omniauth/ldap/adaptor.rbs
- sig/omniauth/strategies/ldap.rbs
This file is intentionally minimal to avoid duplicating declarations.
+
+module OmniAuth
+ module LDAP
+ end
+end
diff --git a/docs/file.sasl.html b/docs/file.sasl.html
index a015996..f171ef9 100644
--- a/docs/file.sasl.html
+++ b/docs/file.sasl.html
@@ -71,7 +71,7 @@
diff --git a/docs/file.version.html b/docs/file.version.html
index b5fd981..51d9eee 100644
--- a/docs/file.version.html
+++ b/docs/file.version.html
@@ -69,7 +69,7 @@
diff --git a/docs/index.html b/docs/index.html
index be9533c..6f955ad 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -101,7 +101,7 @@
๐ OmniAuth LDAP
- 
+ 
if ci_badges.map(&:color).detect { it != "green"} โ๏ธ let me know, as I may have missed the discord notification.
@@ -125,9 +125,17 @@ ๐ป Synopsis
name_proc: proc { |name| name.gsub(/@.*$/, "") },
bind_dn: "default_bind_dn",
password: "password",
+ # Optional timeouts (seconds)
+ connect_timeout: 3,
+ read_timeout: 7,
tls_options: {
ssl_version: "TLSv1_2",
ciphers: ["AES-128-CBC", "AES-128-CBC-HMAC-SHA1", "AES-128-CBC-HMAC-SHA256"],
+ },
+ mapping: {
+ "name" => "cn;lang-en",
+ "email" => ["preferredEmail", "mail"],
+ "nickname" => ["uid", "userid", "sAMAccountName"],
}
# Or, alternatively:
# use OmniAuth::Strategies::LDAP, filter: '(&(uid=%{username})(memberOf=cn=myapp-users,ou=groups,dc=example,dc=com))'
@@ -345,8 +353,38 @@ Optional Options
:allow_anonymous - Whether to allow anonymous binding (default: false).
-
:logger - A logger instance for debugging (optional, for internal use).
+ -
+
:password_policy - When true, the strategy will request the LDAP Password Policy response control (OID 1.3.6.1.4.1.42.2.27.8.5.1) during the user bind. If the server supports it, the adaptor exposes:
+
+ -
+
adaptor.last_operation_result โ the last Net::LDAP operation result object.
+ -
+
adaptor.last_password_policy_response โ the matching password policy response control (implementation-specific object). This can indicate conditions such as password expired, account locked, reset required, or grace logins remaining (per the draft RFC).
+
+
+ -
+
:connect_timeout - Maximum time in seconds to wait when establishing the TCP connection to the LDAP server. Forwarded to Net::LDAP.
+ -
+
:read_timeout - Maximum time in seconds to wait for reads during LDAP operations (search/bind). Forwarded to Net::LDAP.
+ -
+
:mapping - Customize how LDAP attributes map to the returned auth.info hash. A sensible default mapping is built into the strategy and will be merged with your overrides. See lib/omniauth/strategies/ldap.rb for the default keys and behavior; values can be a String (single attribute), an Array (first present attribute wins), or a Hash (string pattern with placeholders like %0 combined from multiple attributes).
+Example enabling password policy:
+
+use OmniAuth::Builder do
+ provider :ldap,
+ host: "ldap.example.com",
+ base: "dc=example,dc=com",
+ uid: "uid",
+ bind_dn: "cn=search,dc=example,dc=com",
+ password: ENV["LDAP_SEARCH_PASSWORD"],
+ password_policy: true
+end
+
+
+Note: This is best-effort and compatible with a range of net-ldap versions. If your server supports the control, you can inspect the response via the adaptor instance during/after authentication (for example in a failure handler) to tailor error messages.
+
Auth Hash UID vs LDAP :uid (search attribute)
@@ -457,6 +495,65 @@ Rails (initializer) example
Then link users to /auth/ldap in your app (for example, in a Devise sign-in page).
+Use JSON Body
+
+This gem is compatible with JSON-encoded POST bodies as well as traditional form-encoded.
+
+
+ - Set header
Content-Type to application/json.
+ - Send a JSON object containing
username and password.
+ - Rails automatically exposes parsed JSON params via
env["action_dispatch.request.request_parameters"], which this strategy reads first. In non-Rails Rack apps, ensure you use a JSON parser middleware if you post raw JSON.
+
+
+Examples
+
+
+ -
+
curl (JSON):
+
+ curl -i \
+ -X POST \
+ -H 'Content-Type: application/json' \
+ -d '{"username":"alice","password":"secret"}' \
+ http://localhost:3000/auth/ldap
+
+
+ The request phase will redirect to /auth/ldap/callback when both fields are present.
+
+ -
+
curl (form-encoded, still supported):
+
+ curl -i \
+ -X POST \
+ -H 'Content-Type: application/x-www-form-urlencoded' \
+ --data-urlencode 'username=alice' \
+ --data-urlencode 'password=secret' \
+ http://localhost:3000/auth/ldap
+
+
+ -
+
Browser (JavaScript fetch):
+
+ fetch('/auth/ldap', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username: 'alice', password: 'secret' })
+}).then(res => {
+ if (res.redirected) {
+ window.location = res.url; // typically /auth/ldap/callback
+ }
+});
+
+
+
+
+Notes
+
+
+ - You can still initiate authentication by visiting
GET /auth/ldap to render the HTML form and then submitting it (form-encoded). JSON is an additional option, not a replacement.
+ - In the callback phase (
POST /auth/ldap/callback), the strategy reads JSON credentials the same way; Rails exposes them via action_dispatch.request.request_parameters and non-Rails apps should use a JSON parser middleware.
+
+
Using a custom filter
If you need to restrict authentication to a group or use a more complex lookup, pass :filter. Use %{username} โ it will be replaced with the processed username (after :name_proc).
@@ -624,7 +721,10 @@ Mounted under a subdirectory (
title: "Acme LDAP",
host: "ldap.acme.internal",
base: "dc=acme,dc=corp",
- uid: "uid"
+ uid: "uid",
+ bind_dn: "cn=search,dc=acme,dc=corp",
+ password: ENV["LDAP_SEARCH_PASSWORD"],
+ name_proc: proc { |n| n.split("@").first }
end
@@ -785,8 +885,6 @@ Code Coverage

-
-
๐ช Code of Conduct
Everyone interacting with this projectโs codebases, issue trackers,
@@ -868,6 +966,9 @@ ยฉ Copyright
, and omniauth-ldap contributors.
+ -
+ Copyright (C) 2014 David Benko
+
-
Copyright (c) 2011 by Ping Yu and Intridea, Inc.
@@ -888,7 +989,7 @@ ๐ค A request for help
To say โthanks!โ โ๏ธ Join the Discord or ๐๏ธ send money.
- ๐ ๐ 
+ ๐ ๐ 
Please give the project a star โญ โฅ.
@@ -897,7 +998,7 @@ Please give the project a star โญ โฅ
diff --git a/docs/method_list.html b/docs/method_list.html
index a14fe81..d32ed05 100644
--- a/docs/method_list.html
+++ b/docs/method_list.html
@@ -119,6 +119,22 @@
+ -
+
+
+
+
+ -
+
+
+
+
-
map_user
@@ -136,6 +152,14 @@
-
+
+
+
+
+ -
#request_phase
OmniAuth::Strategies::LDAP
@@ -143,7 +167,7 @@
- -
+
-
#uid
OmniAuth::LDAP::Adaptor
@@ -151,7 +175,7 @@
- -
+
-
validate
OmniAuth::LDAP::Adaptor
diff --git a/docs/top-level-namespace.html b/docs/top-level-namespace.html
index 1df7fce..1181177 100644
--- a/docs/top-level-namespace.html
+++ b/docs/top-level-namespace.html
@@ -100,7 +100,7 @@ Defined Under Namespace
diff --git a/lib/omniauth/strategies/ldap.rb b/lib/omniauth/strategies/ldap.rb
index a5e6313..2b01d40 100644
--- a/lib/omniauth/strategies/ldap.rb
+++ b/lib/omniauth/strategies/ldap.rb
@@ -66,7 +66,7 @@ def request_phase
# 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.params["username"].to_s != "" && request.params["password"].to_s != ""
+ if request.post? && request_data["username"].to_s != "" && request_data["password"].to_s != ""
return Rack::Response.new([], 302, "Location" => callback_url).finish
end
@@ -100,12 +100,12 @@ def callback_phase
return fail!(:missing_credentials) if missing_credentials?
begin
- @ldap_user_info = @adaptor.bind_as(filter: filter(@adaptor), size: 1, password: request.params["password"])
+ @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.params["username"]}"))
+ return fail!(:invalid_credentials, InvalidCredentialsError.new("Invalid credentials for #{request_data["username"]}"))
end
# Optionally attach policy info even on success (e.g., timeBeforeExpiration)
@@ -121,10 +121,10 @@ def callback_phase
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.params["username"]))
+ 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.params["username"]))
+ Net::LDAP::Filter.equals(adaptor.uid, @options[:name_proc].call(username_override || request_data["username"]))
end
end
@@ -182,7 +182,11 @@ def valid_request_method?
end
def missing_credentials?
- request.params["username"].nil? || request.params["username"].empty? || request.params["password"].nil? || request.params["password"].empty?
+ request_data["username"].nil? || request_data["username"].empty? || request_data["password"].nil? || request_data["password"].empty?
+ end
+
+ def request_data
+ @env["action_dispatch.request.request_parameters"] || request.params
end
# Extract a normalized username from a trusted header when enabled.
diff --git a/spec/integration/middleware_spec.rb b/spec/integration/middleware_spec.rb
index 2ccfc0c..5482e3f 100644
--- a/spec/integration/middleware_spec.rb
+++ b/spec/integration/middleware_spec.rb
@@ -61,6 +61,55 @@
end
end
+ it "POST /auth/ldap accepts JSON-style credentials via Rails env and sets omniauth.auth" do
+ begin
+ OmniAuth.config.test_mode = true
+ OmniAuth.config.mock_auth[:ldap] = OmniAuth::AuthHash.new(provider: "ldap", uid: "json-bob", info: {"name" => "Bob"})
+
+ env = {
+ "CONTENT_TYPE" => "application/json",
+ "action_dispatch.request.request_parameters" => {"username" => "bob", "password" => "secret"},
+ }
+ post "/auth/ldap", nil, env
+
+ # Follow redirects to callback
+ max_redirects = 5
+ redirects = 0
+ while last_response.status == 302 && redirects < max_redirects
+ follow_redirect!
+ redirects += 1
+ end
+
+ expect(last_response.status).to eq 200
+ expect(last_response.body).to include("true")
+ ensure
+ OmniAuth.config.mock_auth.delete(:ldap)
+ OmniAuth.config.test_mode = false
+ end
+ end
+
+ it "POST /auth/ldap/callback with JSON missing username and password redirects with missing_credentials" do
+ env = {
+ "CONTENT_TYPE" => "application/json",
+ "action_dispatch.request.request_parameters" => {},
+ }
+ post "/auth/ldap/callback", nil, env
+
+ expect(last_response.status).to eq 302
+ expect(last_response.headers["Location"]).to match(/missing_credentials/)
+ end
+
+ it "POST /auth/ldap/callback with JSON username but missing password redirects with missing_credentials" do
+ env = {
+ "CONTENT_TYPE" => "application/json",
+ "action_dispatch.request.request_parameters" => {"username" => "bob"},
+ }
+ post "/auth/ldap/callback", nil, env
+
+ expect(last_response.status).to eq 302
+ expect(last_response.headers["Location"]).to match(/missing_credentials/)
+ end
+
it "honors SCRIPT_NAME when mounted under a subdirectory for redirect to callback" do
begin
OmniAuth.config.test_mode = true
diff --git a/spec/omniauth/strategies/ldap_spec.rb b/spec/omniauth/strategies/ldap_spec.rb
index 18ed2ca..e081ce8 100644
--- a/spec/omniauth/strategies/ldap_spec.rb
+++ b/spec/omniauth/strategies/ldap_spec.rb
@@ -87,6 +87,18 @@ def make_env(path = "/auth/ldap", props = {})
expect(last_response.headers["Location"]).to eq "http://example.org/auth/ldap/callback"
end
+ it "redirects to callback when JSON POST includes username and password (Rails env)" do
+ env = {
+ "REQUEST_METHOD" => "POST",
+ "CONTENT_TYPE" => "application/json",
+ # Rails places parsed JSON params here
+ "action_dispatch.request.request_parameters" => {"username" => "json_alice", "password" => "json_secret"},
+ }
+ post "/auth/ldap", nil, env
+ expect(last_response).to be_redirect
+ expect(last_response.headers["Location"]).to eq "http://example.org/auth/ldap/callback"
+ end
+
context "when mounted under a subdirectory" do
let(:sub_env) do
make_env("/auth/ldap", {
|