diff --git a/.github/workflows/ancient.yml b/.github/workflows/ancient.yml index 3cc6e37..92cb484 100644 --- a/.github/workflows/ancient.yml +++ b/.github/workflows/ancient.yml @@ -36,7 +36,49 @@ jobs: include: # Ruby 2.3 - ruby: "ruby-2.3" - appraisal: "ruby-2-3" + appraisal: "ruby-2-3-omni-v1.2" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: "3.3.27" + bundler: "2.3.27" + + - ruby: "ruby-2.3" + appraisal: "ruby-2-3-omni-v1.3" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: "3.3.27" + bundler: "2.3.27" + + - ruby: "ruby-2.3" + appraisal: "ruby-2-3-omni-v1.4" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: "3.3.27" + bundler: "2.3.27" + + - ruby: "ruby-2.3" + appraisal: "ruby-2-3-omni-v1.5" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: "3.3.27" + bundler: "2.3.27" + + - ruby: "ruby-2.3" + appraisal: "ruby-2-3-omni-v1.6" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: "3.3.27" + bundler: "2.3.27" + + - ruby: "ruby-2.3" + appraisal: "ruby-2-3-omni-v1.7" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: "3.3.27" + bundler: "2.3.27" + + - ruby: "ruby-2.3" + appraisal: "ruby-2-3-omni-v1.8" exec_cmd: "rake test" gemfile: "Appraisal.root" rubygems: "3.3.27" diff --git a/.github/workflows/opencollective.yml b/.github/workflows/opencollective.yml deleted file mode 100644 index 6122df4..0000000 --- a/.github/workflows/opencollective.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Open Collective Backers - -on: - schedule: - # Run once a week on Sunday at 12:00 AM UTC - - cron: '0 0 * * 0' - workflow_dispatch: - -permissions: - contents: write - -jobs: - update-backers: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - name: Setup Ruby & RubyGems - uses: ruby/setup-ruby@v1 - with: - ruby-version: ruby - rubygems: default - bundler: default - bundler-cache: true - - - name: README Update - env: - # Keep GITHUB_TOKEN for any tools/scripts expecting it, mapped to the same secret - GITHUB_TOKEN: ${{ secrets.README_UPDATER_TOKEN }} - README_UPDATER_TOKEN: ${{ secrets.README_UPDATER_TOKEN }} - REPO: ${{ github.repository }} - run: | - git config user.name 'autobolt' - git config user.email 'autobots@9thbit.net' - # Use the configured token for authenticated pushes - git remote set-url origin "https://x-access-token:${README_UPDATER_TOKEN}@github.com/${REPO}.git" - bin/kettle-readme-backers - # Push back to the same branch/ref that triggered the workflow (default branch for schedule) - git push origin HEAD diff --git a/.rubocop_gradual.lock b/.rubocop_gradual.lock index bd5000d..f8d88ab 100644 --- a/.rubocop_gradual.lock +++ b/.rubocop_gradual.lock @@ -5,10 +5,10 @@ [114, 30, 3, "Style/AndOr: Use `&&` instead of `and`.", 193409806], [114, 37, 1, "Lint/AssignmentInCondition: Wrap assignment in parentheses if intentional", 177560] ], - "spec/integration/middleware_spec.rb:4062046892": [ + "spec/integration/middleware_spec.rb:4142891586": [ [3, 16, 39, "RSpec/DescribeClass: The first argument to describe should be the class or module being tested.", 638096201], [30, 14, 10, "RSpec/ExpectActual: Provide the actual value you are testing to `expect(...)`.", 837117997], - [65, 5, 317, "RSpec/LeakyConstantDeclaration: Stub class constant instead of declaring explicitly.", 424933157] + [81, 5, 317, "RSpec/LeakyConstantDeclaration: Stub class constant instead of declaring explicitly.", 424933157] ], "spec/integration/roda_integration_spec.rb:1921252381": [ [3, 16, 50, "RSpec/DescribeClass: The first argument to describe should be the class or module being tested.", 3681952328], @@ -30,14 +30,14 @@ [47, 7, 38, "RSpec/AnyInstance: Avoid stubbing using `allow_any_instance_of`.", 3627954156], [84, 7, 48, "RSpec/AnyInstance: Avoid stubbing using `allow_any_instance_of`.", 2759780562] ], - "spec/omniauth/strategies/ldap_spec.rb:783052937": [ - [93, 13, 9, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1130140517], - [148, 17, 28, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3444838747], - [157, 17, 23, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1584148894], - [168, 17, 32, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1515076977], - [177, 19, 19, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2526348694], - [203, 17, 56, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2413495789], - [218, 13, 9, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3182939526], - [251, 15, 19, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2526348694] + "spec/omniauth/strategies/ldap_spec.rb:2044523926": [ + [120, 13, 9, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1130140517], + [175, 17, 28, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3444838747], + [184, 17, 23, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1584148894], + [195, 17, 32, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1515076977], + [204, 19, 19, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2526348694], + [230, 17, 56, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2413495789], + [245, 13, 9, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3182939526], + [278, 15, 19, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2526348694] ] } diff --git a/Appraisals b/Appraisals index 27b567b..a9e20bf 100644 --- a/Appraisals +++ b/Appraisals @@ -56,14 +56,50 @@ appraise "dep-heads" do eval_gemfile "modular/runtime_heads.gemfile" end -appraise "ruby-2-3" do - eval_gemfile "modular/omniauth/r2/v1.1.gemfile" +appraise "ruby-2-3-omni-v1.2" do + 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" +end + +appraise "ruby-2-3-omni-v1.3" do + 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" +end + +appraise "ruby-2-3-omni-v1.4" do + 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" +end + +appraise "ruby-2-3-omni-v1.5" do + 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" +end + +appraise "ruby-2-3-omni-v1.6" do + 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" +end + +appraise "ruby-2-3-omni-v1.7" do + 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" +end + +appraise "ruby-2-3-omni-v1.8" do + 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" end appraise "ruby-2-4" do - 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" eval_gemfile "modular/x_std_libs/r2.4/libs.gemfile" end diff --git a/CHANGELOG.md b/CHANGELOG.md index a488390..ca85c0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,8 +22,15 @@ Please file a bug if you notice a violation of semantic versioning. ### Added +- Support for SCRIPT_NAME for proper URL generation + - behind certain proxies/load balancers, or + - under a subdirectory + ### Changed +- Make support for OmniAuth v1.2+ explicit + - Versions < 1.2 do not support SCRIPT_NAME properly, and may cause other issues + ### Deprecated ### Removed diff --git a/Gemfile.lock b/Gemfile.lock index 259b4c9..33b35d3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -15,7 +15,7 @@ PATH specs: omniauth-ldap (2.3.1) net-ldap (~> 0.16, < 1) - omniauth (>= 1, < 3) + omniauth (>= 1.2, < 3) pyu-ruby-sasl (>= 0.0.3.3, < 0.1) rack (>= 1, < 4) rubyntlm (~> 0.6.2, < 1) diff --git a/README.md b/README.md index f8d7bb6..b525090 100644 --- a/README.md +++ b/README.md @@ -401,6 +401,81 @@ provider :ldap, 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`. + +Rack example (mounted at /myapp): + +```ruby +# config.ru +require "rack" +require "omniauth-ldap" + +app = Rack::Builder.new do + use(Rack::Session::Cookie, secret: "change_me") + use(OmniAuth::Builder) do + provider( + :ldap, + host: "ldap.example.com", + base: "dc=example,dc=com", + uid: "uid", + title: "Example LDAP", + ) + end + + run(->(env) { [404, {"Content-Type" => "text/plain"}, [env.key?("omniauth.auth").to_s]] }) +end + +run Rack::URLMap.new( + "/myapp" => app, +) +``` + +- 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): + +```ruby +# 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`: + +```ruby +OmniAuth.config.full_host = lambda do |env| + scheme = (env["HTTP_X_FORWARDED_PROTO"] || env["rack.url_scheme"]).to_s.split(",").first + host = env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"] || [env["SERVER_NAME"], env["SERVER_PORT"]].compact.join(":") + script = env["SCRIPT_NAME"].to_s + "#{scheme}://#{host}#{script}" +end +``` + +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`. diff --git a/docs/OmniAuth.html b/docs/OmniAuth.html index acfb3ea..a62fd41 100644 --- a/docs/OmniAuth.html +++ b/docs/OmniAuth.html @@ -107,7 +107,7 @@

Defined Under Namespace

diff --git a/docs/OmniAuth/LDAP.html b/docs/OmniAuth/LDAP.html index 8487bea..345d15f 100644 --- a/docs/OmniAuth/LDAP.html +++ b/docs/OmniAuth/LDAP.html @@ -135,7 +135,7 @@

diff --git a/docs/OmniAuth/LDAP/Adaptor.html b/docs/OmniAuth/LDAP/Adaptor.html index 18be523..ef639b3 100644 --- a/docs/OmniAuth/LDAP/Adaptor.html +++ b/docs/OmniAuth/LDAP/Adaptor.html @@ -1036,7 +1036,7 @@

diff --git a/docs/OmniAuth/LDAP/Adaptor/AuthenticationError.html b/docs/OmniAuth/LDAP/Adaptor/AuthenticationError.html index e4a1d6a..741dd00 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 4e01047..5b84f08 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 796b8b0..532f5cc 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 d528845..9a907d5 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 49fcb6c..c67a473 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 8c3ef0a..c5db66f 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 983a3db..bd39ec2 100644 --- a/docs/OmniAuth/Strategies/LDAP.html +++ b/docs/OmniAuth/Strategies/LDAP.html @@ -285,7 +285,6 @@

 
 
-133
 134
 135
 136
@@ -318,10 +317,11 @@ 

163 164 165 -166

+166 +167 -
# File 'lib/omniauth/strategies/ldap.rb', line 133
+      
# File 'lib/omniauth/strategies/ldap.rb', line 134
 
 def map_user(mapper, object)
   user = {}
@@ -482,15 +482,17 @@ 

117 118 119 -120

+120 +121
# File 'lib/omniauth/strategies/ldap.rb', line 113
 
 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
@@ -556,18 +558,18 @@ 

# 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") @@ -583,7 +585,7 @@

diff --git a/docs/_index.html b/docs/_index.html index 9f12b79..344bc7f 100644 --- a/docs/_index.html +++ b/docs/_index.html @@ -87,6 +87,12 @@

File Listing

  • CITATION
  • +
  • omniauth-ldap-2.3.1.gem
  • + + +
  • omniauth-ldap-2.3.1.gem
  • + +
  • REEK
  • @@ -248,7 +254,7 @@

    Namespace Listing A-Z

    diff --git a/docs/file.CHANGELOG.html b/docs/file.CHANGELOG.html index 5b8253d..68adc67 100644 --- a/docs/file.CHANGELOG.html +++ b/docs/file.CHANGELOG.html @@ -74,6 +74,15 @@

    Added

    +
      +
    • Support for SCRIPT_NAME for proper URL generation +
        +
      • behind certain proxies/load balancers, or
      • +
      • under a subdirectory
      • +
      +
    • +
    +

    Changed

    Deprecated

    @@ -284,7 +293,7 @@

    diff --git a/docs/file.CITATION.html b/docs/file.CITATION.html index 5a62985..a0f652b 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 0bfd65a..6e8b968 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 bad3f60..c25b68f 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 0cf42e0..fcef94f 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 836550a..457a3ab 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 faddfcc..f747a0f 100644 --- a/docs/file.README.html +++ b/docs/file.README.html @@ -567,6 +567,89 @@

    Name processing and examples

    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.

    + + + +

    Rack example (mounted at /myapp):

    + +
    # config.ru
    +require "rack"
    +require "omniauth-ldap"
    +
    +app = Rack::Builder.new do
    +  use(Rack::Session::Cookie, secret: "change_me")
    +  use(OmniAuth::Builder) do
    +    provider(
    +      :ldap,
    +      host: "ldap.example.com",
    +      base: "dc=example,dc=com",
    +      uid: "uid",
    +      title: "Example LDAP",
    +    )
    +  end
    +
    +  run(->(env) { [404, {"Content-Type" => "text/plain"}, [env.key?("omniauth.auth").to_s]] })
    +end
    +
    +run Rack::URLMap.new(
    +  "/myapp" => app,
    +)
    +
    + + + +

    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
    +
    + + + +

    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:

    + +
    OmniAuth.config.full_host = lambda do |env|
    +  scheme = (env["HTTP_X_FORWARDED_PROTO"] || env["rack.url_scheme"]).to_s.split(",").first
    +  host = env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"] || [env["SERVER_NAME"], env["SERVER_PORT"]].compact.join(":")
    +  script = env["SCRIPT_NAME"].to_s
    +  "#{scheme}://#{host}#{script}"
    +end
    +
    + +

    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.

    + + +

    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/file.REEK.html b/docs/file.REEK.html index db96dc8..cbbef16 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 0ea6cfc..535c4d0 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 93f628e..282d6cd 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 aa339f2..ffab603 100644 --- a/docs/file.adaptor.html +++ b/docs/file.adaptor.html @@ -113,7 +113,7 @@ diff --git a/docs/file.ldap.html b/docs/file.ldap.html index 480a0d9..80cb27a 100644 --- a/docs/file.ldap.html +++ b/docs/file.ldap.html @@ -91,7 +91,7 @@ diff --git a/docs/file.net-ldap.html b/docs/file.net-ldap.html index cd043ae..d5781b4 100644 --- a/docs/file.net-ldap.html +++ b/docs/file.net-ldap.html @@ -78,7 +78,7 @@ diff --git a/docs/file.net-ntlm.html b/docs/file.net-ntlm.html index 9e05759..beea2ad 100644 --- a/docs/file.net-ntlm.html +++ b/docs/file.net-ntlm.html @@ -74,7 +74,7 @@ diff --git a/docs/file.omniauth-ldap-2.3.1.gem.html b/docs/file.omniauth-ldap-2.3.1.gem.html new file mode 100644 index 0000000..114d84a --- /dev/null +++ b/docs/file.omniauth-ldap-2.3.1.gem.html @@ -0,0 +1,71 @@ + + + + + + + File: omniauth-ldap-2.3.1.gem + + — Documentation by YARD 0.9.37 + + + + + + + + + + + + + + + + + + + +
    + + +

    69f8474644ce03850236011fe6579aca167b4fbaf69bdcbea648b3000737c7e46dcb0b63f61d6c4e045b138110187dec55941c5f56b2c8438df63ee472807813

    +
    + + + +
    + + \ 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 @@

    Th diff --git a/docs/file.sasl.html b/docs/file.sasl.html index 0e6e571..a015996 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 225e243..b5fd981 100644 --- a/docs/file.version.html +++ b/docs/file.version.html @@ -69,7 +69,7 @@ diff --git a/docs/file_list.html b/docs/file_list.html index fcada38..ec1fd71 100644 --- a/docs/file_list.html +++ b/docs/file_list.html @@ -92,6 +92,16 @@

    File List

    +
  • + +
  • + + +
  • + +
  • + +
  • diff --git a/docs/index.html b/docs/index.html index cdc8c27..be9533c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -567,6 +567,89 @@

    Name processing and examples

    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.

    + + + +

    Rack example (mounted at /myapp):

    + +
    # config.ru
    +require "rack"
    +require "omniauth-ldap"
    +
    +app = Rack::Builder.new do
    +  use(Rack::Session::Cookie, secret: "change_me")
    +  use(OmniAuth::Builder) do
    +    provider(
    +      :ldap,
    +      host: "ldap.example.com",
    +      base: "dc=example,dc=com",
    +      uid: "uid",
    +      title: "Example LDAP",
    +    )
    +  end
    +
    +  run(->(env) { [404, {"Content-Type" => "text/plain"}, [env.key?("omniauth.auth").to_s]] })
    +end
    +
    +run Rack::URLMap.new(
    +  "/myapp" => app,
    +)
    +
    + + + +

    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
    +
    + + + +

    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:

    + +
    OmniAuth.config.full_host = lambda do |env|
    +  scheme = (env["HTTP_X_FORWARDED_PROTO"] || env["rack.url_scheme"]).to_s.split(",").first
    +  host = env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"] || [env["SERVER_NAME"], env["SERVER_PORT"]].compact.join(":")
    +  script = env["SCRIPT_NAME"].to_s
    +  "#{scheme}://#{host}#{script}"
    +end
    +
    + +

    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.

    + + +

    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