I am trying to implement a simple LDAP authentication in my Symfony application.
A user should first be authenticated against LDAP, whereby a custom user entity should be returned from the database.
If a user is not in the database but could be authenticated successfully, I want to create the user.
Except for the automatic creation of the user in the database, it works so far.
providers:
users_db:
entity:
# the class of the entity that represents users
class: 'App\Entity\User'
# the property to query by - e.g. email, username, etc
property: 'username'
users_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: '%env(LDAP_BASE_DN)%'
search_dn: '%env(LDAP_SEARCH_DN)%'
search_password: '%env(LDAP_SEARCH_PASSWORD)%'
default_roles: ROLE_USER
uid_key: uid
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_db
http_basic_ldap:
service: Symfony\Component\Ldap\Ldap
dn_string: '%env(LDAP_DN_STRING)%'
With the above configuration, the auth runs against LDAP but the user comes from the DB. So if I have not created a corresponding user, the login attempt won't work.
I have tried the automatic creation of users via a UserChecker, a UserProvider and a LoginEventListener (listens to event onAuthenticationSuccess
), unfortunately without success.
The onAuthenticationSuccess event is only called after successful authentication and can therefore not be used in the configuration described above, because the users_db provider does not (yet) contain the user, even if the LDAP Basic auth works.
Then I tried it with a UserChecker and chained provider [users_ldap, users_db]
. This is also executed, but then I no longer get a user object but an LDAP user. So I tried to create my own UserProvider, unfortunately without success.
If anyone knows a good way to do this, I would appreciate an answer or a short comment. Thank you!
Thanks to this answer I now got it working.
config/services.yaml
services:
App\Security\UserProvider:
arguments:
$em: '@Doctrine\ORM\EntityManagerInterface'
$ldap: '@Symfony\Component\Ldap\Ldap'
$baseDn: "%env(LDAP_BASE_DN)%"
$searchDn: "%env(LDAP_SEARCH_DN)%"
$searchPassword: "%env(LDAP_SEARCH_PASSWORD)%"
$defaultRoles: ["ROLE_USER"]
$uidKey: "uid"
$extraFields: []
App\EventListener\LoginListener:
arguments:
- "@doctrine.orm.entity_manager"
config/packages/security.yml
security:
enable_authenticator_manager: true
password_hashers:
App\Entity\User: 'auto'
providers:
users:
id: App\Security\UserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users
stateless: false
http_basic_ldap:
service: Symfony\Component\Ldap\Ldap
dn_string: 'uid={username},ou=accounts,dc=example,dc=com'
src/Security/UserProvider.php
<?php
namespace App\Security;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Ldap\Security\LdapUserProvider;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\Security\LdapUser;
class UserProvider extends LdapUserProvider
{
private $em;
public function __construct(EntityManagerInterface $em, Ldap $ldap, string $baseDn, string $searchDn = null, string $searchPassword = null, array $defaultRoles = [], string $uidKey = null, string $filter = null, string $passwordAttribute = null, array $extraFields = [])
{
parent::__construct($ldap, $baseDn, $searchDn, $searchPassword, $defaultRoles, $uidKey, $filter, $passwordAttribute, $extraFields);
$this->em = $em;
}
/**
* Refreshes the user after being reloaded from the session.
*
* @return UserInterface
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
$refreshUser = $this->em->getRepository(User::class)->findOneBy(['username' => $user->getUserIdentifier()]);
return $refreshUser;
}
/**
* Tells Symfony to use this provider for this User class.
*/
public function supportsClass(string $class): bool
{
return User::class === $class || is_subclass_of($class, User::class) || LdapUser::class === $class || is_subclass_of($class, LdapUser::class);
}
}
src/EventListener/LoginListener.php
<?php
namespace App\EventListener;
use App\Entity\User;
use Doctrine\ORM\EntityManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Ldap\Security\LdapUser;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class LoginListener implements EventSubscriberInterface
{
private $em;
private $tokenStorage;
function __construct(EntityManager $em, TokenStorageInterface $tokenStorage)
{
$this->em = $em;
$this->tokenStorage = $tokenStorage;
}
public static function getSubscribedEvents(): array
{
return [LoginSuccessEvent::class => 'onLoginSuccess'];
}
public function onLoginSuccess(LoginSuccessEvent $loginSuccessEvent)
{
$ldapUser = $loginSuccessEvent->getAuthenticatedToken()->getUser();
if (!($ldapUser instanceof LdapUser)) {
return;
}
$localUser = $this->em->getRepository(User::class)->findOneBy(['username' => $ldapUser->getUserIdentifier()]);
if (!$localUser) {
// No local user found in database -> create new user
$localUser = new User();
$localUser->setUsername($ldapUser->getUserIdentifier());
}
// We don't store user passwords -> generate random token
$rmdBytes = random_bytes(32);
$localUser->setPassword($rmdBytes);
$this->em->persist($localUser);
$this->em->flush();
// Login user
$token = new UsernamePasswordToken($localUser, $rmdBytes, 'main', $localUser->getRoles());
$this->tokenStorage->setToken($token);
}
}
I also implemented EquatableInterface
in the User
entity as suggested in the referenced stackoverflow.