Connexion LDAP ou AD

De Wiki de Jordan LE NUFF
Sauter à la navigation Sauter à la recherche
 
(12 révisions intermédiaires par le même utilisateur non affichées)
Ligne 19 : Ligne 19 :
  
 
La finalité est de pouvoir réaliser une gestion des droits de l'utilisateur en fonction de ses groupes d'appartenance dans l'Active Directory.
 
La finalité est de pouvoir réaliser une gestion des droits de l'utilisateur en fonction de ses groupes d'appartenance dans l'Active Directory.
 +
 
== Marche à suivre ==
 
== Marche à suivre ==
 
La marche à suivre est plutôt bien présentée dans la [https://symfony.com/doc/current/security.html documentation officielle].
 
La marche à suivre est plutôt bien présentée dans la [https://symfony.com/doc/current/security.html documentation officielle].
Ligne 25 : Ligne 26 :
 
*[[#Installer le module de sécurité|Installer le module de sécurité]]
 
*[[#Installer le module de sécurité|Installer le module de sécurité]]
 
*[[#Créer la gestion des utilisateurs|Créer la gestion des utilisateurs]]
 
*[[#Créer la gestion des utilisateurs|Créer la gestion des utilisateurs]]
*Mise en place de l'authentification
+
*[[#Mise en place de l'authentification|Mise en place de l'authentification]]
*Mise en place du filtrage et des rôles
+
*[[#Mise en place du filtrage et des rôles|Mise en place du filtrage et des rôles]]
  
 
== Installer le module de sécurité ==
 
== Installer le module de sécurité ==
Ligne 128 : Ligne 129 :
  
 
'''Explication :''' Le paramètre "providers:ldap_users:id" permet de définir un <u>fournisseur d'utilisateur</u> (''providers'') référencé <u>ldap_users</u> (''ldap_users'') <u>personnalisé</u> (''id'') pointant vers le service précédemment défini <code>App\Security\CustomLdapUserProvider</code>.
 
'''Explication :''' Le paramètre "providers:ldap_users:id" permet de définir un <u>fournisseur d'utilisateur</u> (''providers'') référencé <u>ldap_users</u> (''ldap_users'') <u>personnalisé</u> (''id'') pointant vers le service précédemment défini <code>App\Security\CustomLdapUserProvider</code>.
 +
 +
=== Surcharger le fournisseur d'utilisateurs de type "LDAP" ===
 +
Une fois que les groupes d'appartenance de l'utilisateur seront extraits depuis l'Active Directory, il faudra les stockés dans une variable. Le but étant de réaliser un filtrage selon ces derniers, il faudra donc les stocker dans la propriété <code>$roles</code> exigée pour tout utilisateur de Symfony fonctionnant avec le module de sécurité de base.
 +
 +
Ainsi, créer le fichier <code>src/Security/CustomLdapUserProvider.php</code> avec le contenu suivant :
 +
<syntaxhighlight lang="php">
 +
<?php
 +
 +
namespace App\Security;
 +
 +
use Symfony\Component\Ldap\Entry;
 +
use Symfony\Component\Ldap\Security\LdapUser;
 +
use Symfony\Component\Ldap\Security\LdapUserProvider;
 +
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
 +
use Symfony\Component\Security\Core\User\UserInterface;
 +
 +
class CustomLdapUserProvider extends LdapUserProvider
 +
{
 +
    private $defaultRoles;
 +
    private $passwordAttribute;
 +
    private $extraFields = array();
 +
 +
    /**
 +
    * Loads a user from an LDAP entry.
 +
    *
 +
    * @param string $username
 +
    * @param Entry $entry
 +
    * @return UserInterface
 +
    */
 +
    protected function loadUser(string $username, Entry $entry)
 +
    {
 +
        $password = null;
 +
        $extraFields = [];
 +
 +
        if (null !== $this->passwordAttribute) {
 +
            $password = $this->getAttributeValue($entry, $this->passwordAttribute);
 +
        }
 +
 +
        foreach ($this->extraFields as $field) {
 +
            $extraFields[$field] = $this->getAttributeValue($entry, $field);
 +
        }
 +
 +
        $results=array();
 +
        foreach ($entry->getAttribute("memberOf") as $LdapGroupDn)
 +
        {
 +
            $results[]= "ROLE_".ldap_explode_dn($LdapGroupDn,1)[0];
 +
        }
 +
 +
        if (!empty($results))
 +
            $roles=$results;
 +
        else
 +
            $roles=$this->defaultRoles;
 +
 +
        return new LdapUser($entry, $username, $password, $roles, $extraFields);
 +
    }
 +
 +
    private function getAttributeValue(Entry $entry, string $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];
 +
    }
 +
 +
}
 +
</syntaxhighlight>
 +
 +
'''Explications :'''
 +
*<syntaxhighlight lang="php" inline>class CustomLdapUserProvider extends LdapUserProvider</syntaxhighlight>
 +
**Création d'une classe enfant <code>CustomLdapUserProvider</code> qui hérite de toutes les propriétés et méthodes de la classe parente <code>LdapUserProvider</code>
 +
*Propriétés privées <syntaxhighlight lang="php" inline>$defaultRoles</syntaxhighlight>, <syntaxhighlight lang="php" inline>$passwordAttribute</syntaxhighlight> et <syntaxhighlight lang="php" inline>$extraFields</syntaxhighlight>
 +
**Déclaration des propriétés nécessaires à l'utilisation de classe <code>CustomLdapUserProvider</code>
 +
**Obligation de surcharger la propriété <syntaxhighlight lang="php" inline>$extraFields</syntaxhighlight> avec un <syntaxhighlight lang="php" inline>= array()</syntaxhighlight> pour assurer la compatibilité avec la classe parente (problème d'incompatibilité encore inexpliqué)
 +
*<syntaxhighlight lang="php" inline>protected function loadUser(string $username, Entry $entry)</syntaxhighlight>
 +
**C'est cette fonction <code>loadUser</code> qu'il est nécessaire de ré-écrire. En effet, c'est à cette étape qu'il faudra alimenter l'utilisateur Symfony avec les données désirées extraites depuis le LDAP
 +
*<syntaxhighlight lang="php" inline>$results=array();</syntaxhighlight>
 +
**Variable qui contiendra les groupes au format attendu par Symfony
 +
*<syntaxhighlight lang="php" inline>foreach ($entry->getAttribute("memberOf") as $LdapGroupDn)</syntaxhighlight>
 +
**Boucle pour récupérer chaque contenu de l'attribut <code>memberOf</code> depuis l'Active Directory dans une variable <syntaxhighlight lang="php" inline>$LdapGroupDn</syntaxhighlight>
 +
*<syntaxhighlight lang="php" inline>$results[]= "ROLE_".ldap_explode_dn($LdapGroupDn,1)[0];</syntaxhighlight>
 +
**Utilisation de la fonction PHP <code>ldap_explode_dn</code> pour éclater la variable <syntaxhighlight lang="php" inline>$LdapGroupDn</syntaxhighlight> et récupérer la ligne 0 du tableau PHP (correspondant au CN du groupe AD)
 +
**Ajout de la chaîne de caractère "ROLE_" au début du CN pour que cela corresponde au format attendu par Symfony
 +
**Ajout du résultat du traitement dans le tableau de variable <syntaxhighlight lang="php" inline>$results[]</syntaxhighlight>
 +
*<syntaxhighlight lang="php" inline>if (!empty($results)) $roles=$results; else $roles=$this->defaultRoles;</syntaxhighlight>
 +
**Si la variable <syntaxhighlight lang="php" inline>$results[]</syntaxhighlight> n'est pas vide, on affecte les résultats aux rôles de l'utilisateur Symfony
 +
**Si la variable <syntaxhighlight lang="php" inline>$results[]</syntaxhighlight> est vide, on affecte à l'utilisateur Symfony les rôles par défaut
 +
*<syntaxhighlight lang="php" inline>private function getAttributeValue(Entry $entry, string $attribute)</syntaxhighlight>
 +
**Simple copier/coller de la fonction privée <code>getAttributeValue</code> de la classe parente (car utilisée dans dans la fonction <code>loadUser</code>)
 +
 +
=== Activer le fournisseur d'utilisateurs de type "LDAP" ===
 +
Pour activer le fournisseur d'utilisateurs de type "LDAP" précédemment créé, définir dans le fichier <code>config/packages/security.yaml</code> le paramètre "security:firewalls:main:provider" à <code>ldap_users</code> (par défaut <code>users_in_memory</code>). Cela donner le code suivant :
 +
<syntaxhighlight lang="yaml">
 +
security:
 +
...
 +
    firewalls:
 +
...
 +
        main:
 +
...
 +
            provider: ldap_users
 +
...
 +
</syntaxhighlight>
 +
 +
== Mise en place de l'authentification ==
 +
=== Principes de base ===
 +
==== Pare-feux ====
 +
Le principe du système d'authentification du module de sécurité de Symfony repose sur la définition de ''"pare-feux"'' (<code>firewalls</code>) dans le fichier <code>config/packages/security.yaml</code>. Dans la suite de cette procédure, le nom du pare-feu utilisé est <code>main</code> (pare-feu par défaut).
 +
 +
Un seul pare-feu est actif à chaque requête. Le nom du pare-feu importe peu. Cela est seulement utile pour savoir quel chemin a pris la requête d'un utilisateur pour s'authentifier.
 +
 +
Les paramètres <code>pattern</code>, <code>host</code> et <code>methods</code> du pare-feu permettent de restreindre l'accès aux ressources de l'application. C'est la première restriction qui correspond à la requête qui activera le pare-feu Symfony correspondant. Voir la [https://symfony.com/doc/current/security/firewall_restriction.html documentation sur les restrictions dans un pare-feu Symfony].
 +
 +
A moins de définir le paramètre "security:firewalls:myfirewall:security" à <code>false</code>, l'authentification est activée par défaut dans le pare-feu par défaut (<code>main</code>). Cela implique que l'utilisateur <u>doit être authentifié</u> pour accéder à la ressource demandée.
 +
 +
Or, un utilisateur qui arrive sur l'application pour la première fois est, par définition, <u>non authentifié</u>. De ce fait, sans aucun autre paramètre supplémentaire, cet utilisateur ne pourra jamais accéder à l'application.
 +
 +
Pour pouvoir accorder au minimum un accès aux pages de base de l'application, il faut que l'option "security:firewalls:myfirewall:anonymous" soit présente et définie à ''"lazy"'' (valeur par défaut). Ainsi, techniquement, du point de vue de Symfony, les utilisateurs non identifiés sont considérés authentifiés comme anonyme (<code>anonymous</code>).
 +
 +
Plus d'informations sur https://symfony.com/doc/current/security.html#a-authentication-firewalls.
 +
 +
==== Authentification ====
 +
De la même manière qu'il y a des fournisseurs d'utilisateurs au sein de Symfony, il y a des fournisseurs d'authentification.
 +
 +
Les différents fournisseurs d'authentification natifs à Symfony sont décrits ici : https://symfony.com/doc/current/security/auth_providers.html.
 +
 +
Toutefois, Symfony recommande vivement d'utiliser ''"Guard"'', un système d'authentification apparu dans la version 2.8 de Symfony ayant pour vocation de simplifier la mise en place d'une stratégie d'authentification. Il suffit de créer une classe implémentant l'interface <code>AuthenticatorInterface</code> ou étendant la classe <code>AbstractGuardAuthenticator</code>, et d'adapter les méthodes de ''"Guard"'' en fonction des besoin. Se référer à la [https://symfony.com/doc/current/security/guard_authentication.html documentation de Guard Authentifcator] pour plus d'informations.
 +
 +
Ainsi, il faudra créer un système d'authentification avec ''"Guard"'' et indiquer au pare-feu de l'utiliser.
 +
 +
:[[Fichier:ClipCapIt-200312-120830.PNG|none|thumb|400px|Appel des méthodes Guard par Symfony]]
 +
 +
=== Mise en oeuvre ===
 +
==== Utiliser MakerBundle pour générer le système d'authentification ====
 +
Grâce au [https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html bundle MakerBundle de Symfony], il est possible de générer automatiquement beaucoup de choses dans le processus d'authentification.
 +
 +
Pour ce faire, lancer la commande <code>make:auth</code>. Des questions seront posées, il faudra répondre en fonction des besoins. Cela ressemblera donc au résultat suivant :
 +
{{terminal|text=
 +
[php-fpm@myserver test]$ php bin/console make:auth
 +
 +
What style of authentication do you want? [Empty authenticator]:
 +
  [0] Empty authenticator
 +
  [1] Login form authenticator
 +
> 1
 +
 +
The class name of the authenticator to create (e.g. AppCustomAuthenticator):
 +
> LdapFormAuthenticator
 +
 +
Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
 +
>
 +
 +
Enter the User class that you want to authenticate (e.g. App\Entity\User) []:
 +
> Symfony\Component\Ldap\Security\LdapUser
 +
 +
Do you want to generate a '/logout' URL? (yes/no) [yes]:
 +
>
 +
 +
created: src/Security/LdapFormAuthenticator.php
 +
updated: config/packages/security.yaml
 +
created: src/Controller/SecurityController.php
 +
created: templates/security/login.html.twig
 +
 +
 +
  Success!
 +
 +
 +
Next:
 +
- Customize your new authenticator.
 +
- Finish the redirect "TODO" in the App\Security\LdapFormAuthenticator::onAuthenticationSuccess() method.
 +
- Review App\Security\LdapFormAuthenticator::getUser() to make sure it matches your needs.
 +
- Check the user's password in App\Security\LdapFormAuthenticator::checkCredentials().
 +
- Review & adapt the login template: templates/security/login.html.twig.
 +
}}
 +
 +
Cette commande a généré les éléments suivants :
 +
*Un gestionnaire d'authentification <code>src/Security/LdapFormAuthenticator.php</code>
 +
*Un contrôleur <code>src/Controller/SecurityController.php</code>
 +
*Un modèle Twig de formulaire d'identification <code>templates/security/login.html.twig</code>
 +
 +
Et a mis à jour le fichier de configuration <code>config/packages/security.yaml</code> en y ajoutant les paramètres suivants :
 +
<syntaxhighlight lang="yaml">
 +
security:
 +
...
 +
    firewalls:
 +
...
 +
        main:
 +
...
 +
            guard:
 +
                authenticators:
 +
                    - App\Security\LdapFormAuthenticator
 +
            logout:
 +
                path: app_logout
 +
                # where to redirect after logout
 +
                # target: app_any_route
 +
...
 +
</syntaxhighlight>
 +
 +
==== Adapter les fichiers gérénés ====
 +
Comme indiqué par la commande à la fin de son exécution, il faut modifier le fichier <code>src/Security/LdapFormAuthenticator.php</code> pour l'adapter aux besoins. Voici le fichier généré :
 +
<syntaxhighlight lang="php">
 +
<?php
 +
 +
namespace App\Security;
 +
 +
use Symfony\Component\HttpFoundation\RedirectResponse;
 +
use Symfony\Component\HttpFoundation\Request;
 +
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 +
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 +
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
 +
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
 +
use Symfony\Component\Security\Core\Security;
 +
use Symfony\Component\Security\Core\User\UserInterface;
 +
use Symfony\Component\Security\Core\User\UserProviderInterface;
 +
use Symfony\Component\Security\Csrf\CsrfToken;
 +
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
 +
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
 +
use Symfony\Component\Security\Http\Util\TargetPathTrait;
 +
 +
class LdapFormAuthenticator extends AbstractFormLoginAuthenticator
 +
{
 +
    use TargetPathTrait;
 +
 +
    private $urlGenerator;
 +
    private $csrfTokenManager;
 +
 +
    public function __construct(UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager)
 +
    {
 +
        $this->urlGenerator = $urlGenerator;
 +
        $this->csrfTokenManager = $csrfTokenManager;
 +
    }
 +
 +
    public function supports(Request $request)
 +
    {
 +
        return 'app_login' === $request->attributes->get('_route')
 +
            && $request->isMethod('POST');
 +
    }
 +
 +
    public function getCredentials(Request $request)
 +
    {
 +
        $credentials = [
 +
            'username' => $request->request->get('username'),
 +
            'password' => $request->request->get('password'),
 +
            'csrf_token' => $request->request->get('_csrf_token'),
 +
        ];
 +
        $request->getSession()->set(
 +
            Security::LAST_USERNAME,
 +
            $credentials['username']
 +
        );
 +
 +
        return $credentials;
 +
    }
 +
 +
    public function getUser($credentials, UserProviderInterface $userProvider)
 +
    {
 +
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
 +
        if (!$this->csrfTokenManager->isTokenValid($token)) {
 +
            throw new InvalidCsrfTokenException();
 +
        }
 +
 +
        // Load / create our user however you need.
 +
        // You can do this by calling the user provider, or with custom logic here.
 +
        $user = $userProvider->loadUserByUsername($credentials['username']);
 +
 +
        if (!$user) {
 +
            // fail authentication with a custom error
 +
            throw new CustomUserMessageAuthenticationException('Username could not be found.');
 +
        }
 +
 +
        return $user;
 +
    }
 +
 +
    public function checkCredentials($credentials, UserInterface $user)
 +
    {
 +
        // Check the user's password or other credentials and return true or false
 +
        // If there are no credentials to check, you can just return true
 +
        throw new \Exception('TODO: check the credentials inside '.__FILE__);
 +
    }
 +
 +
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
 +
    {
 +
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
 +
            return new RedirectResponse($targetPath);
 +
        }
 +
 +
        // For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));
 +
        throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
 +
    }
 +
 +
    protected function getLoginUrl()
 +
    {
 +
        return $this->urlGenerator->generate('app_login');
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 +
Et voici les modifications à apporter :
 +
<syntaxhighlight lang="php" line highlight="18-19,27,27,29,33,75-81,86-89,95">
 +
<?php
 +
 +
namespace App\Security;
 +
 +
use Symfony\Component\HttpFoundation\RedirectResponse;
 +
use Symfony\Component\HttpFoundation\Request;
 +
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 +
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 +
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
 +
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
 +
use Symfony\Component\Security\Core\Security;
 +
use Symfony\Component\Security\Core\User\UserInterface;
 +
use Symfony\Component\Security\Core\User\UserProviderInterface;
 +
use Symfony\Component\Security\Csrf\CsrfToken;
 +
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
 +
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
 +
use Symfony\Component\Security\Http\Util\TargetPathTrait;
 +
use Symfony\Component\Ldap\Exception\ConnectionException;
 +
use Symfony\Component\Ldap\Ldap;
 +
 +
class LdapFormAuthenticator extends AbstractFormLoginAuthenticator
 +
{
 +
    use TargetPathTrait;
 +
 +
    private $urlGenerator;
 +
    private $csrfTokenManager;
 +
    protected $ldap;
 +
 +
    public function __construct(UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, Ldap $ldap)
 +
    {
 +
        $this->urlGenerator = $urlGenerator;
 +
        $this->csrfTokenManager = $csrfTokenManager;
 +
        $this->ldap = $ldap;
 +
    }
 +
 +
    public function supports(Request $request)
 +
    {
 +
        return 'app_login' === $request->attributes->get('_route')
 +
            && $request->isMethod('POST');
 +
    }
 +
 +
    public function getCredentials(Request $request)
 +
    {
 +
        $credentials = [
 +
            'username' => $request->request->get('username'),
 +
            'password' => $request->request->get('password'),
 +
            'csrf_token' => $request->request->get('_csrf_token'),
 +
        ];
 +
        $request->getSession()->set(
 +
            Security::LAST_USERNAME,
 +
            $credentials['username']
 +
        );
 +
 +
        return $credentials;
 +
    }
 +
 +
    public function getUser($credentials, UserProviderInterface $userProvider)
 +
    {
 +
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
 +
        if (!$this->csrfTokenManager->isTokenValid($token)) {
 +
            throw new InvalidCsrfTokenException();
 +
        }
 +
 +
        $user = $userProvider->loadUserByUsername($credentials['username']);
 +
 +
        if (!$user) {
 +
            throw new CustomUserMessageAuthenticationException('Username could not be found.');
 +
        }
 +
 +
        return $user;
 +
    }
 +
 +
    public function checkCredentials($credentials, UserInterface $user)
 +
    {
 +
        try {
 +
            $this->ldap->bind($user->getEntry()->getDn(), $credentials['password']);
 +
        } catch (ConnectionException $e) {
 +
            return false;
 +
        }
 +
 +
        return true;
 +
    }
 +
 +
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
 +
    {
 +
        $request->getSession()->getFlashBag()->add(
 +
            'info',
 +
            'Bienvenue. Vous êtes connecté !'
 +
        );
 +
 +
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
 +
            return new RedirectResponse($targetPath);
 +
        }
 +
 +
        return new RedirectResponse($this->urlGenerator->generate('home_index')); // <== Adapter la route selon les besoins
 +
    }
 +
 +
    protected function getLoginUrl()
 +
    {
 +
        return $this->urlGenerator->generate('app_login');
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 +
'''Explications :'''
 +
*Lignes 18 et 19
 +
**Ajout des classes nécessaires dans le contrôleur
 +
*Lignes 27, 29 et 33
 +
**Injection du service LDAP dans la classe du contrôleur
 +
*Lignes 75 à 81
 +
**Ré-écriture de la méthode <code>checkCredentials</code> afin d'utiliser le service LDAP pour vérifier le mot de passe entré par l'utilisateur
 +
*Lignes 86 à 89
 +
**Ajout d'un message flash notifiant du succès de la connexion
 +
*Ligne 95
 +
**Définition d'une route par défaut vers laquelle rediriger si la valeur <code>$targetPath</code> n'est pas définie (la valeur de cette variable correspond à la route dont l'accès a été demandé avant d'être redirigé vers la page d'authentification)
 +
 +
Pour le fichier <code>templates/security/login.html.twig</code>, de base, il répond aux besoins de l'authentification sans modification supplémentaire. Il est toutefois possible de le modifier pour adapter le contenu visuel.
 +
 +
== Mise en place du filtrage et des rôles ==
 +
Les rôles déjà existants dans Symfony sont les suivants :
 +
*'''ROLE_USER'''
 +
::Ce rôle est affecté automatiquement à chaque utilisateur
 +
*'''ROLE_ADMIN'''
 +
::C'est un rôle par défaut pour les actions d'administration. Il n'est pas obligatoire de l'utiliser.
 +
*'''ROLE_ALLOWED_TO_SWITCH'''
 +
::Rôle donnant l'autorisation de pouvoir basculer vers un autre utilisateur sans s'authentifier.
 +
</pre>
 +
 +
Comme indiqué lors de [[#Surcharger_le_fournisseur_d.27utilisateurs_de_type_.22LDAP.22|la surcharge du fournisseur LDAP]], à chaque groupe d'appartenance de l'utilisateur sera associé un rôle équivalent de la forme <code>ROLE_<nom-du-groupe></code>. De ce fait, si un utilisateur appartient aux groupes <code>BUREAU_ETUDES</code> et <code>DEVELOPPEURS</code>, lors de son authentification au travers du LDAP, il se verra affecter les rôles <code>ROLES_BUREAU_ETUDES</code> et <code>ROLES_DEVELOPPEURS</code>.
 +
 +
'''Attention !''' Le fournisseur LDAP va créer des rôles en fonction des groupes d'appartenance des utilisateurs, mais rien n'empêche de créer des rôles supplémentaires dans l'application.
 +
 +
Ainsi, en s'inspirant de [https://symfony.com/doc/current/security.html#roles la documentation officielle de Symfony sur les rôles], il est possible de produire ce genre de filtrage :
 +
<syntaxhighlight lang="yaml">
 +
security:
 +
    providers:
 +
        ldap_users:
 +
            id: App\Security\CustomLdapUserProvider
 +
    firewalls:
 +
        dev:
 +
            pattern: ^/(_(profiler|wdt)|css|images|js)/
 +
            security: false
 +
        main:
 +
            anonymous: lazy
 +
            provider: ldap_users
 +
            guard:
 +
                authenticators:
 +
                    - App\Security\LoginFormAuthenticator
 +
            logout:
 +
                path: app_logout
 +
                target: home_index
 +
 +
    role_hierarchy:
 +
        ROLE_MANAGERS:      [ROLE_ADMIN,ROLE_ALLOWED_TO_SWITCH]
 +
    # Note: Only the *first* access control that matches will be used
 +
    access_control:
 +
        - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
 +
        - { path: ^/(admin|manage/users), roles: 'ROLE_MANAGERS' }
 +
        - { path: ^/(add|edit|delete), roles: ROLE_ADMIN, ip: 192.168.0.33 }
 +
        - { path: ^/, roles: IS_AUTHENTICATED_REMEMBERED }
 +
</syntaxhighlight>
 +
 +
<u>'''Explications :'''</u>
 +
*'''role_hierarchy'''
 +
:Cette section permet d'imbriquer des rôles dans d'autres rôles
 +
:Par exemple, si un utilisateur du LDAP appartient au groupe <code>MANAGERS</code>, le fournisseur LDAP lui affectera le rôles <code>ROLE_MANAGERS</code> et la configuration du module de sécurité lui affectera les rôles <code>ROLE_ADMIN</code> et <code>ROLE_ALLOWED_TO_SWITCH</code>
 +
*'''access_control'''
 +
:Cette section est le cœur même du filtrage dans Symfony
 +
:Pour chaque requête, chaque règle de filtrage est analysée. Seule la première qui correspond à la requête sera utilisée, les suivantes seront ignorées.
 +
:*<code>{ path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }</code>
 +
::Cette règle autorise quiconque à accéder à la page <code>/login</code>. Dans cet exemple, cette page correspond à l'authentification, cette règle est donc obligatoire quand on veut authentifier ses utilisateurs.
 +
:*<code>{ path: ^/(admin|manage|users), roles: 'ROLE_MANAGERS' }</code>
 +
::Seuls les utilisateurs ayant le rôle <code>ROLE_MANAGERS</code> peuvent accéder aux pages <code>/admin</code>, <code>/manage</code> et <code>/users</code>.
 +
:*<code>{ path: ^/(add|edit|delete), roles: ROLE_ADMIN, ip: 192.168.0.33 }</code>
 +
::Seuls les utilisateurs ayant le rôle <code>ROLE_ADMIN</code> et ayant l'adresse IP <code>192.168.0.33</code> peuvent accéder aux pages <code>/add</code>, <code>/edit</code> et <code>/delete</code>.
 +
:*<code>{ path: ^/, roles: IS_AUTHENTICATED_REMEMBERED }</code>
 +
::Seuls les utilisateurs authentifiés peuvent accéder au contenu du site.
 +
 +
== Résumé ==

Version actuelle datée du 11 janvier 2021 à 10:44

Présentation

Cette page a pour objet de décrire une procédure pour interfacer Symfony avec une authentification LDAP ou AD.

Le contenu de cette procédure est majoritairement inspiré de la documentation officielle de Symfony :

Une autre partie de cette procédure s'est inspirée de la solution de Stanislav Drozdov de ré-écriture du fournisseur d'utilisateurs LDAP :

Objectifs

Cette procédure a deux objectifs :

  1. Rassembler dans une unique page les informations nécessaires à l'interfaçage avec un LDAP ou AD
    • En effet, pour ce type de besoin, les informations sont dispersées au sein de la documentation officielle de Symfony
  2. Aller au-delà des limitations du composant natif LDAP de Symfony
    • Bien que le composant LDAP simplifie énormément la mise en place de l'interfaçage avec Symfony, il n'embarque pas nativement d'option permettant la récupération automatique de certains champs. Il faut donc le surcharger en fonction des besoins

La finalité est de pouvoir réaliser une gestion des droits de l'utilisateur en fonction de ses groupes d'appartenance dans l'Active Directory.

Marche à suivre

La marche à suivre est plutôt bien présentée dans la documentation officielle.

Elle se présente sous la forme suivante :

Installer le module de sécurité

Cette partie est la plus simple de la procédure. Si Symfony a été installé avec l'option --full, elle n'est pas nécessaire.

Pour installer le module de sécurité, lancer la commande suivante à la racine du projet :

Putty icon.png Console SSH

Créer la gestion des utilisateurs

Le module security s'appuie sur des fournisseurs d'utilisateurs pour gérer les utilisateurs.

Comme indiqué dans la documentation à propos des fournisseurs d'utilisateurs, Symfony embarque nativement les fournisseurs suivants :

Bien que la documentation principale relative au module de sécurité laisse entendre qu'il faut créer une classe d'utilisateur, cette étape est inutile pour des utilisateurs LDAP (à moins de vouloir utiliser une classe d'utilisateur personnalisée). En effet, le composant symfony/ldap embarque nativement une classe d'utilisateur LdapUser.

Installer le composant LDAP

Pour installer le composant LDAP, lancer la commande suivante à la racine du projet :

Putty icon.png Console SSH

Configurer le client LDAP

Pour configurer le client LDAP, ajouter la section suivante dans le fichier config/services.yaml :

parameters:
...
    app_ldap_host_default: my-server
    app_ldap_port_default: 389
    app_ldap_encryption_default: tls
...
    env(APP_LDAP_HOST): '%env(default:app_ldap_host_default:LDAP_HOST)%'
    env(APP_LDAP_PORT): '%env(default:app_ldap_port_default:LDAP_PORT)%'
    env(APP_LDAP_ENCRYPTION): '%env(default:app_ldap_encryption_default:LDAP_ENCRYPTION)%'
...
services:
...
    # LDAP configuration
    Symfony\Component\Ldap\Ldap:
        arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
    Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
        arguments:
            -   host: '%env(resolve:APP_LDAP_HOST)%'
                port: '%env(resolve:APP_LDAP_PORT)%'
                encryption: '%env(resolve:APP_LDAP_ENCRYPTION)%'
                options:
                    protocol_version: 3
                    referrals: false

Le contenu de cette section diffère par rapport à la documentation officielle au niveau du contenu des variables host, port et encryption. En effet, afin de rendre l'application complètement paramétrable depuis des variables d'environnement du serveur, il a été défini des paramètres par défaut et des variables internes à l'application.

Attention ! Il faut que l'extension LDAP pour PHP soit installée pour que le composant soit fonctionnel.

Par ailleurs, il est recommandé d'ajouter "ext-ldap": "*", dans la section "require" du fichier composer.json afin d'indiquer que l'application Symfony a besoin de cette extension PHP pour fonctionner correctement.

Plus d'information sur : https://symfony.com/doc/current/security/ldap.html#configuring-the-ldap-client

Définir le fournisseur d'utilisateurs de type "LDAP"

Comme indiqué dans la documentation Symfony pour le paramétrage du fournisseur d'utilisateurs de type "LDAP", il est possible d'utiliser le fournisseur natif en suivant l'exemple donné dans la documentation. Toutefois, comme évoqué en introduction de cette procédure, les besoins de l'application peuvent se heurter aux limites de ce fournisseur. Il va donc falloir utiliser un fournisseur d'utilisateurs LDAP personnalisé.

Ainsi, en s'appuyant sur la relative à la mise en place d'un fournisseur d'utilisateur personnalisé, la définition du nouveau fournisseur se fait dans les fichiers config/services.yaml et config/packages/security.yaml.

Dans le fichier config/services.yaml, ajouter la section suivante :

parameters:
...
    app_ldap_basedn_default: dc=example,dc=com
    app_ldap_searchdn_default: "cn=read-only-admin,dc=example,dc=com"
    app_ldap_searchpassword_default: password
...
    env(APP_LDAP_BASEDN): '%env(default:app_ldap_basedn_default:LDAP_BASEDN)%'
    env(APP_LDAP_SEARCHDN): '%env(default:app_ldap_searchdn_default:LDAP_SEARCHDN)%'
    env(APP_LDAP_SEARCHPASSWORD): '%env(default:app_ldap_searchpassword_default:LDAP_SEARCHPASSWORD)%'
...
services:
...
    # Custom LDAP User Provider
    App\Security\CustomLdapUserProvider:
        arguments:
            - '@Symfony\Component\Ldap\Ldap'
            - '%env(resolve:APP_LDAP_BASEDN)%'
            - '%env(resolve:APP_LDAP_SEARCHDN)%'
            - '%env(resolve:APP_LDAP_SEARCHPASSWORD)%'

Explication : Cette section permet de définir un service déclaré dans la classe App\Security\CustomLdapUserProvider (cette classe est ensuite à créer) et s'appuie sur le client LDAP existant (Symfony\Component\Ldap\Ldap). C'est ce service qui va récupérer de façon personnalisée les utilisateurs LDAP ou AD. Les variables APP_LDAP_BASEDN, APP_LDAP_SEARCHDN et APP_LDAP_SEARCHPASSWORD reposent sur l'existence de variables d'environnement du serveur, comme expliqué précédemment.

Dans le fichier config/packages/security.yaml, ajouter la section suivante :

security:
    providers:
        ldap_users:
            id: App\Security\CustomLdapUserProvider
...

Explication : Le paramètre "providers:ldap_users:id" permet de définir un fournisseur d'utilisateur (providers) référencé ldap_users (ldap_users) personnalisé (id) pointant vers le service précédemment défini App\Security\CustomLdapUserProvider.

Surcharger le fournisseur d'utilisateurs de type "LDAP"

Une fois que les groupes d'appartenance de l'utilisateur seront extraits depuis l'Active Directory, il faudra les stockés dans une variable. Le but étant de réaliser un filtrage selon ces derniers, il faudra donc les stocker dans la propriété $roles exigée pour tout utilisateur de Symfony fonctionnant avec le module de sécurité de base.

Ainsi, créer le fichier src/Security/CustomLdapUserProvider.php avec le contenu suivant :

<?php

namespace App\Security;

use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Security\LdapUser;
use Symfony\Component\Ldap\Security\LdapUserProvider;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Core\User\UserInterface;

class CustomLdapUserProvider extends LdapUserProvider
{
    private $defaultRoles;
    private $passwordAttribute;
    private $extraFields = array();

    /**
     * Loads a user from an LDAP entry.
     *
     * @param string $username
     * @param Entry $entry
     * @return UserInterface
     */
    protected function loadUser(string $username, Entry $entry)
    {
        $password = null;
        $extraFields = [];

        if (null !== $this->passwordAttribute) {
            $password = $this->getAttributeValue($entry, $this->passwordAttribute);
        }

        foreach ($this->extraFields as $field) {
            $extraFields[$field] = $this->getAttributeValue($entry, $field);
        }

        $results=array();
        foreach ($entry->getAttribute("memberOf") as $LdapGroupDn)
        {
            $results[]= "ROLE_".ldap_explode_dn($LdapGroupDn,1)[0];
        }

        if (!empty($results))
            $roles=$results;
        else
            $roles=$this->defaultRoles;

        return new LdapUser($entry, $username, $password, $roles, $extraFields);
    }

    private function getAttributeValue(Entry $entry, string $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];
    }

}

Explications :

  • class CustomLdapUserProvider extends LdapUserProvider
    • Création d'une classe enfant CustomLdapUserProvider qui hérite de toutes les propriétés et méthodes de la classe parente LdapUserProvider
  • Propriétés privées $defaultRoles, $passwordAttribute et $extraFields
    • Déclaration des propriétés nécessaires à l'utilisation de classe CustomLdapUserProvider
    • Obligation de surcharger la propriété $extraFields avec un = array() pour assurer la compatibilité avec la classe parente (problème d'incompatibilité encore inexpliqué)
  • protected function loadUser(string $username, Entry $entry)
    • C'est cette fonction loadUser qu'il est nécessaire de ré-écrire. En effet, c'est à cette étape qu'il faudra alimenter l'utilisateur Symfony avec les données désirées extraites depuis le LDAP
  • $results=array();
    • Variable qui contiendra les groupes au format attendu par Symfony
  • foreach ($entry->getAttribute("memberOf") as $LdapGroupDn)
    • Boucle pour récupérer chaque contenu de l'attribut memberOf depuis l'Active Directory dans une variable $LdapGroupDn
  • $results[]= "ROLE_".ldap_explode_dn($LdapGroupDn,1)[0];
    • Utilisation de la fonction PHP ldap_explode_dn pour éclater la variable $LdapGroupDn et récupérer la ligne 0 du tableau PHP (correspondant au CN du groupe AD)
    • Ajout de la chaîne de caractère "ROLE_" au début du CN pour que cela corresponde au format attendu par Symfony
    • Ajout du résultat du traitement dans le tableau de variable $results[]
  • if (!empty($results)) $roles=$results; else $roles=$this->defaultRoles;
    • Si la variable $results[] n'est pas vide, on affecte les résultats aux rôles de l'utilisateur Symfony
    • Si la variable $results[] est vide, on affecte à l'utilisateur Symfony les rôles par défaut
  • private function getAttributeValue(Entry $entry, string $attribute)
    • Simple copier/coller de la fonction privée getAttributeValue de la classe parente (car utilisée dans dans la fonction loadUser)

Activer le fournisseur d'utilisateurs de type "LDAP"

Pour activer le fournisseur d'utilisateurs de type "LDAP" précédemment créé, définir dans le fichier config/packages/security.yaml le paramètre "security:firewalls:main:provider" à ldap_users (par défaut users_in_memory). Cela donner le code suivant :

security:
...
    firewalls:
...
        main:
...
            provider: ldap_users
...

Mise en place de l'authentification

Principes de base

Pare-feux

Le principe du système d'authentification du module de sécurité de Symfony repose sur la définition de "pare-feux" (firewalls) dans le fichier config/packages/security.yaml. Dans la suite de cette procédure, le nom du pare-feu utilisé est main (pare-feu par défaut).

Un seul pare-feu est actif à chaque requête. Le nom du pare-feu importe peu. Cela est seulement utile pour savoir quel chemin a pris la requête d'un utilisateur pour s'authentifier.

Les paramètres pattern, host et methods du pare-feu permettent de restreindre l'accès aux ressources de l'application. C'est la première restriction qui correspond à la requête qui activera le pare-feu Symfony correspondant. Voir la documentation sur les restrictions dans un pare-feu Symfony.

A moins de définir le paramètre "security:firewalls:myfirewall:security" à false, l'authentification est activée par défaut dans le pare-feu par défaut (main). Cela implique que l'utilisateur doit être authentifié pour accéder à la ressource demandée.

Or, un utilisateur qui arrive sur l'application pour la première fois est, par définition, non authentifié. De ce fait, sans aucun autre paramètre supplémentaire, cet utilisateur ne pourra jamais accéder à l'application.

Pour pouvoir accorder au minimum un accès aux pages de base de l'application, il faut que l'option "security:firewalls:myfirewall:anonymous" soit présente et définie à "lazy" (valeur par défaut). Ainsi, techniquement, du point de vue de Symfony, les utilisateurs non identifiés sont considérés authentifiés comme anonyme (anonymous).

Plus d'informations sur https://symfony.com/doc/current/security.html#a-authentication-firewalls.

Authentification

De la même manière qu'il y a des fournisseurs d'utilisateurs au sein de Symfony, il y a des fournisseurs d'authentification.

Les différents fournisseurs d'authentification natifs à Symfony sont décrits ici : https://symfony.com/doc/current/security/auth_providers.html.

Toutefois, Symfony recommande vivement d'utiliser "Guard", un système d'authentification apparu dans la version 2.8 de Symfony ayant pour vocation de simplifier la mise en place d'une stratégie d'authentification. Il suffit de créer une classe implémentant l'interface AuthenticatorInterface ou étendant la classe AbstractGuardAuthenticator, et d'adapter les méthodes de "Guard" en fonction des besoin. Se référer à la documentation de Guard Authentifcator pour plus d'informations.

Ainsi, il faudra créer un système d'authentification avec "Guard" et indiquer au pare-feu de l'utiliser.

Appel des méthodes Guard par Symfony

Mise en oeuvre

Utiliser MakerBundle pour générer le système d'authentification

Grâce au bundle MakerBundle de Symfony, il est possible de générer automatiquement beaucoup de choses dans le processus d'authentification.

Pour ce faire, lancer la commande make:auth. Des questions seront posées, il faudra répondre en fonction des besoins. Cela ressemblera donc au résultat suivant :

Putty icon.png Console SSH

Cette commande a généré les éléments suivants :

  • Un gestionnaire d'authentification src/Security/LdapFormAuthenticator.php
  • Un contrôleur src/Controller/SecurityController.php
  • Un modèle Twig de formulaire d'identification templates/security/login.html.twig

Et a mis à jour le fichier de configuration config/packages/security.yaml en y ajoutant les paramètres suivants :

security:
...
    firewalls:
...
        main:
...
            guard:
                authenticators:
                    - App\Security\LdapFormAuthenticator
            logout:
                path: app_logout
                # where to redirect after logout
                # target: app_any_route
...

Adapter les fichiers gérénés

Comme indiqué par la commande à la fin de son exécution, il faut modifier le fichier src/Security/LdapFormAuthenticator.php pour l'adapter aux besoins. Voici le fichier généré :

<?php

namespace App\Security;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class LdapFormAuthenticator extends AbstractFormLoginAuthenticator
{
    use TargetPathTrait;

    private $urlGenerator;
    private $csrfTokenManager;

    public function __construct(UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager)
    {
        $this->urlGenerator = $urlGenerator;
        $this->csrfTokenManager = $csrfTokenManager;
    }

    public function supports(Request $request)
    {
        return 'app_login' === $request->attributes->get('_route')
            && $request->isMethod('POST');
    }

    public function getCredentials(Request $request)
    {
        $credentials = [
            'username' => $request->request->get('username'),
            'password' => $request->request->get('password'),
            'csrf_token' => $request->request->get('_csrf_token'),
        ];
        $request->getSession()->set(
            Security::LAST_USERNAME,
            $credentials['username']
        );

        return $credentials;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
        if (!$this->csrfTokenManager->isTokenValid($token)) {
            throw new InvalidCsrfTokenException();
        }

        // Load / create our user however you need.
        // You can do this by calling the user provider, or with custom logic here.
        $user = $userProvider->loadUserByUsername($credentials['username']);

        if (!$user) {
            // fail authentication with a custom error
            throw new CustomUserMessageAuthenticationException('Username could not be found.');
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        // Check the user's password or other credentials and return true or false
        // If there are no credentials to check, you can just return true
        throw new \Exception('TODO: check the credentials inside '.__FILE__);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

        // For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));
        throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
    }

    protected function getLoginUrl()
    {
        return $this->urlGenerator->generate('app_login');
    }
}

Et voici les modifications à apporter :

  1 <?php
  2 
  3 namespace App\Security;
  4 
  5 use Symfony\Component\HttpFoundation\RedirectResponse;
  6 use Symfony\Component\HttpFoundation\Request;
  7 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  8 use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  9 use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
 10 use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
 11 use Symfony\Component\Security\Core\Security;
 12 use Symfony\Component\Security\Core\User\UserInterface;
 13 use Symfony\Component\Security\Core\User\UserProviderInterface;
 14 use Symfony\Component\Security\Csrf\CsrfToken;
 15 use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
 16 use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
 17 use Symfony\Component\Security\Http\Util\TargetPathTrait;
 18 use Symfony\Component\Ldap\Exception\ConnectionException;
 19 use Symfony\Component\Ldap\Ldap;
 20 
 21 class LdapFormAuthenticator extends AbstractFormLoginAuthenticator
 22 {
 23     use TargetPathTrait;
 24 
 25     private $urlGenerator;
 26     private $csrfTokenManager;
 27     protected $ldap;
 28 
 29     public function __construct(UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, Ldap $ldap)
 30     {
 31         $this->urlGenerator = $urlGenerator;
 32         $this->csrfTokenManager = $csrfTokenManager;
 33         $this->ldap = $ldap;
 34     }
 35 
 36     public function supports(Request $request)
 37     {
 38         return 'app_login' === $request->attributes->get('_route')
 39             && $request->isMethod('POST');
 40     }
 41 
 42     public function getCredentials(Request $request)
 43     {
 44         $credentials = [
 45             'username' => $request->request->get('username'),
 46             'password' => $request->request->get('password'),
 47             'csrf_token' => $request->request->get('_csrf_token'),
 48         ];
 49         $request->getSession()->set(
 50             Security::LAST_USERNAME,
 51             $credentials['username']
 52         );
 53 
 54         return $credentials;
 55     }
 56 
 57     public function getUser($credentials, UserProviderInterface $userProvider)
 58     {
 59         $token = new CsrfToken('authenticate', $credentials['csrf_token']);
 60         if (!$this->csrfTokenManager->isTokenValid($token)) {
 61             throw new InvalidCsrfTokenException();
 62         }
 63 
 64         $user = $userProvider->loadUserByUsername($credentials['username']);
 65 
 66         if (!$user) {
 67             throw new CustomUserMessageAuthenticationException('Username could not be found.');
 68         }
 69 
 70         return $user;
 71     }
 72 
 73     public function checkCredentials($credentials, UserInterface $user)
 74     {
 75         try {
 76             $this->ldap->bind($user->getEntry()->getDn(), $credentials['password']);
 77         } catch (ConnectionException $e) {
 78             return false;
 79         }
 80 
 81         return true;
 82     }
 83 
 84     public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
 85     {
 86         $request->getSession()->getFlashBag()->add(
 87             'info',
 88             'Bienvenue. Vous êtes connecté !'
 89         );
 90 
 91         if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
 92             return new RedirectResponse($targetPath);
 93         }
 94 
 95         return new RedirectResponse($this->urlGenerator->generate('home_index')); // <== Adapter la route selon les besoins
 96     }
 97 
 98     protected function getLoginUrl()
 99     {
100         return $this->urlGenerator->generate('app_login');
101     }
102 }

Explications :

  • Lignes 18 et 19
    • Ajout des classes nécessaires dans le contrôleur
  • Lignes 27, 29 et 33
    • Injection du service LDAP dans la classe du contrôleur
  • Lignes 75 à 81
    • Ré-écriture de la méthode checkCredentials afin d'utiliser le service LDAP pour vérifier le mot de passe entré par l'utilisateur
  • Lignes 86 à 89
    • Ajout d'un message flash notifiant du succès de la connexion
  • Ligne 95
    • Définition d'une route par défaut vers laquelle rediriger si la valeur $targetPath n'est pas définie (la valeur de cette variable correspond à la route dont l'accès a été demandé avant d'être redirigé vers la page d'authentification)

Pour le fichier templates/security/login.html.twig, de base, il répond aux besoins de l'authentification sans modification supplémentaire. Il est toutefois possible de le modifier pour adapter le contenu visuel.

Mise en place du filtrage et des rôles

Les rôles déjà existants dans Symfony sont les suivants :

  • ROLE_USER
Ce rôle est affecté automatiquement à chaque utilisateur
  • ROLE_ADMIN
C'est un rôle par défaut pour les actions d'administration. Il n'est pas obligatoire de l'utiliser.
  • ROLE_ALLOWED_TO_SWITCH
Rôle donnant l'autorisation de pouvoir basculer vers un autre utilisateur sans s'authentifier.

Comme indiqué lors de la surcharge du fournisseur LDAP, à chaque groupe d'appartenance de l'utilisateur sera associé un rôle équivalent de la forme ROLE_<nom-du-groupe>. De ce fait, si un utilisateur appartient aux groupes BUREAU_ETUDES et DEVELOPPEURS, lors de son authentification au travers du LDAP, il se verra affecter les rôles ROLES_BUREAU_ETUDES et ROLES_DEVELOPPEURS.

Attention ! Le fournisseur LDAP va créer des rôles en fonction des groupes d'appartenance des utilisateurs, mais rien n'empêche de créer des rôles supplémentaires dans l'application.

Ainsi, en s'inspirant de la documentation officielle de Symfony sur les rôles, il est possible de produire ce genre de filtrage :

security:
    providers:
        ldap_users:
            id: App\Security\CustomLdapUserProvider
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: lazy
            provider: ldap_users
            guard:
                authenticators:
                    - App\Security\LoginFormAuthenticator
            logout:
                path: app_logout
                target: home_index

    role_hierarchy:
        ROLE_MANAGERS:       [ROLE_ADMIN,ROLE_ALLOWED_TO_SWITCH]
    # Note: Only the *first* access control that matches will be used
    access_control:
        - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/(admin|manage/users), roles: 'ROLE_MANAGERS' }
        - { path: ^/(add|edit|delete), roles: ROLE_ADMIN, ip: 192.168.0.33 }
        - { path: ^/, roles: IS_AUTHENTICATED_REMEMBERED }

Explications :

  • role_hierarchy
Cette section permet d'imbriquer des rôles dans d'autres rôles
Par exemple, si un utilisateur du LDAP appartient au groupe MANAGERS, le fournisseur LDAP lui affectera le rôles ROLE_MANAGERS et la configuration du module de sécurité lui affectera les rôles ROLE_ADMIN et ROLE_ALLOWED_TO_SWITCH
  • access_control
Cette section est le cœur même du filtrage dans Symfony
Pour chaque requête, chaque règle de filtrage est analysée. Seule la première qui correspond à la requête sera utilisée, les suivantes seront ignorées.
  • { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
Cette règle autorise quiconque à accéder à la page /login. Dans cet exemple, cette page correspond à l'authentification, cette règle est donc obligatoire quand on veut authentifier ses utilisateurs.
  • { path: ^/(admin|manage|users), roles: 'ROLE_MANAGERS' }
Seuls les utilisateurs ayant le rôle ROLE_MANAGERS peuvent accéder aux pages /admin, /manage et /users.
  • { path: ^/(add|edit|delete), roles: ROLE_ADMIN, ip: 192.168.0.33 }
Seuls les utilisateurs ayant le rôle ROLE_ADMIN et ayant l'adresse IP 192.168.0.33 peuvent accéder aux pages /add, /edit et /delete.
  • { path: ^/, roles: IS_AUTHENTICATED_REMEMBERED }
Seuls les utilisateurs authentifiés peuvent accéder au contenu du site.

Résumé