fr3d/ldap-bundle
Symfony bundle that adds LDAP authentication to your app. Integrates with Symfony Security, binds against LDAP servers, maps directory attributes to user objects, and supports configurable connections/domains for directory-backed login.
FR3DLdapBundle is glue between Symfony's security firewall and an LDAP directory: it replaces the credential check with an LDAP bind, then uses your existing user manager (FOSUserBundle, a custom manager, etc.) to find, create, and update the local user record. It is not an LDAP client; the actual protocol work is delegated to a Zend/Laminas LDAP library.
Minimal setup on the versions v4.0.0 targets (PHP 7.1+, Symfony 3.4+):
composer require fr3d/ldap-bundle:^4.0
Run composer show afterwards to confirm which LDAP library came in transitively (zendframework/zend-ldap or laminas/laminas-ldap) and pin that explicitly too.app/AppKernel.php:
new FR3D\LdapBundle\FR3DLdapBundle(),
fr3d_ldap:
driver:
host: ldap.example.com
port: 389
# username/password: only needed if anonymous search is denied
# bindRequiresDn: true
user:
base_dn: ou=people,dc=example,dc=com
filter: (&(objectClass=person)(uid=%s))
attributes:
- { ldap_attr: mail, user_method: setEmail }
- { ldap_attr: cn, user_method: setDisplayName }
fr3d_ldap.user_manager implementing FR3D\LdapBundle\Manager\UserManagerInterface. With FOSUserBundle this is a thin adapter, not an alias, because the interfaces differ:
services:
fr3d_ldap.user_manager:
class: AppBundle\Ldap\FOSUserManagerAdapter
arguments: ['@fos_user.user_manager', '@security.encoder_factory']
namespace AppBundle\Ldap;
use FR3D\LdapBundle\Manager\UserManagerInterface;
use FOS\UserBundle\Model\UserManagerInterface as FOSUserManager;
class FOSUserManagerAdapter implements UserManagerInterface
{
private $users;
public function __construct(FOSUserManager $users)
{
$this->users = $users;
}
public function createUser() { return $this->users->createUser(); }
public function findUserByUsername($name) { return $this->users->findUserByUsername($name); }
public function supportsClass($class) { return $this->users->supportsClass($class); }
public function updateUser($user) { $this->users->updateUser($user); }
}
fr3d_ldap.security.user.provider) and a firewall listener enabled with fr3d_ldap: ~:
security:
providers:
fr3d_ldapbundle:
id: fr3d_ldap.security.user.provider
fos_userbundle:
id: fos_user.user_provider.username
chain_provider:
chain:
providers: [fr3d_ldapbundle, fos_userbundle]
firewalls:
main:
provider: chain_provider
fr3d_ldap: ~
form_login:
login_path: /login
check_path: /login_check
logout: ~
First use case to target: an existing FOSUserBundle application where employees should log in with corporate credentials while local roles and profile data stay in your database. Verify the directory side with ldapsearch -x -H ldap://ldap.example.com -b "ou=people,dc=example,dc=com" "(&(objectClass=person)(uid=jdoe))" before touching Symfony config, then submit the login form and confirm a fos_user row is created or updated.
If you are upgrading a working 3.x install, do that login test on a branch before anything else: the only documented breaking change in 4.0 is in the form validator, so a form_login-only integration should survive, but you want the proof early.
Where to look first: the README's "Full example" section, DependencyInjection/Configuration.php for every available option, Ldap/LdapManager.php for the query/bind logic, Security/Authentication/Provider/LdapAuthenticationProvider.php for the authentication flow, and Manager/UserManagerInterface.php for the persistence contract. When upgrading, read the release notes and the form validator class side by side — that is where the Symfony 3.4 class swap landed.
Upgrading 3.x to 4.0. Treat this as a dependency-floor release, not a feature release. Before bumping, run composer why-not fr3d/ldap-bundle ^4.0 to see exactly which of your pinned PHP or Symfony constraints block it; the failure will be at solve time, and the message is usually clearer than the alternative of unpicking a half-upgraded composer.lock. The one code-level break is the form validator now targeting Symfony 3.4 form classes — if you extended, decorated, or overrode it, re-test that path specifically. If you only use the firewall listener with form_login, the config is unchanged: bump, clear the cache, log in.
Adapter over your user manager. The UserManagerInterface implementation is the only place your domain code should live. Keep it thin: delegate create/find/update, and put profile enrichment elsewhere. This is also the seam that lets you swap FOSUserBundle for a custom entity later.
Chain provider for hybrid account populations. Put the LDAP provider and your local provider behind a chain provider so service accounts, break-glass admins, and pre-provisioned local users still work. Be aware that ordering affects which authentication provider is consulted first: a user who exists in both LDAP and the local database will be authenticated against LDAP unless you deliberately restrict the firewall or listener.
Attribute mapping as a sync contract. Use user.attributes only for identity/profile fields where LDAP is authoritative (email, display name, employee ID). Roles, preferences, and application state should stay local. Guard setters, since a missing LDAP attribute can be passed as null, and a multi-valued attribute arrives as an array.
Active Directory specifics. AD almost always needs filter: (&(objectClass=user)(sAMAccountName=%s)), username_attribute: sAMAccountName, and a service-account bind for the initial search (driver.username / driver.password, plus bindRequiresDn: true if you must bind by DN). For OpenLDAP the common shape is (uid=%s) with bindRequiresDn: false. Keep these differences in environment-specific config, not in code.
Roles from LDAP. There is no built-in group-to-role mapping. The pragmatic pattern is to map memberOf to a single user field (a custom setLdapGroups method, or a custom attribute mapping that stores the array), then compute roles in getRoles() or in a security event subscriber after login. Avoid calling LDAP again on every request.
Testing. Mock FR3D\LdapBundle\Driver\LdapDriverInterface for unit tests of the manager and the authentication provider. For integration tests, run osixia/openldap in Docker, load an LDIF fixture, and point fr3d_ldap.driver.host at it via a test-specific config. Keep those tests in a separate suite so CI without a directory still passes. Pin the PHP/Symfony matrix explicitly now that 4.0 has a hard floor — a matrix entry on Symfony 3.3 will fail to install the bundle rather than fail a test.
Local development. Run OpenLDAP locally and seed a couple of users. Use ldapsearch and ldapwhoami to prove the filter and bind work, and only then exercise the firewall. This turns "Invalid credentials" from a Symfony debugging problem into a directory debugging problem.
Migration path. v4.0.0 modernizes the dependency floor but still does not reach Symfony 4+; the bundle's authentication-provider wiring no longer exists there. The native replacement is Symfony's ldap user provider with form_login_ldap, or a custom Authenticator using Laminas\Ldap. Landing on 3.4 LTS with 4.0 is the cleanest springboard for that rewrite — your adapter's find/create/update logic and your attribute-mapping decisions transfer directly; only the security wiring is replaced.
^3.0 explicitly. A floating constraint will attempt 4.0, fail on the PHP or Symfony floor, and surface as a confusing install error rather than an obvious version mismatch. Pin bundle, LDAP library, and PHP version together in CI.%s is interpolated before you trust it. If the username is sprintf'd straight into the filter without Ldap\Filter::escape() or ldap_escape(), characters like *, ), and \ become an LDAP injection surface. Test with hostile usernames, and override LdapManager if needed.findUserByUsername returns null after a successful bind, check casing and whitespace on the username before assuming the directory query is wrong.null; memberOf and similar attributes arrive as arrays. Both cases need explicit handling in your entity or adapter.LdapException, log the underlying message in dev.log, and reproduce the exact query with ldapsearch.ldaps:// on 636 or useStartTls: true; certificate validation issues surface as generic connection failures. Keep directory service credentials out of VCS via %env()% parameters.fr3d_ldap.ldap_driver for a custom LdapDriverInterface, decorate fr3d_ldap.ldap_manager, or override the fr3d_ldap.security.user.provider and authentication provider service definitions. A compiler pass is the cleanest way to adjust service definitions without forking the bundle's config.How can I help you explore Laravel packages today?