Skip to content

Commit 9904e7e

Browse files
authored
Merge pull request #107 from omniauth/feat/custom-mapping
2 parents 345b2a2 + c132b89 commit 9904e7e

File tree

5 files changed

+65
-10
lines changed

5 files changed

+65
-10
lines changed

.rubocop_gradual.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
[47, 7, 38, "RSpec/AnyInstance: Avoid stubbing using `allow_any_instance_of`.", 3627954156],
2424
[84, 7, 48, "RSpec/AnyInstance: Avoid stubbing using `allow_any_instance_of`.", 2759780562]
2525
],
26-
"spec/omniauth/strategies/ldap_spec.rb:1834231975": [
26+
"spec/omniauth/strategies/ldap_spec.rb:4166458344": [
2727
[126, 13, 9, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1130140517],
2828
[181, 17, 28, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3444838747],
2929
[190, 17, 23, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1584148894],

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ use OmniAuth::Strategies::LDAP,
6969
tls_options: {
7070
ssl_version: "TLSv1_2",
7171
ciphers: ["AES-128-CBC", "AES-128-CBC-HMAC-SHA1", "AES-128-CBC-HMAC-SHA256"],
72+
},
73+
mapping: {
74+
"name" => "cn;lang-en",
75+
"email" => ["preferredEmail", "mail"],
76+
"nickname" => ["uid", "userid", "sAMAccountName"],
7277
}
7378
# Or, alternatively:
7479
# use OmniAuth::Strategies::LDAP, filter: '(&(uid=%{username})(memberOf=cn=myapp-users,ou=groups,dc=example,dc=com))'
@@ -199,6 +204,7 @@ The following options are available for configuring the OmniAuth LDAP strategy:
199204
- `adaptor.last_password_policy_response` — the matching password policy response control (implementation-specific object). This can indicate conditions such as password expired, account locked, reset required, or grace logins remaining (per the draft RFC).
200205
- `:connect_timeout` - Maximum time in seconds to wait when establishing the TCP connection to the LDAP server. Forwarded to `Net::LDAP`.
201206
- `:read_timeout` - Maximum time in seconds to wait for reads during LDAP operations (search/bind). Forwarded to `Net::LDAP`.
207+
- `: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).
202208

203209
Example enabling password policy:
204210

lib/omniauth/strategies/ldap.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class LDAP
99

1010
InvalidCredentialsError = Class.new(StandardError)
1111

12-
CONFIG = {
12+
option :mapping, {
1313
"name" => "cn",
1414
"first_name" => "givenName",
1515
"last_name" => "sn",
@@ -23,7 +23,7 @@ class LDAP
2323
"url" => ["wwwhomepage"],
2424
"image" => "jpegPhoto",
2525
"description" => "description",
26-
}.freeze
26+
}
2727
option :title, "LDAP Authentication" # default title for authentication form
2828
# For OmniAuth >= 2.0 the default allowed request method is POST only.
2929
# Ensure the strategy follows that default so GET /auth/:provider returns 404 as expected in tests.
@@ -91,7 +91,7 @@ def callback_phase
9191
return fail!(:invalid_credentials, InvalidCredentialsError.new("User not found for header #{hu}"))
9292
end
9393
@ldap_user_info = entry
94-
@user_info = self.class.map_user(CONFIG, @ldap_user_info)
94+
@user_info = self.class.map_user(@options[:mapping], @ldap_user_info)
9595
return super
9696
rescue => e
9797
return fail!(:ldap_error, e)
@@ -111,7 +111,7 @@ def callback_phase
111111
# Optionally attach policy info even on success (e.g., timeBeforeExpiration)
112112
attach_password_policy_env(@adaptor)
113113

114-
@user_info = self.class.map_user(CONFIG, @ldap_user_info)
114+
@user_info = self.class.map_user(@options[:mapping], @ldap_user_info)
115115
super
116116
rescue => e
117117
fail!(:ldap_error, e)

sig/omniauth/strategies/ldap.rbs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ module OmniAuth
33
class LDAP
44
OMNIAUTH_GTE_V2: bool
55

6-
# CONFIG is a read-only mapping of string keys to mapping definitions
7-
CONFIG: Hash[String, untyped]
8-
96
# The request_phase either returns a Rack-compatible response or the form response.
107
def request_phase: () -> (Rack::Response | Array[untyped] | String)
118

spec/omniauth/strategies/ldap_spec.rb

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ def make_env(path = "/auth/ldap", props = {})
347347
it "escapes special characters in username when building filter" do
348348
allow(@adaptor).to receive(:filter).and_return("uid=%{username}")
349349
# '(' => \28 and ')' => \29 per RFC 4515 escaping
350-
expect(Net::LDAP::Filter).to receive(:construct).with("uid=al\\28ice\\29")
350+
expect(Net::LDAP::Filter).to receive(:construct).with('uid=al\28ice\29')
351351
post("/auth/ldap/callback", {username: "al(ice)", password: "secret"})
352352
end
353353

@@ -432,6 +432,22 @@ def make_env(path = "/auth/ldap", props = {})
432432
expect(info.image).to eq "http://www.intridea.com/ping.jpg"
433433
expect(info.description).to eq "omniauth-ldap"
434434
end
435+
436+
context "when mapping is set" do
437+
let(:app) do
438+
Rack::Builder.new {
439+
use OmniAuth::Test::PhonySession
440+
use MyLdapProvider, name: "ldap", host: "192.168.1.145", base: "dc=score, dc=local", mapping: {"phone" => "mobile"}
441+
run lambda { |env| [404, {"Content-Type" => "text/plain"}, [env.key?("omniauth.auth").to_s]] }
442+
}.to_app
443+
end
444+
445+
it "maps user info according to customized mapping" do
446+
post("/auth/ldap/callback", {username: "ping", password: "password"})
447+
expect(auth_hash.info.phone).to eq "444-444-4444"
448+
expect(auth_hash.info.mobile).to eq "444-444-4444"
449+
end
450+
end
435451
end
436452
end
437453

@@ -588,7 +604,7 @@ def connection_returning(entry)
588604
connection: connection_returning(entry),
589605
filter: "uid=%{username}",
590606
)
591-
expect(Net::LDAP::Filter).to receive(:construct).with("uid=al\\28ice\\29").and_call_original
607+
expect(Net::LDAP::Filter).to receive(:construct).with('uid=al\28ice\29').and_call_original
592608

593609
post "/auth/ldap/callback", nil, {"REMOTE_USER" => "al(ice)"}
594610
expect(last_response).not_to be_redirect
@@ -616,5 +632,41 @@ def connection_returning(entry)
616632
post "/auth/ldap/callback", nil, {"REMOTE_USER" => "[email protected]"}
617633
expect(last_response).not_to be_redirect
618634
end
635+
636+
context "with custom mapping option" do
637+
let(:app) do
638+
Rack::Builder.new do
639+
use OmniAuth::Test::PhonySession
640+
use MyHeaderProvider,
641+
name: "ldap",
642+
title: "Header LDAP",
643+
host: "ldap.example.com",
644+
base: "dc=example,dc=com",
645+
uid: "uid",
646+
header_auth: true,
647+
header_name: "REMOTE_USER",
648+
name_proc: proc { |n| n },
649+
mapping: {"phone" => "mobile"}
650+
run lambda { |env| [404, {"Content-Type" => "text/plain"}, [env.key?("omniauth.auth").to_s]] }
651+
end.to_app
652+
end
653+
654+
it "applies the custom mapping in header SSO path" do
655+
entry = Net::LDAP::Entry.from_single_ldif_string(%{dn: cn=bob, dc=example, dc=com
656+
uid: bob
657+
mobile: 444-444-4444
658+
telephonenumber: 555-555-5555
659+
})
660+
allow(@adaptor).to receive(:connection).and_return(connection_returning(entry))
661+
662+
post "/auth/ldap/callback", nil, {"REMOTE_USER" => "bob"}
663+
expect(last_response).not_to be_redirect
664+
auth = last_request.env["omniauth.auth"]
665+
# phone should come from mobile due to custom mapping override
666+
expect(auth.info.phone).to eq "444-444-4444"
667+
# mobile remains available as well
668+
expect(auth.info.mobile).to eq "444-444-4444"
669+
end
670+
end
619671
end
620672
end

0 commit comments

Comments
 (0)