Connexion LDAP ou AD
(10 révisions intermédiaires par le même utilisateur non affichées) | |||
Ligne 26 : | 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 140 : | Ligne 140 : | ||
use Symfony\Component\Ldap\Entry; | use Symfony\Component\Ldap\Entry; | ||
− | |||
use Symfony\Component\Ldap\Security\LdapUser; | use Symfony\Component\Ldap\Security\LdapUser; | ||
use Symfony\Component\Ldap\Security\LdapUserProvider; | use Symfony\Component\Ldap\Security\LdapUserProvider; | ||
Ligne 206 : | Ligne 205 : | ||
'''Explications :''' | '''Explications :''' | ||
*<syntaxhighlight lang="php" inline>class CustomLdapUserProvider extends LdapUserProvider</syntaxhighlight> | *<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 | + | **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> | *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> | **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 | + | **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> | *<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 | **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 | ||
Ligne 223 : | Ligne 222 : | ||
**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> 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 | **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
Sommaire
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 :
- Mise en place de la sécurité
- Fournisseurs d'utilisateurs
- Méthodes d'interfaçage avec un LDAP
- Utiliser Guard pour s'authentifier
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 :
- 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
- 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é
- Créer la gestion des utilisateurs
- Mise en place de l'authentification
- Mise en place du filtrage et des rôles
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 :
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 :
- Fournisseur d'utilisateurs de type "entité" qui charge les utilisateurs depuis une base de données
- Fournisseur d'utilisateurs de type "LDAP" qui charge les utilisateurs depuis un serveur LDAP ou AD
- Fournisseur d'utilisateurs de type "mémoire" qui charge les utilisateurs depuis un fichier de configuration
- Fournisseur d'utilisateurs de type "chaîne" qui charge les utilisateurs depuis d'autres fournisseurs d'utilisateurs
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 :
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 parenteLdapUserProvider
- Création d'une classe enfant
- 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é)
- Déclaration des propriétés nécessaires à l'utilisation de classe
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
- C'est cette fonction
$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
- Boucle pour récupérer chaque contenu de l'attribut
$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[]
- Utilisation de la fonction PHP
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
- Si la variable
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 fonctionloadUser
)
- Simple copier/coller de la fonction privée
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.
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 :
[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
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
- Ré-écriture de la méthode
- 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)
- Définition d'une route par défaut vers laquelle rediriger si la valeur
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ôlesROLE_MANAGERS
et la configuration du module de sécurité lui affectera les rôlesROLE_ADMIN
etROLE_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 IP192.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.