S'authentifier dans Symfony 6 avec LDAP
L’objectif de ce tutoriel est d’apprendre à générer un token et un refresh_token dans Symfony en utilisant un LDAP en tant qu’Identity Provider.
Sommaire
Prérequis
Mise en situation
On va imaginer qu’on a développé une API avec une route /v1/books. Cette route est protégée et accessible uniquement lorsqu’on est authentifié à l’API. Notre besoin est d’accéder à cette route pour récupérer la collection de livre.
Résultat attendu
- L’utilisateur consomme la route /login avec son username et son password à travers une requête de type POST.
- Symfony génère un token et un refresh_token pour l’utilisateur à travers le LDAP.
- L’utilisateur accéde à la ressource protégée /v1/books.
Créer le projet
Initions le projet Symfony :
symfony new ldap_demo
On se place ensuite à la racine du projet :
cd ldap_demo
Bundles à installer
Ci-dessous la liste des bundles à installer :
composer require lexik/jwt-authentication-bundle # Gère la génération du token à l'authentification
composer require doctrine # Gère la base de données
composer require gesdinet/jwt-refresh-token-bundle # Gère la génération du refresh_token
composer require symfony/ldap # Gère la fonctionnalité LDAP dans Symfony
composer require symfony/maker-bundle # Permet de créer des contrôleurs/entités/services plus rapidement
Création du contrôleur de sécurité
Grâce au dernier bundle que l’on a installé, on peut créer un contrôleur facilement :
php bin/console make:controller
App\Controller\SecurityController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SecurityController extends AbstractController
{
/**
* @Route("/login", name="login", methods={"POST"})
*/
public function login(): Response
{
/* THIS CONTROLLER NEVER RESPOND */
return $this->json('', Response::HTTP_OK);
}
}
Paramétrer le firewall dans Symfony
On va ajouter un firewall dans la configuration de Symfony :
config\packages\security.yaml
security:
# ...
provider:
# ...
firewalls:
# ...
login:
pattern: ^/login
lazy: true # Accepte les requêtes anonymes
stateless: true
provider: ldap_server
# ...
access_control:
# ...
- { path: ^/login, roles: PUBLIC_ACCESS }
Configurer le provider
Dans notre fichier de configuration security.yaml, on a déclaré :
provider: ldap_server
Pour que Symfony interprète correctement cette information, on va devoir effectuer 2 actions :
- Déclarer le service LDAP
- Déclarer le provider
Déclarer le service LDAP
On commence par déclarer le service LDAP :
config\services.yaml
Symfony\Component\Ldap\Ldap:
arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
tags: ['ldap']
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- host: '%env(LDAP_HOST)%'
port: 389
#encryption: tls
options:
protocol_version: 3
referrals: false
# ...
.env
# ...
LDAP_HOST=XXX.XXX.XXX.XXX
# ...
Déclarer le provider
Comme le LDAP est identifié en tant que service par Symfony , on peut déclarer correctement le provider :
config\packages\security.yaml
providers:
ldap_server:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: '%env(BASE_DN_LDAP_USER_PROVIDER)%' # Domaine dans le LDAP
search_dn: '%env(SEARCH_DN_LDAP_USER_PROVIDER)%' # Utilisateur dans l'AD (readonly)
search_password: '%env(SEARCH_PASSWORD_LDAP_USER_PROVIDER)%' # Mot passe de l'utilisateur dans l'AD (readonly)
uid_key: sAMAccountName
default_roles: ROLE_USER
# ...
PS : Le champ sAMAccountName est à remplacé par uid si vous utilisez OpenLDAP et pas ActiveDirectory
.env
# ...
BASE_DN_LDAP_USER_PROVIDER="OU=SERVICEQUICONTIENTUTILISATEUR,DC=EXAMPLE,DC=COM" # Unité d'organisation ou Domaine
SEARCH_DN_LDAP_USER_PROVIDER="CN=adm-account-readonly,OU=SERVICEQUICONTIENTUTILISATEUR,DC=EXAMPLE,DC=COM" # Nom d'utilisateur du compte administrateur (readonly)
SEARCH_PASSWORD_LDAP_USER_PROVIDER="password-adm-account-readonly" # Mot de passe du compte administrateur (readonly)
# ...
Création d'un authenticator
Initions l’authenticator :
php bin/console make:auth
On va répondre aux questions :
What style of authentication do you want? [Empty authenticator]:
[0] Empty authenticator
[1] Login form authenticator
> 0
The class name of the authenticator to create (e.g. AppCustomAuthenticator):
> customLdapAuthenticator
Which firewall do you want to update? [login]:
[0] login
[1] main
> 0
La configuration est automatiquement mise à jour par Symfony :
config\packages\security.yaml
firewalls:
login:
pattern: ^/login
lazy: true # Accepte les requêtes anonymes
stateless: true
provider: ldap_server
custom_authenticator: App\Security\CustomLdapAuthenticator # Ligne ajoutée par Symfony
# ...
Code initial
On va effacer le code automatiquement généré dans l’authenticator pour le remplacer par ce contenu :
App\Security\CustomLdapAuthenticator.php
namespace App\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class CustomLdapAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
return false;
}
public function authenticate(Request $request): SelfValidatingPassport
{
return new SelfValidatingPassport(new UserBadge(''));
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return new JsonResponse('');
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse('');
}
}
Implémentation de l'authenticator
Pour implémenter l’authenticator, nous avons besoin de créer quelques éléments :
Classes
Qui représentent des exceptions
Entité
Qui représente un utilisateur
Service
Qui va implémenter l'Active Directory.
Création des exceptions
App\Exception\CustomBadRequestException.php
namespace App\Exception;
use Symfony\Component\Config\Definition\Exception\Exception;
class CustomBadRequestException extends Exception
{
}
App\Exception\CustomUnsupportedMediaTypeException.php
namespace App\Exception;
use Symfony\Component\Config\Definition\Exception\Exception;
class CustomUnsupportedMediaTypeException extends Exception
{
}
Création de l'entité "User"
- login (string), non nulle
- roles (json), non nulle
- nomUser (string), possiblement nulle.
- prenomUser (string), possiblement nulle.
- isActive (bool), non nulle.
- lastLogin (datetime), non nulle.
- lastRefresh (datetime), possiblement nulle.
On créer l’entité :
php bin/console make:entity
App\Entity\User.php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\Column(type: 'string', length: 255)]
private $login;
#[ORM\Column(type: 'json')]
private $roles = [];
#[ORM\Column(type: 'string', length: 255, nullable: true)]
private $nomUser;
#[ORM\Column(type: 'string', length: 255, nullable: true)]
private $prenomUser;
#[ORM\Column(type: 'boolean')]
private $isActive;
#[ORM\Column(type: 'datetime')]
private $lastLogin;
#[ORM\Column(type: 'datetime', nullable: true)]
private $lastRefresh;
public function getId(): ?int
{
return $this->id;
}
public function getLogin(): ?string
{
return $this->login;
}
public function setLogin(string $login): self
{
$this->login = $login;
return $this;
}
public function getRoles(): ?array
{
return $this->roles;
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
public function getNomUser(): ?string
{
return $this->nomUser;
}
public function setNomUser(?string $nomUser): self
{
$this->nomUser = $nomUser;
return $this;
}
public function getPrenomUser(): ?string
{
return $this->prenomUser;
}
public function setPrenomUser(?string $prenomUser): self
{
$this->prenomUser = $prenomUser;
return $this;
}
public function getIsActive(): ?bool
{
return $this->isActive;
}
public function setIsActive(bool $isActive): self
{
$this->isActive = $isActive;
return $this;
}
public function getLastLogin(): ?\DateTimeInterface
{
return $this->lastLogin;
}
public function setLastLogin(\DateTimeInterface $lastLogin): self
{
$this->lastLogin = $lastLogin;
return $this;
}
public function getLastRefresh(): ?\DateTimeInterface
{
return $this->lastRefresh;
}
public function setLastRefresh(?\DateTimeInterface $lastRefresh): self
{
$this->lastRefresh = $lastRefresh;
return $this;
}
}
Création du service AD
config\services.yaml
App\Service\ActiveDirectory:
bind:
$ldapServiceDn: '%env(SERVICE_DN_AD)%'
$ldapServiceUser: '%env(SERVICE_USER_AD)%'
$ldapServicePassword: '%env(SERVICE_PASSWORD_AD)%'
# ...
App\Service\ActiveDirectory.php
namespace App\Service;
use Symfony\Component\Ldap\Adapter\ExtLdap\Adapter;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Ldap\Ldap;
class ActiveDirectory
{
public function __construct(
private Adapter $ldapAdapter,
private string $ldapServiceDn,
private string $ldapServiceUser,
private string $ldapServicePassword,
private Ldap $ldap,
) {
$this->ldap = new Ldap($this->ldapAdapter);
$this->ldap->bind(implode(',', [$ldapServiceUser, $ldapServiceDn]), $ldapServicePassword);
}
// Récupère un utilisateur AD via le LDAP à partir des informations envoyées
public function getEntryFromActiveDirectory(string $username, string $password): ?Entry
{
$ldap = new Ldap($this->ldapAdapter);
$search = false;
$value = null;
try {
$ldap->bind(implode(',', ['CN='.$username, $this->ldapServiceDn]), $password);
if ($this->ldapAdapter->getConnection()->isBound()) {
$search = $ldap->query(
'DC=example,DC=com',
'(&(objectClass=user)(| (cn='.$username.')))'
)->execute()->toArray();
}
} catch (ConnectionException) {
return null;
}
if ($search && 1 === count($search)) {
$value = $search[0];
}
return $value;
}
}
Code actualisé
App\Security\CustomLdapAuthenticator.php
namespace App\Security;
use App\Entity\User;
use App\Exception\CustomBadRequestException;
use App\Exception\CustomUnsupportedMediaTypeException;
use App\Repository\UserRepository;
use App\Service\ActiveDirectory;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGeneratorInterface;
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface;
use InvalidArgumentException;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class CustomLdapAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
/* AUTOWIRING DES OBJETS PAR SYMFONY */
public function __construct(
private ActiveDirectory $activeDirectory,
private JWTEncoderInterface $encoder,
private UserRepository $userRepository,
private EntityManagerInterface $entityManager,
private RefreshTokenGeneratorInterface $refreshTokenGenerator,
private RefreshTokenManagerInterface $refreshTokenManager)
{}
/* CETTE FONCTION EST APPELLE POUR CHAQUE REQUÊTE, C'EST A NOUS DE DÉCIDER SI ON APPLIQUE LE CONTRÔLE D'IDENTIFICATION OU NON */
public function supports(Request $request): ?bool
{
/* TEST SI LA ROUTE est /login A PARTIR DE L'API && SI LA METHODE EST DE TYPE POST
-> SI C'EST LE CAS, ON DECLENCHE L'AUTHENTIFICATION,
-> SINON, ON IGNORE L'AUTHENTICATOR
*/
return 'login' === $request->attributes->get('_route') && $request->isMethod(Request::METHOD_POST);
}
public function authenticate(Request $request): SelfValidatingPassport
{
/* TEST SI LE CONTENT-TYPE EST OK */
if ('json' != $request->getContentType() || null == $request->getContentType()) {
throw new CustomUnsupportedMediaTypeException('WRONG CONTENT-TYPE');
}
/* ON RECUPERE LES INFOS A PARTIR DU CORPS DE LA REQUÊTE */
$body = json_decode($request->getContent());
if (!isset($body->password) || !isset($body->username) || null == $body->username || null == $body->password) {
throw new CustomBadRequestException('ERROR IN REQUEST');
}
$loginFromRequest = $body->username;
$passwordFromRequest = $body->password;
/* RECUPERE L'UTILISATEUR DANS LE LDAP (AUTHENTIFIE AU PASSAGE L'UTILISATEUR) */
$ldapEntry = $this->activeDirectory->getEntryFromActiveDirectory($loginFromRequest, $passwordFromRequest);
if (null == $ldapEntry) { /* SI ON NE RECUPERE RIEN */
throw new UserNotFoundException('IMPOSSIBLE TO RETRIEVE THE RESOURCE'); /* ON RENVOIE UN ERREUR D'AUTHENTIFICATION */
} else { /* SINON L'UTILISATEUR EXISTE DANS LE LDAP */
$userFromRepo = $this->userRepository->findOneBy(['login' => $loginFromRequest]); /* ON VERIFIE QU'IL EXISTE EN BDD */
if (null == $userFromRepo) { /* SI NON ON LE CREE */
$userToPersist = new User();
$userToPersist->setLogin($loginFromRequest);
try {
$userToPersist->setRoles(['ROLE_USER']);
$userToPersist->setLastLogin(new DateTime());
$userToPersist->setIsActive(true);
} catch (InvalidArgumentException) {
throw new AuthenticationException();
}
$this->entityManager->persist($userToPersist);
} else { /* SI OUI, ON METS UNIQUEMENT A JOUR SA DATE DE DERNIERE CONNEXION */
$userFromRepo->setLastLogin(new Datetime());
$this->entityManager->persist($userFromRepo);
}
$this->entityManager->flush();
}
/* SI TOUT S'EST BIEN PASSE ON RENVOIE UN BADGE AUTORISÉ A PASSER DANS SYMFONY */
return new SelfValidatingPassport(new UserBadge($loginFromRequest));
}
/* CETTE FONCTION EST APPELLÉE SI L'AUTHENTIFICATION A RÉUSSIE */
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
/* ON RECUPERE LES ROLES DE L'UTILISATEUR A PARTIR DE LA BDD */
try {
$user = $this->userRepository->findOneBy(['login' => $token->getUserIdentifier()]);
/* RECUPERE LES ROLES DE L'UTILISATEUR */
if (null !== $user) {
$roles = $user->getRoles();
} else {
$roles = null;
}
/* ON GENERE LE TOKEN AVEC UNE DUREE DE VIE DE 1200s (20mn) */
$jwtToken = $this->encoder->encode(['username' => $token->getUserIdentifier(), 'roles' => $roles, 'id' => $user->getId(), 'exp' => time() + 1200]);
/* ON VA VERIFIER QU'UN REFRESH_TOKEN N'EXISTE PAS DEJA ET SI C'EST LE CAS ON LE RETIRE */
$refreshToken = $this->refreshTokenManager->getLastFromUsername($token->getUserIdentifier());
if( null !== $refreshToken) {
$this->refreshTokenManager->delete($refreshToken);
}
/* ON GENERE LE REFRESH TOKEN AVEC UNE DUREE DE VIE DE 30 JOURS */
$refreshToken = $this->refreshTokenGenerator->createForUserWithTtl($token->getUser(), 2592000);
/* ON PERSISTE LE REFRESH TOKEN EN BASE */
$this->entityManager->persist($refreshToken);
/* ON ENREGISTRE TOUS LES TOKENS */
$this->entityManager->flush();
} catch (JWTEncodeFailureException $JWTEncodeFailureException) {
return new JsonResponse(['message' => $JWTEncodeFailureException->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
} catch (Exception $e) {
return new JsonResponse(['message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
/* ON RENVOIE LE TOKEN A L'UTILISATEUR */
return new JsonResponse(['token' => $jwtToken, 'refresh_token' => $refreshToken->getRefreshToken()], Response::HTTP_CREATED);
}
/* CETTE FONCTION EST APPELLEE SI L'AUTHENTIFICATION A ÉCHOUÉE */
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
/* ON GENERE L'ERREUR DE FACON DYNAMIQUE */
if ($exception instanceof CustomUnsupportedMediaTypeException) {
$codeResponse = Response::HTTP_UNSUPPORTED_MEDIA_TYPE;
} elseif ($exception instanceof CustomBadRequestException) {
$codeResponse = Response::HTTP_BAD_REQUEST;
} elseif ($exception instanceof UserNotFoundException) {
$codeResponse = Response::HTTP_NOT_FOUND;
} else {
$codeResponse = Response::HTTP_UNAUTHORIZED;
}
$data = [
/* ON RECUPERE LE MESSAGE D'EXCEPTION */
'message' => strtr($exception->getMessageKey(), $exception->getMessageData()),
];
/* ON RETOURNE LE MESSAGE D'ERREUR A L'UTILISATEUR */
return new JsonResponse($data, $codeResponse);
}
/* CETTE FONCTION EST APPELLÉE SI L'UTILISATEUR CONSOMME UNE ROUTE PROTEGEE, SANS FOURNIR D'INFORMATION D'AUTHENTIFICATION */
public function start(Request $request, AuthenticationException $authException = null): ?Response
{
$exception = new MissingTokenException('JWT Token not found', 0, $authException);
$event = new JWTNotFoundEvent($exception, new JWTAuthenticationFailureResponse($exception->getMessageKey()));
return $event->getResponse();
}
}
Configurer les clés JWT
php bin/console lexik:jwt:generate-keypair
Générer la base de données
php bin/console d:s:d --force
php bin/console d:s:c
Authentification à l'API
Démarrer l'API
Il faut commencer par démarrer l’API :
symfony server:start --no-tls
Créer le token avec Postman
On va essayer de consommer la route http://127.0.0.1:8000/login pour récupérer nos tokens :
Résultat
Exposer la collection de livre
On souhaite exposer une ressource qui expose la collection de livre, pour ça il faut générer un nouveau contrôleur :
php bin/console make:controller
App\Controller\BookController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class BookController extends AbstractController
{
/**
* @Route("/v1/books", name="getAllBooks", methods={"GET"})
*/
public function getAllBooks(): Response
{
return $this->json([[ "id" => 1, "nomLivre" => "nomLivre1", "nombrePageLivre1" => 10], [ "id" => 2, "nomLivre" => "nomLivre2", "nombrePageLivre2" => 15 ]]);
}
}
Sécuriser l'accès à l'API
config\packages\security.yaml
main:
stateless: true # Stateless veut dire qu'on va essayer d'authentifier le consommateur de la requête à chaque fois.
pattern: ^/(v1/*) # On sécurise toutes les routes qui comment par /v1/
provider: ldap_server # Le user provider est notre LDAP
entry_point: App\Security\CustomLdapAuthenticator # Le point d'entrée est l'authenticator custom que l'on a crée, il est appelé si aucun token n'est détécté lorsque la requête s'exécute.
# ...
En complément, on va ajouter un contrôle d’accès sur les routes :
config\packages\security.yaml
access_control:
- { path: ^/login, roles: PUBLIC_ACCESS } # La route /login est accessible pour tout le monde
- { path: ^/v1/*, roles: ROLE_USER} # Toutes les routes qui commencent par /v1/ sont soumise à la condition que l'utilisateur aie au moins le rôle ROLE_USER pour accéder à la ressource
# ...
Accéder à la ressource protégée
Créer le nouvel authenticator
php bin/console make:auth
App\Security\CustomJWTAuthenticator.php
namespace App\Security;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\ExpiredTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidPayloadException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class CustomJWTAuthenticator extends AbstractAuthenticator
{
/** AUTOWIRING PAR SYMFONY */
public function __construct(
private JWTTokenManagerInterface $jwtManager,
private EventDispatcherInterface $eventDispatcher,
private TokenExtractorInterface $tokenExtractor,
private UserProviderInterface $userProvider)
{}
/* CONTRÔLE SI ON DÉCLENCHE L'AUTHENTIFICATION OU NON -> BASÉ SUR L'EXISTENCE DU TOKEN DANS LA REQUÊTE */
public function supports(Request $request): ?bool
{
return false !== $this->getTokenExtractor()->extract($request);
}
/* SI LE TOKEN EXISTE, ALORS ON VA VERIFIER QU'ON RETROUVE BIEN NOTRE USER DANS L'AD */
public function authenticate(Request $request): SelfValidatingPassport
{
/* RÉCUPERE LE TOKEN A PARTIR DE LA REQUÊTE */
$token = $this->getTokenExtractor()->extract($request);
if (!$payload = $this->jwtManager->parse($token)) {
throw new InvalidTokenException('Invalid JWT Token');
}
/* RÉCUPERE L'IDENTIFIANT DE L'UTILISATEUR */
$idClaim = $this->jwtManager->getUserIdClaim();
if (!isset($payload[$idClaim])) {
throw new InvalidPayloadException($idClaim);
}
/* GENERE AUTOMATIQUEMENT LE BADGE POUR PASSER LA SECURITÉ DE SYMFONY */
$passport = new SelfValidatingPassport(
new UserBadge($payload[$idClaim],
function ($userIdentifier) { /* CALLBACK QUI CONTACTE LE LDAP POUR CHERCHER L'UTILISATEUR DANS L'AD, LE SYSTEME VA AUTOMATIQUEMENT CHERCHER VIA LE LDAP CAR ON A CONFIGURÉ LE PROVIDER A "ldap_server" DANS LE FIREWALL "main" */
return $this->userProvider->loadUserByIdentifier($userIdentifier);
})
);
/* ON AJOUTE LE CONTENU DE LA REPONSE AU PAYLOAD DU PASSPORT POUR QUE SYMFONY PUISSE FONCTIONNER */
$passport->setAttribute('payload', $payload);
return $passport;
}
/* SI L'AUTHENTIFICATION EST UN SUCCESS, ALORS ON LAISSE PASSER LA REQUÊTE SANS RIEN FAIRE */
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
/* SI L'AUTHENTIFICATION EST UN ÉCHEC */
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
/* ON GÉNÉRE UN MESSAGE D'ERREUR CLAIR */
$errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
$response = new JWTAuthenticationFailureResponse($errorMessage);
if ($exception instanceof ExpiredTokenException) {
$event = new JWTExpiredEvent($exception, $response);
$eventName = Events::JWT_EXPIRED;
} else {
$event = new JWTInvalidEvent($exception, $response);
$eventName = Events::JWT_INVALID;
}
$this->eventDispatcher->dispatch($event, $eventName);
return $event->getResponse();
}
/* FONCTION QUI RETOURNE LE TOKEN EXTRACTOR POUR EXTRAIRE LE TOKEN DE LA REQUÊTE */
public function getTokenExtractor(): ?TokenExtractorInterface
{
return $this->tokenExtractor;
}
}
18 réflexions au sujet de « S’authentifier dans Symfony 6 avec LDAP »
Super ! Merci beaucoup !
Ton tuto est très clair.
Je mets en place et je te laisse mon retour aprés 😉
Merci beaucoup !
Salut, merci pour ce tuto très clair
j’ai essayé de le mettre en place, j’ai eu aucun soucis jusqu’à la fin avec le CustomJWTAuthenticator.
Cannot autowire service “App\Security\CustomJWTAuthenticator”: argument “$userProvider” of method “__construct()” references interface “Symfony\Component\Security\Core\User\UserProviderInterface” but no such service exists.
A priori, vu le message d’erreur que tu obtiens, ton injection de service est mal configuré dans le fichier services.yaml ou ton userProvider est mal déclaré dans security.yaml!
Bonjour,
Merci pour le tuto mais j’ai un souci.
La connexion fonctionne parfaitement mais j’ai un souci lorsque j’utilise le token.
Symfony ne rentre jamais dans “CustomJWTAuthenticator”.
J’ai un message d’erreur : {“code”:401,”message”:”JWT Token not found”} qui vient de “CustomLdapAuthenticator” dans la fonction start.
Ce qui me semble logique.
Aurais-tu une idée de mon erreur ?
Merci
Bonjour Laurent,
C’est parce qu’il manque un bout dans le tutoriel (c’est ma faute, j’ai oublié de la rajouter, il faudra que je mette à jour cet article), voici en gros :
https://github.com/markitosgv/JWTRefreshTokenBundle#step-4-symfony-54
Et ce qu’il faut faire ensuite :
1) Il faut définir une route de refresh dans la
config/routes.yml
2) Et après t’as juste a renseigner ça dans le
security.yaml
:refresh_jwt: check_path: /api/token/refresh # or, you may use the `api_refresh_token` route name
Mes excuses pour le temps de réponse, j’ai été débordé de travail ces derniers temps.
Super ! Merci beaucoup !
j’ai un problème consternent les trois variables, SERVICE_DN_AD, SERVICE_USER_AD, SERVICE_PASSWORD_AD, c’est quoi le contenues des c’est trois variable
Bonjour !
Les valeurs à renseigner dans des constantes viennent de l’active directory, l’url de connexion à l’AD ressemble à quelquechose comme ça je crois (SERVICE_DN_AD) = DN=localhost,DOMAIN=local…etc , sinon pour les constantes SERVICE_USER_AD, ça va être un nom d’utilisateur de l’AD et la constante SERVICE_PASSWORD_AD, c’est le mot de passe du user dans l’AD (de mémoire l’user doit avoir des accès admin)
Bonjour!
J’ai un problème de credentials lié aux variables SERVICE_DN_AD, SERVICE_USER_AD et SERVICE_PASSWORD_AD. que faut-il exactement mettre dans ces variables?
Merci!
Bonjour !
Les valeurs à renseigner dans des constantes viennent de l’active directory, l’url de connexion à l’AD ressemble à quelquechose comme ça je crois (SERVICE_DN_AD) = DN=localhost,DOMAIN=local…etc , sinon pour les constantes SERVICE_USER_AD, ça va être un nom d’utilisateur de l’AD et la constante SERVICE_PASSWORD_AD, c’est le mot de passe du user dans l’AD (de mémoire l’user doit avoir des accès admin)
mercii pour votre tuto mais j’aimerai savoir quel est le systeme d’exploitation utilise ici ?
Bonjour, c’est effectué sur une instance ALPINE LINUX
merci beaucoup pour ce tuto détaillé. Je suis à l’étape Code actualisé et j’ai cette erreur: Cannot autowire service “App\Security\CustomLdapAuthenticator”: argument “$refreshTokenGenerator” of method “__construct()” references interface “Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGeneratorInterface” but no such service exists. Did you create a class that implements this interface?
Bonjour,
Je t’invites à regarder ma réponse à Laurent plus haut, je pense que c’est le même problème que tu rencontres (bien que tu aies un message d’erreur différents).
Bonne journée.
Bonjour,
Super tuto merci beaucoup. Je bloque par contre sur le même problème que sam et Hugo :
Cannot autowire service “App\Security\CustomLdapAuthenticator”: argument “$refreshTokenGenerator” of method “__construct()” references interface “Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGeneratorInterfa
ce” but no such service exists. Did you create a class that implements this interface?
Voici mon fichier services.yaml :
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: ‘../src/’
exclude:
– ‘../src/DependencyInjection/’
– ‘../src/Entity/’
– ‘../src/Kernel.php’
Symfony\Component\Ldap\Ldap:
arguments: [‘@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter’]
tags: [‘ldap’]
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
– host: ‘%env(LDAP_HOST)%’
port: 389
#encryption: tls
options:
protocol_version: 3
referrals: false
App\Service\ActiveDirectory:
bind:
$ldapServiceDn: ‘%env(SERVICE_DN_AD)%’
$ldapServiceUser: ‘%env(SERVICE_USER_AD)%’
$ldapServicePassword: ‘%env(SERVICE_PASSWORD_AD)%’
et mon fichier security.yaml :
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: ‘auto’
providers:
ldap_server:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: ‘%env(BASE_DN_LDAP_USER_PROVIDER)%’ # Domaine dans le LDAP
search_dn: ‘%env(SEARCH_DN_LDAP_USER_PROVIDER)%’ # Utilisateur dans l’AD (readonly)
search_password: ‘%env(SEARCH_PASSWORD_LDAP_USER_PROVIDER)%’ # Mot passe de l’utilisateur dans l’AD (readonly)
uid_key: sAMAccountName
default_roles: ROLE_USER
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: ldap_server
custom_authenticator: App\Security\CustomLdapAuthenticator
login:
pattern: ^/login
lazy: true # Accepte les requêtes anonymes
stateless: true
provider: ldap_server
access_control:
– { path: ^/login, roles: PUBLIC_ACCESS }
y’a t il quelque chose que j’ai loupé? Merci beaucoup.
Bonne journée
Bonjour Krish, voici ce que j’avais répondu plus haut pour ce problème :
—–
C’est parce qu’il manque un bout dans le tutoriel (c’est ma faute, j’ai oublié de la rajouter, il faudra que je mette à jour cet article), voici en gros :
https://github.com/markitosgv/JWTRefreshTokenBundle#step-4-symfony-54
Et ce qu’il faut faire ensuite :
1) Il faut définir une route de refresh dans la config/routes.yml
2) Et après t’as juste a renseigner ça dans le security.yaml :
refresh_jwt: check_path: /api/token/refresh # or, you may use the `api_refresh_token` route name
—-
Bonjour,
J’ai essayé d’implémenter en suivant à la lettre le tuto, mais le token n’est pas généré.
Je tombe sur ‘ ‘ juste
#[Route(path: ‘/login’, name: ‘login’, methods: [‘POST’])]
public function login(): Response
{
/* THIS CONTROLLER NEVER RESPOND */
return $this->json(‘ICI’, Response::HTTP_OK);
}
j’ai donc ‘ICI’ quand je lance la requête sur postman
Il faut changer cette route?
Merci d’avance
Bonjour Mathieu, je me demande s’il te manque pas un module Symfony pour que l’authenticator fonctionne !
Bon courage !
Sinon peut-être un réglage dans security.yaml
Et bonne journée !