Astuce Les étapes d'authentification via Ldap dans Symfony

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 est m0tDeP4sseAdm1n.
  • 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é.