diff --git a/.rubocop_gradual.lock b/.rubocop_gradual.lock index ed990c9..850745e 100644 --- a/.rubocop_gradual.lock +++ b/.rubocop_gradual.lock @@ -2,10 +2,10 @@ "lib/omniauth-ldap/adaptor.rb:3925200886": [ [68, 7, 413, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 105664470] ], - "spec/integration/middleware_spec.rb:4142891586": [ + "spec/integration/middleware_spec.rb:2185613788": [ [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], - [81, 5, 317, "RSpec/LeakyConstantDeclaration: Stub class constant instead of declaring explicitly.", 424933157] + [130, 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], @@ -23,14 +23,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: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], - [201, 17, 32, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1515076977], - [243, 19, 19, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2526348694], - [269, 17, 56, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2413495789], - [284, 13, 9, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3182939526], - [338, 15, 19, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2526348694] + "spec/omniauth/strategies/ldap_spec.rb:2130811218": [ + [138, 13, 9, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1130140517], + [193, 17, 28, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3444838747], + [202, 17, 23, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1584148894], + [213, 17, 32, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1515076977], + [255, 19, 19, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2526348694], + [281, 17, 56, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2413495789], + [296, 13, 9, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3182939526], + [350, 15, 19, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2526348694] ] } diff --git a/README.md b/README.md index 53331ef..5a4d38f 100644 --- a/README.md +++ b/README.md @@ -329,6 +329,58 @@ end Then link users to `/auth/ldap` in your app (for example, in a Devise sign-in page). +### Use JSON Body + +This gem is compatible with JSON-encoded POST bodies as well as traditional form-encoded. + +- Set header `Content-Type` to `application/json`. +- Send a JSON object containing `username` and `password`. +- Rails automatically exposes parsed JSON params via `env["action_dispatch.request.request_parameters"]`, which this strategy reads first. In non-Rails Rack apps, ensure you use a JSON parser middleware if you post raw JSON. + +Examples + +- curl (JSON): + + ```bash + curl -i \ + -X POST \ + -H 'Content-Type: application/json' \ + -d '{"username":"alice","password":"secret"}' \ + http://localhost:3000/auth/ldap + ``` + + The request phase will redirect to `/auth/ldap/callback` when both fields are present. + +- curl (form-encoded, still supported): + + ```bash + curl -i \ + -X POST \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'username=alice' \ + --data-urlencode 'password=secret' \ + http://localhost:3000/auth/ldap + ``` + +- Browser (JavaScript fetch): + + ```js + fetch('/auth/ldap', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: 'alice', password: 'secret' }) + }).then(res => { + if (res.redirected) { + window.location = res.url; // typically /auth/ldap/callback + } + }); + ``` + +Notes + +- 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. +- 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. + ### Using a custom filter If you need to restrict authentication to a group or use a more complex lookup, pass `:filter`. Use `%{username}` โ€” it will be replaced with the processed username (after `:name_proc`). @@ -708,6 +760,9 @@ See [LICENSE.txt][๐Ÿ“„license] for the official [Copyright Notice][๐Ÿ“„copyright , and omniauth-ldap contributors. +
  • + Copyright (C) 2014 David Benko +
  • Copyright (c) 2011 by Ping Yu and Intridea, Inc.
  • diff --git a/docs/OmniAuth.html b/docs/OmniAuth.html index a62fd41..3f3e454 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 345d15f..84e7fe0 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 ef639b3..a1957cc 100644 --- a/docs/OmniAuth/LDAP/Adaptor.html +++ b/docs/OmniAuth/LDAP/Adaptor.html @@ -136,6 +136,10 @@

    :allow_anonymous, :filter, :tls_options, + :password_policy, + # Timeouts + :connect_timeout, + :read_timeout, # Deprecated :method, @@ -322,6 +326,62 @@

    Instance Attribute Summary collaps

    Returns the value of attribute filter.

    + + + +
  • + + + #last_operation_result ⇒ Object + + + + + + + + + readonly + + + + + + + + + +

    Returns the value of attribute last_operation_result.

    +
    + +
  • + + +
  • + + + #last_password_policy_response ⇒ Object + + + + + + + + + readonly + + + + + + + + + +

    Returns the value of attribute last_password_policy_response.

    +
    +
  • @@ -348,6 +408,34 @@

    Instance Attribute Summary collaps

    Returns the value of attribute password.

    + + + +
  • + + + #password_policy ⇒ Object + + + + + + + + + readonly + + + + + + + + + +

    Returns the value of attribute password_policy.

    +
    +
  • @@ -503,10 +591,6 @@

     
     
    -76
    -77
    -78
    -79
     80
     81
     82
    @@ -532,10 +616,31 @@ 

    102 103 104 -105

    +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 -
    # File 'lib/omniauth-ldap/adaptor.rb', line 76
    +      
    # File 'lib/omniauth-ldap/adaptor.rb', line 80
     
     def initialize(configuration = {})
       Adaptor.validate(configuration)
    @@ -552,7 +657,9 @@ 

    port: @port, encryption: encryption_options, } - @bind_method = if @try_sasl + # Remove passing timeouts here to avoid issues on older net-ldap versions. + # We'll set them after initialization if the connection responds to writers. + @bind_method = if @try_sasl :sasl else ((@allow_anonymous || !@bind_dn || !@password) ? :anonymous : :simple) @@ -566,6 +673,21 @@

    } config[:auth] = @auth @connection = Net::LDAP.new(config) + # Apply optional timeout settings if supported by the installed net-ldap version + if !@connect_timeout.nil? + if @connection.respond_to?(:connect_timeout=) + @connection.connect_timeout = @connect_timeout + else + @connection.instance_variable_set(:@connect_timeout, @connect_timeout) + end + end + if !@read_timeout.nil? + if @connection.respond_to?(:read_timeout=) + @connection.read_timeout = @read_timeout + else + @connection.instance_variable_set(:@read_timeout, @read_timeout) + end + end end

    @@ -604,12 +726,12 @@

     
     
    -62
    -63
    -64
    +66 +67 +68

    -
    # File 'lib/omniauth-ldap/adaptor.rb', line 62
    +      
    # File 'lib/omniauth-ldap/adaptor.rb', line 66
     
     def auth
       @auth
    @@ -646,12 +768,12 @@ 

     
     
    -62
    -63
    -64
    +66 +67 +68

    -
    # File 'lib/omniauth-ldap/adaptor.rb', line 62
    +      
    # File 'lib/omniauth-ldap/adaptor.rb', line 66
     
     def base
       @base
    @@ -688,12 +810,12 @@ 

     
     
    -61
    -62
    -63
    +65 +66 +67

    -
    # File 'lib/omniauth-ldap/adaptor.rb', line 61
    +      
    # File 'lib/omniauth-ldap/adaptor.rb', line 65
     
     def bind_dn
       @bind_dn
    @@ -730,12 +852,12 @@ 

     
     
    -62
    -63
    -64
    +66 +67 +68

    -
    # File 'lib/omniauth-ldap/adaptor.rb', line 62
    +      
    # File 'lib/omniauth-ldap/adaptor.rb', line 66
     
     def connection
       @connection
    @@ -772,12 +894,12 @@ 

     
     
    -62
    -63
    -64
    +66 +67 +68

    -
    # File 'lib/omniauth-ldap/adaptor.rb', line 62
    +      
    # File 'lib/omniauth-ldap/adaptor.rb', line 66
     
     def filter
       @filter
    @@ -788,6 +910,90 @@ 

    + +
    +

    + + #last_operation_resultObject (readonly) + + + + + +

    +
    +

    Returns the value of attribute last_operation_result.

    + + +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +66
    +67
    +68
    +
    +
    # File 'lib/omniauth-ldap/adaptor.rb', line 66
    +
    +def last_operation_result
    +  @last_operation_result
    +end
    +
    +
    + + + +
    +

    + + #last_password_policy_responseObject (readonly) + + + + + +

    +
    +

    Returns the value of attribute last_password_policy_response.

    + + +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +66
    +67
    +68
    +
    +
    # File 'lib/omniauth-ldap/adaptor.rb', line 66
    +
    +def last_password_policy_response
    +  @last_password_policy_response
    +end
    +
    +
    + +

    @@ -814,12 +1020,12 @@

     
     
    -61
    -62
    -63
    +65 +66 +67

    -
    # File 'lib/omniauth-ldap/adaptor.rb', line 61
    +      
    # File 'lib/omniauth-ldap/adaptor.rb', line 65
     
     def password
       @password
    @@ -830,6 +1036,48 @@ 

    + +
    +

    + + #password_policyObject (readonly) + + + + + +

    +
    +

    Returns the value of attribute password_policy.

    + + +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +66
    +67
    +68
    +
    +
    # File 'lib/omniauth-ldap/adaptor.rb', line 66
    +
    +def password_policy
    +  @password_policy
    +end
    +
    +
    + +

    @@ -856,12 +1104,12 @@

     
     
    -62
    -63
    -64
    +66 +67 +68

    -
    # File 'lib/omniauth-ldap/adaptor.rb', line 62
    +      
    # File 'lib/omniauth-ldap/adaptor.rb', line 66
     
     def uid
       @uid
    @@ -916,20 +1164,20 @@ 

     
     
    -64
    -65
    -66
    -67
     68
     69
     70
     71
     72
     73
    -74
    +74 +75 +76 +77 +78

    -
    # File 'lib/omniauth-ldap/adaptor.rb', line 64
    +      
    # File 'lib/omniauth-ldap/adaptor.rb', line 68
     
     def self.validate(configuration = {})
       message = []
    @@ -980,47 +1228,97 @@ 

     
     
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176

    -
    # File 'lib/omniauth-ldap/adaptor.rb', line 110
    +      
    # File 'lib/omniauth-ldap/adaptor.rb', line 131
     
     def bind_as(args = {})
       result = false
    +  @last_operation_result = nil
    +  @last_password_policy_response = nil
       @connection.open do |me|
         rs = me.search(args)
    -    if rs and rs.first and dn = rs.first.dn
    -      password = args[:password]
    -      method = args[:method] || @method
    -      password = password.call if password.respond_to?(:call)
    -      if method == "sasl"
    -        result = rs.first if me.bind(sasl_auths({username: dn, password: password}).first)
    -      elsif me.bind(
    -        method: :simple,
    -        username: dn,
    -        password: password,
    -      )
    -        result = rs.first
    +    if rs && rs.first
    +      dn = rs.first.dn
    +      if dn
    +        password = args[:password]
    +        password = password.call if password.respond_to?(:call)
    +
    +        bind_args = if @bind_method == :sasl
    +          sasl_auths({username: dn, password: password}).first
    +        else
    +          {
    +            method: :simple,
    +            username: dn,
    +            password: password,
    +          }
    +        end
    +
    +        # Optionally request LDAP Password Policy control (RFC Draft - de facto standard)
    +        if @password_policy
    +          # Always request by OID using a simple hash; avoids depending on gem-specific control classes
    +          control = {oid: "1.3.6.1.4.1.42.2.27.8.5.1", criticality: true, value: nil}
    +          if bind_args.is_a?(Hash)
    +            bind_args = bind_args.merge({controls: [control]})
    +          else
    +            # Some Net::LDAP versions allow passing a block for SASL only; ensure we still can add controls if hash
    +            # When not a Hash, we can't merge; rely on server default behavior.
    +          end
    +        end
    +
    +        begin
    +          success = bind_args ? me.bind(bind_args) : me.bind
    +        ensure
    +          capture_password_policy(me)
    +        end
    +
    +        result = rs.first if success
           end
         end
       end
    @@ -1036,7 +1334,7 @@ 

    diff --git a/docs/OmniAuth/LDAP/Adaptor/AuthenticationError.html b/docs/OmniAuth/LDAP/Adaptor/AuthenticationError.html index 741dd00..353f275 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 5b84f08..c065402 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 532f5cc..d41a9bc 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 9a907d5..94e94ca 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 c67a473..83f9fc2 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 c5db66f..dd54494 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 bd39ec2..7157854 100644 --- a/docs/OmniAuth/Strategies/LDAP.html +++ b/docs/OmniAuth/Strategies/LDAP.html @@ -123,25 +123,6 @@

    Class.new(StandardError)
    -
    CONFIG = - -
    -
    {
    -  "name" => "cn",
    -  "first_name" => "givenName",
    -  "last_name" => "sn",
    -  "email" => ["mail", "email", "userPrincipalName"],
    -  "phone" => ["telephoneNumber", "homePhone", "facsimileTelephoneNumber"],
    -  "mobile" => ["mobile", "mobileTelephoneNumber"],
    -  "nickname" => ["uid", "userid", "sAMAccountName"],
    -  "title" => "title",
    -  "location" => {"%0, %1, %2, %3 %4" => [["address", "postalAddress", "homePostalAddress", "street", "streetAddress"], ["l"], ["st"], ["co"], ["postOfficeBox"]]},
    -  "uid" => "dn",
    -  "url" => ["wwwhomepage"],
    -  "image" => "jpegPhoto",
    -  "description" => "description",
    -}.freeze
    - @@ -285,14 +266,6 @@

     
     
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
     142
     143
     144
    @@ -318,10 +291,18 @@ 

    164 165 166 -167

    +167 +168 +169 +170 +171 +172 +173 +174 +175

    -
    # File 'lib/omniauth/strategies/ldap.rb', line 134
    +      
    # File 'lib/omniauth/strategies/ldap.rb', line 142
     
     def map_user(mapper, object)
       user = {}
    @@ -383,9 +364,6 @@ 

     
     
    -78
    -79
    -80
     81
     82
     83
    @@ -416,10 +394,18 @@ 

    108 109 110 -111

    +111 +112 +113 +114 +115 +116 +117 +118 +119

    -
    # File 'lib/omniauth/strategies/ldap.rb', line 78
    +      
    # File 'lib/omniauth/strategies/ldap.rb', line 81
     
     def callback_phase
       @adaptor = OmniAuth::LDAP::Adaptor.new(@options)
    @@ -434,7 +420,7 @@ 

    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) @@ -443,13 +429,18 @@

    return fail!(:missing_credentials) if missing_credentials? begin - @ldap_user_info = @adaptor.bind_as(filter: filter(@adaptor), size: 1, password: request.params["password"]) + @ldap_user_info = @adaptor.bind_as(filter: filter(@adaptor), size: 1, password: request_data["password"]) unless @ldap_user_info - return fail!(:invalid_credentials, InvalidCredentialsError.new("Invalid credentials for #{request.params["username"]}")) + # Attach password policy info to env if available (best-effort) + attach_password_policy_env(@adaptor) + return fail!(:invalid_credentials, InvalidCredentialsError.new("Invalid credentials for #{request_data["username"]}")) end - @user_info = self.class.map_user(CONFIG, @ldap_user_info) + # Optionally attach policy info even on success (e.g., timeBeforeExpiration) + attach_password_policy_env(@adaptor) + + @user_info = self.class.map_user(@options[:mapping], @ldap_user_info) super rescue => e fail!(:ldap_error, e) @@ -475,26 +466,26 @@

     
     
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    +121 +122 +123 +124 +125 +126 +127 +128 +129

    -
    # File 'lib/omniauth/strategies/ldap.rb', line 113
    +      
    # File 'lib/omniauth/strategies/ldap.rb', line 121
     
     def filter(adaptor, username_override = nil)
       flt = adaptor.filter
       if flt && !flt.to_s.empty?
    -    username = Net::LDAP::Filter.escape(@options[:name_proc].call(username_override || request.params["username"]))
    +    username = Net::LDAP::Filter.escape(@options[:name_proc].call(username_override || request_data["username"]))
         Net::LDAP::Filter.construct(flt % {username: username})
       else
    -    Net::LDAP::Filter.equals(adaptor.uid, @options[:name_proc].call(username_override || request.params["username"]))
    +    Net::LDAP::Filter.equals(adaptor.uid, @options[:name_proc].call(username_override || request_data["username"]))
       end
     end
    @@ -517,9 +508,6 @@

     
     
    -50
    -51
    -52
     53
     54
     55
    @@ -543,10 +531,13 @@ 

    73 74 75 -76

    +76 +77 +78 +79

    -
    # File 'lib/omniauth/strategies/ldap.rb', line 50
    +      
    # File 'lib/omniauth/strategies/ldap.rb', line 53
     
     def request_phase
       # OmniAuth >= 2.0 expects the request phase to be POST-only for /auth/:provider.
    @@ -564,7 +555,7 @@ 

    # 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 != "" + if request.post? && request_data["username"].to_s != "" && request_data["password"].to_s != "" return Rack::Response.new([], 302, "Location" => callback_url).finish end @@ -585,7 +576,7 @@

    diff --git a/docs/_index.html b/docs/_index.html index 344bc7f..e8944cd 100644 --- a/docs/_index.html +++ b/docs/_index.html @@ -254,7 +254,7 @@

    Namespace Listing A-Z

    diff --git a/docs/file.CHANGELOG.html b/docs/file.CHANGELOG.html index 68adc67..36c5361 100644 --- a/docs/file.CHANGELOG.html +++ b/docs/file.CHANGELOG.html @@ -81,10 +81,34 @@

    Added

  • under a subdirectory
  • +
  • Password Policy for LDAP Directories +
      +
    • + + + + + + + +
      password_policy: truefalse (default: false)
      +
    • +
    • on authentication failure, if the server returns password policy controls, the info will be included in the failure message
    • +
    • https://datatracker.ietf.org/doc/html/draft-behera-ldap-password-policy-11
    • +
    +
  • Changed

    +
      +
    • Make support for OmniAuth v1.2+ explicit +
        +
      • Versions < 1.2 do not support SCRIPT_NAME properly, and may cause other issues
      • +
      +
    • +
    +

    Deprecated

    Removed

    @@ -293,7 +317,7 @@

    diff --git a/docs/file.CITATION.html b/docs/file.CITATION.html index a0f652b..91e946d 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 6e8b968..36775d6 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 c25b68f..cc7c0b9 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 fcef94f..33a9f14 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 457a3ab..ab7bff6 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 f747a0f..3728b3d 100644 --- a/docs/file.README.html +++ b/docs/file.README.html @@ -101,7 +101,7 @@

    ๐Ÿ“ OmniAuth LDAP

    -

    Version GitHub tag (latest SemVer) License: MIT Downloads Rank Open Source Helpers CodeCov Test Coverage Coveralls Test Coverage QLTY Test Coverage QLTY Maintainability CI Heads CI Runtime Dependencies @ HEAD CI Current CI Truffle Ruby CI JRuby Deps Locked Deps Unlocked CI Supported CI Legacy CI Unsupported CI Ancient CI Test Coverage CI Style CodeQL Apache SkyWalking Eyes License Compatibility Check

    +

    Version GitHub tag (latest SemVer) License: MIT Downloads Rank Open Source Helpers CodeCov Test Coverage Coveralls Test Coverage CI Heads CI Runtime Dependencies @ HEAD CI Current CI Truffle Ruby CI JRuby Deps Locked Deps Unlocked CI Supported CI Legacy CI Unsupported CI Ancient CI Test Coverage CI Style CodeQL Apache SkyWalking Eyes License Compatibility Check

    if ci_badges.map(&:color).detect { it != "green"} โ˜๏ธ let me know, as I may have missed the discord notification.

    @@ -125,9 +125,17 @@

    ๐ŸŒป Synopsis

    name_proc: proc { |name| name.gsub(/@.*$/, "") }, bind_dn: "default_bind_dn", password: "password", + # Optional timeouts (seconds) + connect_timeout: 3, + read_timeout: 7, 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))' @@ -345,8 +353,38 @@

    Optional Options

    :allow_anonymous - Whether to allow anonymous binding (default: false).
  • :logger - A logger instance for debugging (optional, for internal use).
  • +
  • +:password_policy - When true, the strategy will request the LDAP Password Policy response control (OID 1.3.6.1.4.1.42.2.27.8.5.1) during the user bind. If the server supports it, the adaptor exposes: +
      +
    • +adaptor.last_operation_result โ€” the last Net::LDAP operation result object.
    • +
    • +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 - 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:

    + +
    use OmniAuth::Builder do
    +  provider :ldap,
    +    host: "ldap.example.com",
    +    base: "dc=example,dc=com",
    +    uid: "uid",
    +    bind_dn: "cn=search,dc=example,dc=com",
    +    password: ENV["LDAP_SEARCH_PASSWORD"],
    +    password_policy: true
    +end
    +
    + +

    Note: This is best-effort and compatible with a range of net-ldap versions. If your server supports the control, you can inspect the response via the adaptor instance during/after authentication (for example in a failure handler) to tailor error messages.

    +

    Auth Hash UID vs LDAP :uid (search attribute)

      @@ -457,6 +495,65 @@

      Rails (initializer) example

      Then link users to /auth/ldap in your app (for example, in a Devise sign-in page).

      +

      Use JSON Body

      + +

      This gem is compatible with JSON-encoded POST bodies as well as traditional form-encoded.

      + +
        +
      • Set header Content-Type to application/json.
      • +
      • Send a JSON object containing username and password.
      • +
      • Rails automatically exposes parsed JSON params via env["action_dispatch.request.request_parameters"], which this strategy reads first. In non-Rails Rack apps, ensure you use a JSON parser middleware if you post raw JSON.
      • +
      + +

      Examples

      + +
        +
      • +

        curl (JSON):

        + +
        curl -i \
        +  -X POST \
        +  -H 'Content-Type: application/json' \
        +  -d '{"username":"alice","password":"secret"}' \
        +  http://localhost:3000/auth/ldap
        +
        + +

        The request phase will redirect to /auth/ldap/callback when both fields are present.

        +
      • +
      • +

        curl (form-encoded, still supported):

        + +
        curl -i \
        +  -X POST \
        +  -H 'Content-Type: application/x-www-form-urlencoded' \
        +  --data-urlencode 'username=alice' \
        +  --data-urlencode 'password=secret' \
        +  http://localhost:3000/auth/ldap
        +
        +
      • +
      • +

        Browser (JavaScript fetch):

        + +
        fetch('/auth/ldap', {
        +  method: 'POST',
        +  headers: { 'Content-Type': 'application/json' },
        +  body: JSON.stringify({ username: 'alice', password: 'secret' })
        +}).then(res => {
        +  if (res.redirected) {
        +    window.location = res.url; // typically /auth/ldap/callback
        +  }
        +});
        +
        +
      • +
      + +

      Notes

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

      Using a custom filter

      If you need to restrict authentication to a group or use a more complex lookup, pass :filter. Use %{username} โ€” it will be replaced with the processed username (after :name_proc).

      @@ -624,7 +721,10 @@

      Mounted under a subdirectory ( title: "Acme LDAP", host: "ldap.acme.internal", base: "dc=acme,dc=corp", - uid: "uid" + uid: "uid", + bind_dn: "cn=search,dc=acme,dc=corp", + password: ENV["LDAP_SEARCH_PASSWORD"], + name_proc: proc { |n| n.split("@").first } end

    @@ -785,8 +885,6 @@

    Code Coverage

    Coveralls Test Coverage

    -

    QLTY Test Coverage

    -

    ๐Ÿช‡ Code of Conduct

    Everyone interacting with this projectโ€™s codebases, issue trackers,
    @@ -868,6 +966,9 @@

    , and omniauth-ldap contributors. +
  • + Copyright (C) 2014 David Benko +
  • Copyright (c) 2011 by Ping Yu and Intridea, Inc.
  • @@ -888,7 +989,7 @@

    ๐Ÿค‘ A request for help

    To say โ€œthanks!โ€ โ˜๏ธ Join the Discord or ๐Ÿ‘‡๏ธ send money.

    -

    Sponsor me on GitHub Sponsors ๐Ÿ’Œ Sponsor me on Liberapay ๐Ÿ’Œ Donate on PayPal

    +

    Sponsor me on GitHub Sponsors ๐Ÿ’Œ Sponsor me on Liberapay ๐Ÿ’Œ Donate on PayPal

    Please give the project a star โญ โ™ฅ.

    @@ -897,7 +998,7 @@

    Please give the project a star โญ โ™ฅ diff --git a/docs/file.REEK.html b/docs/file.REEK.html index cbbef16..2365083 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 535c4d0..d86061a 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 282d6cd..1ee7bdb 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 ffab603..3dd8918 100644 --- a/docs/file.adaptor.html +++ b/docs/file.adaptor.html @@ -74,7 +74,7 @@ VALID_ADAPTER_CONFIGURATION_KEYS: Array[Symbol] MUST_HAVE_KEYS: Array[untyped] - METHOD: Hash[Symbol, Symbol?] + ENCRYPTION_METHOD: Hash[Symbol, Symbol?] attr_accessor bind_dn: String? attr_accessor password: String? @@ -87,6 +87,10 @@ attr_reader auth: Hash[Symbol, untyped] # filter is an LDAP filter string when configured attr_reader filter: String? + # optional: request password policy control and capture response + attr_reader password_policy: bool? + attr_reader last_operation_result: untyped + attr_reader last_password_policy_response: untyped # Validate that required keys exist in the configuration def self.validate: (?Hash[Symbol, untyped]) -> void @@ -97,23 +101,37 @@ private - # Returns a Net::LDAP encryption symbol (e.g. :simple_tls, :start_tls) or nil - def ensure_method: (untyped) -> Symbol? + # Returns encryption settings hash or nil + def encryption_options: () -> Hash[Symbol, untyped]? + # Translate configured method/encryption into Net::LDAP symbol + def translate_method: () -> Symbol? + # Returns a Net::LDAP encryption default options hash + def default_options: () -> Hash[Symbol, untyped] + # Sanitize provided TLS options + def sanitize_hash_values: (Hash[untyped, untyped]) -> Hash[untyped, untyped] + # Symbolize option keys + def symbolize_hash_keys: (Hash[untyped, untyped]) -> Hash[Symbol, untyped] + # Capture password policy control from last operation + def capture_password_policy: (Net::LDAP) -> void # Returns an array of SASL auth hashes def sasl_auths: (?Hash[Symbol, untyped]) -> Array[Hash[Symbol, untyped]] - # Returns initial credential (string) and a proc that accepts a challenge and returns the response - # Use Array[untyped] here to avoid tuple syntax issues in some linters; the runtime value - # is commonly a two-element array [initial_credential, proc]. + # Returns initial credential and a proc that accepts a challenge and returns the response def sasl_bind_setup_digest_md5: (?Hash[Symbol, untyped]) -> Array[untyped] def sasl_bind_setup_gss_spnego: (?Hash[Symbol, untyped]) -> Array[untyped] + + @try_sasl: bool? + @allow_anonymous: bool? + @tls_options: Hash[untyped, untyped]? + @sasl_mechanisms: Array[String]? + @disable_verify_certificates: bool? end end end
    diff --git a/docs/file.ldap.html b/docs/file.ldap.html index 80cb27a..d044ea7 100644 --- a/docs/file.ldap.html +++ b/docs/file.ldap.html @@ -62,10 +62,7 @@ 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.
    +
      # The request_phase either returns a Rack-compatible response or the form response.
       def request_phase: () -> (Rack::Response | Array[untyped] | String)
     
       # The callback_phase may call super (untyped) or return a failure symbol
    @@ -86,12 +83,18 @@
     
       # Perform a directory lookup for a given username; returns an Entry or nil
       def directory_lookup: (OmniAuth::LDAP::Adaptor, String) -> untyped
    +
    +  def uid: () { () -> String } -> void
    +
    +  def info: () { () -> Hash[untyped, untyped] } -> void
    +
    +  def extra: () { () -> Hash[Symbol, untyped] } -> void
     end   end end
     
    diff --git a/docs/file.net-ldap.html b/docs/file.net-ldap.html index d5781b4..9380b92 100644 --- a/docs/file.net-ldap.html +++ b/docs/file.net-ldap.html @@ -64,6 +64,7 @@ def open: () { (self) -> untyped } -> untyped
    def search: (?Hash[Symbol, untyped]) -> Array[Net::LDAP::Entry]
    def bind: (?Hash[Symbol, untyped]) -> bool
    + def get_operation_result: () -> Net::LDAP::PDU
    end

    class LDAP::Entry
    @@ -73,12 +74,28 @@

    class LDAP::Filter
    def self.construct: (String) -> Net::LDAP::Filter
    def self.eq: (String, String) -> Net::LDAP::Filter
    + end

    + +

    class LDAP::Control
    + def initialize: (String, bool, untyped) -> void
    + def oid: () -> String
    + end

    + +

    module LDAP::Controls
    + class PasswordPolicy
    + def initialize: () -> void
    + def oid: () -> String
    + end
    + end

    + +

    class LDAP::PDU
    + def controls: () -> Array[untyped]
    end
    end

    diff --git a/docs/file.net-ntlm.html b/docs/file.net-ntlm.html index beea2ad..b4d0821 100644 --- a/docs/file.net-ntlm.html +++ b/docs/file.net-ntlm.html @@ -63,6 +63,8 @@ class Message
    def self.parse: (untyped) -> Net::NTLM::Message
    def response: (?Hash[Symbol, untyped], ?Hash[Symbol, untyped]) -> Net::NTLM::Message
    + # writer used by adaptor to set target name when a domain is present
    + def target_name=: (String) -> String
    end

    class Message::Type1
    @@ -74,7 +76,7 @@
     
     
           
    diff --git a/docs/file.omniauth-ldap-2.3.1.gem.html b/docs/file.omniauth-ldap-2.3.1.gem.html
    index 114d84a..7912f44 100644
    --- a/docs/file.omniauth-ldap-2.3.1.gem.html
    +++ b/docs/file.omniauth-ldap-2.3.1.gem.html
    @@ -61,7 +61,7 @@
     
     
           
    diff --git a/docs/file.omniauth-ldap.html b/docs/file.omniauth-ldap.html
    index 3f250e0..2fd85c4 100644
    --- a/docs/file.omniauth-ldap.html
    +++ b/docs/file.omniauth-ldap.html
    @@ -62,10 +62,15 @@ 

    - sig/omniauth/ldap/version.rbs

    - sig/omniauth/ldap/adaptor.rbs

    - sig/omniauth/strategies/ldap.rbs

    This file is intentionally minimal to avoid duplicating declarations.

    + +

    module OmniAuth
    + module LDAP
    + end
    +end

    diff --git a/docs/file.sasl.html b/docs/file.sasl.html index a015996..f171ef9 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 b5fd981..51d9eee 100644 --- a/docs/file.version.html +++ b/docs/file.version.html @@ -69,7 +69,7 @@ diff --git a/docs/index.html b/docs/index.html index be9533c..6f955ad 100644 --- a/docs/index.html +++ b/docs/index.html @@ -101,7 +101,7 @@

    ๐Ÿ“ OmniAuth LDAP

    -

    Version GitHub tag (latest SemVer) License: MIT Downloads Rank Open Source Helpers CodeCov Test Coverage Coveralls Test Coverage QLTY Test Coverage QLTY Maintainability CI Heads CI Runtime Dependencies @ HEAD CI Current CI Truffle Ruby CI JRuby Deps Locked Deps Unlocked CI Supported CI Legacy CI Unsupported CI Ancient CI Test Coverage CI Style CodeQL Apache SkyWalking Eyes License Compatibility Check

    +

    Version GitHub tag (latest SemVer) License: MIT Downloads Rank Open Source Helpers CodeCov Test Coverage Coveralls Test Coverage CI Heads CI Runtime Dependencies @ HEAD CI Current CI Truffle Ruby CI JRuby Deps Locked Deps Unlocked CI Supported CI Legacy CI Unsupported CI Ancient CI Test Coverage CI Style CodeQL Apache SkyWalking Eyes License Compatibility Check

    if ci_badges.map(&:color).detect { it != "green"} โ˜๏ธ let me know, as I may have missed the discord notification.

    @@ -125,9 +125,17 @@

    ๐ŸŒป Synopsis

    name_proc: proc { |name| name.gsub(/@.*$/, "") }, bind_dn: "default_bind_dn", password: "password", + # Optional timeouts (seconds) + connect_timeout: 3, + read_timeout: 7, 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))' @@ -345,8 +353,38 @@

    Optional Options

    :allow_anonymous - Whether to allow anonymous binding (default: false).
  • :logger - A logger instance for debugging (optional, for internal use).
  • +
  • +:password_policy - When true, the strategy will request the LDAP Password Policy response control (OID 1.3.6.1.4.1.42.2.27.8.5.1) during the user bind. If the server supports it, the adaptor exposes: +
      +
    • +adaptor.last_operation_result โ€” the last Net::LDAP operation result object.
    • +
    • +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 - 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:

    + +
    use OmniAuth::Builder do
    +  provider :ldap,
    +    host: "ldap.example.com",
    +    base: "dc=example,dc=com",
    +    uid: "uid",
    +    bind_dn: "cn=search,dc=example,dc=com",
    +    password: ENV["LDAP_SEARCH_PASSWORD"],
    +    password_policy: true
    +end
    +
    + +

    Note: This is best-effort and compatible with a range of net-ldap versions. If your server supports the control, you can inspect the response via the adaptor instance during/after authentication (for example in a failure handler) to tailor error messages.

    +

    Auth Hash UID vs LDAP :uid (search attribute)

      @@ -457,6 +495,65 @@

      Rails (initializer) example

      Then link users to /auth/ldap in your app (for example, in a Devise sign-in page).

      +

      Use JSON Body

      + +

      This gem is compatible with JSON-encoded POST bodies as well as traditional form-encoded.

      + +
        +
      • Set header Content-Type to application/json.
      • +
      • Send a JSON object containing username and password.
      • +
      • Rails automatically exposes parsed JSON params via env["action_dispatch.request.request_parameters"], which this strategy reads first. In non-Rails Rack apps, ensure you use a JSON parser middleware if you post raw JSON.
      • +
      + +

      Examples

      + +
        +
      • +

        curl (JSON):

        + +
        curl -i \
        +  -X POST \
        +  -H 'Content-Type: application/json' \
        +  -d '{"username":"alice","password":"secret"}' \
        +  http://localhost:3000/auth/ldap
        +
        + +

        The request phase will redirect to /auth/ldap/callback when both fields are present.

        +
      • +
      • +

        curl (form-encoded, still supported):

        + +
        curl -i \
        +  -X POST \
        +  -H 'Content-Type: application/x-www-form-urlencoded' \
        +  --data-urlencode 'username=alice' \
        +  --data-urlencode 'password=secret' \
        +  http://localhost:3000/auth/ldap
        +
        +
      • +
      • +

        Browser (JavaScript fetch):

        + +
        fetch('/auth/ldap', {
        +  method: 'POST',
        +  headers: { 'Content-Type': 'application/json' },
        +  body: JSON.stringify({ username: 'alice', password: 'secret' })
        +}).then(res => {
        +  if (res.redirected) {
        +    window.location = res.url; // typically /auth/ldap/callback
        +  }
        +});
        +
        +
      • +
      + +

      Notes

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

      Using a custom filter

      If you need to restrict authentication to a group or use a more complex lookup, pass :filter. Use %{username} โ€” it will be replaced with the processed username (after :name_proc).

      @@ -624,7 +721,10 @@

      Mounted under a subdirectory ( title: "Acme LDAP", host: "ldap.acme.internal", base: "dc=acme,dc=corp", - uid: "uid" + uid: "uid", + bind_dn: "cn=search,dc=acme,dc=corp", + password: ENV["LDAP_SEARCH_PASSWORD"], + name_proc: proc { |n| n.split("@").first } end

    @@ -785,8 +885,6 @@

    Code Coverage

    Coveralls Test Coverage

    -

    QLTY Test Coverage

    -

    ๐Ÿช‡ Code of Conduct

    Everyone interacting with this projectโ€™s codebases, issue trackers,
    @@ -868,6 +966,9 @@

    , and omniauth-ldap contributors. +
  • + Copyright (C) 2014 David Benko +
  • Copyright (c) 2011 by Ping Yu and Intridea, Inc.
  • @@ -888,7 +989,7 @@

    ๐Ÿค‘ A request for help

    To say โ€œthanks!โ€ โ˜๏ธ Join the Discord or ๐Ÿ‘‡๏ธ send money.

    -

    Sponsor me on GitHub Sponsors ๐Ÿ’Œ Sponsor me on Liberapay ๐Ÿ’Œ Donate on PayPal

    +

    Sponsor me on GitHub Sponsors ๐Ÿ’Œ Sponsor me on Liberapay ๐Ÿ’Œ Donate on PayPal

    Please give the project a star โญ โ™ฅ.

    @@ -897,7 +998,7 @@

    Please give the project a star โญ โ™ฅ diff --git a/docs/method_list.html b/docs/method_list.html index a14fe81..d32ed05 100644 --- a/docs/method_list.html +++ b/docs/method_list.html @@ -119,6 +119,22 @@

    Method List

    +
  • +
    + #last_operation_result + OmniAuth::LDAP::Adaptor +
    +
  • + + +
  • +
    + #last_password_policy_response + OmniAuth::LDAP::Adaptor +
    +
  • + +
  • map_user @@ -136,6 +152,14 @@

    Method List

  • +
    + #password_policy + OmniAuth::LDAP::Adaptor +
    +
  • + + +
  • #request_phase OmniAuth::Strategies::LDAP @@ -143,7 +167,7 @@

    Method List

  • -
  • +
  • #uid OmniAuth::LDAP::Adaptor @@ -151,7 +175,7 @@

    Method List

  • -
  • +
  • validate OmniAuth::LDAP::Adaptor diff --git a/docs/top-level-namespace.html b/docs/top-level-namespace.html index 1df7fce..1181177 100644 --- a/docs/top-level-namespace.html +++ b/docs/top-level-namespace.html @@ -100,7 +100,7 @@

    Defined Under Namespace

    diff --git a/lib/omniauth/strategies/ldap.rb b/lib/omniauth/strategies/ldap.rb index a5e6313..2b01d40 100644 --- a/lib/omniauth/strategies/ldap.rb +++ b/lib/omniauth/strategies/ldap.rb @@ -66,7 +66,7 @@ def request_phase # 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 != "" + if request.post? && request_data["username"].to_s != "" && request_data["password"].to_s != "" return Rack::Response.new([], 302, "Location" => callback_url).finish end @@ -100,12 +100,12 @@ def callback_phase return fail!(:missing_credentials) if missing_credentials? begin - @ldap_user_info = @adaptor.bind_as(filter: filter(@adaptor), size: 1, password: request.params["password"]) + @ldap_user_info = @adaptor.bind_as(filter: filter(@adaptor), size: 1, password: request_data["password"]) unless @ldap_user_info # Attach password policy info to env if available (best-effort) attach_password_policy_env(@adaptor) - return fail!(:invalid_credentials, InvalidCredentialsError.new("Invalid credentials for #{request.params["username"]}")) + return fail!(:invalid_credentials, InvalidCredentialsError.new("Invalid credentials for #{request_data["username"]}")) end # Optionally attach policy info even on success (e.g., timeBeforeExpiration) @@ -121,10 +121,10 @@ def callback_phase def filter(adaptor, username_override = nil) flt = adaptor.filter if flt && !flt.to_s.empty? - username = Net::LDAP::Filter.escape(@options[:name_proc].call(username_override || request.params["username"])) + username = Net::LDAP::Filter.escape(@options[:name_proc].call(username_override || request_data["username"])) Net::LDAP::Filter.construct(flt % {username: username}) else - Net::LDAP::Filter.equals(adaptor.uid, @options[:name_proc].call(username_override || request.params["username"])) + Net::LDAP::Filter.equals(adaptor.uid, @options[:name_proc].call(username_override || request_data["username"])) end end @@ -182,7 +182,11 @@ def valid_request_method? end def missing_credentials? - request.params["username"].nil? || request.params["username"].empty? || request.params["password"].nil? || request.params["password"].empty? + request_data["username"].nil? || request_data["username"].empty? || request_data["password"].nil? || request_data["password"].empty? + end + + def request_data + @env["action_dispatch.request.request_parameters"] || request.params end # Extract a normalized username from a trusted header when enabled. diff --git a/spec/integration/middleware_spec.rb b/spec/integration/middleware_spec.rb index 2ccfc0c..5482e3f 100644 --- a/spec/integration/middleware_spec.rb +++ b/spec/integration/middleware_spec.rb @@ -61,6 +61,55 @@ end end + it "POST /auth/ldap accepts JSON-style credentials via Rails env and sets omniauth.auth" do + begin + OmniAuth.config.test_mode = true + OmniAuth.config.mock_auth[:ldap] = OmniAuth::AuthHash.new(provider: "ldap", uid: "json-bob", info: {"name" => "Bob"}) + + env = { + "CONTENT_TYPE" => "application/json", + "action_dispatch.request.request_parameters" => {"username" => "bob", "password" => "secret"}, + } + post "/auth/ldap", nil, env + + # Follow redirects to callback + max_redirects = 5 + redirects = 0 + while last_response.status == 302 && redirects < max_redirects + follow_redirect! + redirects += 1 + end + + expect(last_response.status).to eq 200 + expect(last_response.body).to include("true") + ensure + OmniAuth.config.mock_auth.delete(:ldap) + OmniAuth.config.test_mode = false + end + end + + it "POST /auth/ldap/callback with JSON missing username and password redirects with missing_credentials" do + env = { + "CONTENT_TYPE" => "application/json", + "action_dispatch.request.request_parameters" => {}, + } + post "/auth/ldap/callback", nil, env + + expect(last_response.status).to eq 302 + expect(last_response.headers["Location"]).to match(/missing_credentials/) + end + + it "POST /auth/ldap/callback with JSON username but missing password redirects with missing_credentials" do + env = { + "CONTENT_TYPE" => "application/json", + "action_dispatch.request.request_parameters" => {"username" => "bob"}, + } + post "/auth/ldap/callback", nil, env + + expect(last_response.status).to eq 302 + expect(last_response.headers["Location"]).to match(/missing_credentials/) + end + it "honors SCRIPT_NAME when mounted under a subdirectory for redirect to callback" do begin OmniAuth.config.test_mode = true diff --git a/spec/omniauth/strategies/ldap_spec.rb b/spec/omniauth/strategies/ldap_spec.rb index 18ed2ca..e081ce8 100644 --- a/spec/omniauth/strategies/ldap_spec.rb +++ b/spec/omniauth/strategies/ldap_spec.rb @@ -87,6 +87,18 @@ def make_env(path = "/auth/ldap", props = {}) expect(last_response.headers["Location"]).to eq "http://example.org/auth/ldap/callback" end + it "redirects to callback when JSON POST includes username and password (Rails env)" do + env = { + "REQUEST_METHOD" => "POST", + "CONTENT_TYPE" => "application/json", + # Rails places parsed JSON params here + "action_dispatch.request.request_parameters" => {"username" => "json_alice", "password" => "json_secret"}, + } + post "/auth/ldap", nil, env + expect(last_response).to be_redirect + expect(last_response.headers["Location"]).to eq "http://example.org/auth/ldap/callback" + end + context "when mounted under a subdirectory" do let(:sub_env) do make_env("/auth/ldap", {