Theory: Is there a way to store an Ldap User in your own database, necassarly for relations, without the password and somehow still make refreshUser work?
I have changed my system to work without having to store the pwd by checking on every login if the password matches the ldap server user password.
Symfony calls on every request the refreshUser
method of your Provider, I have a custom LDAP Provider, to refresh the user using doctrine from the database. Symfony insures so that the object is always upToDate after an edit.
The problem is that this method call the User Entity getPassword()
method, which is not implement due to none pwd in db.
I have seen an approach to fix it, which is saving the Entity in an session and everytime when refreshUser()
is called, returning the session entity. But then there is no way to get it always updated after edit. And I don't want to ruin the symfony infrastructure.
So is there an way to refreshUser()
with maybe calling the LDAP Server again and asking if informations still match?
CodePudding user response:
-SOLVED-
Anyone having an application which relies on a ldap server authentification and a custom user entity inside your app database because of reasons, but you do not want to add the password for security reasons. Here is one answer:
0. Remove pwd attribute equalivant inside db
Inside your custom User Entity remove the @Orm Annotations to not persist the pwd into your database. Keep the attribute and Getter/Setter tho.
For example:
//...
private string $password;
//...
1. Add an custom LdapProvider
Create a new class which extends Symfony\Component\Ldap\Security\LdapUserProvider
. You may need to handle the DependencyInjections manuelly by adding your custom Provider inside your services.yaml:
fullyQualified\NameSpace\CustomLdapProvider:
arguments:
- '@Doctrine\ORM\EntityManagerInterface' # I need the EM in __construct
- '@Symfony\Component\Ldap\Ldap'
- # baseDn
- # searchDN
- # searchPassword
- # roles
- # uidKey
- # filter optionaly, add null
- # passwordAttribute
Override the methods 'supportsClass' and 'refreshUser' with the following code:
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
return $this->entityManager->getRepository(User::class)->find($user);
}
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);
}
Your refreshUser()
method needs to return an User Entity. In my case I just return the user from the database.
It is important, that your supportsClass()
allows LdapUser and your own User Entity since on first login you will get an user from the ldap server which is Symfony\Component\Ldap\Security\LdapUser
.
2. Add an EventSubscriber on InteractiveLogin
Now you need to create an own User, if there is not already own, then continue with logging him in with the own user entity.
Create a new class which extends Symfony\Component\EventDispatcher\EventSubscriberInterface
and use the event 'onInteractiveLogIn'.
public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => [
['onInteractiveLogIn', 0],
],
];
}
Inside 'onInteractiveLogIn()' you need to implement your way on creating a new custom User entity, if no found and persist it. Also creating a new Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
for login.
My example:
public function onInteractiveLogIn(InteractiveLoginEvent $interactiveLoginEvent)
{
$ldapUser = $interactiveLoginEvent->getAuthenticationToken()->getUser();
if (!($ldapUser instanceof LdapUser)) {
return;
}
$localUser = // find local User with LdapUser credentials (Query);
if (!$localUser) {
// no custom user found, create own
$localUser = new User();
//...
}
// since we do not have pwd, fill it with anything to generate random token
$rmdBytes = random_bytes(32);
$localUser->setPassword($rmdBytes);
$this->entityManager->persist($localUser);
$this->entityManager->flush();
// login user with new created/already found custom user entity
$token = new UsernamePasswordToken($localUser, $rmdBytes, 'main', $localUser->getRoles());
$this->tokenStorage->setToken($token);
}
Important: onInteractiveLogIn
already validates that the entered pwd is equal to the real ldap pwd.
3. Implement own way to refresh User
Since Symfony calls on every new request the refreshUser() method to provide an always fresh data object, we need to override the default behaviour.
Your User
Entity needs to implement Symfony\Component\Security\Core\User\EquatableInterface
.
There you can change how on call of refreshUser()
, the new refreshed User and the original User are compared. Default is password, but since we do not have a password, we do not want this behaviour.
In this example im comparing the UserIdentifieres, since they are unique:
public function isEqualTo(UserInterface $user)
{
return $this->getUserIdentifier() === $user->getUserIdentifier();
}
- Finished, Test it.
You should now be avaible to login without saving the pwd inside your own database.
The new system will call on every login the Ldap Server and asks for an user with given credentials. If then logged in it will not call the server anymore, instead will refresh the User automatically if matches the isEqualTo()
.