Commit a86e481d authored by Thibaut Vallee's avatar Thibaut Vallee
Browse files

Release: mise à jour version API 0.0.9

parent 912520b7
Loading
Loading
Loading
Loading
+1 −1
Changes for .version: 1 added line, 1 removed line.
Original line number Diff line number Diff line
0.0.8
0.0.9

config/services.yaml

deleted100644 → 0
+0 −7
Changes for config/services.yaml: 0 added lines, 7 removed lines.
Original line number Diff line number Diff line
services:
  _defaults:
    autowire: true
    autoconfigure: true

  Unicaen\Authentification\:
    resource: '../src/*'
 No newline at end of file
+12 −0
Changes for src/src/Entity/RefreshToken.php: 12 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Unicaen\Authentification\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken as BaseRefreshToken;

#[ORM\Entity]
#[ORM\Table(name: 'refresh_tokens')]
class RefreshToken extends BaseRefreshToken
{
}
+143 −0
Changes for src/src/Security/LdapPasswordAuthenticator.php: 143 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Unicaen\Authentification\Security;

use Unicaen\Authentification\Service\LdapService;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Unicaen\Utilisateur\Model\UserInterface;

/**
 * Authentificateur personnalisé pour gérer la connexion via l'annuaire LDAP.
 * Il intercepte les tentatives de connexion pour les utilisateurs marqués 'ldap' en base de données.
 */
class LdapPasswordAuthenticator extends AbstractAuthenticator
{
    private Ldap $ldap;
    private LdapService $ldapService;
    private UserProviderInterface $userProvider;
    private AuthenticationSuccessHandlerInterface $successHandler;
    private AuthenticationFailureHandlerInterface $failureHandler;

    /**
     * Injection des dépendances nécessaires :
     * - LdapService : Pour la recherche d'UID et la vérification du mot de passe (bind).
     * - UserProvider : Pour charger l'utilisateur local et vérifier son type.
     * - Handlers : Pour générer les réponses JWT (LexikJWT) en cas de succès ou d'échec.
     */
    public function __construct(

        //Ldap $ldap,
        LdapService $ldapService,
        UserProviderInterface $userProvider,
        AuthenticationSuccessHandlerInterface $successHandler,
        AuthenticationFailureHandlerInterface $failureHandler
    )
    {
        //$this->ldap = $ldap;
        $this->ldapService = $ldapService;
        $this->userProvider = $userProvider;
        $this->successHandler = $successHandler;
        $this->failureHandler = $failureHandler;
    }

    /**
     * Étape 1 : Déterminer si cet authentificateur doit gérer la requête.
     * On ne l'active que si :
     * 1. La route correspond à l'authentification (/auth).
     * 2. L'utilisateur existe en base locale et possède le marqueur 'ldap' dans son champ password.
     */
    public function supports(Request $request): ?bool
    {
        // Vérifie que nous sommes sur la bonne route en méthode POST
        if (!($request->isMethod('POST') && $request->attributes->get('_route') === 'auth')) {
            return false;
        }

        // Extraction du login depuis le corps de la requête JSON
        $data = json_decode($request->getContent(), true);
        $login = $data['username'] ?? null;

        if (!$login) {
            return false;
        }
        try {
            // On charge l'utilisateur local pour vérifier s'il doit être authentifié via LDAP
            $user = $this->userProvider->loadUserByIdentifier($login);
            // Si le mot de passe commence par 'ldap', cet authentificateur prend la main.
            // Sinon (utilisateur local), on retourne false pour laisser passer l'authentificateur standard (JsonLogin).

            return $user instanceof UserInterface && str_starts_with($user->getPassword() ?? '', 'ldap');
        } catch (\Exception $e) {
            // Si l'utilisateur n'est pas trouvé, on ne supporte pas la requête ici.
            return false;
        }
    }

    /**
     * Étape 2 : Procéder à la vérification des identifiants (Authentification réelle).
     * Cette méthode n'est appelée que si supports() a renvoyé true.
     */
    public function authenticate(Request $request): Passport
    {
        // Récupération des données JSON
        $data = json_decode($request->getContent(), true);
        $login = $data['username'] ?? '';
        $password = $data['password'] ?? '';

        if (empty($login) || empty($password)) {
            throw new CustomUserMessageAuthenticationException('Identifiants manquants.');
        }


        // Le login peut être un alias (ex: baloche191), on doit trouver l'UID réel (ex: p00147990)
            $realUid = $this->ldapService->findUidByLogin($login);

            if (!$realUid) {
                throw new CustomUserMessageAuthenticationException('Compte LDAP introuvable.');
            }

            try {
                // Tentative de "Bind" LDAP avec l'UID et le mot de passe saisi.
                // Si le bind réussit, le mot de passe est valide.
                $this->ldapService->attemptBind($realUid, $password);

                // Création du Passeport.
                // SelfValidatingPassport est utilisé car le mot de passe a déjà été validé par le LDAP.
                return new SelfValidatingPassport(new UserBadge($login));

            } catch (\Exception $e) {
                // En cas d'échec du bind (mauvais mot de passe ou erreur technique)
                throw new CustomUserMessageAuthenticationException('Mot de passe LDAP invalide.');
            }
    }

    /**
     * Étape 3 : Gestion du succès.
     * On délègue au handler de LexikJWT pour générer et renvoyer le jeton JSON.
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        return $this->successHandler->onAuthenticationSuccess($request, $token);
    }

    /**
     * Étape 4 : Gestion de l'échec.
     * On délègue au handler de LexikJWT pour renvoyer une erreur 401 standard.
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        return $this->failureHandler->onAuthenticationFailure($request, $exception);
    }
}
+175 −0
Changes for src/src/Service/LdapService.php: 175 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php
namespace Unicaen\Authentification\Service;

use Symfony\Component\Ldap\Adapter\QueryInterface;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Ldap;

/**
 * Service gérant la logique métier liée à l'annuaire LDAP.
 * Il permet la recherche, l'importation et la vérification des identifiants (Bind).
 */
class LdapService
{
    private $ldap;
    private $searchDn;  // Racine de recherche (ex: ou=people,dc=unicaen,dc=fr)
    private $uidKey;    // Attribut de l'identifiant technique (ex: 'uid')
    private string $bindDn;     // DN du compte de service (lecteur LDAP)
    private string $bindPassword;   // Mot de passe du compte de service

    /**
     * Initialisation de la connexion LDAP avec les paramètres du .env
     */
    public function __construct(string $host, int $port, string $searchDn, string $uidKey, string $bindDn, string $bindPassword)
    {
        // Création de l'adaptateur LDAP avec chiffrement TLS
        $this->ldap = Ldap::create('ext_ldap', [
            'host' => $host,
            'port' => $port,
            'encryption' => 'tls',
            'options' => [
                'protocol_version' => 3,
                'referrals' => false,   // Important pour éviter les boucles de redirection
            ],
        ]);

        $this->searchDn = $searchDn;
        $this->uidKey = $uidKey;
        $this->bindDn = $bindDn;
        $this->bindPassword = $bindPassword;
    }

    /**
     * Construit le Distinguished Name (DN) complet d'un utilisateur à partir de son UID.
     * @param string $username L'identifiant technique (ex: p00147990)
     * @return string Le DN complet (ex: uid=p00147990,ou=people,dc=unicaen,dc=fr)
     */
    private function buildUserDn(string $username): string
    {
        return sprintf('%s=%s,%s', $this->uidKey, $username, $this->searchDn);
    }

    /**
     * Tente une liaison (Bind) avec les identifiants d'un utilisateur.
     * C'est la méthode qui valide techniquement le mot de passe.
     */
    public function attemptBind(string $uid, string $password): void
    {
        $userDn = $this->buildUserDn($uid);
        try {
            // Si le bind échoue, une exception est levée
            $this->ldap->bind($userDn, $password);
        } catch (\Exception $e) {
            dd($e->getMessage());
            throw $e;
        }
    }

    /**
     * Recherche des utilisateurs pour l'auto-complétion.
     * @param string $query Terme recherché (nom, prénom ou login)
     * @return array Liste formatée pour le frontend
     */
    public function searchUsers(string $query): array
    {
        if (strlen($query) < 2) {
            return [];
        }

        // Filtre de recherche : on cherche dans le nom d'affichage OU l'identifiant technique
        $filter = sprintf('(|(displayName=*%s*)(%s=*%s*))',
            $query,
            $this->uidKey,
            $query
        );

        $results = [];

        try {
            // On se lie d'abord avec le compte de service pour avoir le droit de chercher
            $this->ldap->bind($this->bindDn, $this->bindPassword);

            // Exécution de la requête avec limitation des attributs retournés
            $search = $this->ldap->query($this->searchDn, $filter, ['filter' => ['id', 'uid', 'supannaliaslogin', 'supannAliasLogin','displayName', 'mail']]);
            $entries = $search->execute()->toArray();

            foreach ($entries as $entry) {
                /* @var Entry $entry */
                // On privilégie l'alias de login (ex: baloche191) pour le frontend
                $username = $entry->getAttribute('supannAliasLogin')[0] ?? null;

                if ($username)
                {
                    $results[] = [
                        'displayName' => $entry->getAttribute('displayName')[0] ?? $username,
                        'username' => $username,
                        'mail' => $entry->getAttribute('mail')[0] ?? null,
                    ];
                }
            }

        } catch (\Exception $e) {
            return [];
        }
        return $results;
    }

    /**
     * Récupère les détails d'un utilisateur spécifique via son alias.
     * Utilisé lors de l'importation de l'utilisateur dans la BDD locale.
     */
    public function findUserByUsername(string $username): ?array
    {
        $usernameAttribute = 'supannAliasLogin';
        $filter = sprintf('(%s=%s)', $usernameAttribute, $username);

        try {
            $this->ldap->bind($this->bindDn, $this->bindPassword);
            $search = $this->ldap->query($this->searchDn, $filter, [
                'filter' => ['mail', 'displayName', $usernameAttribute]
            ]);
            $entries = $search->execute()->toArray();

            if (count($entries) === 1) {
                $entry = $entries[0];
                return [
                    'username' => $entry->getAttribute($usernameAttribute)[0] ?? $username,
                    'displayName' => $entry->getAttribute('displayName')[0] ?? $username,
                    'mail' => $entry->getAttribute('mail')[0] ?? null,
                ];
            }
        } catch (\Exception $e) {
            return null;
        }
        return null;
    }

    /**
     * Résout un login (alias) en identifiant technique (UID).
     * Essentiel pour le processus d'authentification car le Bind nécessite le DN basé sur l'UID.
     * @param string $login L'alias saisi (ex: baloche191)
     * @return string|null L'UID réel (ex: p00147990)
     */
    public function findUidByLogin(string $login): ?string
    {
        try {
            $this->ldap->bind($this->bindDn, $this->bindPassword);

            // Filtre spécifique SupAnn pour trouver la personne par son alias
            $filter = sprintf('(&(objectClass=supannPerson)(supannAliasLogin=%s))', $this->ldap->escape($login));
            $query = $this->ldap->query($this->searchDn, $filter, [
                'scope' => QueryInterface::SCOPE_SUB,
                'filter' => ['uid']
            ]);

            $entries = $query->execute()->toArray();

            if (count($entries) === 1) {
                return $entries[0]->getAttribute('uid')[0] ?? null;
            }
        } catch (\Exception $e) {

        }
        return null;
    }
}
Loading