7K 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
. . .

Comment

Add Your Comment

Be the first to comment.

css.php