# 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.
ifheader_username
- returnRack::Response.new([],302,"Location"=>callback_path).finish
+ returnRack::Response.new([],302,"Location"=>callback_url).finishend# 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.
ifrequest.post?&&request.params["username"].to_s!=""&&request.params["password"].to_s!=""
- returnRack::Response.new([],302,"Location"=>callback_path).finish
+ returnRack::Response.new([],302,"Location"=>callback_url).finishendOmniAuth::LDAP::Adaptor.validate(@options)
- f=OmniAuth::Form.new(title:options[:title]||"LDAP Authentication",url:callback_path)
+ 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")
@@ -583,7 +585,7 @@
+
+
\ No newline at end of file
diff --git a/docs/file.omniauth-ldap.html b/docs/file.omniauth-ldap.html
index a9ca408..3f250e0 100644
--- a/docs/file.omniauth-ldap.html
+++ b/docs/file.omniauth-ldap.html
@@ -65,7 +65,7 @@
This trims alice@example.com to alice before searching.
+
Mounted under a subdirectory (SCRIPT_NAME)
+
+
If your app is served from a path prefix (for example, behind a reverse proxy at /myapp, or mounted via Rack::URLMap, or Rails relative_url_root), the OmniAuth callback must include that subdirectory. This strategy uses callback_url for the form action and redirects, so it automatically includes any SCRIPT_NAME set by Rack/Rails. In other words, you typically do not need any special configuration beyond ensuring SCRIPT_NAME is correct in the request environment.
+
+
+
Works out-of-the-box when:
+
+
You mount the app at a path using Rack’s map/URLMap.
+
You set Rails’ config.relative_url_root (or RAILS_RELATIVE_URL_ROOT) or deploy under a prefix with a reverse proxy that sets SCRIPT_NAME.
Visiting POST /myapp/auth/ldap renders the login form with action='http://host/myapp/auth/ldap/callback'.
+
Any redirects (including header-based SSO fast path) will also point to http://host/myapp/auth/ldap/callback.
+
+
+
Rails example (relative_url_root):
+
+
# config/environments/production.rb (or an initializer)
+Rails.application.configure do
+ config.relative_url_root = "/myapp" # or set ENV["RAILS_RELATIVE_URL_ROOT"]
+end
+
+# config/initializers/omniauth.rb
+Rails.application.config.middleware.use(OmniAuth::Builder) do
+ provider :ldap,
+ title: "Acme LDAP",
+ host: "ldap.acme.internal",
+ base: "dc=acme,dc=corp",
+ uid: "uid"
+end
+
+
+
+
With relative_url_root set, Rails/Rack provide SCRIPT_NAME=/myapp, and this strategy will issue a form with action='.../myapp/auth/ldap/callback' and redirect accordingly.
+
+
+
Behind proxies with unusual host/proto handling (optional):
+
+
OmniAuth usually derives the correct scheme/host/prefix from Rack (and standard X-Forwarded-* headers). If your environment produces incorrect absolute URLs, you can override the computed host and prefix by setting OmniAuth.config.full_host:
Note: You generally do not need this override. Prefer configuring your proxy to pass standard X-Forwarded-Proto and X-Forwarded-Host headers and let Rack/OmniAuth compute the full URL.
+
+
+
Header-based SSO (header_auth: true) also respects SCRIPT_NAME; when a trusted header is present on POST /myapp/auth/ldap, the strategy redirects to http://host/myapp/auth/ldap/callback.
+
+
Trusted header SSO (REMOTE_USER and friends)
Some deployments terminate SSO at a reverse proxy or portal and forward the already-authenticated user identity via an HTTP header such as REMOTE_USER.
@@ -814,7 +897,7 @@
Please give the project a star ⭐ ♥
diff --git a/docs/top-level-namespace.html b/docs/top-level-namespace.html
index fc1ae00..1df7fce 100644
--- a/docs/top-level-namespace.html
+++ b/docs/top-level-namespace.html
@@ -100,7 +100,7 @@
Defined Under Namespace
diff --git a/gemfiles/modular/rack/r2.1/v1.0.gemfile b/gemfiles/modular/rack/r2.1/v1.0.gemfile
new file mode 100644
index 0000000..2fa0824
--- /dev/null
+++ b/gemfiles/modular/rack/r2.1/v1.0.gemfile
@@ -0,0 +1 @@
+gem "rack", "~> 1.0", ">= 1.0.1"
diff --git a/gemfiles/modular/rack/r2.1/v1.1.gemfile b/gemfiles/modular/rack/r2.1/v1.1.gemfile
new file mode 100644
index 0000000..66d0790
--- /dev/null
+++ b/gemfiles/modular/rack/r2.1/v1.1.gemfile
@@ -0,0 +1 @@
+gem "rack", "~> 1.1", ">= 1.1.6"
diff --git a/gemfiles/modular/rack/r2.1/v1.2.gemfile b/gemfiles/modular/rack/r2.1/v1.2.gemfile
new file mode 100644
index 0000000..f3326b8
--- /dev/null
+++ b/gemfiles/modular/rack/r2.1/v1.2.gemfile
@@ -0,0 +1 @@
+gem "rack", "~> 1.2", ">= 1.2.8"
diff --git a/gemfiles/modular/rack/r2.1/v1.3.gemfile b/gemfiles/modular/rack/r2.1/v1.3.gemfile
new file mode 100644
index 0000000..c428315
--- /dev/null
+++ b/gemfiles/modular/rack/r2.1/v1.3.gemfile
@@ -0,0 +1 @@
+gem "rack", "~> 1.3", ">= 1.3.10"
diff --git a/gemfiles/modular/rack/r2.1/v1.4.gemfile b/gemfiles/modular/rack/r2.1/v1.4.gemfile
new file mode 100644
index 0000000..da0e01c
--- /dev/null
+++ b/gemfiles/modular/rack/r2.1/v1.4.gemfile
@@ -0,0 +1 @@
+gem "rack", "~> 1.4", ">= 1.4.7"
diff --git a/gemfiles/modular/rack/r2.1/v1.5.gemfile b/gemfiles/modular/rack/r2.1/v1.5.gemfile
new file mode 100644
index 0000000..7b3744a
--- /dev/null
+++ b/gemfiles/modular/rack/r2.1/v1.5.gemfile
@@ -0,0 +1 @@
+gem "rack", "~> 1.5", ">= 1.5.5"
diff --git a/gemfiles/ruby_2_3_omni_v1.0.gemfile b/gemfiles/ruby_2_3_omni_v1.0.gemfile
new file mode 100644
index 0000000..05416ec
--- /dev/null
+++ b/gemfiles/ruby_2_3_omni_v1.0.gemfile
@@ -0,0 +1,11 @@
+# This file was generated by Appraisal2
+
+source "https://gem.coop"
+
+gemspec path: "../"
+
+eval_gemfile("modular/omniauth/r2/v1.0.gemfile")
+
+eval_gemfile("modular/rack/r2.1/v1.6.gemfile")
+
+eval_gemfile("modular/x_std_libs/r2.3/libs.gemfile")
diff --git a/gemfiles/ruby_2_3.gemfile b/gemfiles/ruby_2_3_omni_v1.1.gemfile
similarity index 100%
rename from gemfiles/ruby_2_3.gemfile
rename to gemfiles/ruby_2_3_omni_v1.1.gemfile
diff --git a/gemfiles/ruby_2_3_omni_v1.2.gemfile b/gemfiles/ruby_2_3_omni_v1.2.gemfile
new file mode 100644
index 0000000..add5661
--- /dev/null
+++ b/gemfiles/ruby_2_3_omni_v1.2.gemfile
@@ -0,0 +1,11 @@
+# This file was generated by Appraisal2
+
+source "https://gem.coop"
+
+gemspec path: "../"
+
+eval_gemfile("modular/omniauth/r2/v1.2.gemfile")
+
+eval_gemfile("modular/rack/r2.1/v1.0.gemfile")
+
+eval_gemfile("modular/x_std_libs/r2.3/libs.gemfile")
diff --git a/gemfiles/ruby_2_3_omni_v1.3.gemfile b/gemfiles/ruby_2_3_omni_v1.3.gemfile
new file mode 100644
index 0000000..7069009
--- /dev/null
+++ b/gemfiles/ruby_2_3_omni_v1.3.gemfile
@@ -0,0 +1,11 @@
+# This file was generated by Appraisal2
+
+source "https://gem.coop"
+
+gemspec path: "../"
+
+eval_gemfile("modular/omniauth/r2/v1.3.gemfile")
+
+eval_gemfile("modular/rack/r2.1/v1.1.gemfile")
+
+eval_gemfile("modular/x_std_libs/r2.3/libs.gemfile")
diff --git a/gemfiles/ruby_2_3_omni_v1.4.gemfile b/gemfiles/ruby_2_3_omni_v1.4.gemfile
new file mode 100644
index 0000000..0608efa
--- /dev/null
+++ b/gemfiles/ruby_2_3_omni_v1.4.gemfile
@@ -0,0 +1,11 @@
+# This file was generated by Appraisal2
+
+source "https://gem.coop"
+
+gemspec path: "../"
+
+eval_gemfile("modular/omniauth/r2/v1.4.gemfile")
+
+eval_gemfile("modular/rack/r2.1/v1.2.gemfile")
+
+eval_gemfile("modular/x_std_libs/r2.3/libs.gemfile")
diff --git a/gemfiles/ruby_2_3_omni_v1.5.gemfile b/gemfiles/ruby_2_3_omni_v1.5.gemfile
new file mode 100644
index 0000000..51f922b
--- /dev/null
+++ b/gemfiles/ruby_2_3_omni_v1.5.gemfile
@@ -0,0 +1,11 @@
+# This file was generated by Appraisal2
+
+source "https://gem.coop"
+
+gemspec path: "../"
+
+eval_gemfile("modular/omniauth/r2/v1.5.gemfile")
+
+eval_gemfile("modular/rack/r2.1/v1.3.gemfile")
+
+eval_gemfile("modular/x_std_libs/r2.3/libs.gemfile")
diff --git a/gemfiles/ruby_2_3_omni_v1.6.gemfile b/gemfiles/ruby_2_3_omni_v1.6.gemfile
new file mode 100644
index 0000000..02327b4
--- /dev/null
+++ b/gemfiles/ruby_2_3_omni_v1.6.gemfile
@@ -0,0 +1,11 @@
+# This file was generated by Appraisal2
+
+source "https://gem.coop"
+
+gemspec path: "../"
+
+eval_gemfile("modular/omniauth/r2/v1.6.gemfile")
+
+eval_gemfile("modular/rack/r2.1/v1.4.gemfile")
+
+eval_gemfile("modular/x_std_libs/r2.3/libs.gemfile")
diff --git a/gemfiles/ruby_2_3_omni_v1.7.gemfile b/gemfiles/ruby_2_3_omni_v1.7.gemfile
new file mode 100644
index 0000000..f4becd6
--- /dev/null
+++ b/gemfiles/ruby_2_3_omni_v1.7.gemfile
@@ -0,0 +1,11 @@
+# This file was generated by Appraisal2
+
+source "https://gem.coop"
+
+gemspec path: "../"
+
+eval_gemfile("modular/omniauth/r2/v1.7.gemfile")
+
+eval_gemfile("modular/rack/r2.1/v1.5.gemfile")
+
+eval_gemfile("modular/x_std_libs/r2.3/libs.gemfile")
diff --git a/gemfiles/ruby_2_3_omni_v1.8.gemfile b/gemfiles/ruby_2_3_omni_v1.8.gemfile
new file mode 100644
index 0000000..5b09553
--- /dev/null
+++ b/gemfiles/ruby_2_3_omni_v1.8.gemfile
@@ -0,0 +1,11 @@
+# This file was generated by Appraisal2
+
+source "https://gem.coop"
+
+gemspec path: "../"
+
+eval_gemfile("modular/omniauth/r2/v1.8.gemfile")
+
+eval_gemfile("modular/rack/r2.1/v1.6.gemfile")
+
+eval_gemfile("modular/x_std_libs/r2.3/libs.gemfile")
diff --git a/gemfiles/ruby_2_4.gemfile b/gemfiles/ruby_2_4.gemfile
index b4bc15f..8b40eb9 100644
--- a/gemfiles/ruby_2_4.gemfile
+++ b/gemfiles/ruby_2_4.gemfile
@@ -4,7 +4,7 @@ source "https://gem.coop"
gemspec path: "../"
-eval_gemfile("modular/omniauth/r2/v1.5.gemfile")
+eval_gemfile("modular/omniauth/r2/v1.8.gemfile")
eval_gemfile("modular/rack/r2.3/v2.1.gemfile")
diff --git a/lib/omniauth/strategies/ldap.rb b/lib/omniauth/strategies/ldap.rb
index a00e7e7..5ec29fa 100644
--- a/lib/omniauth/strategies/ldap.rb
+++ b/lib/omniauth/strategies/ldap.rb
@@ -57,18 +57,18 @@ def request_phase
# 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_path).finish
+ 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.params["username"].to_s != "" && request.params["password"].to_s != ""
- return Rack::Response.new([], 302, "Location" => callback_path).finish
+ 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_path)
+ 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")
@@ -111,9 +111,10 @@ def callback_phase
end
def filter(adaptor, username_override = nil)
- if adaptor.filter && !adaptor.filter.empty?
+ flt = adaptor.filter
+ if flt && !flt.to_s.empty?
username = Net::LDAP::Filter.escape(@options[:name_proc].call(username_override || request.params["username"]))
- Net::LDAP::Filter.construct(adaptor.filter % {username: username})
+ Net::LDAP::Filter.construct(flt % {username: username})
else
Net::LDAP::Filter.equals(adaptor.uid, @options[:name_proc].call(username_override || request.params["username"]))
end
@@ -174,7 +175,7 @@ def valid_request_method?
def missing_credentials?
request.params["username"].nil? || request.params["username"].empty? || request.params["password"].nil? || request.params["password"].empty?
- end # missing_credentials?
+ end
# Extract a normalized username from a trusted header when enabled.
# Returns nil when not configured or not present.
@@ -193,9 +194,9 @@ def header_username
# (bind_dn/password or anonymous). Does not attempt to bind as the user.
def directory_lookup(adaptor, username)
entry = nil
- filter = filter(adaptor, username)
+ search_filter = filter(adaptor, username)
adaptor.connection.open do |conn|
- rs = conn.search(filter: filter, size: 1)
+ rs = conn.search(filter: search_filter, size: 1)
entry = rs.first if rs && rs.first
end
entry
diff --git a/omniauth-ldap.gemspec b/omniauth-ldap.gemspec
index aff328f..257b815 100644
--- a/omniauth-ldap.gemspec
+++ b/omniauth-ldap.gemspec
@@ -98,7 +98,7 @@ Gem::Specification.new do |spec|
spec.executables = []
spec.add_dependency("net-ldap", "~> 0.16", "< 1") # ruby >= 2.0
- spec.add_dependency("omniauth", ">= 1", "< 3") # ruby >= 0.0
+ spec.add_dependency("omniauth", ">= 1.2", "< 3") # ruby >= 0.0
spec.add_dependency("pyu-ruby-sasl", ">= 0.0.3.3", "< 0.1") # ruby >= 0.0
spec.add_dependency("rack", ">= 1", "< 4") # ruby >= 0.0
spec.add_dependency("rubyntlm", "~> 0.6.2", "< 1") # ruby >= 1.8.7
diff --git a/spec/integration/middleware_spec.rb b/spec/integration/middleware_spec.rb
index c812134..2ccfc0c 100644
--- a/spec/integration/middleware_spec.rb
+++ b/spec/integration/middleware_spec.rb
@@ -61,6 +61,22 @@
end
end
+ it "honors SCRIPT_NAME when mounted under a subdirectory for redirect to callback" do
+ begin
+ OmniAuth.config.test_mode = true
+ OmniAuth.config.mock_auth[:ldap] = OmniAuth::AuthHash.new(provider: "ldap", uid: "bob", info: {"name" => "Bob"})
+
+ # Simulate subdirectory mount by setting SCRIPT_NAME and posting credentials to request phase
+ env = {"SCRIPT_NAME" => "/subdir"}
+ post "/auth/ldap", {"username" => "bob", "password" => "secret"}, env
+ expect(last_response.status).to eq 302
+ expect(last_response.headers["Location"]).to eq "http://example.org/subdir/auth/ldap/callback"
+ ensure
+ OmniAuth.config.mock_auth.delete(:ldap)
+ OmniAuth.config.test_mode = false
+ end
+ end
+
unless defined?(TestCallbackSetter)
class TestCallbackSetter
def initialize(app)
diff --git a/spec/omniauth/strategies/ldap_spec.rb b/spec/omniauth/strategies/ldap_spec.rb
index bf2564f..e65160b 100644
--- a/spec/omniauth/strategies/ldap_spec.rb
+++ b/spec/omniauth/strategies/ldap_spec.rb
@@ -70,7 +70,7 @@ def make_env(path = "/auth/ldap", props = {})
end
it "has the callback as the action for the form" do
- expect(last_response.body).to include("action='/auth/ldap/callback'")
+ expect(last_response.body).to include("action='http://example.org/auth/ldap/callback'")
end
it "has a text field for each of the fields" do
@@ -80,6 +80,33 @@ def make_env(path = "/auth/ldap", props = {})
it "has a label of the form title" do
expect(last_response.body.scan("MyLdap Form").size).to be > 1
end
+
+ context "when mounted under a subdirectory" do
+ let(:sub_env) do
+ make_env("/auth/ldap", {
+ "SCRIPT_NAME" => "/subdirectory",
+ "rack.session" => {csrf: csrf_token},
+ "rack.input" => StringIO.new("authenticity_token=#{escaped_token}"),
+ })
+ end
+
+ it "renders form with full callback_url including subdirectory" do
+ post "/auth/ldap", nil, sub_env
+ expect(last_response.status).to eq 200
+ expect(last_response.body).to include("action='http://example.org/subdirectory/auth/ldap/callback'")
+ end
+
+ it "renders form with full callback_url including nested subdirectory" do
+ nested_env = make_env("/auth/ldap", {
+ "SCRIPT_NAME" => "/nested/app",
+ "rack.session" => {csrf: csrf_token},
+ "rack.input" => StringIO.new("authenticity_token=#{escaped_token}"),
+ })
+ post "/auth/ldap", nil, nested_env
+ expect(last_response.status).to eq 200
+ expect(last_response.body).to include("action='http://example.org/nested/app/auth/ldap/callback'")
+ end
+ end
end
describe "post /auth/ldap/callback" do
@@ -434,7 +461,21 @@ def connection_returning(entry)
env = {"rack.session" => {}, "REQUEST_METHOD" => "POST", "PATH_INFO" => "/auth/ldap", "REMOTE_USER" => "alice"}
post "/auth/ldap", nil, env
expect(last_response).to be_redirect
- expect(last_response.headers["Location"]).to eq "/auth/ldap/callback"
+ expect(last_response.headers["Location"]).to eq "http://example.org/auth/ldap/callback"
+ end
+
+ it "redirects including subdirectory when header present and app is mounted under a subdirectory" do
+ env = {"rack.session" => {}, "REQUEST_METHOD" => "POST", "PATH_INFO" => "/auth/ldap", "SCRIPT_NAME" => "/subdir", "REMOTE_USER" => "alice"}
+ post "/auth/ldap", nil, env
+ expect(last_response).to be_redirect
+ expect(last_response.headers["Location"]).to eq "http://example.org/subdir/auth/ldap/callback"
+ end
+
+ it "redirects including nested subdirectory when header present and app is mounted under a nested subdirectory" do
+ env = {"rack.session" => {}, "REQUEST_METHOD" => "POST", "PATH_INFO" => "/auth/ldap", "SCRIPT_NAME" => "/nested/app", "REMOTE_USER" => "alice"}
+ post "/auth/ldap", nil, env
+ expect(last_response).to be_redirect
+ expect(last_response.headers["Location"]).to eq "http://example.org/nested/app/auth/ldap/callback"
end
it "authenticates on callback without password using REMOTE_USER" do