Si vous utilisez l'authentification via Ldap de Symfony 4, avec les composants symfony/ldap
et symfony/security-bundle
voici les 3 étapes qui se jouent en arrière plan :
- Authentification au Ldap
- Recherche de l'utilisateur correspondant au login
- Vérification du couple login/mot de passe
Selon l'étape, différentes classes et paramètres du framework seront utilisés.
Authentification au Ldap et recherche de l'utilisateur
Les deux premières étapes ont lieu au même endroit : dans la méthode loadUserByUsername()
du LdapUserProvider (Symfony\Component\Security\Core\User\LdapUserProvider
).
public function loadUserByUsername($username)
{
try {
// Etape 1
$this->ldap->bind($this->searchDn, $this->searchPassword);
// Etape 2
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER);
$query = str_replace('{username}', $username, $this->defaultSearch);
$search = $this->ldap->query($this->baseDn, $query);
} catch (ConnectionException $e) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username), 0, $e);
}
$entries = $search->execute();
$count = \count($entries);
if (!$count) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
}
if ($count > 1) {
throw new UsernameNotFoundException('More than one user found');
}
$entry = $entries[0];
try {
if (null !== $this->uidKey) {
$username = $this->getAttributeValue($entry, $this->uidKey);
}
} catch (InvalidArgumentException $e) {
}
return $this->loadUser($username, $entry);
}
Remarque :
Vous pouvez surcharger ce comportement en créant un UserProvider
héritant de LdapUserProvider
.
Il faut alors le déclarer dans le config/security.yaml
, et l'affecter à un firewall :
security:
providers:
# Je déclare mon provider spécifique
my_ldap_provider:
id: App\Security\MyLdapUserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/
stateless: true
anonymous: true
# J'indique que je souhaite utiliser le provider déclaré en haut
provider: my_ldap_provider
json_login_ldap:
service: Symfony\Component\Ldap\Ldap
dn_string: '%env(resolve:LDAP_SEARCH_DN_FOR_BIND)%'
check_path: api_login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
Authentification
C'est l'instruction $this->ldap->bind($this->searchDn, $this->searchPassword);
qui se charge de l'authentification.
Les arguments de la méthode sont récupérés dans les paramètres yaml du config/services.yaml
:
parameters:
# Alimentera $this->searchDn (par exemple "cn=Admin User,dc=mycompany,dc=local")
ldap.search_dn: '%env(resolve:LDAP_SEARCH_DN)%'
# Alimentera $this->searchPassword (par exemple "m0tDeP4sseAdm1n")
ldap.search_password: '%env(resolve:LDAP_SEARCH_PASSWORD)%'
Explications :
Admin User
correspond à l'identifiant d'un utilisateur ayant le droit d'accéder au Ldap, dont le mot de passe estm0tDeP4sseAdm1n
.- Le searchDn est un "chemin" pour Ldap, permettant de trouver un utilisateur. Il est composé de son CN (Common Name) et de la baseDn.
Remarque : Les valeurs de ces paramètres sont stockées dans le .env
(ou .env.local
). Elles sont récupérées via le %env(resolve:XXX)%
.
Recherche de l'utilisateur
C'est l'instruction $search = $this->ldap->query($this->baseDn, $query);
qui s'en charge.
La valeur de $this->baseDn
provient à nouveau du fichier config/services.yaml
:
parameters:
# Alimentera $this->baseDn (à priori la même chose que pour le searchDn, mais sans le nom d'utilisateur
# par exemple "dc=mycompany,dc=local")
ldap.base_dn: '%env(resolve:LDAP_BASE_DN)%'
La valeur de la $query
est un "filtre" Ldap pour trouver l'utilisateur. Il contiendra à priori son identifiant unique. Par exemple (uid=jl.david@mycompany.com)
.
Vérification du couple login/mot de passe
Cette fois ça se passe dans la méthode checkAuthentication()
de la classe LdapBindAuthenticationProvider
(Symfony\Component\Security\Core\Authentication\Provider
) :
protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
{
$username = $token->getUsername();
$password = $token->getCredentials();
if ('' === (string) $password) {
throw new BadCredentialsException('The presented password must not be empty.');
}
try {
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_DN);
if ($this->queryString) {
$query = str_replace('{username}', $username, $this->queryString);
$result = $this->ldap->query($this->dnString, $query)->execute();
if (1 !== $result->count()) {
throw new BadCredentialsException('The presented username is invalid.');
}
$dn = $result[0]->getDn();
} else {
$dn = str_replace('{username}', $username, $this->dnString);
}
// Etape 3
$this->ldap->bind($dn, $password);
} catch (ConnectionException $e) {
throw new BadCredentialsException('The presented password is invalid.');
}
}
Comme pour l'authentification, c'est l'instruction $this->ldap->bind($dn, $password);
qui est utilisée.
Cette fois le DN contiendra le nom de l'utilisateur à authentifier et $password
son mot de passe.
Il est configuré dans le fichier config/security.yaml
:
security:
api:
json_login_ldap:
# Alimentera $this->dnString (par exemple "uid={username},ou=SomeRandomOU,dc=mycompany,dc=local")
dn_string: '%env(resolve:LDAP_SEARCH_DN_FOR_BIND)%'
# Il est aussi possible d'utiliser un query_string
#query_string: '%env(resolve:LDAP_SEARCH_QUERY_STRING)%'
Le {username}
sera remplacé par le nom de l'utilisateur recherché.