omniauth-ldap 2.3.2 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64fced98d7ab577e6c9abc446ace5678b829670e9d241f0a759c61efc47ecf5e
4
- data.tar.gz: 7fda93687c96509833b9d72f277995f53eb2368ebe44740180ceadd3afac6ebe
3
+ metadata.gz: cfb42b529db19042a0140d9bdb39698b177710df48bc80eb9c1470bfebda0ae2
4
+ data.tar.gz: 411ba1e8817fc5814c248cbf096471ebe2f4a013a69dff3b6771143526a73f8b
5
5
  SHA512:
6
- metadata.gz: 8750eeed19ed13d89d041b14123b68cc459e7f5595d04d6d0fe67227c06c312f7952264f6fdd19a8f57957ce98c9ea3620d125fd01c445446c8848400a668e12
7
- data.tar.gz: ed9dc416b2eba5c6e9d9cc5e889f50bd70347ff4a6ce9261cb8703e77d010c7834a5a566ef8ed7f93f5fbaf4baba0afa9965191decebdf84ed8ffd8b79590a8d
6
+ metadata.gz: e5cd173bb9dd917627ba9d96d3fe3a5e713bb74e3623d8ab1247fe71da0243e0e66658f64dd356247e060fbddda4a9031e76242dd8b0ea0575d9233bfcee1b0d
7
+ data.tar.gz: 70d8dd160e9793037bb4564cde716dfc8cd98c3805b73c65f49c5ecd2383b13c920828affab59e458b224b8f1eebfd4a127d9bd06a55d967e1d397f21f01a1da
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -32,6 +32,47 @@ Please file a bug if you notice a violation of semantic versioning.
32
32
 
33
33
  ### Security
34
34
 
35
+ ## [2.3.4] - 2026-05-18
36
+
37
+ - TAG: [v2.3.4][2.3.4t]
38
+ - COVERAGE: 97.44% -- 304/312 lines in 4 files
39
+ - BRANCH COVERAGE: 79.58% -- 113/142 branches in 4 files
40
+ - 94.44% documented
41
+
42
+ ### Added
43
+
44
+ - Add `header_auth_source` to require explicit selection of trusted header identity source (`:env` or `:http_header`)
45
+ - Add `header_auth_require_tls` to require TLS for trusted header SSO by default
46
+ - Log a prominent security warning when `header_auth` is enabled
47
+
48
+ ### Changed
49
+
50
+ - Trusted header SSO now defaults to trusting only server-set env variables and no longer checks Rack `HTTP_` header variants unless `header_auth_source: :http_header` is configured
51
+
52
+ ### Fixed
53
+
54
+ - Fix OpenSSL 3/Ruby 4 compatibility in the TLS options adaptor spec
55
+
56
+ ### Security
57
+
58
+ - Harden trusted header SSO against spoofing by removing automatic fallback from `REMOTE_USER` to `HTTP_REMOTE_USER`
59
+
60
+ ## [2.3.3] - 2025-11-10
61
+
62
+ - TAG: [v2.3.3][2.3.3t]
63
+ - COVERAGE: 97.61% -- 286/293 lines in 4 files
64
+ - BRANCH COVERAGE: 79.69% -- 102/128 branches in 4 files
65
+ - 94.44% documented
66
+
67
+ ### Added
68
+
69
+ - Documentation cleanup & updates
70
+ - YARD documentation covering 94% of the code
71
+
72
+ ### Changed
73
+
74
+ - kettle-dev v1.1.54
75
+
35
76
  ## [2.3.2] - 2025-11-06
36
77
 
37
78
  - TAG: [v2.3.2][2.3.2t]
@@ -50,14 +91,14 @@ Please file a bug if you notice a violation of semantic versioning.
50
91
  - https://datatracker.ietf.org/doc/html/draft-behera-ldap-password-policy-11
51
92
  - Support for JSON bodies
52
93
  - Support custom LDAP attributes mapping
53
- - Raise a distinct error when LDAP server is unreachable
54
- - Previously raised an invalid credentials authentication failure error, which is technically incorrect
55
94
  - Documentation of TLS verification options
56
95
 
57
96
  ### Changed
58
97
 
59
98
  - Make support for OmniAuth v1.2+ explicit
60
99
  - Versions < 1.2 do not support SCRIPT_NAME properly, and may cause other issues
100
+ - Raise a distinct error when LDAP server is unreachable
101
+ - Previously raised an invalid credentials authentication failure error, which is technically incorrect
61
102
 
62
103
  ## [2.3.1] - 2025-11-05
63
104
 
@@ -224,7 +265,11 @@ Please file a bug if you notice a violation of semantic versioning.
224
265
  [1.0.0]: https://github.com/omniauth/omniauth-ldap/compare/5656da80d4193e0d0584f44bac493a87695e580f...v1.0.0
225
266
  [1.0.0t]: https://github.com/omniauth/omniauth-ldap/releases/tag/v1.0.0
226
267
 
227
- [Unreleased]: https://github.com/omniauth/omniauth-ldap/compare/v2.3.2...HEAD
268
+ [Unreleased]: https://github.com/omniauth/omniauth-ldap/compare/v2.3.4...HEAD
269
+ [2.3.4]: https://github.com/omniauth/omniauth-ldap/compare/v2.3.3...v2.3.4
270
+ [2.3.4t]: https://github.com/omniauth/omniauth-ldap/releases/tag/v2.3.4
271
+ [2.3.3]: https://github.com/omniauth/omniauth-ldap/compare/v2.3.2...v2.3.3
272
+ [2.3.3t]: https://github.com/omniauth/omniauth-ldap/releases/tag/v2.3.3
228
273
  [2.3.2]: https://github.com/omniauth/omniauth-ldap/compare/v2.3.1...v2.3.2
229
274
  [2.3.2t]: https://github.com/omniauth/omniauth-ldap/releases/tag/v2.3.2
230
275
  [2.3.1]: https://github.com/omniauth/omniauth-ldap/compare/v2.0.0...v2.3.1
data/CITATION.cff CHANGED
File without changes
data/CODE_OF_CONDUCT.md CHANGED
File without changes
data/CONTRIBUTING.md CHANGED
@@ -24,13 +24,13 @@ Follow these instructions:
24
24
 
25
25
  ## Executables vs Rake tasks
26
26
 
27
- Executables shipped by dependencies, such as omniauth-ldap, and stone_checksums, are available
27
+ Executables shipped by dependencies, such as kettle-dev, and stone_checksums, are available
28
28
  after running `bin/setup`. These include:
29
29
 
30
30
  - gem_checksums
31
31
  - kettle-changelog
32
32
  - kettle-commit-msg
33
- - omniauth-ldap-setup
33
+ - kettle-dev-setup
34
34
  - kettle-dvcs
35
35
  - kettle-pre-release
36
36
  - kettle-readme-backers
@@ -68,7 +68,9 @@ GitHub API and CI helpers
68
68
  Releasing and signing
69
69
  - SKIP_GEM_SIGNING: If set, skip gem signing during build/release
70
70
  - GEM_CERT_USER: Username for selecting your public cert in `certs/<USER>.pem` (defaults to $USER)
71
- - SOURCE_DATE_EPOCH: Reproducible build timestamp. `kettle-release` will set this automatically for the session.
71
+ - SOURCE_DATE_EPOCH: Reproducible build timestamp.
72
+ - `kettle-release` will set this automatically for the session.
73
+ - Not needed on bundler >= 2.7.0, as reproducible builds have become the default.
72
74
 
73
75
  Git hooks and commit message helpers (exe/kettle-commit-msg)
74
76
  - GIT_HOOK_BRANCH_VALIDATE: Branch name validation mode (e.g., `jira`) or `false` to disable
@@ -166,6 +168,7 @@ NOTE: To build without signing the gem set `SKIP_GEM_SIGNING` to any value in th
166
168
  1. Update version.rb to contain the correct version-to-be-released.
167
169
  2. Run `bundle exec kettle-changelog`.
168
170
  3. Run `bundle exec kettle-release`.
171
+ 4. Stay awake and monitor the release process for any errors, and answer any prompts.
169
172
 
170
173
  #### Manual process
171
174
 
data/FUNDING.md CHANGED
@@ -6,7 +6,7 @@ Many paths lead to being a sponsor or a backer of this project. Are you on such
6
6
 
7
7
  [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal]
8
8
 
9
- [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS or refugee efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS or refugee efforts using Patreon][🖇patreon-img]][🖇patreon]
9
+ [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS efforts using Patreon][🖇patreon-img]][🖇patreon]
10
10
 
11
11
  [⛳liberapay-img]: https://img.shields.io/liberapay/goal/pboling.svg?logo=liberapay&color=a51611&style=flat
12
12
  [⛳liberapay]: https://liberapay.com/pboling/donate
@@ -27,11 +27,11 @@ Many paths lead to being a sponsor or a backer of this project. Are you on such
27
27
 
28
28
  <!-- RELEASE-NOTES-FOOTER-END -->
29
29
 
30
- # 🤑 Request for Help
30
+ # 🤑 A request for help
31
31
 
32
32
  Maintainers have teeth and need to pay their dentists.
33
- After getting laid off in an RIF in March and filled with many dozens of rejections,
34
- I'm now spending ~60+ hours a week building open source tools.
33
+ After getting laid off in an RIF in March, and encountering difficulty finding a new one,
34
+ I began spending most of my time building open source tools.
35
35
  I'm hoping to be able to pay for my kids' health insurance this month,
36
36
  so if you value the work I am doing, I need your support.
37
37
  Please consider sponsoring me or the project.
@@ -40,16 +40,13 @@ To join the community or get help 👇️ Join the Discord.
40
40
 
41
41
  [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite]
42
42
 
43
- To say "thanks for maintaining such a great tool" ☝️ Join the Discord or 👇️ send money.
43
+ To say "thanks!" ☝️ Join the Discord or 👇️ send money.
44
44
 
45
- [![Sponsor me on GitHub Sponsors][🖇sponsor-bottom-img]][🖇sponsor] 💌 [![Sponsor me on Liberapay][⛳liberapay-bottom-img]][⛳liberapay-img] 💌 [![Donate on PayPal][🖇paypal-bottom-img]][🖇paypal-img]
45
+ [![Sponsor me on GitHub Sponsors][🖇sponsor-bottom-img]][🖇sponsor] 💌 [![Sponsor me on Liberapay][⛳liberapay-bottom-img]][⛳liberapay] 💌 [![Donate on PayPal][🖇paypal-bottom-img]][🖇paypal]
46
46
 
47
47
  # Another Way to Support Open Source Software
48
48
 
49
- > How wonderful it is that nobody need wait a single moment before starting to improve the world.<br/>
50
- >—Anne Frank
51
-
52
- I’m driven by a passion to foster a thriving open-source community – a space where people can tackle complex problems, no matter how small. Revitalizing libraries that have fallen into disrepair, and building new libraries focused on solving real-world challenges, are my passions — totaling 79 hours of FLOSS coding over just the past seven days, a pretty regular week for me. I was recently affected by layoffs, and the tech jobs market is unwelcoming. I’m reaching out here because your support would significantly aid my efforts to provide for my family, and my farm (11 🐔 chickens, 2 🐶 dogs, 3 🐰 rabbits, 8 🐈‍ cats).
49
+ I’m driven by a passion to foster a thriving open-source community – a space where people can tackle complex problems, no matter how small. Revitalizing libraries that have fallen into disrepair, and building new libraries focused on solving real-world challenges, are my passions. I was recently affected by layoffs, and the tech jobs market is unwelcoming. I’m reaching out here because your support would significantly aid my efforts to provide for my family, and my farm (11 🐔 chickens, 2 🐶 dogs, 3 🐰 rabbits, 8 🐈‍ cats).
53
50
 
54
51
  If you work at a company that uses my work, please encourage them to support me as a corporate sponsor. My work on gems you use might show up in `bundle fund`.
55
52
 
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Peter H. Boling, and omniauth-ldap contributors
3
+ Copyright (c) 2025 - 2026 Peter H. Boling, and omniauth-ldap contributors
4
4
  Copyright (c) 2014 David Benko
5
5
  Copyright (c) 2011 by Ping Yu and Intridea, Inc.
6
6
 
data/README.md CHANGED
@@ -1,32 +1,3 @@
1
- | 📍 NOTE |
2
- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
3
- | RubyGems (the [GitHub org][rubygems-org], not the website) [suffered][draper-security] a [hostile takeover][ellen-takeover] in September 2025. |
4
- | Ultimately [4 maintainers][simi-removed] were [hard removed][martin-removed] and a reason has been given for only 1 of those, while 2 others resigned in protest. |
5
- | It is a [complicated story][draper-takeover] which is difficult to [parse quickly][draper-lies]. |
6
- | I'm adding notes like this to gems because I [don't condone theft][draper-theft] of repositories or gems from their rightful owners. |
7
- | If a similar theft happened with my repos/gems, I'd hope some would stand up for me. |
8
- | Disenfranchised former-maintainers have started [gem.coop][gem-coop]. |
9
- | Once available I will publish there exclusively; unless RubyCentral makes amends with the community. |
10
- | The ["Technology for Humans: Joel Draper"][reinteractive-podcast] podcast episode by [reinteractive][reinteractive] is the most cogent summary I'm aware of. |
11
- | See [here][gem-naming], [here][gem-coop] and [here][martin-ann] for more info on what comes next. |
12
- | What I'm doing: A (WIP) proposal for [bundler/gem scopes][gem-scopes], and a (WIP) proposal for a federated [gem server][gem-server]. |
13
-
14
- [rubygems-org]: https://github.com/rubygems/
15
- [draper-security]: https://joel.drapper.me/p/ruby-central-security-measures/
16
- [draper-takeover]: https://joel.drapper.me/p/ruby-central-takeover/
17
- [ellen-takeover]: https://pup-e.com/blog/goodbye-rubygems/
18
- [simi-removed]: https://www.reddit.com/r/ruby/s/gOk42POCaV
19
- [martin-removed]: https://bsky.app/profile/martinemde.com/post/3m3occezxxs2q
20
- [draper-lies]: https://joel.drapper.me/p/ruby-central-fact-check/
21
- [draper-theft]: https://joel.drapper.me/p/ruby-central/
22
- [reinteractive]: https://reinteractive.com/ruby-on-rails
23
- [gem-coop]: https://gem.coop
24
- [gem-naming]: https://github.com/gem-coop/gem.coop/issues/12
25
- [martin-ann]: https://martinemde.com/2025/10/05/announcing-gem-coop.html
26
- [gem-scopes]: https://github.com/galtzo-floss/bundle-namespace
27
- [gem-server]: https://github.com/galtzo-floss/gem-server
28
- [reinteractive-podcast]: https://youtu.be/_H4qbtC5qzU?si=BvuBU90R2wAqD2E6
29
-
30
1
  [![Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0][🖼️galtzo-i]][🖼️galtzo-discord] [![ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5][🖼️ruby-lang-i]][🖼️ruby-lang] [![omniauth Logo (presumed to be) by tomeara, (presumed to be) MIT License][🖼️omniauth-i]][🖼️omniauth]
31
2
 
32
3
  [🖼️galtzo-i]: https://logos.galtzo.com/assets/images/galtzo-floss/avatar-192px.svg
@@ -48,6 +19,13 @@
48
19
 
49
20
  [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate at ko-fi.com][🖇kofi-img]][🖇kofi]
50
21
 
22
+ <details>
23
+ <summary>👣 How will this project approach the September 2025 hostile takeover of RubyGems? 🚑️</summary>
24
+
25
+ I've summarized my thoughts in [this blog post](https://dev.to/galtzo/hostile-takeover-of-rubygems-my-thoughts-5hlo).
26
+
27
+ </details>
28
+
51
29
  ## 🌻 Synopsis
52
30
 
53
31
  Use the LDAP strategy as a middleware in your application:
@@ -79,9 +57,11 @@ use OmniAuth::Strategies::LDAP,
79
57
  # use OmniAuth::Strategies::LDAP, filter: '(&(uid=%{username})(memberOf=cn=myapp-users,ou=groups,dc=example,dc=com))'
80
58
  ```
81
59
 
82
- All of the listed options are required, with the exception of `:title`, `:name_proc`, `:bind_dn`, and `:password`.
60
+ At minimum you normally configure `:host`, `:base`, and either `:uid` or `:filter`. The other options shown above customize connection behavior, TLS, username normalization, timeouts, and returned auth info.
83
61
 
84
- ## TLS certificate verification
62
+ For trusted header SSO, enable `header_auth: true` and explicitly choose the trusted identity source with `header_auth_source: :env` or `header_auth_source: :http_header`. See [Trusted header SSO](#trusted-header-sso-remote_user-and-friends) for the security requirements.
63
+
64
+ ### TLS certificate verification
85
65
 
86
66
  This gem enables TLS certificate verification by default when you use `encryption: "ssl"` (LDAPS / simple TLS) or `encryption: "tls"` (STARTTLS). We always pass `tls_options` to Net::LDAP based on `OpenSSL::SSL::SSLContext::DEFAULT_PARAMS`, which includes `verify_mode: OpenSSL::SSL::VERIFY_PEER` and sane defaults.
87
67
 
@@ -153,7 +133,7 @@ Compatible with MRI Ruby 2.0+, and concordant releases of JRuby, and TruffleRuby
153
133
 
154
134
  Available as part of the Tidelift Subscription.
155
135
 
156
- <details>
136
+ <details markdown="1">
157
137
  <summary>Need enterprise-level guarantees?</summary>
158
138
 
159
139
  The maintainers of this and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use.
@@ -188,7 +168,7 @@ gem install omniauth-ldap
188
168
 
189
169
  ### 🔒 Secure Installation
190
170
 
191
- <details>
171
+ <details markdown="1">
192
172
  <summary>For Medium or High Security Installations</summary>
193
173
 
194
174
  This gem is cryptographically signed, and has verifiable [SHA-256 and SHA-512][💎SHA_checksums] checksums by
@@ -228,14 +208,14 @@ The following options are available for configuring the OmniAuth LDAP strategy:
228
208
  ### Required Options
229
209
 
230
210
  - `:host` - The hostname or IP address of the LDAP server.
231
- - `:port` - The port number of the LDAP server (default: 389).
232
- - `:method` - The connection method. Allowed values: `:plain`, `:ssl`, `:tls` (default: `:plain`).
233
211
  - `:base` - The base DN for the LDAP search.
234
212
  - `:uid` or `:filter` - Either `:uid` (the LDAP attribute for username, default: "sAMAccountName") or `:filter` (LDAP filter for searching user entries). If `:filter` is provided, `:uid` is not required. Note: This `:uid` option is the search attribute, not the top-level `auth.uid` in the OmniAuth result.
235
213
 
236
214
  ### Optional Options
237
215
 
238
216
  - `:title` - The title for the authentication form (default: "LDAP Authentication").
217
+ - `:port` - The port number of the LDAP server (default: 389).
218
+ - `:encryption` - The connection method. Allowed values: `:plain`, `:ssl`, `:tls` (default: `:plain`). `:method` is still accepted for compatibility, but is deprecated.
239
219
  - `:bind_dn` - The DN to bind with for searching users (required if anonymous access is not allowed).
240
220
  - `:password` - The password for the bind DN.
241
221
  - `:name_proc` - A proc to process the username before using it in the search (default: identity proc that returns the username unchanged).
@@ -249,6 +229,10 @@ The following options are available for configuring the OmniAuth LDAP strategy:
249
229
  - `:connect_timeout` - Maximum time in seconds to wait when establishing the TCP connection to the LDAP server. Forwarded to `Net::LDAP`.
250
230
  - `:read_timeout` - Maximum time in seconds to wait for reads during LDAP operations (search/bind). Forwarded to `Net::LDAP`.
251
231
  - `: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).
232
+ - `:header_auth` - Enable trusted upstream identity SSO (default: false). When enabled, the strategy trusts the configured header/env key, performs an LDAP lookup, and skips the user password bind.
233
+ - `:header_name` - Header/env key used for trusted header SSO (default: "REMOTE_USER").
234
+ - `:header_auth_source` - Trusted identity source for header SSO (default: `:env`). Use `:env` to read only `env["REMOTE_USER"]`-style server variables. Use `:http_header` to read only Rack `HTTP_` header keys such as `env["HTTP_REMOTE_USER"]`; only configure this behind a proxy that strips client-supplied copies.
235
+ - `:header_auth_require_tls` - Require TLS for trusted header SSO requests (default: true).
252
236
 
253
237
  Example enabling password policy:
254
238
 
@@ -284,7 +268,7 @@ Where to find the "username"-style value
284
268
  - You can also read the raw attribute from `auth.extra.raw_info` (a `Net::LDAP::Entry`):
285
269
 
286
270
  ```ruby
287
- get "/auth/ldap/callback" do
271
+ post "/auth/ldap/callback" do
288
272
  auth = request.env["omniauth.auth"]
289
273
  dn = auth.uid # => "cn=alice,ou=users,dc=example,dc=com"
290
274
  username = auth.info.nickname # => "alice" (from uid/sAMAccountName)
@@ -300,7 +284,7 @@ If you need top-level `auth.uid` to be something other than the DN (for example,
300
284
  ## 🔧 Basic Usage
301
285
 
302
286
  The strategy exposes a simple Rack middleware and can be used in plain Rack apps, Sinatra, or Rails.
303
- Direct users to `/auth/ldap` to start authentication and handle the callback at `/auth/ldap/callback`.
287
+ With OmniAuth 2.x, initiate authentication with `POST /auth/ldap`; `GET /auth/ldap` returns 404 by default. Older OmniAuth 1.x deployments may still render the form on `GET /auth/ldap`. Handle the callback at `/auth/ldap/callback`.
304
288
 
305
289
  Below are several concrete examples to get you started.
306
290
 
@@ -316,7 +300,7 @@ use OmniAuth::Builder do
316
300
  provider :ldap,
317
301
  host: "ldap.example.com",
318
302
  port: 389,
319
- method: :plain,
303
+ encryption: :plain,
320
304
  base: "dc=example,dc=com",
321
305
  uid: "uid",
322
306
  title: "Example LDAP"
@@ -325,7 +309,7 @@ end
325
309
  run lambda { |env| [404, {"Content-Type" => "text/plain"}, [env.key?("omniauth.auth").to_s]] }
326
310
  ```
327
311
 
328
- Visit `GET /auth/ldap` to initiate authentication (the middleware will render a login form unless you POST to `/auth/ldap`).
312
+ Submit `POST /auth/ldap` to initiate authentication. With OmniAuth 2.x, the middleware renders the login form on POST when credentials are not already present; with OmniAuth 1.x, `GET /auth/ldap` can also render the form.
329
313
 
330
314
  ### Sinatra example
331
315
 
@@ -344,10 +328,10 @@ use OmniAuth::Builder do
344
328
  end
345
329
 
346
330
  get "/" do
347
- '<a href="/auth/ldap">Sign in with LDAP</a>'
331
+ '<form action="/auth/ldap" method="post"><button type="submit">Sign in with LDAP</button></form>'
348
332
  end
349
333
 
350
- get "/auth/ldap/callback" do
334
+ post "/auth/ldap/callback" do
351
335
  auth = request.env["omniauth.auth"]
352
336
  "Hello, #{auth.info["name"]}"
353
337
  end
@@ -371,7 +355,7 @@ Rails.application.config.middleware.use(OmniAuth::Builder) do
371
355
  end
372
356
  ```
373
357
 
374
- Then link users to `/auth/ldap` in your app (for example, in a Devise sign-in page).
358
+ Then submit users to `/auth/ldap` with POST in your app (for example, from a Devise sign-in page).
375
359
 
376
360
  ### Use JSON Body
377
361
 
@@ -422,7 +406,7 @@ Examples
422
406
 
423
407
  Notes
424
408
 
425
- - 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.
409
+ - You can still initiate authentication with a regular form POST and then submit credentials as form-encoded data. JSON is an additional option, not a replacement.
426
410
  - 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.
427
411
 
428
412
  ### Using a custom filter
@@ -607,15 +591,19 @@ Note: You generally do not need this override. Prefer configuring your proxy to
607
591
 
608
592
  ### Trusted header SSO (REMOTE_USER and friends)
609
593
 
610
- 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`.
594
+ Some deployments terminate SSO at a reverse proxy or portal and forward the already-authenticated user identity via a server-set environment variable or HTTP header such as `REMOTE_USER`.
611
595
  When you enable this mode, the LDAP strategy will trust the upstream header, perform a directory lookup for that user, and complete OmniAuth without asking the user for a password.
612
596
 
613
- Important: Only enable this behind a trusted front-end that strips and sets the header itself. Never enable on a public endpoint without such a gateway, or an attacker could spoof the header.
597
+ Important: Only enable this behind a trusted front-end that authenticates users before they can reach the OmniAuth endpoint. When `header_auth` is enabled the strategy logs a prominent security warning because it trusts the upstream identity completely.
614
598
 
615
599
  Configuration options:
616
600
 
617
601
  - `:header_auth` (Boolean, default: false) — Enable trusted header SSO.
618
- - `:header_name` (String, default: "REMOTE_USER") — The env/header key to read. The strategy checks both `env["REMOTE_USER"]` and the Rack variant `env["HTTP_REMOTE_USER"]`.
602
+ - `:header_name` (String, default: "REMOTE_USER") — The env/header key to read.
603
+ - `:header_auth_source` (`:env` or `:http_header`, default: `:env`) — Which Rack env key form to trust.
604
+ - `:env` reads only the exact server-set environment key, such as `env["REMOTE_USER"]`.
605
+ - `:http_header` reads only the Rack HTTP header key, such as `env["HTTP_REMOTE_USER"]`. Only use this behind a proxy that strips client-sent copies of the header before setting its trusted value.
606
+ - `:header_auth_require_tls` (Boolean, default: true) — Raise an error if trusted header SSO is used on a non-TLS request.
619
607
  - `:name_proc` is applied to the header value before search (e.g., to strip a domain part).
620
608
  - Search is done using your configured `:uid` or `:filter` and the service bind (`:bind_dn`/`:password`) or anonymous bind if allowed.
621
609
 
@@ -629,8 +617,9 @@ use OmniAuth::Builder do
629
617
  uid: "uid",
630
618
  bind_dn: "cn=search,dc=example,dc=com",
631
619
  password: ENV["LDAP_SEARCH_PASSWORD"],
632
- header_auth: true, # trust REMOTE_USER
633
- header_name: "REMOTE_USER", # default
620
+ header_auth: true, # trust the configured upstream identity
621
+ header_name: "REMOTE_USER", # default
622
+ header_auth_source: :env, # default; reads env["REMOTE_USER"]
634
623
  name_proc: proc { |n| n.split("@").first }
635
624
  end
636
625
  ```
@@ -648,6 +637,7 @@ Rails.application.config.middleware.use(OmniAuth::Builder) do
648
637
  password: ENV["LDAP_SEARCH_PASSWORD"],
649
638
  header_auth: true,
650
639
  header_name: "REMOTE_USER",
640
+ header_auth_source: :env,
651
641
  # Optionally restrict with a group filter while using the header value
652
642
  filter: "(&(sAMAccountName=%{username})(memberOf=cn=myapp-users,ou=groups,dc=acme,dc=corp))",
653
643
  name_proc: proc { |n| n.gsub(/@.*$/, "") }
@@ -662,13 +652,14 @@ Flow:
662
652
 
663
653
  Security checklist:
664
654
 
665
- - Ensure your reverse proxy strips user-controlled copies of the header and sets the canonical `REMOTE_USER` itself.
666
- - Prefer TLS-secured internal links between the proxy and your app.
655
+ - Prefer `header_auth_source: :env` for server-set variables such as `REMOTE_USER`.
656
+ - Use `header_auth_source: :http_header` only when your reverse proxy strips user-controlled copies of the header and sets the canonical value itself.
657
+ - Keep `header_auth_require_tls` enabled unless a separate trusted channel protects traffic between the proxy and your app.
667
658
  - Consider also restricting with a group-based `:filter` so only authorized users can sign in.
668
659
 
669
660
  ## 🦷 FLOSS Funding
670
661
 
671
- While these tools are free software and will always be, the project would benefit immensely from some funding.
662
+ While omniauth tools are free software and will always be, the project would benefit immensely from some funding.
672
663
  Raising a monthly budget of... "dollars" would make the project more sustainable.
673
664
 
674
665
  We welcome both individual and corporate sponsors! We also offer a
@@ -678,7 +669,7 @@ Currently, [GitHub Sponsors][🖇sponsor], and [Liberapay][⛳liberapay] are our
678
669
  **If you're working in a company that's making significant use of omniauth tools we'd
679
670
  appreciate it if you suggest to your company to become a omniauth sponsor.**
680
671
 
681
- You can support me in development of OmniAuth tools via
672
+ You can support the development of omniauth tools via
682
673
  [GitHub Sponsors][🖇sponsor],
683
674
  [Liberapay][⛳liberapay],
684
675
  [PayPal][🖇paypal],
@@ -698,7 +689,7 @@ I’m developing a new library, [floss_funding][🖇floss-funding-gem], designed
698
689
 
699
690
  **[Floss-Funding.dev][🖇floss-funding.dev]: 👉️ No network calls. 👉️ No tracking. 👉️ No oversight. 👉️ Minimal crypto hashing. 💡 Easily disabled nags**
700
691
 
701
- [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS or refugee efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS or refugee efforts using Patreon][🖇patreon-img]][🖇patreon]
692
+ [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS efforts using Patreon][🖇patreon-img]][🖇patreon]
702
693
 
703
694
  ## 🔐 Security
704
695
 
@@ -770,12 +761,11 @@ For example:
770
761
  spec.add_dependency("omniauth-ldap", "~> 1.0")
771
762
  ```
772
763
 
773
- <details>
764
+ <details markdown="1">
774
765
  <summary>📌 Is "Platform Support" part of the public API? More details inside.</summary>
775
766
 
776
767
  SemVer should, IMO, but doesn't explicitly, say that dropping support for specific Platforms
777
- is a *breaking change* to an API.
778
- It is obvious to many, but not all, and since the spec is silent, the bike shedding is endless.
768
+ is a *breaking change* to an API, and for that reason the bike shedding is endless.
779
769
 
780
770
  To get a better understanding of how SemVer is intended to work over a project's lifetime,
781
771
  read this article from the creator of SemVer:
@@ -796,7 +786,7 @@ See [LICENSE.txt][📄license] for the official [Copyright Notice][📄copyright
796
786
 
797
787
  <ul>
798
788
  <li>
799
- Copyright (c) 2025 Peter H. Boling, of
789
+ Copyright (c) 2025 - 2026 Peter H. Boling, of
800
790
  <a href="https://discord.gg/3qme4XHNKN">
801
791
  Galtzo.com
802
792
  <picture>
@@ -991,7 +981,7 @@ Thanks for RTFM. ☺️
991
981
  [📌gitmoji]: https://gitmoji.dev
992
982
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
993
983
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
994
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.297-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
984
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.312-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
995
985
  [🔐security]: SECURITY.md
996
986
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
997
987
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/REEK CHANGED
File without changes
data/RUBOCOP.md CHANGED
File without changes
data/SECURITY.md CHANGED
File without changes
@@ -1,14 +1,64 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "omniauth"
2
4
  require "omniauth/version"
3
5
 
6
+ # OmniAuth strategies namespace.
7
+ #
8
+ # This file implements an LDAP authentication strategy for OmniAuth.
9
+ # It provides both an interactive request phase (login form) and a
10
+ # callback phase which binds to an LDAP directory to authenticate the
11
+ # user or performs a lookup for header-based SSO.
12
+ #
13
+ # The strategy exposes a number of options (see `option` calls below)
14
+ # that control LDAP connection, mapping of LDAP attributes to the
15
+ # OmniAuth `info` hash, header-based SSO behavior, and SSL/timeouts.
16
+ #
17
+ # @example Minimal Rack mounting
18
+ # use OmniAuth::Builder do
19
+ # provider :ldap, {
20
+ # host: 'ldap.example.com',
21
+ # base: 'dc=example,dc=com'
22
+ # }
23
+ # end
24
+ #
4
25
  module OmniAuth
5
26
  module Strategies
27
+ # LDAP OmniAuth strategy
28
+ #
29
+ # This class implements the OmniAuth::Strategy interface and performs
30
+ # LDAP authentication using an `Adaptor` object. It supports three
31
+ # primary flows:
32
+ #
33
+ # - Interactive login form (request_phase) where users POST username/password
34
+ # - Callback binding where the strategy attempts to bind as the user
35
+ # - Header-based SSO (trusted upstream) where a header identifies the user
36
+ #
37
+ # The mapping from LDAP attributes to resulting `info` fields is
38
+ # configurable via the `:mapping` option. See `map_user` for the
39
+ # mapping algorithm.
40
+ #
41
+ # @see OmniAuth::Strategy
6
42
  class LDAP
43
+ # Whether the loaded OmniAuth version is >= 2.0.0; used to set default request methods.
44
+ # @return [Boolean]
7
45
  OMNIAUTH_GTE_V2 = Gem::Version.new(OmniAuth::VERSION) >= Gem::Version.new("2.0.0")
46
+
8
47
  include OmniAuth::Strategy
9
48
 
49
+ # Raised when credentials are invalid or the user cannot be authenticated.
50
+ # @example
51
+ # raise InvalidCredentialsError, 'Invalid credentials'
10
52
  InvalidCredentialsError = Class.new(StandardError)
11
53
 
54
+ # Default mapping for converting LDAP attributes to OmniAuth `info` keys.
55
+ # Keys are the resulting `info` hash keys (strings). Values may be:
56
+ # - String: single LDAP attribute name
57
+ # - Array: list of attribute names in priority order
58
+ # - Hash: pattern mapping where pattern keys contain %<n> placeholders
59
+ # that are substituted from a list of possible attribute names
60
+ #
61
+ # @return [Hash<String, String|Array|Hash>]
12
62
  option :mapping, {
13
63
  "name" => "cn",
14
64
  "first_name" => "givenName",
@@ -24,7 +74,11 @@ module OmniAuth
24
74
  "image" => "jpegPhoto",
25
75
  "description" => "description",
26
76
  }
27
- option :title, "LDAP Authentication" # default title for authentication form
77
+
78
+ # Default title shown on the login form.
79
+ # @return [String]
80
+ option :title, "LDAP Authentication"
81
+
28
82
  # For OmniAuth >= 2.0 the default allowed request method is POST only.
29
83
  # Ensure the strategy follows that default so GET /auth/:provider returns 404 as expected in tests.
30
84
  if OMNIAUTH_GTE_V2
@@ -32,6 +86,8 @@ module OmniAuth
32
86
  else
33
87
  option(:request_methods, [:get, :post])
34
88
  end
89
+
90
+ # Default LDAP connection options / behavior
35
91
  option :port, 389
36
92
  option :method, :plain
37
93
  option :disable_verify_certificates, false
@@ -39,17 +95,34 @@ module OmniAuth
39
95
  option :ssl_version, nil # use OpenSSL default if nil
40
96
  option :uid, "sAMAccountName"
41
97
  option :name_proc, lambda { |n| n }
98
+
42
99
  # Trusted header SSO support (disabled by default)
43
- # :header_auth - when true and the header is present, the strategy trusts the upstream gateway
44
- # and searches the directory for the user without requiring a user password.
45
- # :header_name - which header/env key to read (default: "REMOTE_USER"). We will also check the
46
- # standard Rack "HTTP_" variant automatically.
100
+ # :header_auth - when true and the configured header/env key is present, the strategy trusts
101
+ # the upstream gateway and searches the directory for the user without
102
+ # requiring a user password.
103
+ # :header_name - which header/env key to read (default: "REMOTE_USER").
104
+ # :header_auth_source - :env trusts only server-set env variables. :http_header trusts only
105
+ # Rack HTTP header keys, and should only be used behind a proxy that
106
+ # strips client-supplied copies of the header.
107
+ # :header_auth_require_tls - require a TLS request for header auth.
47
108
  option :header_auth, false
48
109
  option :header_name, "REMOTE_USER"
110
+ option :header_auth_source, :env
111
+ option :header_auth_require_tls, true
112
+
49
113
  # Optional timeouts (forwarded to Net::LDAP when supported)
50
114
  option :connect_timeout, nil
51
115
  option :read_timeout, nil
52
116
 
117
+ # Request phase: Render the login form or redirect to callback for header-auth or direct POSTed credentials
118
+ #
119
+ # This will behave differently depending on OmniAuth version and request method:
120
+ # - For OmniAuth >= 2.0 a GET to /auth/:provider should return 404 (so we return a 404 for GET requests).
121
+ # - If header-based SSO is enabled and a trusted header is present we immediately redirect to the callback.
122
+ # - If credentials are POSTed directly to /auth/:provider we redirect to the callback so the test helpers
123
+ # that populate `env['omniauth.auth']` can operate on the callback request.
124
+ #
125
+ # @return [Array] A Rack response triple from the login form or redirect.
53
126
  def request_phase
54
127
  # OmniAuth >= 2.0 expects the request phase to be POST-only for /auth/:provider.
55
128
  # Some test environments (and OmniAuth itself) enforce this by returning 404 on GET.
@@ -57,6 +130,8 @@ module OmniAuth
57
130
  return Rack::Response.new("", 404, {"Content-Type" => "text/plain"}).finish
58
131
  end
59
132
 
133
+ validate_header_auth_configuration!
134
+
60
135
  # Fast-path: if a trusted identity header is present, skip the login form
61
136
  # and jump to the callback where we will complete using directory lookup.
62
137
  if header_username
@@ -78,11 +153,26 @@ module OmniAuth
78
153
  f.to_response
79
154
  end
80
155
 
156
+ # Callback phase: Authenticate user or perform header-based lookup
157
+ #
158
+ # This method executes on the callback URL and implements the main
159
+ # authentication logic. There are two primary paths:
160
+ #
161
+ # - Header-based lookup: when `options[:header_auth]` is enabled and a header value is present,
162
+ # we perform a read-only directory lookup for the user and, if found, map attributes and finish.
163
+ # - Password bind: when username/password are provided we attempt a bind as the user using the adaptor.
164
+ #
165
+ # Errors raised by the LDAP adaptor are captured and turned into OmniAuth failures.
166
+ #
167
+ # @raise [InvalidCredentialsError] when credentials are invalid
168
+ # @return [Object] result of calling `super` from the OmniAuth::Strategy chain
81
169
  def callback_phase
82
170
  @adaptor = OmniAuth::LDAP::Adaptor.new(@options)
83
171
 
84
172
  return fail!(:invalid_request_method) unless valid_request_method?
85
173
 
174
+ validate_header_auth_configuration!
175
+
86
176
  # Header-based SSO (REMOTE_USER-style) path
87
177
  if (hu = header_username)
88
178
  begin
@@ -118,6 +208,15 @@ module OmniAuth
118
208
  end
119
209
  end
120
210
 
211
+ # Build an LDAP filter for searching/binding the user.
212
+ #
213
+ # If the adaptor has a custom `filter` option set it will be used (with
214
+ # interpolation of `%{username}`). Otherwise a simple equality filter for
215
+ # the configured uid attribute is used.
216
+ #
217
+ # @param adaptor [OmniAuth::LDAP::Adaptor] the adaptor used to build connection/filters
218
+ # @param username_override [String, nil] optional username to build the filter for (defaults to request username)
219
+ # @return [Net::LDAP::Filter] the constructed filter object
121
220
  def filter(adaptor, username_override = nil)
122
221
  flt = adaptor.filter
123
222
  if flt && !flt.to_s.empty?
@@ -128,17 +227,37 @@ module OmniAuth
128
227
  end
129
228
  end
130
229
 
131
- uid {
132
- @user_info["uid"]
133
- }
134
- info {
135
- @user_info
136
- }
137
- extra {
138
- {raw_info: @ldap_user_info}
139
- }
230
+ # The uid exposed to OmniAuth consumers.
231
+ #
232
+ # This block-based DSL is part of OmniAuth::Strategy; document the value
233
+ # returned by the block.
234
+ #
235
+ # @return [String] the user's uid as determined from the mapped info
236
+ uid { @user_info["uid"] }
237
+
238
+ # The `info` hash returned to OmniAuth consumers. Usually contains name, email, etc.
239
+ # @return [Hash<String, Object>]
240
+ info { @user_info }
241
+
242
+ # Extra information exposed under `extra[:raw_info]` containing the raw LDAP entry.
243
+ # @return [Hash{Symbol => Object}]
244
+ extra { {raw_info: @ldap_user_info} }
140
245
 
141
246
  class << self
247
+ # Map LDAP attributes from the directory entry into a simple Hash used
248
+ # for the OmniAuth `info` hash according to the provided `mapper`.
249
+ #
250
+ # The mapper supports three types of values:
251
+ # - String: a single attribute name. The method will call the attribute
252
+ # reader (downcased symbol) on the `object` and take the first value.
253
+ # - Array: iterate values and pick the first attribute that exists on the object.
254
+ # - Hash: a mapping of a pattern string to an array of attribute-name lists
255
+ # where each `%<n>` placeholder in the pattern will be substituted by the
256
+ # first available attribute from the corresponding list.
257
+ #
258
+ # @param mapper [Hash] mapping configuration (see option :mapping)
259
+ # @param object [#respond_to?, #[]] directory entry (commonly a Net::LDAP::Entry or similar)
260
+ # @return [Hash<String, Object>] the mapped user info hash
142
261
  def map_user(mapper, object)
143
262
  user = {}
144
263
  mapper.each do |key, value|
@@ -177,33 +296,93 @@ module OmniAuth
177
296
 
178
297
  protected
179
298
 
299
+ # Validate that the incoming request method is allowed.
300
+ #
301
+ # For OmniAuth >= 2.0 the default is POST only. This method checks the
302
+ # Rack env REQUEST_METHOD directly so tests and environments that stub
303
+ # request.HTTP_METHOD are handled deterministically.
304
+ #
305
+ # @return [Boolean] true when the request method is POST
180
306
  def valid_request_method?
181
307
  request.env["REQUEST_METHOD"] == "POST"
182
308
  end
183
309
 
310
+ # Determine if the request is missing required credentials.
311
+ #
312
+ # @return [Boolean] true when username or password are nil/empty
184
313
  def missing_credentials?
185
314
  request_data["username"].nil? || request_data["username"].empty? || request_data["password"].nil? || request_data["password"].empty?
186
315
  end
187
316
 
317
+ # Extract request parameters in a way compatible with Rails/Rack.
318
+ #
319
+ # @return [Hash] parameters hash containing at least "username" and "password" when provided
188
320
  def request_data
189
321
  @env["action_dispatch.request.request_parameters"] || request.params
190
322
  end
191
323
 
192
- # Extract a normalized username from a trusted header when enabled.
324
+ # Extract a normalized username from a trusted header/env key when enabled.
193
325
  # Returns nil when not configured or not present.
326
+ #
327
+ # The source is intentionally explicit: :env reads the raw env key
328
+ # (e.g. "REMOTE_USER"), while :http_header reads the Rack HTTP_ variant
329
+ # (e.g. "HTTP_REMOTE_USER" or "HTTP_X_REMOTE_USER").
330
+ #
331
+ # @return [String, nil] normalized username or nil if not present
194
332
  def header_username
195
333
  return unless options[:header_auth]
196
334
 
197
- name = options[:header_name] || "REMOTE_USER"
198
- # Try both the raw env var (e.g., REMOTE_USER) and the Rack HTTP_ variant (e.g., HTTP_REMOTE_USER or HTTP_X_REMOTE_USER)
199
- raw = request.env[name] || request.env["HTTP_#{name.upcase.tr("-", "_")}"]
335
+ raw = request.env[header_auth_env_key]
200
336
  return if raw.nil? || raw.to_s.strip.empty?
201
337
 
202
338
  options[:name_proc].call(raw.to_s)
203
339
  end
204
340
 
341
+ # Validate trusted header auth before reading the configured identity key.
342
+ #
343
+ # @raise [ArgumentError] when the header auth options are unsafe or invalid
344
+ # @return [void]
345
+ def validate_header_auth_configuration!
346
+ return unless options[:header_auth]
347
+
348
+ log_header_auth_warning
349
+
350
+ source = (options[:header_auth_source] || :env).to_sym
351
+ unless [:env, :http_header].include?(source)
352
+ raise ArgumentError, "header_auth_source must be :env or :http_header"
353
+ end
354
+
355
+ if options[:header_auth_require_tls] && !request.ssl?
356
+ raise ArgumentError, "header_auth requires TLS unless header_auth_require_tls is disabled"
357
+ end
358
+ end
359
+
360
+ # Rack env key selected by the explicit header auth source option.
361
+ #
362
+ # @return [String]
363
+ def header_auth_env_key
364
+ name = options[:header_name] || "REMOTE_USER"
365
+ return name if (options[:header_auth_source] || :env).to_sym == :env
366
+
367
+ "HTTP_#{name.upcase.tr("-", "_")}"
368
+ end
369
+
370
+ # Warn operators that trusted header auth delegates authentication to the upstream gateway.
371
+ #
372
+ # @return [void]
373
+ def log_header_auth_warning
374
+ logger = OmniAuth.config.respond_to?(:logger) ? OmniAuth.config.logger : nil
375
+ return unless logger && logger.respond_to?(:warn)
376
+
377
+ logger.warn("[omniauth-ldap] SECURITY WARNING: header_auth is enabled. This trusts upstream authentication completely; only enable it behind a trusted proxy that strips client-supplied identity headers.")
378
+ end
379
+
205
380
  # Perform a directory lookup for the given username using the strategy configuration
206
381
  # (bind_dn/password or anonymous). Does not attempt to bind as the user.
382
+ #
383
+ # @param adaptor [OmniAuth::LDAP::Adaptor] initialized adaptor
384
+ # @param username [String] username to look up
385
+ # @return [Object, nil] first directory entry found or nil
207
386
  def directory_lookup(adaptor, username)
208
387
  entry = nil
209
388
  search_filter = filter(adaptor, username)
@@ -216,6 +395,11 @@ module OmniAuth
216
395
 
217
396
  # If the adaptor captured a Password Policy response control, expose a minimal, stable hash
218
397
  # in the Rack env for applications to inspect.
398
+ #
399
+ # The structure is available at `request.env['omniauth.ldap.password_policy']`.
400
+ #
401
+ # @param adaptor [OmniAuth::LDAP::Adaptor]
402
+ # @return [void]
219
403
  def attach_password_policy_env(adaptor)
220
404
  return unless adaptor.respond_to?(:password_policy) && adaptor.password_policy
221
405
  ctrl = adaptor.respond_to?(:last_password_policy_response) ? adaptor.last_password_policy_response : nil
@@ -226,6 +410,10 @@ module OmniAuth
226
410
  end
227
411
 
228
412
  # Best-effort extraction across net-ldap versions; if fields are not available, returns a raw payload.
413
+ #
414
+ # @param control [Object, nil] the password policy response control if available
415
+ # @param operation [Object, nil] the last operation result if available
416
+ # @return [Hash] normalized password policy info with keys :raw, :error, :time_before_expiration, :grace_authns_remaining, :oid, :operation
229
417
  def extract_password_policy(control, operation)
230
418
  data = {raw: control}
231
419
  if control
@@ -10,12 +10,36 @@ require "sasl"
10
10
 
11
11
  module OmniAuth
12
12
  module LDAP
13
+ # Adaptor encapsulates the behavior required to connect to an LDAP server
14
+ # and perform searches and binds. It maps user-provided configuration into
15
+ # a Net::LDAP connection and provides compatibility helpers for different
16
+ # net-ldap and SASL versions. The adaptor is intentionally defensive and
17
+ # provides a small, stable API used by the OmniAuth strategy.
18
+ #
19
+ # @example Initialize with minimal config
20
+ # adaptor = OmniAuth::LDAP::Adaptor.new(base: 'dc=example,dc=com', host: 'ldap.example.com')
21
+ #
22
+ # @note Public API: {validate}, {initialize}, {bind_as}, and attr readers such as {connection}, {uid}
13
23
  class Adaptor
24
+ # Generic adaptor error super-class
25
+ # @see Error classes that inherit from this class
14
26
  class LdapError < StandardError; end
27
+
28
+ # Raised when configuration is invalid
29
+ # @example
30
+ # raise ConfigurationError, 'missing base'
15
31
  class ConfigurationError < StandardError; end
32
+
33
+ # Raised when authentication fails
16
34
  class AuthenticationError < StandardError; end
35
+
36
+ # Raised on connection-related failures
17
37
  class ConnectionError < StandardError; end
18
38
 
39
+ # Valid configuration keys accepted by the adaptor. These correspond to
40
+ # the options supported by the gem and are used during initialization.
41
+ #
42
+ # @return [Array<Symbol>]
19
43
  VALID_ADAPTER_CONFIGURATION_KEYS = [
20
44
  :hosts,
21
45
  :host,
@@ -42,7 +66,9 @@ module OmniAuth
42
66
  :ssl_version,
43
67
  ]
44
68
 
45
- # A list of needed keys. Possible alternatives are specified using sub-lists.
69
+ # Required configuration keys. This may include alternatives as sub-lists
70
+ # (e.g., [:hosts, :host] means either key is acceptable).
71
+ # @return [Array]
46
72
  MUST_HAVE_KEYS = [
47
73
  :base,
48
74
  [:encryption, :method], # :method is deprecated
@@ -51,6 +77,8 @@ module OmniAuth
51
77
  [:uid, :filter],
52
78
  ]
53
79
 
80
+ # Supported encryption method mapping for configuration readability.
81
+ # @return [Hash<Symbol,Symbol,nil>]
54
82
  ENCRYPTION_METHOD = {
55
83
  simple_tls: :simple_tls,
56
84
  start_tls: :start_tls,
@@ -62,21 +90,70 @@ module OmniAuth
62
90
  tls: :start_tls,
63
91
  }
64
92
 
93
+ # @!attribute [rw] bind_dn
94
+ # The distinguished name used for binding when provided in configuration.
95
+ # @return [String, nil]
96
+ # @!attribute [rw] password
97
+ # The bind password (may be nil for anonymous binds)
98
+ # @return [String, nil]
65
99
  attr_accessor :bind_dn, :password
100
+
101
+ # Read-only attributes exposing connection and configuration state.
102
+ # @!attribute [r] connection
103
+ # The underlying Net::LDAP connection object.
104
+ # @return [Net::LDAP]
105
+ # @!attribute [r] uid
106
+ # The user id attribute used for lookups (e.g., 'sAMAccountName')
107
+ # @return [String]
108
+ # @!attribute [r] base
109
+ # The base DN for searches.
110
+ # @return [String]
111
+ # @!attribute [r] auth
112
+ # The final auth structure used by net-ldap.
113
+ # @return [Hash]
114
+ # @!attribute [r] filter
115
+ # Custom filter pattern when provided in configuration.
116
+ # @return [String, nil]
117
+ # @!attribute [r] password_policy
118
+ # Whether to request LDAP Password Policy controls.
119
+ # @return [Boolean]
120
+ # @!attribute [r] last_operation_result
121
+ # Last operation result object returned by the ldap library (if any)
122
+ # @return [Object, nil]
123
+ # @!attribute [r] last_password_policy_response
124
+ # Last extracted password policy response control (if any)
125
+ # @return [Object, nil]
66
126
  attr_reader :connection, :uid, :base, :auth, :filter, :password_policy, :last_operation_result, :last_password_policy_response
67
127
 
68
- def self.validate(configuration = {})
69
- message = []
70
- MUST_HAVE_KEYS.each do |names|
71
- names = [names].flatten
72
- missing_keys = names.select { |name| configuration[name].nil? }
73
- if missing_keys == names
74
- message << names.join(" or ")
128
+ # Validate that a minimal configuration is present. Raises ArgumentError when required
129
+ # keys are missing. This is a convenience to provide early feedback to callers.
130
+ #
131
+ # @param configuration [Hash] configuration hash passed to the adaptor
132
+ # @raise [ArgumentError] when required keys are missing
133
+ # @return [void]
134
+ class << self
135
+ def validate(configuration = {})
136
+ message = []
137
+ MUST_HAVE_KEYS.each do |names|
138
+ names = [names].flatten
139
+ missing_keys = names.select { |name| configuration[name].nil? }
140
+ if missing_keys == names
141
+ message << names.join(" or ")
142
+ end
75
143
  end
144
+ raise ArgumentError.new(message.join(",") + " MUST be provided") unless message.empty?
76
145
  end
77
- raise ArgumentError.new(message.join(",") + " MUST be provided") unless message.empty?
78
146
  end
79
147
 
148
+ # Create a new adaptor instance backed by a Net::LDAP connection.
149
+ #
150
+ # The constructor does not immediately open a network connection but
151
+ # prepares the Net::LDAP instance according to the provided configuration.
152
+ # It also applies timeout settings where supported by the installed net-ldap version.
153
+ #
154
+ # @param configuration [Hash] user-provided configuration options
155
+ # @raise [ArgumentError, ConfigurationError] on invalid configuration
156
+ # @return [OmniAuth::LDAP::Adaptor]
80
157
  def initialize(configuration = {})
81
158
  Adaptor.validate(configuration)
82
159
  @configuration = configuration.dup
@@ -128,6 +205,16 @@ module OmniAuth
128
205
  #:base => "dc=yourcompany, dc=com",
129
206
  # :filter => "(mail=#{user})",
130
207
  # :password => psw
208
+ #
209
+ # Attempt to locate a user entry and bind as that entry using the supplied
210
+ # password. Returns the entry on success, or false/nil on failure.
211
+ #
212
+ # @param args [Hash] search and bind options forwarded to net-ldap's search
213
+ # @option args [Net::LDAP::Filter,String] :filter LDAP filter to use
214
+ # @option args [Integer] :size maximum number of results to fetch
215
+ # @option args [String,Proc] :password a password string or callable returning a password
216
+ # @return [Net::LDAP::Entry, false, nil] the found entry on successful bind, otherwise false/nil
217
+ # @raise [ConnectionError] if the underlying LDAP search fails
131
218
  def bind_as(args = {})
132
219
  result = false
133
220
  @last_operation_result = nil
@@ -179,6 +266,9 @@ module OmniAuth
179
266
 
180
267
  private
181
268
 
269
+ # Build encryption options for Net::LDAP given the configured method
270
+ #
271
+ # @return [Hash, nil] encryption options or nil for plain (no encryption)
182
272
  def encryption_options
183
273
  translated_method = translate_method
184
274
  return unless translated_method
@@ -189,6 +279,10 @@ module OmniAuth
189
279
  }
190
280
  end
191
281
 
282
+ # Normalize the user-provided encryption/method option and map to known values.
283
+ #
284
+ # @raise [ConfigurationError] when an unknown method is provided
285
+ # @return [Symbol, nil]
192
286
  def translate_method
193
287
  method = @encryption || @method
194
288
  method ||= "plain"
@@ -203,6 +297,10 @@ module OmniAuth
203
297
  ENCRYPTION_METHOD[normalized_method]
204
298
  end
205
299
 
300
+ # Build TLS options including backward-compatibility for deprecated keys.
301
+ #
302
+ # @param translated_method [Symbol] the normalized encryption method
303
+ # @return [Hash] a hash suitable for passing as :tls_options
206
304
  def tls_options(translated_method)
207
305
  return {} if translated_method.nil? # (plain)
208
306
 
@@ -223,6 +321,10 @@ module OmniAuth
223
321
  options
224
322
  end
225
323
 
324
+ # Build a list of SASL auth structures for each requested mechanism.
325
+ #
326
+ # @param options [Hash] options such as :username and :password
327
+ # @return [Array<Hash>] list of auth structures
226
328
  def sasl_auths(options = {})
227
329
  auths = []
228
330
  sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms
@@ -241,6 +343,9 @@ module OmniAuth
241
343
  auths
242
344
  end
243
345
 
346
+ # Prepare SASL DIGEST-MD5 bind details
347
+ # @param options [Hash]
348
+ # @return [Array] initial_credential and a challenge response proc
244
349
  def sasl_bind_setup_digest_md5(options)
245
350
  bind_dn = options[:username]
246
351
  initial_credential = ""
@@ -253,6 +358,9 @@ module OmniAuth
253
358
  [initial_credential, challenge_response]
254
359
  end
255
360
 
361
+ # Prepare SASL GSS-SPNEGO bind details
362
+ # @param options [Hash]
363
+ # @return [Array] initial Type1 message and a nego proc
256
364
  def sasl_bind_setup_gss_spnego(options)
257
365
  bind_dn = options[:username]
258
366
  psw = options[:password]
@@ -268,8 +376,9 @@ module OmniAuth
268
376
  [Net::NTLM::Message::Type1.new.serialize, nego]
269
377
  end
270
378
 
271
- private
272
-
379
+ # Default TLS/OpenSSL options used when not explicitly configured.
380
+ #
381
+ # @return [Hash]
273
382
  def default_options
274
383
  if @disable_verify_certificates
275
384
  # It is important to explicitly set verify_mode for two reasons:
@@ -286,6 +395,9 @@ module OmniAuth
286
395
  #
287
396
  # This gem may not always be in the context of Rails so we
288
397
  # do this rather than `.blank?`.
398
+ #
399
+ # @param hash [Hash]
400
+ # @return [Hash] sanitized hash with blank values removed
289
401
  def sanitize_hash_values(hash)
290
402
  hash.delete_if do |_, value|
291
403
  value.nil? ||
@@ -293,6 +405,10 @@ module OmniAuth
293
405
  end
294
406
  end
295
407
 
408
+ # Convert string keys to symbol keys for options hashes.
409
+ #
410
+ # @param hash [Hash]
411
+ # @return [Hash<Symbol, Object>]
296
412
  def symbolize_hash_keys(hash)
297
413
  hash.each_with_object({}) do |(key, value), result|
298
414
  result[key.to_sym] = value
@@ -300,6 +416,12 @@ module OmniAuth
300
416
  end
301
417
 
302
418
  # Capture the operation result and extract any Password Policy response control if present.
419
+ #
420
+ # This method is defensive: if the server or the installed net-ldap gem doesn't
421
+ # support controls, the method will swallow errors and leave policy fields nil.
422
+ #
423
+ # @param conn [Net::LDAP]
424
+ # @return [void]
303
425
  def capture_password_policy(conn)
304
426
  return unless @password_policy
305
427
  return unless conn.respond_to?(:get_operation_result)
@@ -1,8 +1,17 @@
1
1
  module OmniAuth
2
2
  module LDAP
3
+ # Version namespace for the omniauth-ldap gem
4
+ #
5
+ # This module contains the version constant used by rubygems and in code
6
+ # consumers. It intentionally exposes VERSION both inside the Version
7
+ # namespace and as OmniAuth::LDAP::VERSION for compatibility.
3
8
  module Version
4
- VERSION = "2.3.2"
9
+ # Public semantic version for the gem
10
+ # @return [String]
11
+ VERSION = "2.3.4"
5
12
  end
13
+ # Convenience constant for consumers that expect OmniAuth::LDAP::VERSION
14
+ # @return [String]
6
15
  VERSION = Version::VERSION # Make VERSION available in traditional way
7
16
  end
8
17
  end
data/lib/omniauth-ldap.rb CHANGED
@@ -1,3 +1,11 @@
1
+ # Integrate the VersionGem helper into the OmniAuth::LDAP::Version module
2
+ # to expose common version-related helper methods. This file is the public
3
+ # entry point required by consumers of the gem.
4
+ #
5
+ # @example
6
+ # require 'omniauth-ldap'
7
+ # OmniAuth::LDAP::VERSION # => "2.3.2"
8
+
1
9
  require "version_gem"
2
10
 
3
11
  require "omniauth-ldap/version"
File without changes
File without changes
@@ -22,6 +22,12 @@ module OmniAuth
22
22
  # Extract username from a trusted header when enabled
23
23
  def header_username: () -> (String | nil)
24
24
 
25
+ def validate_header_auth_configuration!: () -> void
26
+
27
+ def header_auth_env_key: () -> String
28
+
29
+ def log_header_auth_warning: () -> void
30
+
25
31
  # Perform a directory lookup for a given username; returns an Entry or nil
26
32
  def directory_lookup: (OmniAuth::LDAP::Adaptor, String) -> untyped
27
33
 
File without changes
data/sig/rbs/net-ldap.rbs CHANGED
File without changes
data/sig/rbs/net-ntlm.rbs CHANGED
File without changes
data/sig/rbs/sasl.rbs CHANGED
File without changes
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth-ldap
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.2
4
+ version: 2.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Boling
@@ -337,34 +337,6 @@ dependencies:
337
337
  - - ">="
338
338
  - !ruby/object:Gem::Version
339
339
  version: 1.0.3
340
- - !ruby/object:Gem::Dependency
341
- name: vcr
342
- requirement: !ruby/object:Gem::Requirement
343
- requirements:
344
- - - ">="
345
- - !ruby/object:Gem::Version
346
- version: '4'
347
- type: :development
348
- prerelease: false
349
- version_requirements: !ruby/object:Gem::Requirement
350
- requirements:
351
- - - ">="
352
- - !ruby/object:Gem::Version
353
- version: '4'
354
- - !ruby/object:Gem::Dependency
355
- name: webmock
356
- requirement: !ruby/object:Gem::Requirement
357
- requirements:
358
- - - ">="
359
- - !ruby/object:Gem::Version
360
- version: '3'
361
- type: :development
362
- prerelease: false
363
- version_requirements: !ruby/object:Gem::Requirement
364
- requirements:
365
- - - ">="
366
- - !ruby/object:Gem::Version
367
- version: '3'
368
340
  description: "\U0001F4C1 LDAP strategy for OmniAuth."
369
341
  email:
370
342
  - floss@galtzo.com
@@ -408,10 +380,10 @@ licenses:
408
380
  - MIT
409
381
  metadata:
410
382
  homepage_uri: https://omniauth-ldap.galtzo.com/
411
- source_code_uri: https://github.com/omniauth/omniauth-ldap/tree/v2.3.2
412
- changelog_uri: https://github.com/omniauth/omniauth-ldap/blob/v2.3.2/CHANGELOG.md
383
+ source_code_uri: https://github.com/omniauth/omniauth-ldap/tree/v2.3.4
384
+ changelog_uri: https://github.com/omniauth/omniauth-ldap/blob/v2.3.4/CHANGELOG.md
413
385
  bug_tracker_uri: https://github.com/omniauth/omniauth-ldap/issues
414
- documentation_uri: https://www.rubydoc.info/gems/omniauth-ldap/2.3.2
386
+ documentation_uri: https://www.rubydoc.info/gems/omniauth-ldap/2.3.4
415
387
  funding_uri: https://github.com/sponsors/pboling
416
388
  wiki_uri: https://github.com/omniauth/omniauth-ldap/wiki
417
389
  news_uri: https://www.railsbling.com/tags/omniauth-ldap
@@ -440,7 +412,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
440
412
  - !ruby/object:Gem::Version
441
413
  version: '0'
442
414
  requirements: []
443
- rubygems_version: 3.7.2
415
+ rubygems_version: 4.0.11
444
416
  specification_version: 4
445
417
  summary: "\U0001F4C1 LDAP strategy for OmniAuth."
446
418
  test_files: []
metadata.gz.sig CHANGED
Binary file