From f63a80c8e56a40b4a3ca19a4b56f3b47831edaba Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Sun, 16 Feb 2014 00:21:48 +0100 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20Support=20custom=20ldap=20attri?= =?UTF-8?q?butes=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++++ lib/omniauth/strategies/ldap.rb | 10 +++++----- spec/omniauth/strategies/ldap_spec.rb | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e4f13b3..1003942 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,11 @@ use OmniAuth::Strategies::LDAP, tls_options: { ssl_version: "TLSv1_2", ciphers: ["AES-128-CBC", "AES-128-CBC-HMAC-SHA1", "AES-128-CBC-HMAC-SHA256"], + }, + mapping: { + 'name' => 'cn;lang-en', + 'email' => ['preferredEmail', 'mail'], + 'nickname' => ['uid', 'userid', 'sAMAccountName'] } # Or, alternatively: # 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: - `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). - `:connect_timeout` - Maximum time in seconds to wait when establishing the TCP connection to the LDAP server. Forwarded to `Net::LDAP`. - `:read_timeout` - Maximum time in seconds to wait for reads during LDAP operations (search/bind). Forwarded to `Net::LDAP`. +- `:mapping` - allows you to customize mapping of LDAP attributes to the returned user info hash. The default mappings are defined in [ldap.rb](lib/omniauth/strategies/ldap.rb#L7), it will be merged with yours. Example enabling password policy: diff --git a/lib/omniauth/strategies/ldap.rb b/lib/omniauth/strategies/ldap.rb index c17a36d..9d22a7f 100644 --- a/lib/omniauth/strategies/ldap.rb +++ b/lib/omniauth/strategies/ldap.rb @@ -9,7 +9,7 @@ class LDAP InvalidCredentialsError = Class.new(StandardError) - CONFIG = { + option :mapping, { "name" => "cn", "first_name" => "givenName", "last_name" => "sn", @@ -23,7 +23,7 @@ class LDAP "url" => ["wwwhomepage"], "image" => "jpegPhoto", "description" => "description", - }.freeze + } option :title, "LDAP Authentication" # default title for authentication form # For OmniAuth >= 2.0 the default allowed request method is POST only. # Ensure the strategy follows that default so GET /auth/:provider returns 404 as expected in tests. @@ -91,7 +91,7 @@ def callback_phase return fail!(:invalid_credentials, InvalidCredentialsError.new("User not found for header #{hu}")) end @ldap_user_info = entry - @user_info = self.class.map_user(CONFIG, @ldap_user_info) + @user_info = self.class.map_user(@options[:mapping], @ldap_user_info) return super rescue => e return fail!(:ldap_error, e) @@ -111,7 +111,7 @@ def callback_phase # Optionally attach policy info even on success (e.g., timeBeforeExpiration) attach_password_policy_env(@adaptor) - @user_info = self.class.map_user(CONFIG, @ldap_user_info) + @user_info = self.class.map_user(@options[:mapping], @ldap_user_info) super rescue => e fail!(:ldap_error, e) @@ -205,7 +205,7 @@ def directory_lookup(adaptor, username) search_filter = filter(adaptor, username) adaptor.connection.open do |conn| rs = conn.search(filter: search_filter, size: 1) - entry = rs && rs.first + entry = rs&.first end entry end diff --git a/spec/omniauth/strategies/ldap_spec.rb b/spec/omniauth/strategies/ldap_spec.rb index 92e3efa..9616703 100644 --- a/spec/omniauth/strategies/ldap_spec.rb +++ b/spec/omniauth/strategies/ldap_spec.rb @@ -432,6 +432,22 @@ def make_env(path = "/auth/ldap", props = {}) expect(info.image).to eq "http://www.intridea.com/ping.jpg" expect(info.description).to eq "omniauth-ldap" end + + context 'and mapping is set' do + let(:app) do + Rack::Builder.new { + use OmniAuth::Test::PhonySession + use MyLdapProvider, :name => 'ldap', :host => '192.168.1.145', :base => 'dc=score, dc=local', :mapping => { 'phone' => 'mobile' } + run lambda { |env| [404, {'Content-Type' => 'text/plain'}, [env.key?('omniauth.auth').to_s]] } + }.to_app + end + + it 'should map user info according to customized mapping' do + post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) + auth_hash.info.phone.should == '444-444-4444' + auth_hash.info.mobile.should == '444-444-4444' + end + end end end From d15ac84c0cb357ef1c0dad9ba30b6b194c283e7c Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 6 Nov 2025 00:29:37 -0700 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20RBS=20Types=20updat?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sig/omniauth/strategies/ldap.rbs | 3 --- 1 file changed, 3 deletions(-) diff --git a/sig/omniauth/strategies/ldap.rbs b/sig/omniauth/strategies/ldap.rbs index c73a62c..f0b1815 100644 --- a/sig/omniauth/strategies/ldap.rbs +++ b/sig/omniauth/strategies/ldap.rbs @@ -3,9 +3,6 @@ module OmniAuth class LDAP OMNIAUTH_GTE_V2: bool - # CONFIG is a read-only mapping of string keys to mapping definitions - CONFIG: Hash[String, untyped] - # The request_phase either returns a Rack-compatible response or the form response. def request_phase: () -> (Rack::Response | Array[untyped] | String) From 2a93bb1a2151721f837ad6dc3afbf2bd6a3d19e8 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 6 Nov 2025 00:30:11 -0700 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9C=85=20More=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/omniauth/strategies/ldap_spec.rb | 44 ++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/spec/omniauth/strategies/ldap_spec.rb b/spec/omniauth/strategies/ldap_spec.rb index 9616703..bb70771 100644 --- a/spec/omniauth/strategies/ldap_spec.rb +++ b/spec/omniauth/strategies/ldap_spec.rb @@ -347,7 +347,7 @@ def make_env(path = "/auth/ldap", props = {}) it "escapes special characters in username when building filter" do allow(@adaptor).to receive(:filter).and_return("uid=%{username}") # '(' => \28 and ')' => \29 per RFC 4515 escaping - expect(Net::LDAP::Filter).to receive(:construct).with("uid=al\\28ice\\29") + expect(Net::LDAP::Filter).to receive(:construct).with('uid=al\28ice\29') post("/auth/ldap/callback", {username: "al(ice)", password: "secret"}) end @@ -444,8 +444,8 @@ def make_env(path = "/auth/ldap", props = {}) it 'should map user info according to customized mapping' do post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) - auth_hash.info.phone.should == '444-444-4444' - auth_hash.info.mobile.should == '444-444-4444' + expect(auth_hash.info.phone).to eq '444-444-4444' + expect(auth_hash.info.mobile).to eq '444-444-4444' end end end @@ -604,7 +604,7 @@ def connection_returning(entry) connection: connection_returning(entry), filter: "uid=%{username}", ) - expect(Net::LDAP::Filter).to receive(:construct).with("uid=al\\28ice\\29").and_call_original + expect(Net::LDAP::Filter).to receive(:construct).with('uid=al\28ice\29').and_call_original post "/auth/ldap/callback", nil, {"REMOTE_USER" => "al(ice)"} expect(last_response).not_to be_redirect @@ -632,5 +632,41 @@ def connection_returning(entry) post "/auth/ldap/callback", nil, {"REMOTE_USER" => "alice@example.com"} expect(last_response).not_to be_redirect end + + context "with custom mapping option" do + let(:app) do + Rack::Builder.new do + use OmniAuth::Test::PhonySession + use MyHeaderProvider, + name: "ldap", + title: "Header LDAP", + host: "ldap.example.com", + base: "dc=example,dc=com", + uid: "uid", + header_auth: true, + header_name: "REMOTE_USER", + name_proc: proc { |n| n }, + mapping: { "phone" => "mobile" } + run lambda { |env| [404, {"Content-Type" => "text/plain"}, [env.key?("omniauth.auth").to_s]] } + end.to_app + end + + it "applies the custom mapping in header SSO path" do + entry = Net::LDAP::Entry.from_single_ldif_string(%{dn: cn=bob, dc=example, dc=com +uid: bob +mobile: 444-444-4444 +telephonenumber: 555-555-5555 +}) + allow(@adaptor).to receive(:connection).and_return(connection_returning(entry)) + + post "/auth/ldap/callback", nil, {"REMOTE_USER" => "bob"} + expect(last_response).not_to be_redirect + auth = last_request.env["omniauth.auth"] + # phone should come from mobile due to custom mapping override + expect(auth.info.phone).to eq "444-444-4444" + # mobile remains available as well + expect(auth.info.mobile).to eq "444-444-4444" + end + end end end From c7c944d4e6135ae166889341647952838f96698f Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 6 Nov 2025 00:30:49 -0700 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=9D=20More=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1003942..4716d2c 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ The following options are available for configuring the OmniAuth LDAP strategy: - `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). - `:connect_timeout` - Maximum time in seconds to wait when establishing the TCP connection to the LDAP server. Forwarded to `Net::LDAP`. - `:read_timeout` - Maximum time in seconds to wait for reads during LDAP operations (search/bind). Forwarded to `Net::LDAP`. -- `:mapping` - allows you to customize mapping of LDAP attributes to the returned user info hash. The default mappings are defined in [ldap.rb](lib/omniauth/strategies/ldap.rb#L7), it will be merged with yours. +- `: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). Example enabling password policy: From c132b899f9fcb62a48cd1d1ac902fa1456a78b8b Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 6 Nov 2025 00:42:38 -0700 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=9A=A8=20Linting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .rubocop_gradual.lock | 2 +- README.md | 6 +++--- lib/omniauth/strategies/ldap.rb | 2 +- spec/omniauth/strategies/ldap_spec.rb | 16 ++++++++-------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.rubocop_gradual.lock b/.rubocop_gradual.lock index 95be76c..ed990c9 100644 --- a/.rubocop_gradual.lock +++ b/.rubocop_gradual.lock @@ -23,7 +23,7 @@ [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:1834231975": [ + "spec/omniauth/strategies/ldap_spec.rb:4166458344": [ [126, 13, 9, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1130140517], [181, 17, 28, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3444838747], [190, 17, 23, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1584148894], diff --git a/README.md b/README.md index 4716d2c..2ffda64 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,9 @@ use OmniAuth::Strategies::LDAP, ciphers: ["AES-128-CBC", "AES-128-CBC-HMAC-SHA1", "AES-128-CBC-HMAC-SHA256"], }, mapping: { - 'name' => 'cn;lang-en', - 'email' => ['preferredEmail', 'mail'], - 'nickname' => ['uid', 'userid', 'sAMAccountName'] + "name" => "cn;lang-en", + "email" => ["preferredEmail", "mail"], + "nickname" => ["uid", "userid", "sAMAccountName"], } # Or, alternatively: # use OmniAuth::Strategies::LDAP, filter: '(&(uid=%{username})(memberOf=cn=myapp-users,ou=groups,dc=example,dc=com))' diff --git a/lib/omniauth/strategies/ldap.rb b/lib/omniauth/strategies/ldap.rb index 9d22a7f..a5e6313 100644 --- a/lib/omniauth/strategies/ldap.rb +++ b/lib/omniauth/strategies/ldap.rb @@ -205,7 +205,7 @@ def directory_lookup(adaptor, username) search_filter = filter(adaptor, username) adaptor.connection.open do |conn| rs = conn.search(filter: search_filter, size: 1) - entry = rs&.first + entry = rs && rs.first end entry end diff --git a/spec/omniauth/strategies/ldap_spec.rb b/spec/omniauth/strategies/ldap_spec.rb index bb70771..18ed2ca 100644 --- a/spec/omniauth/strategies/ldap_spec.rb +++ b/spec/omniauth/strategies/ldap_spec.rb @@ -433,19 +433,19 @@ def make_env(path = "/auth/ldap", props = {}) expect(info.description).to eq "omniauth-ldap" end - context 'and mapping is set' do + context "when mapping is set" do let(:app) do Rack::Builder.new { use OmniAuth::Test::PhonySession - use MyLdapProvider, :name => 'ldap', :host => '192.168.1.145', :base => 'dc=score, dc=local', :mapping => { 'phone' => 'mobile' } - run lambda { |env| [404, {'Content-Type' => 'text/plain'}, [env.key?('omniauth.auth').to_s]] } + use MyLdapProvider, name: "ldap", host: "192.168.1.145", base: "dc=score, dc=local", mapping: {"phone" => "mobile"} + run lambda { |env| [404, {"Content-Type" => "text/plain"}, [env.key?("omniauth.auth").to_s]] } }.to_app end - it 'should map user info according to customized mapping' do - post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) - expect(auth_hash.info.phone).to eq '444-444-4444' - expect(auth_hash.info.mobile).to eq '444-444-4444' + it "maps user info according to customized mapping" do + post("/auth/ldap/callback", {username: "ping", password: "password"}) + expect(auth_hash.info.phone).to eq "444-444-4444" + expect(auth_hash.info.mobile).to eq "444-444-4444" end end end @@ -646,7 +646,7 @@ def connection_returning(entry) header_auth: true, header_name: "REMOTE_USER", name_proc: proc { |n| n }, - mapping: { "phone" => "mobile" } + mapping: {"phone" => "mobile"} run lambda { |env| [404, {"Content-Type" => "text/plain"}, [env.key?("omniauth.auth").to_s]] } end.to_app end