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!
CodePudding user response:
I've faced your problem before, I didn't able to save their informations automaticly in my local database, so I followes these steps:
- signing in by using cas auth and Ldap as a user provider.
- redirect them to form and retrieve them inside the fields like this:
<div>
{{ form_row(user_form.uid, { label: 'UID :*',
required: 'true',
attr: {
value : app.user.uid,
readonly: true
}
}) }}
</div>
- After submiting the form thier roles will change from "ROLE_VISIT" to "ROLE_USER" and all LDAP information will be saved at my local database.
- you can save some data privatly like this:
<div >
{{ form_row(user_form.genre,{
attr: { value : app.user.supannCivilite }
}) }}
</div>
I'm using this bundle for LDAP so please let me know if you need any help!
Also, you can take a look at my security configration as below, hope it will be useful:
providers:
chain_provider:
chain:
providers: [in_memory, database, ldap]
in_memory:
memory:
users:
__NO_USER__:
password:
roles: ROLE_ANON
database:
entity:
class: App\Entity\User
property: uid
ldap:
id: ldap_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
l3_firewall:
pattern: ^/
provider: chain_provider
security: true
guard:
authenticators:
- cas.security.authentication.authenticator
logout:
path: /logout
success_handler: authentication_handler
invalidate_session: false
access_denied_handler: App\EventListener\AccessDeniedListener
main:
pattern: ^/
security: true
lazy: true
provider: chain_provider
guard:
authenticators:
- cas.security.authentication.authenticator
CodePudding user response:
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.