Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Eloquent Ldap Laravel Package

sroutier/eloquent-ldap

Bring Eloquent-style syntax to LDAP in Laravel: query and authenticate against directory servers like Active Directory or OpenLDAP using familiar model methods, so user lookups feel like any other database query.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register

composer require sroutier/eloquent-ldap
php artisan vendor:publish --provider="Sroutier\EloquentLDAP\EloquentLDAPServiceProvider"

If you're not on package auto-discovery, add the service provider to config/app.php. Before anything else, check the composer.json constraints in vendor/sroutier/eloquent-ldap against your Laravel version — this package is small (20 stars, 0 public dependents) and was built for the Laravel 5.x era. Confirm it installs and boots before writing code against it.

Configure config/ldap.php

The first place to look after publishing is the config file — this is where 90% of your day-to-day debugging will happen. Fill in:

  • host / port — use 636 or STARTTLS on 389, never send credentials in cleartext
  • base_dn and the user search filter (e.g. (&(objectClass=user)(sAMAccountName=...)) for AD, uid for OpenLDAP)
  • A dedicated bind/service account DN and password for searching, separate from user auth
  • domain / account suffix for AD ([email protected] vs CORP\user)
  • Attribute mappings: username attribute, email attribute, display name

Prepare the users table

The package mirrors LDAP users into your local users table. Ensure password is nullable (LDAP users have no local hash), and that your identifier column (often username or email) is unique and matches whatever attribute LDAP returns. Add domain/source columns if you want to distinguish local vs directory users later.

Point auth at the package

Easiest path: use the package's model in config/auth.php:

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model'  => Sroutier\EloquentLDAP\Models\User::class, // or your App\User with the trait
    ],
],

First use case — and the one that justifies the package

Keep a break-glass local admin account that works when the directory is unreachable, and let everyone else authenticate against AD. In tinker:

Auth::attempt(['username' => 'jdoe', 'password' => 'secret']);

This should hit the local DB first, miss, bind to LDAP, and (depending on config) create the local mirror row. Verify the row is created with a null/blank password and the right email.

Implementation Patterns

Local-first fallback is the whole feature — lean into it

Don't fight the order. The local users table is your operational safety net:

  • Seed 1–2 local admin accounts so you can recover during an LDAP outage or a misconfigured bind DN.
  • Local accounts keep normal Hash::make() passwords; LDAP accounts keep password = null.
  • Never let LDAP overwrite a local admin's email/name on login — gate the mirror-write so it only updates rows flagged as directory-sourced.

Just-in-time provisioning with role mapping

On each successful LDAP bind, the package creates/updates the local user. Extend that in your model (or an event listener on user retrieval) to translate memberOf groups into local roles:

// In your User model or a login listener
protected function syncRolesFromLdap(array $attributes)
{
    $groups = $attributes['memberOf'] ?? [];
    $this->roles()->sync(
        collect($groups)->map(fn ($dn) => $this->roleForGroup($dn))->filter()->all()
    );
}

Keep a mapping array of LDAP group DN → local role name in a config file. This is the single most valuable thing you'll build on top of the package, because raw LDAP group DNs are unusable in Gate/can: checks.

Separate guards for different populations

If some users are local-only (service accounts, contractors on a separate auth) and others go through LDAP, define a second provider/guard:

'guards' => [
    'web'    => ['driver' => 'session', 'provider' => 'ldap_users'],
    'local'  => ['driver' => 'session', 'provider' => 'users'],
],

Then auth:web for the SSO-facing routes and auth:local for internal tooling.

Session/API split

Auth::attempt() is for web sessions. For API tokens, authenticate first, then issue a token:

if (Auth::attempt($credentials)) {
    $token = auth()->user()->createToken('api')->plainTextToken;
}

Don't try to use the LDAP fallback as a token issuer — it's a credential check, nothing more.

Testing

Don't hit a real directory in tests. Either:

  • extract the attempt into a small service (LdapAuthenticator) with a fake driver, or
  • if the package exposes a fake/in-memory adapter, bind it in the test container.

At minimum, test both branches: local-hit short-circuits LDAP, and LDAP-hit provisions the row. A test that asserts "LDAP unreachable → local admin still logs in" will save you at 2am.

Gotchas and Tips

GPL-3.0 is a real consideration, not a footnote. This package is GPL-3.0 while Laravel apps are commonly proprietary. If you're shipping a closed-source product, get a legal read before bundling it — GPL copyleft can be interpreted to apply to the combined work. This is the single biggest reason to consider reimplementing the thin fallback logic yourself (it's genuinely ~200 lines around Auth::attempt) rather than depending on the package.

Maintenance risk is high. 20 stars, 0 dependents, built several Laravel generations ago. Expect:

  • illuminate/* version conflicts on Laravel 9/10/11
  • PHP 8 type/deprecation warnings
  • An unmaintained service provider that may not auto-discover

Mitigation: pin the exact version, fork it into your own repo and maintain it, or budget a day to rewrite the fallback inside your app. Given how thin the functionality is, a fork is often cheaper than fighting compatibility.

LDAP injection. The username from your form goes into a search filter. Verify the package escapes special characters (*, (, ), \, NUL) before interpolation. If it doesn't, escape with ldap_escape($username, '', LDAP_ESCAPE_FILTER) in your own layer. A * in the username field should never widen the search.

Anonymous / empty bind false positives. Some LDAP servers accept an anonymous bind as "success." If you have a blank service-account password in config/ldap.php, you may see every login succeed. Validate that the bind DN is set and the bind returns an actual user entry, not just a successful connection.

AD specifics:

  • sAMAccountName vs userPrincipalName — pick deliberately; they differ for migrated accounts.
  • Referrals must often be disabled (LDAP_OPT_REFERRALS false) or searches hang at the domain root.
  • Nested groups aren't returned by memberOf — you'll need LDAP_MATCHING_RULE_IN_CHAIN (1.2.840.113556.1.4.1941) for full membership resolution.
  • Locked/disabled/expired accounts may still bind. Check userAccountControl (bit 0x2 = disabled) in the search result before treating the login as valid.

Password resets will break for LDAP users. Laravel's default ForgotPassword flow hashes into the local DB, which does nothing for a user whose password lives in AD. Disable password-reset routes for directory users, or integrate ldappasswd / AD's unicodePwd (requires LDAPS). The package does not do password sync in either direction.

"Wrong password" and "LDAP is down" return the same false. Log the distinction yourself — wrap the attempt or hook the bind failure — otherwise you'll spend hours debugging a "bad credentials" report that's actually a firewall change. Set a short LDAP_OPT_NETWORK_TIMEOUT so an unreachable directory doesn't hang requests for 30+ seconds.

Connection cost. Each Auth::attempt() on the LDAP path is a full connect + bind + search. On an internal app that's fine; on a high-traffic login endpoint, cache group memberships per user (5–15 min TTL) rather than re-querying memberOf every request.

Extension points worth using: the user model (for attribute mapping and role sync), the config file (for filters and attribute names), and the service provider binding (for swapping the connection/lookup class). Avoid patching vendor/ directly — fork or subclass.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager