Skip to content

omniauth/omniauth-ldap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
πŸ“ NOTE
RubyGems (the GitHub org, not the website) suffered a hostile takeover in September 2025.
Ultimately 4 maintainers were hard removed and a reason has been given for only 1 of those, while 2 others resigned in protest.
It is a complicated story which is difficult to parse quickly.
I'm adding notes like this to gems because I don't condone theft of repositories or gems from their rightful owners.
If a similar theft happened with my repos/gems, I'd hope some would stand up for me.
Disenfranchised former-maintainers have started gem.coop.
Once available I will publish there exclusively; unless RubyCentral makes amends with the community.
The "Technology for Humans: Joel Draper" podcast episode by reinteractive is the most cogent summary I'm aware of.
See here, here and here for more info on what comes next.
What I'm doing: A (WIP) proposal for bundler/gem scopes, and a (WIP) proposal for a federated gem server.

Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0 ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5 omniauth Logo (presumed to be) by tomeara, (presumed to be) MIT License

πŸ“ OmniAuth LDAP

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.


if ci_badges.map(&:color).all? { it == "green"} πŸ‘‡οΈ send money so I can do more of this. FLOSS maintenance is now my full-time job.

Sponsor Me on Github Liberapay Goal Progress Donate on PayPal Buy me a coffee Donate on Polar Donate at ko-fi.com

🌻 Synopsis

Use the LDAP strategy as a middleware in your application:

use OmniAuth::Strategies::LDAP,
  title: "My LDAP",
  host: "10.101.10.1",
  port: 389,
  encryption: :plain,
  base: "dc=intridea,dc=com",
  uid: "sAMAccountName",
  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))'

All of the listed options are required, with the exception of :title, :name_proc, :bind_dn, and :password.

TLS certificate verification

This gem enables TLS certificate verification by default when you use encryption: "ssl" (LDAPS / simple TLS) or encryption: "tls" (STARTTLS). We always pass tls_options to Net::LDAP based on OpenSSL::SSL::SSLContext::DEFAULT_PARAMS, which includes verify_mode: OpenSSL::SSL::VERIFY_PEER and sane defaults.

  • Secure by default: you do not need to set anything extra to verify the LDAP server certificate.
  • To customize trust or ciphers, supply your own tls_options, which are merged over the safe defaults.
  • If you truly need to skip verification (not recommended), set disable_verify_certificates: true.

Examples:

# Verify server certs (default behavior)
use OmniAuth::Strategies::LDAP,
  host: ENV["LDAP_HOST"],
  port: 636,
  encryption: "ssl",  # or "tls"
  base: "dc=example,dc=com",
  uid:  "uid"

# Use a private CA bundle and restrict protocol/ciphers
use OmniAuth::Strategies::LDAP,
  host: ENV["LDAP_HOST"],
  port: 636,
  encryption: "ssl",
  base: "dc=example,dc=com",
  uid:  "uid",
  tls_options: {
    ca_file: "/etc/ssl/private/my_org_ca.pem",
    ssl_version: "TLSv1_2",
    ciphers: ["TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"],
  }

# Opt out of verification (NOT recommended – use only in trusted test/dev scenarios)
use OmniAuth::Strategies::LDAP,
  host: ENV["LDAP_HOST"],
  port: 636,
  encryption: "ssl",
  base: "dc=example,dc=com",
  uid:  "uid",
  disable_verify_certificates: true

Note: Net::LDAP historically defaulted to no certificate validation when tls_options were not provided. This library mitigates that by always providing secure tls_options unless you explicitly disable verification.

πŸ’‘ Info you can shake a stick at

Tokens to Remember Gem name Gem namespace
Works with JRuby JRuby 9.1 Compat JRuby 9.2 Compat JRuby 9.3 Compat
JRuby 9.4 Compat JRuby 10.0 Compat JRuby HEAD Compat
Works with Truffle Ruby Truffle Ruby 22.3 Compat Truffle Ruby 23.0 Compat
Truffle Ruby 23.1 Compat Truffle Ruby 24.1 Compat
Works with MRI Ruby 3 Ruby 3.0 Compat Ruby 3.1 Compat Ruby 3.2 Compat Ruby 3.3 Compat Ruby 3.4 Compat Ruby HEAD Compat
Works with MRI Ruby 2 Ruby 2.0 Compat Ruby 2.1 Compat Ruby 2.2 Compat
Ruby 2.3 Compat Ruby 2.4 Compat Ruby 2.5 Compat Ruby 2.6 Compat Ruby 2.7 Compat
Support & Community Join Me on Daily.dev's RubyFriends Live Chat on Discord Get help from me on Upwork Get help from me on Codementor
Source Source on Github.com The best SHA: dQw4w9WgXcQ!
Documentation Current release on RubyDoc.info YARD on Galtzo.com Maintainer Blog GitHub Wiki
Compliance License: MIT Compatible with Apache Software Projects: Verified by SkyWalking Eyes πŸ“„ilo-declaration-img Security Policy Contributor Covenant 2.1 SemVer 2.0.0
Style Enforced Code Style Linter Keep-A-Changelog 1.0.0 Gitmoji Commits Compatibility appraised by: appraisal2
Maintainer πŸŽ–οΈ Follow Me on LinkedIn Follow Me on Ruby.Social Follow Me on Bluesky Contact Maintainer My technical writing
... πŸ’– Find Me on WellFound: Find Me on CrunchBase My LinkTree More About Me 🧊 πŸ™ πŸ›– πŸ§ͺ

Compatibility

Compatible with MRI Ruby 2.0+, and concordant releases of JRuby, and TruffleRuby.

🚚 Amazing test matrix was brought to you by πŸ”Ž appraisal2 πŸ”Ž and the color πŸ’š green πŸ’š
πŸ‘Ÿ Check it out! ✨ github.com/appraisal-rb/appraisal2 ✨

Enterprise Support Tidelift

Available as part of the Tidelift Subscription.

Need enterprise-level guarantees?

The maintainers of this and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use.

Get help from me on Tidelift

  • πŸ’‘Subscribe for support guarantees covering all your FLOSS dependencies
  • πŸ’‘Tidelift is part of Sonar
  • πŸ’‘Tidelift pays maintainers to maintain the software you depend on!
    πŸ“Š@Pointy Haired Boss: An enterprise support subscription is "never gonna let you down", and supports open source maintainers

Alternatively:

  • Live Chat on Discord
  • Get help from me on Upwork
  • Get help from me on Codementor

✨ Installation

Install the gem and add to the application's Gemfile by executing:

bundle add omniauth-ldap

If bundler is not being used to manage dependencies, install the gem by executing:

gem install omniauth-ldap

πŸ”’ Secure Installation

For Medium or High Security Installations

This gem is cryptographically signed, and has verifiable SHA-256 and SHA-512 checksums by stone_checksums. Be sure the gem you install hasn’t been tampered with by following the instructions below.

Add my public key (if you haven’t already, expires 2045-04-29) as a trusted certificate:

gem cert --add <(curl -Ls https://raw.github.com/galtzo-floss/certs/main/pboling.pem)

You only need to do that once. Then proceed to install with:

gem install omniauth-ldap -P HighSecurity

The HighSecurity trust profile will verify signed gems, and not allow the installation of unsigned dependencies.

If you want to up your security game full-time:

bundle config set --global trust-policy MediumSecurity

MediumSecurity instead of HighSecurity is necessary if not all the gems you use are signed.

NOTE: Be prepared to track down certs for signed gems and add them the same way you added mine.

βš™οΈ Configuration

The following options are available for configuring the OmniAuth LDAP strategy:

Required Options

  • :host - The hostname or IP address of the LDAP server.
  • :port - The port number of the LDAP server (default: 389).
  • :method - The connection method. Allowed values: :plain, :ssl, :tls (default: :plain).
  • :base - The base DN for the LDAP search.
  • :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.

Optional Options

  • :title - The title for the authentication form (default: "LDAP Authentication").
  • :bind_dn - The DN to bind with for searching users (required if anonymous access is not allowed).
  • :password - The password for the bind DN.
  • :name_proc - A proc to process the username before using it in the search (default: identity proc that returns the username unchanged).
  • :try_sasl - Whether to use SASL authentication (default: false).
  • :sasl_mechanisms - Array of SASL mechanisms to use (e.g., ["DIGEST-MD5", "GSS-SPNEGO"]).
  • :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)

  • By design, the top-level auth.uid returned by this strategy is the entry's Distinguished Name (DN).
  • 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.
  • 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.

Why DN for auth.uid?

  • 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).
  • 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.
  • This trade-off favors cross-directory interoperability and stability for apps that need a unique identifier.

Where to find the "username"-style value

  • auth.info.nickname maps from the first present of: uid, userid, or sAMAccountName.
  • You can also read the raw attribute from auth.extra.raw_info (a Net::LDAP::Entry):
get "/auth/ldap/callback" do
  auth = request.env["omniauth.auth"]
  dn = auth.uid                                # => "cn=alice,ou=users,dc=example,dc=com"
  username = auth.info.nickname                # => "alice" (from uid/sAMAccountName)
  # Or, directly from raw_info (case-insensitive keys):
  sams = auth.extra.raw_info[:samaccountname]
  sam = sams.first if sams
  # ...
end

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.

πŸ”§ Basic Usage

The strategy exposes a simple Rack middleware and can be used in plain Rack apps, Sinatra, or Rails. Direct users to /auth/ldap to start authentication and handle the callback at /auth/ldap/callback.

Below are several concrete examples to get you started.

Minimal Rack setup

# config.ru
require "rack"
require "omniauth-ldap"

use Rack::Session::Cookie, secret: "change_me"
use OmniAuth::Builder do
  provider :ldap,
    host: "ldap.example.com",
    port: 389,
    method: :plain,
    base: "dc=example,dc=com",
    uid: "uid",
    title: "Example LDAP"
end

run lambda { |env| [404, {"Content-Type" => "text/plain"}, [env.key?("omniauth.auth").to_s]] }

Visit GET /auth/ldap to initiate authentication (the middleware will render a login form unless you POST to /auth/ldap).

Sinatra example

require "sinatra"
require "omniauth-ldap"

use Rack::Session::Cookie, secret: "change_me"
use OmniAuth::Builder do
  provider :ldap,
    title: "Company LDAP",
    host: "ldap.company.internal",
    base: "dc=company,dc=local",
    uid: "sAMAccountName",
    name_proc: proc { |username| username.gsub(/@.*$/, "") }
end

get "/" do
  '<a href="/auth/ldap">Sign in with LDAP</a>'
end

get "/auth/ldap/callback" do
  auth = request.env["omniauth.auth"]
  "Hello, #{auth.info["name"]}"
end

Rails (initializer) example

Create config/initializers/omniauth.rb:

Rails.application.config.middleware.use(OmniAuth::Builder) do
  provider :ldap,
    title: "Acme LDAP",
    host: "ldap.acme.internal",
    port: 389,
    base: "dc=acme,dc=corp",
    uid: "uid",
    bind_dn: "cn=search,dc=acme,dc=corp",
    password: ENV["LDAP_SEARCH_PASSWORD"],
    name_proc: proc { |n| n.split("@").first }
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):

    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).

provider :ldap,
  host: "ldap.example.com",
  base: "dc=example,dc=com",
  filter: "(&(uid=%{username})(memberOf=cn=myapp-users,ou=groups,dc=example,dc=com))",
  bind_dn: "cn=search,dc=example,dc=com",
  password: ENV["LDAP_SEARCH_PASSWORD"]

What :filter actually does

  • If :filter is provided, the strategy constructs an LDAP filter string by substituting %{username} with the submitted username after applying :name_proc, escaping special characters per RFC 4515, and passes it to the directory search.
  • In the normal password flow, a successful search returns the user's DN and we then bind as that DN with the submitted password.
  • In trusted header SSO flow (header_auth: true), we only perform the search and skip the user password bind; if the search returns no entry, authentication fails.
  • If :filter is not provided, the strategy falls back to a simple equality filter using :uid (e.g. (uid=alice)).

Notes on escaping and safety

  • We escape the interpolated username with Net::LDAP::Filter.escape, which protects against LDAP injection and handles special characters like (, ), *, and \.
  • Your static filter text is used as-is β€” keep it to a valid LDAP filter expression and only use %{username} for substitution.

Group-based recipes

  • Active Directory (simple group):

    (&(sAMAccountName=%{username})(memberOf=cn=myapp-users,ou=groups,dc=example,dc=com))
    
  • Active Directory (nested groups via matchingRuleInChain):

    (&(sAMAccountName=%{username})(memberOf:1.2.840.113556.1.4.1941:=cn=myapp-users,ou=groups,dc=example,dc=com))
    
  • OpenLDAP (groupOfNames):

    (&(uid=%{username})(memberOf=cn=myapp-users,ou=groups,dc=example,dc=com))
    

    or, if you can't use memberOf overlays, filter on the group and member DN:

    (&(uid=%{username})(|(uniqueMember=uid=%{username},ou=people,dc=example,dc=com)(member=uid=%{username},ou=people,dc=example,dc=com)))
    

Username normalization examples

  • If your users sign in with an email but the directory expects a short name, combine :name_proc with :filter:

    provider :ldap,
      name_proc: proc { |n| n.split("@").first },
      filter: "(&(sAMAccountName=%{username})(memberOf=cn=myapp-users,ou=groups,dc=example,dc=com))"
      # other settings...

Discourse plugin (jonmbake/discourse-ldap-auth)

  • That plugin forwards its filter setting to this gem. You can therefore paste the same filter strings shown above.

  • Example (allow only members of forum-users):

    (&(uid=%{username})(memberOf=cn=forum-users,ou=groups,dc=example,dc=com))
    
  • If users type an email address but your directory matches on a short user id, also configure name_proc accordingly in your app (or the plugin, if supported).

SASL (advanced)

SASL enables alternative bind mechanisms. Only enable if you understand the server-side requirements.

provider :ldap,
  host: "ldap.example.com",
  base: "dc=example,dc=com",
  try_sasl: true,
  sasl_mechanisms: ["DIGEST-MD5"],
  uid: "uid"

Supported mechanisms include "DIGEST-MD5" and "GSS-SPNEGO" depending on your environment and gems.

Name processing and examples

If users log in with an email but LDAP expects a short username, use :name_proc to normalize the submitted value:

provider :ldap,
  host: "ldap.example.com",
  base: "dc=example,dc=com",
  uid: "sAMAccountName",
  name_proc: proc { |name| name.gsub(/@.*$/, "") }

This trims alice@example.com to alice before searching.

Mounted under a subdirectory (SCRIPT_NAME)

If your app is served from a path prefix (for example, behind a reverse proxy at /myapp, or mounted via Rack::URLMap, or Rails relative_url_root), the OmniAuth callback must include that subdirectory. This strategy uses callback_url for the form action and redirects, so it automatically includes any SCRIPT_NAME set by Rack/Rails. In other words, you typically do not need any special configuration beyond ensuring SCRIPT_NAME is correct in the request environment.

  • Works out-of-the-box when:
    • You mount the app at a path using Rack’s map/URLMap.
    • You set Rails’ config.relative_url_root (or RAILS_RELATIVE_URL_ROOT) or deploy under a prefix with a reverse proxy that sets SCRIPT_NAME.

Rack example (mounted at /myapp):

# config.ru
require "rack"
require "omniauth-ldap"

app = Rack::Builder.new do
  use(Rack::Session::Cookie, secret: "change_me")
  use(OmniAuth::Builder) do
    provider(
      :ldap,
      host: "ldap.example.com",
      base: "dc=example,dc=com",
      uid: "uid",
      title: "Example LDAP",
    )
  end

  run(->(env) { [404, {"Content-Type" => "text/plain"}, [env.key?("omniauth.auth").to_s]] })
end

run Rack::URLMap.new(
  "/myapp" => app,
)
  • Visiting POST /myapp/auth/ldap renders the login form with action='http://host/myapp/auth/ldap/callback'.
  • Any redirects (including header-based SSO fast path) will also point to http://host/myapp/auth/ldap/callback.

Rails example (relative_url_root):

# config/environments/production.rb (or an initializer)
Rails.application.configure do
  config.relative_url_root = "/myapp"  # or set ENV["RAILS_RELATIVE_URL_ROOT"]
end

# config/initializers/omniauth.rb
Rails.application.config.middleware.use(OmniAuth::Builder) do
  provider :ldap,
    title: "Acme LDAP",
    host: "ldap.acme.internal",
    base: "dc=acme,dc=corp",
    uid: "uid",
    bind_dn: "cn=search,dc=acme,dc=corp",
    password: ENV["LDAP_SEARCH_PASSWORD"],
    name_proc: proc { |n| n.split("@").first }
end
  • With relative_url_root set, Rails/Rack provide SCRIPT_NAME=/myapp, and this strategy will issue a form with action='.../myapp/auth/ldap/callback' and redirect accordingly.

Behind proxies with unusual host/proto handling (optional):

OmniAuth usually derives the correct scheme/host/prefix from Rack (and standard X-Forwarded-* headers). If your environment produces incorrect absolute URLs, you can override the computed host and prefix by setting OmniAuth.config.full_host:

OmniAuth.config.full_host = lambda do |env|
  scheme = (env["HTTP_X_FORWARDED_PROTO"] || env["rack.url_scheme"]).to_s.split(",").first
  host = env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"] || [env["SERVER_NAME"], env["SERVER_PORT"]].compact.join(":")
  script = env["SCRIPT_NAME"].to_s
  "#{scheme}://#{host}#{script}"
end

Note: You generally do not need this override. Prefer configuring your proxy to pass standard X-Forwarded-Proto and X-Forwarded-Host headers and let Rack/OmniAuth compute the full URL.

  • Header-based SSO (header_auth: true) also respects SCRIPT_NAME; when a trusted header is present on POST /myapp/auth/ldap, the strategy redirects to http://host/myapp/auth/ldap/callback.

Trusted header SSO (REMOTE_USER and friends)

Some deployments terminate SSO at a reverse proxy or portal and forward the already-authenticated user identity via an HTTP header such as REMOTE_USER. When you enable this mode, the LDAP strategy will trust the upstream header, perform a directory lookup for that user, and complete OmniAuth without asking the user for a password.

Important: Only enable this behind a trusted front-end that strips and sets the header itself. Never enable on a public endpoint without such a gateway, or an attacker could spoof the header.

Configuration options:

  • :header_auth (Boolean, default: false) β€” Enable trusted header SSO.
  • :header_name (String, default: "REMOTE_USER") β€” The env/header key to read. The strategy checks both env["REMOTE_USER"] and the Rack variant env["HTTP_REMOTE_USER"].
  • :name_proc is applied to the header value before search (e.g., to strip a domain part).
  • Search is done using your configured :uid or :filter and the service bind (:bind_dn/:password) or anonymous bind if allowed.

Minimal Rack example:

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"],
    header_auth: true,                 # trust REMOTE_USER
    header_name: "REMOTE_USER",       # default
    name_proc: proc { |n| n.split("@").first }
end

Rails initializer example:

Rails.application.config.middleware.use(OmniAuth::Builder) do
  provider :ldap,
    title: "Acme LDAP",
    host: "ldap.acme.internal",
    base: "dc=acme,dc=corp",
    uid: "sAMAccountName",
    bind_dn: "cn=search,dc=acme,dc=corp",
    password: ENV["LDAP_SEARCH_PASSWORD"],
    header_auth: true,
    header_name: "REMOTE_USER",
    # Optionally restrict with a group filter while using the header value
    filter: "(&(sAMAccountName=%{username})(memberOf=cn=myapp-users,ou=groups,dc=acme,dc=corp))",
    name_proc: proc { |n| n.gsub(/@.*$/, "") }
end

Flow:

  • If header_auth is on and the header is present when the request hits /auth/ldap, the strategy immediately redirects to /auth/ldap/callback.
  • In the callback, the strategy searches the directory for that user and maps their attributes; no user password bind is attempted.
  • If the header is missing (or header_auth is false), the normal username/password form flow is used.

Security checklist:

  • Ensure your reverse proxy strips user-controlled copies of the header and sets the canonical REMOTE_USER itself.
  • Prefer TLS-secured internal links between the proxy and your app.
  • Consider also restricting with a group-based :filter so only authorized users can sign in.

🦷 FLOSS Funding

While these tools are free software and will always be, the project would benefit immensely from some funding. Raising a monthly budget of... "dollars" would make the project more sustainable.

We welcome both individual and corporate sponsors! We also offer a wide array of funding channels to account for your preferences. Currently, GitHub Sponsors, and Liberapay are our preferred funding platforms.

If you're working in a company that's making significant use of omniauth tools we'd appreciate it if you suggest to your company to become a omniauth sponsor.

You can support me in development of OmniAuth tools via GitHub Sponsors, Liberapay, PayPal, and Tidelift.

πŸ“ NOTE
If doing a sponsorship in the form of donation is problematic for your company
from an accounting standpoint, we'd recommend the use of Tidelift,
where you can get a support-like subscription instead.

Another way to support open-source

I’m driven by a passion to foster a thriving open-source community – a space where people can tackle complex problems, no matter how small. Revitalizing libraries that have fallen into disrepair, and building new libraries focused on solving real-world challenges, are my passions. I was recently affected by layoffs, and the tech jobs market is unwelcoming. I’m reaching out here because your support would significantly aid my efforts to provide for my family, and my farm (11 πŸ” chickens, 2 🐢 dogs, 3 🐰 rabbits, 8 πŸˆβ€ cats).

If you work at a company that uses my work, please encourage them to support me as a corporate sponsor. My work on gems you use might show up in bundle fund.

I’m developing a new library, floss_funding, designed to empower open-source developers like myself to get paid for the work we do, in a sustainable way. Please give it a look.

Floss-Funding.dev: πŸ‘‰οΈ No network calls. πŸ‘‰οΈ No tracking. πŸ‘‰οΈ No oversight. πŸ‘‰οΈ Minimal crypto hashing. πŸ’‘ Easily disabled nags

Sponsor Me on Github Liberapay Goal Progress Donate on PayPal Buy me a coffee Donate on Polar Donate to my FLOSS or refugee efforts at ko-fi.com Donate to my FLOSS or refugee efforts using Patreon

πŸ” Security

See SECURITY.md.

🀝 Contributing

If you need some ideas of where to help, you could work on adding more code coverage, or if it is already πŸ’― (see below) check reek, issues, or PRs, or use the gem and think about how it could be better.

We Keep A Changelog so if you make changes, remember to update it.

See CONTRIBUTING.md for more detailed instructions.

πŸš€ Release Instructions

See CONTRIBUTING.md.

Code Coverage

Coverage Graph

Coveralls Test Coverage

πŸͺ‡ Code of Conduct

Everyone interacting with this project's codebases, issue trackers, chat rooms and mailing lists agrees to follow the Contributor Covenant 2.1.

🌈 Contributors

Contributors

Made with contributors-img.

⭐️ Star History Star History Chart

πŸ“Œ Versioning

This Library adheres to Semantic Versioning 2.0.0. Violations of this scheme should be reported as bugs. Specifically, if a minor or patch version is released that breaks backward compatibility, a new version should be immediately released that restores compatibility. Breaking changes to the public API will only be introduced with new major versions.

dropping support for a platform is both obviously and objectively a breaking change
β€”Jordan Harband (@ljharb, maintainer of SemVer) in SemVer issue 716

I understand that policy doesn't work universally ("exceptions to every rule!"), but it is the policy here. As such, in many cases it is good to specify a dependency on this library using the Pessimistic Version Constraint with two digits of precision.

For example:

spec.add_dependency("omniauth-ldap", "~> 1.0")
πŸ“Œ Is "Platform Support" part of the public API? More details inside.

SemVer should, IMO, but doesn't explicitly, say that dropping support for specific Platforms is a breaking change to an API. It is obvious to many, but not all, and since the spec is silent, the bike shedding is endless.

To get a better understanding of how SemVer is intended to work over a project's lifetime, read this article from the creator of SemVer:

See CHANGELOG.md for a list of releases.

πŸ“„ License

The gem is available as open source under the terms of the MIT License License: MIT. See LICENSE.txt for the official Copyright Notice.

Β© Copyright

  • Copyright (c) 2025 Peter H.Β Boling, of Galtzo.com Galtzo.com Logo (Wordless) by Aboling0, CC BY-SA 4.0 , and omniauth-ldap contributors.
  • Copyright (c) 2014 David Benko
  • Copyright (c) 2011 by Ping Yu and Intridea, Inc.

πŸ€‘ A request for help

Maintainers have teeth and need to pay their dentists. After getting laid off in an RIF in March, and encountering difficulty finding a new one, I began spending most of my time building open source tools. I'm hoping to be able to pay for my kids' health insurance this month, so if you value the work I am doing, I need your support. Please consider sponsoring me or the project.

To join the community or get help πŸ‘‡οΈ Join the Discord.

Live Chat on Discord

To say "thanks!" ☝️ Join the Discord or πŸ‘‡οΈ send money.

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

Please give the project a star ⭐ β™₯.

Thanks for RTFM. ☺️