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

Security Active Directory Laravel Package

riper/security-active_directory

Laravel package for Active Directory security integration: authenticate users over LDAP, look up AD accounts, and map directory groups to application roles and permissions. Helps simplify enterprise login, access control, and SSO-style workflows with configurable connection settings.

View on GitHub
Deep Wiki
Context7

Getting Started

This bundle predates Symfony's Guard and Authenticator systems, so treat it as a Symfony 2.6–3.4-era component. v2.0.0 raises the minimum to Symfony 2.6 and swaps the deprecated Security context for security.token_storage. That is a real compatibility fix — v1.x was unusable on Symfony 3.x because security.context was removed in 3.0 — but it does not lift the ceiling. Anything from Symfony 4 onward still uses a security stack this bundle was never designed for (SecurityFactoryInterface was removed in 4.2).

There are now two release lines, so pin deliberately:

  • Symfony 2.6–3.4 → composer require "riper/security-active_directory:^2.0"
  • Symfony 2.1–2.5 → you are stuck on ^1.0, which still injects security.context

Minimum viable setup:

  1. Confirm the environment. php -m | grep ldap must return ldap. AD authentication here is done via ext-ldap BIND operations, not via HTTP or SAML. If you are on a container image (Alpine, slim Debian), install libldap2-dev and rebuild PHP with --with-ldap.
  2. Confirm the Symfony version before installing. composer update on a 2.5 app will now hard-fail on v2.0.0's constraint rather than silently downgrading. Set the constraint explicitly in composer.json; do not let composer show pick for you.
  3. Register the bundle in app/AppKernel.php (new Riper\Security\ActiveDirectoryBundle\RiperSecurityActiveDirectoryBundle()), which registers the DI factory that adds the active_directory key to security.yml.
  4. First config file to read is DependencyInjection/Configuration.php in the bundle source — that is the authoritative list of supported keys (domain server, domain name, base DN, service account). Second, read Resources/config/services.xml to see how the provider is now constructed against security.token_storage. Do not trust the README; it and the tree node often drifted in this era of bundles.
  5. Diff v1.x against v2.0.0 before upgrading. The changelog has two lines. git diff 1.x..2.0.0 -- src/ on the provider class takes two minutes and tells you whether anything else moved that the tag message omitted.
  6. Wire it into security.yml. A custom provider plus a form-login listener that delegates the credential check to LDAP:
# config.yml
riper_security_active_directory:
    domain_server: 'ldaps://dc01.corp.example.com'
    domain_name:   'corp.example.com'

# security.yml
security:
    providers:
        active_directory:
            id: riper_security_active_directory.provider
    firewalls:
        main:
            pattern: ^/
            provider: active_directory
            form_login:
                login_path: /login
                check_path: /login_check
  1. First real use case: replacing a local form_login user table with "log in with your Windows credentials." A user hits /login, submits CORP\jdoe and their domain password, the bundle opens an LDAP connection to the DC, binds as that user, and on success returns a Symfony UserInterface populated from AD attributes. No password is ever stored in your database.

Validate the plumbing outside Symfony first — get a working ldapsearch -H ldaps://dc01.corp.example.com -D '[email protected]' -W -b 'dc=corp,dc=example,dc=com' '(sAMAccountName=jdoe)'. If that fails, no amount of Symfony config will fix it.

Implementation Patterns

Bind-as-the-user, not bind-then-compare. The correct security model is: open a connection, ldap_bind() with the DN you constructed from the supplied username, and treat a successful bind as authentication. Never fetch the password hash and compare it — AD will not give you one, and any implementation that tries is a red flag in review.

Two-phase connection for attribute lookup. Because AD restricts anonymous reads, most deployments use a read-only service account for the search (to resolve sAMAccountName → full DN and pull mail, displayName, memberOf), then a second bind as the resolved DN to verify the password. If the bundle's provider doesn't do this out of the box, subclass it and override the retrieveUser/loadUserByUsername step rather than patching vendor code.

Migrate any custom provider off Security context. This is the pattern the v2.0.0 change forces on you if you extended the bundle. security.context is gone from your constructor; inject the token storage instead:

use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

public function __construct(TokenStorageInterface $tokenStorage, Ldap $ldap)
{
    $this->tokenStorage = $tokenStorage;
    $this->ldap = $ldap;
}

// before (v1.x, breaks on Symfony 3):
//   $this->container->get('security.context')->getToken()->getUser();
// after (v2.x):
$token = $this->tokenStorage->getToken();
$user  = $token ? $token->getUser() : null;

If you had hand-patched security.context out of a vendored copy to get Symfony 3 working, discard that patch and re-vendor from the 2.0.0 tag — otherwise you are maintaining two divergent forks of the same class.

Role mapping from AD groups. memberOf returns full DNs like CN=App-Admins,OU=Groups,DC=corp,DC=example,DC=com. Map these to Symfony roles in your custom User class rather than in the provider, so role logic stays testable and lives next to the entity:

private function mapGroups(array $memberOf): array {
    $roles = ['ROLE_USER'];
    foreach ($memberOf as $dn) {
        if (str_contains($dn, 'CN=App-Admins,')) { $roles[] = 'ROLE_ADMIN'; }
    }
    return array_unique($roles);
}

Chain with a local provider. You almost always need both: AD for corporate staff, a Doctrine provider for service accounts, contractors, or CLI users. Use security.providers.chain with active_directory first and doctrine second; Symfony walks them in order.

Prefer [email protected] (UPN) over CORP\jdoe or raw DN. UPN binding is the least fragile across domains, forests, and nested OUs. Keep DOMAIN\user support only as a compatibility shim if legacy usernames are already in a database.

Cache the resolved DN. A DN-to-username lookup that runs on every request is a measurable latency hit and needless load on your DCs. Cache username → DN (and the mapped roles) in your app cache with a short TTL, and invalidate on logout.

Testing. Stand up osixia/openldap or a small AD-LDS container in CI and point the bundle at ldap://localhost with a fixture tree. Assert three cases: valid credentials, wrong password (must not leak whether the user exists), and a disabled account (userAccountControl bit 2 set) which must be rejected even with a correct password — a naive bind will happily authenticate disabled users. Also assert your provider resolves from the container (debug:container riper_security_active_directory.provider) — the token_storage rewrite is a wiring change first and a logic change second.

Gotchas and Tips

  • v2.0.0 is a BC break, and it is a hard floor. Symfony 2.1–2.5 apps cannot install it. If you maintain more than one legacy app, you now have two bundle versions in the estate and two vendored copies to track. Keep the constraint pinned per-application.
  • The security.context → token_storage swap is the only thing the changelog tells you. It says nothing about changed method signatures, constructor argument order, or service aliases. Diff the tag. In particular, if anything in your services.yml passes @security.context into a bundle service by name, that reference is now dead on Symfony 3.x and will fail at container compile time, not runtime — treat compile errors during deploy as the first symptom.
  • This package is still effectively abandoned. 2016-era, tiny install base. No one is going to fix the next compatibility bug for you. Budget for vendoring the provider class into your own codebase (recommended — it's a few hundred lines, and after the v2.0.0 swap it is at its most portable) or replacing it with symfony/ldap plus a Guard/Authenticator adapter on modern Symfony. If you vendor, vendor from the 2.0.0 tag, not from your old 1.x copy.
  • Symfony 4+ will not work, regardless of v2.0.0. The bundle still relies on the pre-4.0 security factory system (SecurityFactoryInterface), removed in 4.2 and fully dropped by 5.x. The firewall config simply will not bind. Also note the direction of travel: security.token_storage itself became an untracked-service concern in Symfony 5.3+, so even the new code is aimed at a stack that no longer exists.
  • Microsoft's LDAP channel-binding hardening (2020+). After Microsoft's March 2020 ADV190023 patches, many DCs reject unsigned/simple binds on port 389. If authentication used to work and now returns Strong(er) authentication required, you must move to ldaps:// on 636 with a valid CA chain. This is the number-one cause of "it broke overnight" tickets with this bundle.
  • Escaping is on you. Usernames and group filters are interpolated into LDAP filter strings. A username of *)(objectClass=* is an injection. Run every input through ldap_escape($v, '', LDAP_ESCAPE_FILTER) before it touches an (sAMAccountName=...) clause.
  • Username case and whitespace. AD treats sAMAccountName case-insensitively but the provider may do an exact string compare. Normalize with strtolower(trim($username)) and test with mixed case.
  • Empty passwords are a trap. Some LDAP servers treat a bind with a non-empty DN and an empty password as an anonymous bind and return success. Explicitly reject empty passwords in the provider before the bind call — this is a genuine auth-bypass vector in older Symfony AD bundles.
  • Referral chasing. In multi-domain forests the DC returns a referral, and ext-ldap does not follow it unless LDAP_OPT_REFERRALS is set. Symptom: users in child domains fail with Invalid credentials while the parent domain works. Point at a Global Catalog (:3269 for LDAPS) if you need forest-wide lookups.
  • Timeout behavior is silent. ldap_connect never actually connects — the first real network call happens at ldap_bind. A dead DC therefore hangs at the auth step, not at construction. Set LDAP_OPT_NETWORK_TIMEOUT and fail closed with a distinct "directory unavailable" path, not a generic "bad credentials" message.
  • Don't log passwords or raw bind failure strings. AD diagnostic messages can echo back the attempted DN. Log the username, the DC hostname, and the LDAP error code only.
  • Extension points worth using: override the user provider service ID in services.yml (it's a plain DI alias, so riper_security_active_directory.provider: @App\Security\AdUserProvider works), and implement UserInterface::getRoles() on your own class so you own the group→role mapping rather than relying on whatever the bundle ships. After the token_storage change, verify your alias still resolves with debug:container <alias>.
  • Debugging toolkit: ldapsearch from the CLI against the same host, ldap_whoami_s in a scratch script, debug:container to confirm the provider is wired to the token storage service, and Symfony's security.access_denied / security.authentication_failure log channels. If those channels are quiet, your listener isn't even firing — check the firewall ordering, not the LDAP config.
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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums