Home > other >  Symfony LDAP with custom User Entity and auto creation of DB user
Symfony LDAP with custom User Entity and auto creation of DB user

Time:11-12

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:

  1. signing in by using cas auth and Ldap as a user provider.
  2. 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>
  1. 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.
  2. 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.

  • Related