wiser/ldap-user-provider-bundle
A Symfony bundle that supplies an LDAP user provider so applications can authenticate against directory servers and load user records from LDAP. It reduces custom integration code by wiring user lookup into Symfony's security layer with minimal configuration.
wiser/ldap-user-provider-bundle is a Symfony 6.4 bundle that plugs LDAP (OpenLDAP, Active Directory, FreeIPA) into the Security component as a user provider. If you're coming from Laravel, think of it as the equivalent of a custom UserProvider/Eloquent user provider, except the "database" is a directory and the "query" is a bind/search pair. It does not replace the firewall — it supplies the users half of it.
Minimal path:
ext-ldap enabled (php -m | grep ldap), Symfony 6.4's symfony/security-bundle, and network reachability to the LDAP host. This is a 6.4-only package — don't expect it to slide into a 7.x upgrade untouched.composer require wiser/ldap-user-provider-bundle. With Flex the bundle registration in config/bundles.php is usually automatic; if not, add it manually. There is no recipe to lean on for config scaffolding, so you will be hand-writing YAML.Resources/config/services.yaml (or *.php service config). With a package this small, the service definitions are the documentation. Note the service ID of the user provider and any parameters it consumes — you'll reference that ID in security.yaml.ldap:// vs ldaps://, StartTLS toggle, the search base DN, the bind DN + password for the service account, the search filter (e.g. (sAMAccountName={username})), and an attribute map (username, email, display name, groups).config/packages/security.yaml under providers: and point your form-login firewall at that provider name.First real use case: an internal app where HR/Sysadmin already owns user accounts in AD and you don't want a second credential store. You authenticate against AD, map memberOf to ROLE_*, and either keep users purely in-session or mirror them into a local table for relations.
Two-phase bind (the one you actually want). Avoid bind-as-the-user (bind with the entered credentials, done). Instead: bind with a read-only service account → search the base DN for the entry matching the username → retrieve the DN → re-bind with that DN and the submitted password. This gives you a stable DN (logins survive a cn rename), lets you read attributes before authentication, and keeps search permissions narrow.
Mirror-on-first-login. refreshUser() runs on every request, and directory lookups are network I/O. The common pattern is to treat LDAP as the source of truth for credentials and your local User entity as the source of truth for application data: on successful bind, upsert the entity, stamp last_login_at, and let the provider return your entity on subsequent lookups (often via a ChainUserProvider with the local provider first). This also gives you somewhere to hang relations, audit rows, and app-specific roles that don't exist in the directory.
Group → role mapping. Map memberOf DNs to ROLE_* in the user factory, or do a second search under a groups base DN. Keep the mapping in config (a DN → role hash), not in code — directory group names are the sort of thing that changes without warning. Remember Symfony expects roles prefixed with ROLE_.
Environment parity. Use .env for everything: host, base DN, bind DN, filter. Run dev/staging against a small test directory (or a containerised osixia/openldap) so you aren't developing blind against production AD. Secrets go in Symfony's secrets vault or your platform's env, not in services.yaml.
Compose with other providers. ChainUserProvider (LDAP + a local ROLE_ADMIN break-glass account) is the standard escape hatch for when the directory is down or when you need a service account that isn't in AD.
Programmatic use. Because it's just a service, you can inject the provider into console commands and Messenger workers to resolve a username to a user without going through HTTP — useful for imports and impersonation tooling.
Testing. Unit-test the attribute mapping and role derivation directly; for functional tests, bind a stub provider in services_test.yaml rather than standing up an LDAP server. Keep one integration test against a real directory in CI, tagged and skipped by default.
composer.json, read the source before upgrading, and assume you may have to fork. Apache-2.0 makes the fork legal and unencumbered, which is the main reason this is viable at all.str_replace — check whether the bundle sanitises, and if not, wrap it.sAMAccountName vs userPrincipalName vs mail are all different things, and objectGUID is binary — convert to a string before storing it as an identifier. OpenLDAP will use uid/mail instead.ldaps:// on 636 and StartTLS on 389 are not interchangeable; StartTLS must be issued before any bind. Certificate validation failures show up as a generic "can't contact" — check ldap.conf / the CA bundle before blaming the bundle.refreshUser() fires on each authenticated request unless you cache or use the mirror pattern. Measure before shipping to a latency-sensitive page.__serialize/__unserialize (or rely on SensitiveParameter) so the password isn't written into the session — Symfony's eraseCredentials() won't save you from a careless serializer.ldapsearch -x -H ldap://host -D 'binddn' -W -b 'base' '(uid=someone)' from the CLI first. If that fails, the bundle will only give you a vague exception. If it succeeds, move to bin/console debug:container <provider-service-id> to confirm wiring, then check that the firewall's provider: key matches the provider name (not the service ID) — a classic silent misconfiguration.composer update. Overriding the service definition in your app's services.yaml is the pragmatic middle ground.How can I help you explore Laravel packages today?