Connexion LDAP ou AD
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
Mise en place de l'authentification
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
.
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.
Dans la suite de cette procédure, le nom du pare-feu utilisé est main
.
Plus d'informations sur https://symfony.com/doc/current/security.html#a-authentication-firewalls.
Authentification
A moins de définir le paramètre "security:firewalls:myfirewall:security" à false
, l'authentification est activée par défaut. 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 ajouter l'option "security:firewalls:myfirewall:anonymous" et la définir à "lazy". Ainsi, techniquement, du point de vue de Symfony, les utilisateurs non identifiés seront considérés authentifiés comme anonyme (anonymous
).