Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down Expand Up @@ -708,6 +760,9 @@ See [LICENSE.txt][📄license] for the official [Copyright Notice][📄copyright
</picture>
</a>, and omniauth-ldap contributors.
</li>
<li>
Copyright (C) 2014 David Benko
</li>
<li>
Copyright (c) 2011 by Ping Yu and Intridea, Inc.
</li>
Expand All @@ -724,7 +779,7 @@ Please consider sponsoring me or the project.

To join the community or get help 👇️ Join the Discord.

[![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite]
[![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord]

To say "thanks!" ☝️ Join the Discord or 👇️ send money.

Expand Down
16 changes: 10 additions & 6 deletions lib/omniauth/strategies/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down
49 changes: 49 additions & 0 deletions spec/integration/middleware_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions spec/omniauth/strategies/ldap_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down
Loading