41K Views

UVDesk Opensource LDAP Integration

Implementing Symfony LDAP Authentication

In this post, I am going to explain, how I implemented ldap authentication in uvdesk opensource using symfony ldap component.

Keywords

core-framework: UVDesk opensource core-framework bundle root directory.

community-skeleton: helpdesk project root project directory

STEP 1: Installing dependencies

  • Add the symfony/ldap dependency in composer.json.
# core-framework/composer.json
 "require": {
        "php": "^7.1.3",
        "uvdesk/composer-plugin": "^1.0",
        "doctrine/doctrine-bundle": "^1.11",
        "doctrine/doctrine-fixtures-bundle": "^3.2",
        "doctrine/doctrine-migrations-bundle": "^2.0",
        "symfony/framework-bundle": "4.3.*",
        "knplabs/knp-paginator-bundle": "^4.0",
        "symfony/ldap": "^4.3"
    },

STEP 2: Adding Sample Configuration

  • Adding sample ldap server configuration in config.yaml and uvdesk.php ( Configuration defined inside these files will be used to create/update community-skeleton/config/packages/uvdesk.yaml)
  • To be more specific the file config.yaml is used to create the uvdesk.yaml when we install composer dependency using composer install.
  • uvdesk.php is used to update support_email container parameters when updating Email settings
  • uvdesk.php is updated when we change website prefixes.
#core-framwork/Templates/config.yaml
parameters:
    app_locales: en|fr|it
    
    # Default Assets
    assets_default_agent_profile_image_path: 'bundles/uvdeskcoreframework/images/uv-avatar-batman.png'
    assets_default_customer_profile_image_path: 'bundles/uvdeskcoreframework/images/uv-avatar-ironman.png'
    assets_default_helpdesk_profile_image_path: 'bundles/uvdeskcoreframework/images/uv-avatar-uvdesk.png'

    uvdesk_site_path.member_prefix: member
    uvdesk_site_path.knowledgebase_customer_prefix: customer

    # File uploads constraints
    # @TODO: Set these parameters via compilers
    max_post_size: 8388608
    max_file_uploads: 20
    upload_max_filesize: 2097152

uvdesk:
    site_url: 'localhost:8000'
    upload_manager:
        id: Webkul\UVDesk\CoreFrameworkBundle\FileSystem\UploadManagers\Localhost
    
    support_email: ~
    
    # Default resources
    default:
        ticket:
            type: support
            status: open
            priority: low
        templates:
            email: mail.html.twig
            
    # Ldap Configuration
    ldap:
        connection:
            host: localhost
            port: 389
            encryption: none
            options: 
                protocol_version: 3
                referrals: false
        base_dn: 'dc=example,dc=com'
        search_dn: 'cn=admin,dc=example,dc=com' 
        search_password: 'password'
        username_attribute: mail
        password_attribute: userPassword


STEP 3: Bundle Configuration

  • Adding LDAP Tree Builder configuration in BundleConfiguration tree builder.
<?php
// core-framework/DependencyInjection/BundleConfiguration.php
namespace Webkul\UVDesk\CoreFrameworkBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class BundleConfiguration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $treeBuilder->root('uvdesk')
            ->children()
                ->node('site_url', 'scalar')->defaultValue('127.0.0.1')->end()
                ->node('upload_manager', 'array')
                    ->children()
                        ->node('id', 'scalar')->defaultValue('uvdesk.core.fs.upload.manager')->end()
                    ->end()
                ->end()
                ->node('support_email', 'array')
                    ->children()
                        ->node('id', 'scalar')->defaultValue('support@localhost')->end()
                        ->node('name', 'scalar')->defaultValue('UVDesk Community')->end()
                        ->node('mailer_id', 'scalar')->defaultValue('default')->end()
                    ->end()
                ->end()
                ->node('default', 'array')
                    ->children()
                        ->node('ticket', 'array')
                            ->children()
                                ->node('type', 'scalar')->defaultValue('support')->end()
                                ->node('status', 'scalar')->defaultValue('open')->end()
                                ->node('priority', 'scalar')->defaultValue('low')->end()
                            ->end()
                        ->end()
                        ->node('templates', 'array')
                            ->children()
                                ->node('email', 'scalar')->defaultValue('mail.html.twig')->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
                ->append($this->getLdapNode())
            ->end();

        return $treeBuilder;
    }

    public function getLdapNode() {
        $treeBuilder = new TreeBuilder('ldap');
        $rootNode = method_exists(TreeBuilder::class, 'getRootNode') ? $treeBuilder->getRootNode() : $treeBuilder->root('ldap');
        
        $rootNode->
            children()
                ->node('connection', 'array')
                    ->children()
                        ->node('host', 'scalar')->defaultValue('127.0.0.1')->end()
                        ->node('port', 'scalar')->defaultValue(389)->end()
                        ->node('encryption', 'enum')
                            ->values(['tls', 'ssl', 'none'])
                            ->defaultValue('none')
                        ->end()
                        ->node('options', 'array')
                            ->children()
                                ->node('protocol_version', 'scalar')->defaultValue(3)->end()
                                ->node('referrals', 'scalar')->defaultValue(false)->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
                ->node('base_dn', 'scalar')->defaultValue(null)->end()
                ->node('search_dn', 'scalar')->defaultValue(null)->end()
                ->node('search_password', 'scalar')->defaultValue(null)->end()
                ->node('username_attribute', 'scalar')->defaultValue('mail')->end()
                ->node('password_attribute', 'scalar')->defaultValue('userPassword')->end()
            ->end()
        ;

        return $rootNode;
    }
}

STEP 4: Processing Bundle Configuration

  • Processing Ldap Configuration defined earlier (in config.yaml and uvdesk.php template) in CoreFramework.php (core-framework bundle extension)
<?php
// core-framework/DependencyInjection/CoreFramework.php

namespace Webkul\UVDesk\CoreFrameworkBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Webkul\UVDesk\CoreFrameworkBundle\Definition\RouterInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Definition\RoutingResourceInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Framework\ExtendableComponentInterface;

use Webkul\UVDesk\CoreFrameworkBundle\Tickets\WidgetInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Tickets\QuickActionButtonInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Dashboard\Segments\SearchItemInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Dashboard\Segments\NavigationInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Dashboard\Segments\HomepageSectionInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Dashboard\Segments\HomepageSectionItemInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Dashboard\Segments\PanelSidebarInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Dashboard\Segments\PanelSidebarItemInterface;

class CoreFramework extends Extension
{
    public function getAlias()
    {
        return 'uvdesk';
    }

    public function getConfiguration(array $configs, ContainerBuilder $container)
    {
        return new BundleConfiguration();
    }

    public function load(array $configs, ContainerBuilder $container)
    {
        $services = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config/services'));

        $services->load('core.yaml');
        $services->load('public.yaml');

        // Register automations conditionally if AutomationBundle has been added as an dependency.
        if (array_key_exists('UVDeskAutomationBundle', $container->getParameter('kernel.bundles'))) {
            $services->load('automations.yaml');
        }

        // Load bundle configurations
        $configuration = $this->getConfiguration($configs, $container);
        foreach ($this->processConfiguration($configuration, $configs) as $param => $value) {
            switch ($param) {
                case 'support_email':
                case 'upload_manager':
                    foreach ($value as $field => $fieldValue) {
                        $container->setParameter("uvdesk.$param.$field", $fieldValue);
                    }
                    break;
                case 'default':
                    foreach ($value as $defaultItem => $defaultItemValue) {
                        switch ($defaultItem) {
                            case 'templates':
                                foreach ($defaultItemValue as $template => $templateValue) {
                                    $container->setParameter("uvdesk.default.templates.$template", $templateValue);
                                }
                                break;
                            case 'ticket':
                                foreach ($defaultItemValue as $option => $optionValue) {
                                    $container->setParameter("uvdesk.default.ticket.$option", $optionValue);
                                }
                                break;
                            default:
                                $container->setParameter("uvdesk.default.$defaultItem", $defaultItemValue);
                                break;
                        }
                    }
                    break;
                case 'ldap':
                    foreach ($value as $ldapItem => $ldapItemValue) {
                        switch ($ldapItem) {
                            case 'connection':
                                foreach ($ldapItemValue as $connectionItem => $connectionItemValue) {
                                    $container->setParameter("uvdesk.ldap.connection.$connectionItem", $connectionItemValue);
                                }
                                break;
                            default:
                                $container->setParameter("uvdesk.ldap.$ldapItem", $ldapItemValue);
                                break;
                        }
                    }
                    break;
                default:
                    $container->setParameter("uvdesk.$param", $value);
                    break;
            }
        }

        $container->registerForAutoconfiguration(RouterInterface::class)->addTag('routing.loader');
        $container->registerForAutoconfiguration(WidgetInterface::class)->addTag(WidgetInterface::class);
        $container->registerForAutoconfiguration(QuickActionButtonInterface::class)->addTag(QuickActionButtonInterface::class);

        $container->registerForAutoconfiguration(RoutingResourceInterface::class)->addTag(RoutingResourceInterface::class);
        $container->registerForAutoconfiguration(ExtendableComponentInterface::class)->addTag(ExtendableComponentInterface::class);

        // $container->registerForAutoconfiguration(EmbeddableResourceInterface::class)->addTag(EmbeddableResourceInterface::class);

        $container->registerForAutoconfiguration(SearchItemInterface::class)->addTag(SearchItemInterface::class);
        $container->registerForAutoconfiguration(NavigationInterface::class)->addTag(NavigationInterface::class);
        $container->registerForAutoconfiguration(HomepageSectionInterface::class)->addTag(HomepageSectionInterface::class);
        $container->registerForAutoconfiguration(HomepageSectionItemInterface::class)->addTag(HomepageSectionItemInterface::class);
        $container->registerForAutoconfiguration(PanelSidebarInterface::class)->addTag(PanelSidebarInterface::class);
        $container->registerForAutoconfiguration(PanelSidebarItemInterface::class)->addTag(PanelSidebarItemInterface::class);
    }
}

STEP 5: Creating LdapUserProvider

  • Creating a new class  LdapUserProvider for providing Ldap users to authentication provider .
<?php
// core-framework/Providers/LdapUserProvider.php
namespace Webkul\UVDesk\CoreFrameworkBundle\Providers;

use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\Entry;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Ldap\LdapInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Entity\User;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\FirewallMapInterface;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Webkul\UVDesk\CoreFrameworkBundle\Entity\SupportRole;
use Webkul\UVDesk\CoreFrameworkBundle\Entity\UserInstance;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;

class LdapUserProvider implements UserProviderInterface
{   
    private $firewall;
    private $container;
    private $entityManager;
    private $requestStack;
    private $ldap;
    private $baseDn;
    private $searchDn;
    private $searchPassword;
    private $usernameAttribute;
    private $defaultSearch;
    private $passwordAttribute;

    public function __construct(FirewallMap $firewall, ContainerInterface $container, EntityManagerInterface $entityManager, RequestStack $requestStack)
    {   
        $this->container = $container;
        $this->firewall = $firewall;
        $this->requestStack = $requestStack;
        $this->entityManager = $entityManager;

        $serverConfigs = [
            'host' => $this->container->getParameter('uvdesk.ldap.connection.host'),
            'port' => $this->container->getParameter('uvdesk.ldap.connection.port'),
            'encryption' => $this->container->getParameter('uvdesk.ldap.connection.encryption'),
            'options' => $this->container->getParameter('uvdesk.ldap.connection.options'),
        ];
        $this->ldap = Ldap::create('ext_ldap', $serverConfigs);
        
        $this->baseDn = $this->container->getParameter('uvdesk.ldap.base_dn');
        $this->searchDn = $this->container->getParameter('uvdesk.ldap.search_dn');
        $this->searchPassword = $this->container->getParameter('uvdesk.ldap.search_password');
        $this->usernameAttribute = $this->container->getParameter('uvdesk.ldap.username_attribute');
        $this->passwordAttribute = $this->container->getParameter('uvdesk.ldap.password_attribute');
        $this->defaultSearch = "({$this->usernameAttribute}={username})";
    }

    
    public function loadUserByUsername($username)
    {   
        try {
            $this->ldap->bind($this->searchDn, $this->searchPassword);
            $username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER);
            $query = str_replace('{username}', $username, $this->defaultSearch);
            $search = $this->ldap->query($this->baseDn, $query);
        } catch (ConnectionException $e) {
            throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username), 0, $e);
        }
        $entries = $search->execute();
        $count = \count($entries);
        if (!$count) {
            throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
        }

        if ($count > 1) {
            throw new UsernameNotFoundException('More than one user found');
        }
        $entry = $entries[0];

        try {
            if (null !== $this->usernameAttribute) {
                $username = $this->getAttributeValue($entry, $this->usernameAttribute);
            }
        } catch (InvalidArgumentException $e) {
        }

        return $this->loadUser($username, $entry);
    }

    
    public function refreshUser(UserInterface $user)
    {
        if ($this->supportsClass(get_class($user))) {
            return $this->loadUserByUsername($user->getEmail());
        }

        throw new UnsupportedUserException('Invalid user type');
    }

   
    public function supportsClass($class)
    {   
        try {
            $this->ldap->bind($this->searchDn, $this->searchPassword);
        } catch(ConnectionException $e) {
            // @TODO: Log errors to log file for debugging
            return false;
        }

        return User::class === $class;
    }

    /**
     * Fetches a required unique attribute value from an LDAP entry.
     *
     * @param Entry|null $entry
     * @param string     $attribute
     */
    protected function getAttributeValue(Entry $entry, $attribute)
    {
        if (!$entry->hasAttribute($attribute)) {
            throw new InvalidArgumentException(sprintf('Missing attribute "%s" for user "%s".', $attribute, $entry->getDn()));
        }

        $values = $entry->getAttribute($attribute);

        if (1 !== \count($values)) {
            throw new InvalidArgumentException(sprintf('Attribute "%s" has multiple values.', $attribute));
        }

        return $values[0];
    }

    
    protected function loadUser($username, Entry $entry)
    {
        if (null !== $this->passwordAttribute) {
            $password = $this->getAttributeValue($entry, $this->passwordAttribute);
        }
        if (empty($password)) {
            throw new UsernameNotFoundException("Partial details");
        }
        
        //Refreshing user from database
        $user = $this->loadUserByUsernameFromDatabase($username);
        if (!empty($user)) {

            return $user;
        }

        throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
    }

    protected function loadUserByUsernameFromDatabase($username)
    {
        $queryBuilder = $this->entityManager->createQueryBuilder()
            ->select('user, userInstance')
            ->from('UVDeskCoreFrameworkBundle:User', 'user')
            ->leftJoin('UVDeskCoreFrameworkBundle:UserInstance', 'userInstance', 'WITH', 'user.id = userInstance.user')
            ->leftJoin('userInstance.supportRole', 'supportRole')
            ->where('user.email = :email')->setParameter('email', trim($username))
            ->andWhere('userInstance.isActive = :isActive')->setParameter('isActive', true)
            ->setMaxResults(1);

        // Retrieve user instances based on active firewall
        $activeFirewall = $this->firewall->getFirewallConfig($this->requestStack->getCurrentRequest())->getName();

        switch (strtolower($activeFirewall)) {
            case 'member':
            case 'back_support':
                $queryBuilder
                    ->andWhere('supportRole.id = :roleOwner OR supportRole.id = :roleAdmin OR supportRole.id = :roleAgent')
                    ->setParameter('roleOwner', 1)
                    ->setParameter('roleAdmin', 2)
                    ->setParameter('roleAgent', 3);
                break;
            case 'customer':
            case 'front_support':
                $queryBuilder
                    ->andWhere('supportRole.id = :roleCustomer')
                    ->setParameter('roleCustomer', 4);
                break;
            default:
                return null;
                break;
        }
        
        $response = $queryBuilder->getQuery()->getResult();

        try {
            if (!empty($response) && is_array($response)) {
                list($user, $userInstance) = $response;

                // Set currently active instance
                $user->setCurrentInstance($userInstance);
                $user->setRoles((array) $userInstance->getSupportRole()->getCode());

                return $user;
            }
        } catch (\Exception $e) {
            // Do nothing...
        }

        return null;
    }
}

STEP 6: LdapUserProvider Configuration

  • Injecting Firewall map into LdapUserProvider defined earlier
# core-framework/Resources/config/services/core.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: false
    
    # Services
    Webkul\UVDesk\CoreFrameworkBundle\:
        resource: '../../../*'
        exclude: '../../../{DependencyInjection,Entity,Package,Templates,Migrations,Tests,UIComponents/Dashboard/Aside}'
    
    Webkul\UVDesk\CoreFrameworkBundle\Controller\:
        resource: '../../../Controller/*'
        tags: ['controller.service_arguments']
    
    Webkul\UVDesk\CoreFrameworkBundle\Providers\UserProvider:
        arguments: ['@security.firewall.map']
        
    Webkul\UVDesk\CoreFrameworkBundle\Providers\LdapUserProvider:
        arguments: ['@security.firewall.map']
    
    Webkul\UVDesk\CoreFrameworkBundle\Fixtures\:
        resource: '../../../Fixtures/*'
        tags: ['doctrine.fixture.orm']
    
    Webkul\UVDesk\CoreFrameworkBundle\Security\TicketVoter:
        tags:
            - { name: security.voter }
    
    Webkul\UVDesk\CoreFrameworkBundle\FileSystem\UploadManagers\:
        public: true
        resource: '../../../FileSystem/UploadManagers/*'
    
    Webkul\UVDesk\CoreFrameworkBundle\EventListener\Doctrine\Lifecycle:
        tags:
            - { name: doctrine.event_listener, event: prePersist }
            - { name: doctrine.event_listener, event: preUpdate }
            - { name: doctrine.event_listener, event: postLoad }
    
    Webkul\UVDesk\CoreFrameworkBundle\EventListener\Console\Console:
        tags:
            - { name: kernel.event_listener, event: console.command }
            - { name: kernel.event_listener, event: console.terminate }

STEP 7: Configuring Bundle security configuration

  • Configuring LdapUserProvider and UserProvider as chained user provider.
# core-framework/Templates/sercurity.yaml
security:
    role_hierarchy:
        ROLE_AGENT: ROLE_AGENT
        ROLE_ADMIN: [ROLE_AGENT, ROLE_ADMIN]
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_SUPER_ADMIN]
        ROLE_CUSTOMER: ROLE_CUSTOMER
    
    providers:
        user_provider:
            id: user.provider
        ldap_user_provider:
            id: user.provider.ldap
        chained_user_provider:
            chain:
                providers: ['ldap_user_provider', 'user_provider']
    
    encoders:
        Webkul\UVDesk\CoreFrameworkBundle\Entity\User: bcrypt
    
    firewalls:
        back_support:
            pattern: /%uvdesk_site_path.member_prefix%
            provider: chained_user_provider
            anonymous: ~
            form_login:
                use_referer: true
                login_path: helpdesk_member_handle_login
                check_path: helpdesk_member_handle_login
                default_target_path: helpdesk_member_dashboard
                always_use_default_target_path: true
            logout:
                path:   helpdesk_member_handle_logout
                target: helpdesk_member_handle_login
        
    access_control:
        - { path: /%uvdesk_site_path.member_prefix%/login, roles: [IS_AUTHENTICATED_REMEMBERED, IS_AUTHENTICATED_ANONYMOUSLY] }
        - { path: /%uvdesk_site_path.member_prefix%/create-account, roles: [IS_AUTHENTICATED_REMEMBERED, IS_AUTHENTICATED_ANONYMOUSLY] }
        - { path: /%uvdesk_site_path.member_prefix%/forgot-password, roles: [IS_AUTHENTICATED_REMEMBERED,IS_AUTHENTICATED_ANONYMOUSLY] }
        - { path: /%uvdesk_site_path.member_prefix%/update-credentials, roles: [IS_AUTHENTICATED_REMEMBERED,IS_AUTHENTICATED_ANONYMOUSLY] }
        - { path: /%uvdesk_site_path.member_prefix%/mailbox/listener, roles: [IS_AUTHENTICATED_ANONYMOUSLY] }
        - { path: /%uvdesk_site_path.member_prefix%/, roles: ROLE_AGENT }


Category(s) Symfony UVdesk
. . .

Leave a Comment

Your email address will not be published. Required fields are marked*


7 comments

  • Lakshay Chorpa
    Hello piyush
    I did the changes as suggested by you in this blog in the core-framework.
    But somehow I am unable to authenticate to my ldap server using this.
    • Himani Gupta
      Hello Lakshay,
      Greetings from the UVdesk!!
      Thanks for implementing the LDAP with UVdesk OpenSource helpdesk. We would like to inform you that this is something which we are working and very soon will implement this. If you want you can develop and contribute for this module. For reference, you can check this pull request – https://github.com/uvdesk/core-framework/pull/268
      Hope this will help you 🙂
      Thanks & Regards
      UVdesk Team
      • Lakshay Chorpa
        Thanks Himani
        Really Appreciate your help.
        I got the things working by taking references from that pull request and I think this blog needs to be updated.
        • Himani Gupta
          Hello Lakshay,
          Thanks for replying back to us!!
          For sure we will update this blog. If you will find something is missing and want to update the pull request code then you can also raise separate pull requests for this feature. We would be glad to add you as a contributor!! since this pull request still required some updates.
          Thank You!!
          • satheesan
            Dear Himani Gupta,

            Thanks for your support,
            I have followed all the steps in the above documentation however i am not clear about completing the the steps described in the pull request described in the link https://github.com/uvdesk/core-framework/pull/268

            could you please post here if there is an alternate link or there is any updated documentation regarding the LDAP integration and configuring LDAP in Uvdesk opensource ?.

            Thanks

        • SAJAN
          Dear Lakshya Chopra,

          I am trying to do the ldap integration by following the steps described in the documentation how ever i am not able to complete the steps in the provided link https://github.com/uvdesk/core-framework/pull/268 could you please help .

          • Himani Gupta
            Hello Sajan,
            Glad to see you here!!
            We are still in a way to update ldap integration with UVdesk community helpdesk. We will definitely update you once done.
            Kind Respects
            UVdesk Team
  • css.php