Skip to content

Commit 5d67e0d

Browse files
authored
Merge pull request #101 from omniauth/feat/spec-auth-uid-as-dn
2 parents 6efd08e + 25968b2 commit 5d67e0d

File tree

3 files changed

+87
-2
lines changed

3 files changed

+87
-2
lines changed

.rubocop_gradual.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
[47, 7, 38, "RSpec/AnyInstance: Avoid stubbing using `allow_any_instance_of`.", 3627954156],
3131
[84, 7, 48, "RSpec/AnyInstance: Avoid stubbing using `allow_any_instance_of`.", 2759780562]
3232
],
33-
"spec/omniauth/strategies/ldap_spec.rb:86189447": [
33+
"spec/omniauth/strategies/ldap_spec.rb:1003951887": [
3434
[14, 3, 54, "RSpec/LeakyConstantDeclaration: Stub class constant instead of declaring explicitly.", 2419068710],
3535
[90, 13, 9, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1130140517],
3636
[145, 17, 28, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3444838747],

README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ The following options are available for configuring the OmniAuth LDAP strategy:
179179
- `:port` - The port number of the LDAP server (default: 389).
180180
- `:method` - The connection method. Allowed values: `:plain`, `:ssl`, `:tls` (default: `:plain`).
181181
- `:base` - The base DN for the LDAP search.
182-
- `: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.
182+
- `: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.
183183

184184
### Optional Options
185185

@@ -192,6 +192,37 @@ The following options are available for configuring the OmniAuth LDAP strategy:
192192
- `:allow_anonymous` - Whether to allow anonymous binding (default: false).
193193
- `:logger` - A logger instance for debugging (optional, for internal use).
194194

195+
### Auth Hash UID vs LDAP :uid (search attribute)
196+
197+
- By design, the top-level `auth.uid` returned by this strategy is the entry's Distinguished Name (DN).
198+
- The configuration option `:uid` controls which LDAP attribute is used to locate the entry (or to build the filter), not the value exposed as `auth.uid`.
199+
- Your LDAP "account name" (for example, `sAMAccountName` on Active Directory or `uid` on many schemas) is exposed via `auth.info.nickname` and is also available in `auth.extra.raw_info`.
200+
201+
Why DN for `auth.uid`?
202+
203+
- DN is the canonical, globally unique identifier for an LDAP entry and is always present in search results. See LDAPv3 and DN syntax: RFC 4511 (LDAP protocol) and RFC 4514 (String Representation of Distinguished Names).
204+
- Attributes like `uid` (defined in RFC 4519) or `sAMAccountName` (Active Directory–specific) may be absent, duplicated across parts of the DIT, or vary between directories. Using DN ensures consistent behavior across AD, OpenLDAP, and other servers.
205+
- This trade-off favors cross-directory interoperability and stability for apps that need a unique identifier.
206+
207+
Where to find the "username"-style value
208+
209+
- `auth.info.nickname` maps from the first present of: `uid`, `userid`, or `sAMAccountName`.
210+
- You can also read the raw attribute from `auth.extra.raw_info` (a `Net::LDAP::Entry`):
211+
212+
```ruby
213+
get "/auth/ldap/callback" do
214+
auth = request.env["omniauth.auth"]
215+
dn = auth.uid # => "cn=alice,ou=users,dc=example,dc=com"
216+
username = auth.info.nickname # => "alice" (from uid/sAMAccountName)
217+
# Or, directly from raw_info (case-insensitive keys):
218+
sams = auth.extra.raw_info[:samaccountname]
219+
sam = sams.first if sams
220+
# ...
221+
end
222+
```
223+
224+
If you need top-level `auth.uid` to be something other than the DN (for example, `sAMAccountName`), you'll currently need to read it from `auth.info.nickname` (or `raw_info`) in your app. Changing the top-level `uid` mapping would be a breaking behavior change for existing users; if you have a use-case, please open an issue to discuss a configurable mapping.
225+
195226
## 🔧 Basic Usage
196227

197228
The strategy exposes a simple Rack middleware and can be used in plain Rack apps, Sinatra, or Rails.
@@ -655,3 +686,8 @@ Thanks for RTFM. ☺️
655686
[💎appraisal2]: https://github.com/appraisal-rb/appraisal2
656687
[💎appraisal2-img]: https://img.shields.io/badge/appraised_by-appraisal2-34495e.svg?plastic&logo=ruby&logoColor=white
657688
[💎d-in-dvcs]: https://railsbling.com/posts/dvcs/put_the_d_in_dvcs/
689+
690+
[//]: # (LDAP RFC references)
691+
[rfc4511]: https://datatracker.ietf.org/doc/html/rfc4511
692+
[rfc4514]: https://datatracker.ietf.org/doc/html/rfc4514
693+
[rfc4519]: https://datatracker.ietf.org/doc/html/rfc4519

spec/omniauth/strategies/ldap_spec.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,4 +308,53 @@ def make_env(path = "/auth/ldap", props = {})
308308
end
309309
end
310310
end
311+
312+
# Validate uid behavior specifically when using sAMAccountName
313+
describe "uid behavior with sAMAccountName option" do
314+
let(:app) do
315+
Rack::Builder.new do
316+
use OmniAuth::Test::PhonySession
317+
use MySamaccountnameProvider,
318+
name: "ldap",
319+
title: "My LDAP",
320+
host: "1.2.3.4",
321+
port: 636,
322+
method: "ssl",
323+
base: "ou=snip,dc=snip,dc=example,dc=com",
324+
uid: "sAMAccountName",
325+
bind_dn: "snip",
326+
password: "snip"
327+
run lambda { |env| [404, {"Content-Type" => "text/plain"}, [env.key?("omniauth.auth").to_s]] }
328+
end.to_app
329+
end
330+
331+
before do
332+
ldap_strategy = Class.new(OmniAuth::Strategies::LDAP)
333+
stub_const("MySamaccountnameProvider", ldap_strategy)
334+
@adaptor = double(OmniAuth::LDAP::Adaptor, {uid: "sAMAccountName"})
335+
allow(@adaptor).to receive(:filter)
336+
allow(OmniAuth::LDAP::Adaptor).to receive(:new) { @adaptor }
337+
# Return an entry that includes sAMAccountName but not uid, so nickname maps from sAMAccountName
338+
allow(@adaptor).to receive(:bind_as).and_return(
339+
Net::LDAP::Entry.from_single_ldif_string(
340+
%{dn: cn=ping, dc=snip, dc=example, dc=com
341+
samaccountname: ping
342+
343+
givenname: Ping
344+
sn: User
345+
},
346+
),
347+
)
348+
end
349+
350+
it "sets auth.uid to the DN (not the sAMAccountName attribute) and maps nickname from sAMAccountName" do
351+
post("/auth/ldap/callback", {username: "ping", password: "secret"})
352+
353+
expect(last_response).not_to be_redirect
354+
355+
auth = last_request.env["omniauth.auth"]
356+
expect(auth.uid).to eq "cn=ping, dc=snip, dc=example, dc=com"
357+
expect(auth.info.nickname).to eq "ping" # comes from sAMAccountName
358+
end
359+
end
311360
end

0 commit comments

Comments
 (0)