Salut,
Ce que je te propose ici c'est de pouvoir authentifier uniquement à l'aide de certain user-agent mais il faut t'être ouvert d'esprit cela pourrait être une api ou autre chose…
Dans un premier temps je te propose d'installer un projet symfony:
- – symfony new my_project_name --full
ou
– composer create-project symfony/website-skeleton my_project_name
- Ici je ne vais pas détailler ces commandes ni les suivantes si tu es là tu les connais .

check point : Évidement si tout va bien vous avez l'accueil de Symfony 5.3
– make:user → email
– doctrine:schema:update --force ou migrate ect..
– make:auth → Login form authenticator → UserCustomAuthenticator
→ tu choisis : Login form authenticator
→ Le nom de la class : UserCustomAuthenticator
→ et par défaut : SecurityController
→ option logout : yes
→ bin/console make:register
→ @UniqueEntity : yes
→ send an email : no
→ automatically authenticate: no
→ redirected to : app_login
Ici en gros on a une authentification traditionnelle qui va très bien en soit.
On va créer 3 contrôleurs:
le premier : → bin/console make:controller →HomeController
le deuxième: → bin/console make:controller →User
le troisième: → bin/console make:controller →OtherSecurityZoneArea
→ Voilà on va configurer nos routes:
Contrôleur : HomeController
class HomeController extends AbstractController
{
#[Route('/', name: 'home')]
public function index(): Response
{
return $this->render('home/index.html.twig', [
'controller_name' => 'HomeController',
]);
}
}
Contrôleur : User (a modifier selon le code selon ce code ci-dessous)
#[Route('/user')]
class UserController extends AbstractController
{
#[Route('/profile', name: 'profile')]
public function index(): Response
{
return $this->render('user/index.html.twig', [
'controller_name' => 'UserController PROFILE',
]);
}
}
Contrôleur : OtherSecurityZoneArea (a modifier selon le code selon ce code ci-dessous)
#[Route('/otherzonearea')]
class OtherSecurityZoneAreaController extends AbstractController
{
#[Route('/', name: 'other_security_zone_area')]
public function index(): Response
{
return $this->render('other_security_zone_area/index.html.twig', [
'controller_name' => 'OtherSecurityZoneAreaController',
]);
}
#[Route('/secret', name: 'other_security_zone_area_secret')]
public function index2(): Response
{
return $this->render('other_security_zone_area/index.html.twig', [
'controller_name' => 'OtherSecurityZoneAreaController SECRET',
]);
}
check point : Ici tu vas vérifier si ça fonctionne
→ ton-serveur/
→ ton-serveur/user/profile
→ ton-serveur/otherzonearea
→ ton-serveur/otherzonearea/secret
Pour être le juste possible je fais ce code au fur et à mesure voici à quoi ressemble notre arborescence:

Là nous allons aller dans le fichier :
→config/packages/security.yaml
En bas du fichier écris ces lignes :
access_control:
- { path: ^/user/login, roles: PUBLIC_ACCESS }
- { path: ^/user, roles: ROLE_USER }
- { path: ^/otherzonearea, roles: ROLE_USER }
Maintenant re-teste ces liens:
→ ton-serveur/
→ ton-serveur/user/profile
→ ton-serveur/otherzonearea
→ ton-serveur/otherzonearea/secret
Comme tu le vois tu es redirigé .
***********************************************************************************
Le tuto commence ici
***********************************************************************************
Symfony nous a crée un UserCustomAuthenticator
Nous on va mettre un point d'entrée custom
Dans le dossier Security
→ on crée un dossier EntryPoint
→ dans ce dossier on crée la class : CustomEntryPoint (Tu peux l'appeler “Brouette” selon les habitudes de la maison..)
Voilà à quoi ressemble arborescence:

On va commencer par créer la class :
namespace App\Security\EntryPoint;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class CustomEntryPoint implements AuthenticationEntryPointInterface
{
public function start(Request $request, AuthenticationException $authException = null) : Response
{
dd('ici');
}
}
Bon pour le moment c'est mystérieux mais laisse toi guider encore un peu.
On retourne au security.yaml
→ Et on rajoute : entry_point: App\Security\EntryPoint\CustomEntryPoint
main:
lazy: true
entry_point: App\Security\EntryPoint\CustomEntryPoint
provider: app_user_provider
custom_authenticator: App\Security\UserCustomAuthenticator
logout:
path: app_logout
on fait encore une modif et tu vas comprendre le mécanisme :
Tu modifies UserCustomAuthenticator : tu rajoutes la méthode supports
class UserCustomAuthenticator extends AbstractLoginFormAuthenticator
{
public function supports(Request $request): bool
{
dd('la');
}
....
}
Maintenant on va regarder ce qui ce passe :
testes : → ton-serveur/user/profile
Si tu as tout fait tu dois tomber sur notre dd de UserCustomAuthenticator

Retourne dans la fonction UserCustomAuthenticator et supprime notre fonction : supports c'était juste pour te faire comprendre le cheminement .
Maintenant actualise à nouveau : → ton-serveur/user/profile
Là tu vas voir :

Ce qu'on vient de voir c'est quand tu veux accéder à une zone sécurisée :
→ tu entres dans UserCustomAuthenticator : function supports et si supports repond faux (qui se trouve dans la class mère ou cas où)
la fonction support teste si on est en POST et que URI actuelle est == à URI login (/user/login) sinon elle renvoie faux
(si la condition est vraie c'est qu'on est entrain de se loguer)
→ tu entres dans entry_point soit notre class : CustomEntryPoint puisque aucun Authenticator ne prend en charge la requête Symfony renvoie sur la route login dans le cheminement traditionnel, mais si tu as une autre logique si c'est une api au autre chose tu revois pour les navigateur le chemin login à une api peut etre une 403 ect..
Maintenant qu'est-ce qu'on doit faire ici ?
→ on va rediriger tu vas modifier CustomEntryPoint comme cela:
namespace App\Security\EntryPoint;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class CustomEntryPoint implements AuthenticationEntryPointInterface
{
public function __construct(Private UrlGeneratorInterface $urlGenerator){}
public function start(Request $request, AuthenticationException $authException = null) : Response
{
return new RedirectResponse($this->urlGenerator->generate('app_login'));
}
}
Pour résumer là on est au point de départ, symfony fonctionne comme avant :
check point : Ici tu vas vérifier si ça fonctionne tu te log et délogue
→ ton-serveur/
→ ton-serveur/user/profile
→ ton-serveur/otherzonearea
→ ton-serveur/otherzonearea/secret
Et là tu te demandes on a fait cela pourquoi…. mais attends on commence à peine..
Là on va carrément faire notre class Authenticator pour authentifier Postman et uniquement Postman mais cela pourrait être des téléphones portables ou alors un contrôle de token Bearer
Si tu ne vois pas ce que c'est (Postman) j'ai vu un article ici sur ce sujet.
Dans le dossier Security tu vas créer la class CustomAuthenticator
Notre arborescence :

namespace App\Security;
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\InteractiveAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
class CustomAuthenticator implements InteractiveAuthenticatorInterface
{
public function supports(Request $request): ?bool
{
// TODO: Implement supports() method.
}
public function authenticate(Request $request): PassportInterface
{
// TODO: Implement authenticate() method.
}
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
// TODO: Implement createAuthenticatedToken() method.
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// TODO: Implement onAuthenticationSuccess() method.
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
// TODO: Implement onAuthenticationFailure() method.
}
public function isInteractive(): bool
{
// TODO: Implement isInteractive() method.
}
}
Alors pas de panique, ces méthodes te seront proposées par ton IDE..
On doit aussi modifier notre security.yaml

On a rajouté ça : - App\Security\CustomAuthenticator
On va commencer, dans l'ordre on va indiquer à Symfony si on prend en charge cette requête :
Tu te rappeler la méthode supports ben c'est la même..
public function supports(Request $request): ?bool
{
return str_starts_with($request->getPathInfo(), '/otherzonearea') && $request->isMethod('POST') ;
}
la fonction support est le point d'entrée de notre class, on test si c'est URI qu'on veut prendre en charge et si c'est la méthode est POST
Mais pour rester cohérent on va modifier aussi notre contrôleur : OtherSecurityZoneAreaController
#[Route('/otherzonearea')]
class OtherSecurityZoneAreaController extends AbstractController
{
#[Route('/', name: 'other_security_zone_area',methods: ['POST'])]
public function index(): Response
{
return new JsonResponse(['message' => 'otherzonearea'. date('d-m-y h:i:s')],Response::HTTP_OK);
}
#[Route('/secret', name: 'other_security_zone_area_secret',methods: ['POST'])]
public function index2(): Response
{
return new JsonResponse(['message' => 'otherzonearea-secret'. date('d-m-y h:i:s')],Response::HTTP_OK);
}
}
On lui dit juste qu'on accepte que les requêtes POST et on répond en JSON.
La seconde méthode est : authenticate
Et là si tu est fatigué, fais un tour, bois un café ou vas dormir et reviens on va coder…
Le but de authenticate est de retourner un passeport mais c'est quoi..?
Un passeport est une class qui va contenir un badge utilisateur (UserInterface) et un badge Credentials (password) et evt. un badge CsrfToken tu sais ce qui se trouve caché dans ton formulaire de soumission.
public function __construct(private UserProviderInterface $userProvider,){}
public function authenticate(Request $request): PassportInterface
{
$email = $request->server->get('PHP_AUTH_USER', '');
$pwd = $request->server->get('PHP_AUTH_PW', '');
return new Passport(
new UserBadge($email, [$this->userProvider, 'loadUserByIdentifier']),
new PasswordCredentials($pwd)
);
}
On ne peut pas encore tester alors on continue mais on part dans l'idée que l'authentification a réussi
On doit continuer je t'explique après..
On va faire la class createAuthenticatedToken
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());
}
Ici on va créer un token utilisé par le moteur interne de Symfony qui va être injecter dans la session par exemple.
On continue avec createAuthenticatedToken
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
Ici nous on va retourner null mais tu pourrais retourner une redirection selon les conditions et le contexte de ton application.
check point : on va faire un test avec Postman

Si tu as tout fait tu envoies ton user avec postman en POST à : → ton-serveur/otherzonearea
Tu va recevoir un code 200 et un petit message
On va attaquer maintenant un problème de login
Avec la méthode onAuthenticationFailure :
Ici on doit fournir la réponse adéquate en cas d'erreur
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse(['error' => 'UNAUTHORIZED'], Response::HTTP_UNAUTHORIZED);
}
Ici simplement on va répondre non autorisé mais on revient après par là.
check point : on va faire un test avec Postman
Si tu as tout fait tu envoies ton user avec postman en POST à : → ton-serveur/otherzonearea avec un mauvais password

On récapitule voilà la class en entier:
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\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
class CustomAuthenticator implements InteractiveAuthenticatorInterface
{
/**
* @param UserProviderInterface $userProvider
*/
public function __construct(private UserProviderInterface $userProvider,
){}
/**
* @param Request $request
* @return bool|null
*/
public function supports(Request $request): ?bool
{
return str_starts_with($request->getPathInfo(), '/otherzonearea') && $request->isMethod('POST') ;
}
/**
* @param Request $request
* @return PassportInterface
*/
public function authenticate(Request $request): PassportInterface
{
$email = $request->server->get('PHP_AUTH_USER', '');
$pwd = $request->server->get('PHP_AUTH_PW', '');
return new Passport(
new UserBadge($email, [$this->userProvider, 'loadUserByIdentifier']),
new PasswordCredentials($pwd)
);
}
/**
* @param PassportInterface $passport
* @param string $firewallName
* @return TokenInterface
*/
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());
}
/**
* @param Request $request
* @param TokenInterface $token
* @param string $firewallName
* @return Response|null
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
/**
* @param Request $request
* @param AuthenticationException $exception
* @return Response|null
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse(['error' => 'UNAUTHORIZED'], Response::HTTP_UNAUTHORIZED);
}
public function isInteractive(): bool
{
return true;
}
}
Bon mais on a un problème c'est qu'on laisse passer tout le monde pour autant que l'user et le pass soient corrects
On va régler cela :
Tu vas créer dans le dossier security un dossier badges et dedans une class : UserAgentBadge
Notre arborescence :

Voici notre class :
namespace App\Security\badges;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
class UserAgentBadge implements BadgeInterface
{
public function isResolved(): bool
{
// TODO: Implement isResolved() method.
}
}
Bon et tu vas la modifier ainsi :
namespace App\Security\badges;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
class UserAgentBadge implements BadgeInterface
{
private bool $resolved;
public function __construct(private Request $request){
$this->resolved = str_starts_with($this->request->headers->get("user-agent"), 'Postman');
}
public function isResolved(): bool
{
return $this->resolved;
}
}
Le but d'un badge est de répondre par oui ou par non par la méthode : isResolved() obligatoire
check point : on va faire un test avec Postman
Si tu as tout fais tu envois ton user avec postman en POST à : → ton-serveur/otherzonearea ton utilisateur avec le bon mot de passe
Essaie un autre agent peu importe mets ex :
$this->resolved = str_starts_with($this->request->headers->get("user-agent"), 'Supermann');
Bon ça marche … mais on a un problème on renvoie une 401 et pas une 403..
Bon alors on continue…
Dans le dossier Security tu vas créer un dossier Exceptions et dedans une class UserAgentNotAuthorizedException
Notre arborescence :

Et voila le code :
namespace App\Security\Exceptions;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class UserAgentNotAuthorizedException extends AuthenticationException{}
Le but est d'utiliser Symfony avec notre nom de class
Maintenant on va modifier à nouveau : UserAgentBadge
namespace App\Security\badges;
use App\Security\Exceptions\UserAgentNotAuthorizedException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
class UserAgentBadge implements BadgeInterface
{
private bool $resolved;
public function __construct(private Request $request){
$this->resolved = str_starts_with($this->request->headers->get("user-agent"), 'tPostman');
}
public function isResolved(): bool
{
if (!$this->resolved)
throw new UserAgentNotAuthorizedException('User Agent FORBIDDEN', Response::HTTP_FORBIDDEN);
return $this->resolved;
}
}
Comme tu le vois on dit dans la méthode isResolved si resolved est faux on rajoute une exception.
Maintenant tu t'en doutes on va modifier la class : CustomAuthenticator → onAuthenticationFailure
Et tu dois te douter que dans cette méthode : public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
l'argument AuthenticationException n'est pas là pour rien.
/**
* @param Request $request
* @param AuthenticationException $exception
* @return Response|null
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$code = 403;
$message = 'FORBIDDEN';
if($exception instanceof BadCredentialsException){
$code = 401;
$message = $exception->getMessage();
}else if ($exception instanceof UserAgentNotAuthorizedException) {
$code = $exception->getCode();
$message = $exception->getMessage();
}
return new JsonResponse(['error' => $message], $code);
}
public function isInteractive(): bool
{
return true;
}
check point : Ici tu dois pouvoir gérer les connections avec Postman tester un mauvais email ou pass ou d'autoriser Supermann au lieu de Postman .
Au cas où je te remets la class complète :
<?php
namespace App\Security;
use App\Security\badges\UserAgentBadge;
use App\Security\Exceptions\UserAgentNotAuthorizedException;
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\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
class CustomAuthenticator implements InteractiveAuthenticatorInterface
{
/**
* @param UserProviderInterface $userProvider
*/
public function __construct(private UserProviderInterface $userProvider,
){}
/**
* @param Request $request
* @return bool|null
*/
public function supports(Request $request): ?bool
{
return str_starts_with($request->getPathInfo(), '/otherzonearea') && $request->isMethod('POST') ;
}
/**
* @param Request $request
* @return PassportInterface
*/
public function authenticate(Request $request): PassportInterface
{
$email = $request->server->get('PHP_AUTH_USER', '');
$pwd = $request->server->get('PHP_AUTH_PW', '');
return new Passport(
new UserBadge($email, [$this->userProvider, 'loadUserByIdentifier']),
new PasswordCredentials($pwd),
[
new UserAgentBadge($request),
]
);
}
/**
* @param PassportInterface $passport
* @param string $firewallName
* @return TokenInterface
*/
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());
}
/**
* @param Request $request
* @param TokenInterface $token
* @param string $firewallName
* @return Response|null
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
/**
* @param Request $request
* @param AuthenticationException $exception
* @return Response|null
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$code = 403;
$message = 'FORBIDDEN';
if($exception instanceof BadCredentialsException){
$code = 401;
$message = $exception->getMessage();
}else if ($exception instanceof UserAgentNotAuthorizedException) {
$code = $exception->getCode();
$message = $exception->getMessage();
}
return new JsonResponse(['error' => $message], $code);
}
public function isInteractive(): bool
{
return true;
}
}
Bon la tu vas devoir imaginer car je suis a court d'idée dans ton user tu as des préférences que tu gères et tu interdis l'accès à certaines ressources n'oublie pas que tu as la méthode supports et que tu fais comme tu veux!
Allez on fait quelques modifs:
On va dans le terminal
→ bin/console make:entity → User
→ et on ajoute secretaccess
→ type : boolean
→ nullable: true
et on termine avec un :
→ doctrine:schema:update --force ou migrate ect..
On va créer un nouveau badge avec la class → SecretAccessBadge
Et dans le dossier Security tu vas créer un dossier Subscribers
Et dedans tu vas créer une class SecretAccessBadgeSubscriber
Notre arborescence :

On va commencer par SecretAccessBadge enfin son squelette :
namespace App\Security\badges;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
class SecretAccessBadge implements BadgeInterface
{
private bool $resolved = false;
public function isResolved(): bool
{
return $this->resolved;
}
}
On passe maintenant à SecretAccessBadgeSubscriber
namespace App\Security\Subscribers;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SecretAccessBadgeSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
// TODO: Implement getSubscribedEvents() method.
}
}
Maintenant on doit lui dire sur quel événement on veut se connecter :
Nous on veut voir les événements passeport donc → CheckPassportEvent::class => 'onCheckPassportEvent',
Notre class ressemble maintenant à ça:
namespace App\Security\Subscribers;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
class SecretAccessBadgeSubscriber implements EventSubscriberInterface
{
public function onCheckPassportEvent(CheckPassportEvent $event)
{
}
public static function getSubscribedEvents()
{
return [
CheckPassportEvent::class => 'onCheckPassportEvent',
];
}
}
Alors on est câblé on continu:
Bon je te montre la class et je l'explique :
namespace App\Security\Subscribers;
use App\Security\badges\SecretAccessBadge;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
class SecretAccessBadgeSubscriber implements EventSubscriberInterface
{
public function onCheckPassportEvent(CheckPassportEvent $event)
{
// On recupere le passeport
/** @var Passport $passport */
$passport = $event->getPassport();
// Comme on est abonné à tous les événements on controle que le passeport contient ces badges
if ($passport->hasBadge(SecretAccessBadge::class) === false) {
return;
}
// Voir plus haut
if ($passport->hasBadge(UserBadge::class) === false) {
return;
}
/** @var UserBadge $badgeUser */
$badgeUser = $passport->getBadge(UserBadge::class);
$secretAccessBadge = $passport->getBadge(SecretAccessBadge::class);
// Ici on a les instances donc on va checker
/** @var SecretAccessBadge $badgeToken */
$secretAccessBadge->check($badgeUser->getUser());
}
public static function getSubscribedEvents()
{
return [
CheckPassportEvent::class => 'onCheckPassportEvent',
];
}
}
A / On vérifie que l'événement nous concerne il doit avoir nos deux badges
B / On récupère nos instances
C / On va exécuter → $secretAccessBadge->check($badgeUser->getUser()) sur SecretAccessBadge (elle arrive plus bas).
Et pourquoi on a fait comme ça cette fois ci à cause de l'utilisateur afin d'avoir son instance de
UserInterface
et la passer à notre badge SecretAccessBadge, je te montre juste une technique rien n'est vraiment figé….Il est vrai qu'il y a mieux comme exemple mais bon.
On va encore créer une class dans Exceptions → SecretAccessException
namespace App\Security\Exceptions;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class SecretAccessException extends AuthenticationException{}
C'est la même chose que l'autre fois..
Bon on retourne à notre class → SecretAccessBadge
namespace App\Security\badges;
use App\Repository\UserRepository;
use App\Security\Exceptions\SecretAccessException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
class SecretAccessBadge implements BadgeInterface
{
private bool $resolved = false;
public function __construct(private UserRepository $userRepository)
{
}
public function check(?UserInterface $userBadge){
if($userBadge){
$this->resolved = ($this->userRepository->findOneBy(['email'=>$userBadge->getUserIdentifier()]))->getSecretaccess()??false;
}
}
public function isResolved(): bool
{
if (!$this->resolved)
throw new SecretAccessException('FORBIDDEN', Response::HTTP_FORBIDDEN);
return $this->resolved;
}
}
Donc dans la méthode check on vérifie si cet utilisateur a droit à cette ressource par exemple . (secretaccess true/false)
Bon tu dois savoir qu'il y a plusieurs type de passeport si on travaillerait avec un token bearer j'aurais utilisé plutôt SelfValidatingPassport mais si ce tuto présente un intérêt je ferais un complément..
Bon attends c'est pas fini c'est le bonus !
Mais pour faire ça on va modifier security.yaml
main:
lazy: true
entry_point: App\Security\EntryPoint\CustomEntryPoint
provider: app_user_provider
custom_authenticator:
- App\Security\UserCustomAuthenticator
- App\Security\CustomAuthenticator
login_throttling:
max_attempts: 2
interval: '2 minutes'
Tu vas rajouter la partie login_throttling
Et là tu vas avoir une erreur
Bon t'as compris → composer require symfony/rate-limiter
Bon plus d'erreur ouf..
Ce composant limite le nombre d'erreur par minute(s)
Mais ok mais nous on veut le notifier à notre utilisateur.
ok alors on modifie onAuthenticationFailure
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$code = 403;
$message = 'FORBIDDEN';
if ($exception instanceof TooManyLoginAttemptsAuthenticationException) {
$code = Response::HTTP_UNAUTHORIZED;
$message = 'Too many failed login attempts, please try again in a few minutes.';
$MessageData = $exception->getMessageData();
if (is_array($MessageData)) {
if (key_exists('%minutes%', $MessageData)) {
$message = $exception->getMessageKey();
$message = str_replace("%minutes%", $MessageData['%minutes%'], $message);
}
}
} else if ($exception instanceof BadCredentialsException) {
$code = 401;
$message = $exception->getMessage();
} else if ($exception instanceof UserAgentNotAuthorizedException) {
$code = $exception->getCode();
$message = $exception->getMessage();
}
return new JsonResponse(['error' => $message], $code);
}
Et ici la class au complet si j'ai oublie de te dire un truc comme d'où vient le UserRepository..
<?php
namespace App\Security;
use App\Repository\UserRepository;
use App\Security\badges\SecretAccessBadge;
use App\Security\badges\UserAgentBadge;
use App\Security\Exceptions\UserAgentNotAuthorizedException;
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\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\TooManyLoginAttemptsAuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
class CustomAuthenticator implements InteractiveAuthenticatorInterface
{
/**
* @param UserProviderInterface $userProvider
*/
public function __construct(private UserProviderInterface $userProvider,
private UserRepository $userRepository
)
{
}
/**
* @param Request $request
* @return bool|null
*/
public function supports(Request $request): ?bool
{
return str_starts_with($request->getPathInfo(), '/otherzonearea') && $request->isMethod('POST');
}
/**
* @param Request $request
* @return PassportInterface
*/
public function authenticate(Request $request): PassportInterface
{
$email = $request->server->get('PHP_AUTH_USER', '');
$pwd = $request->server->get('PHP_AUTH_PW', '');
return new Passport(
new UserBadge($email, [$this->userProvider, 'loadUserByIdentifier']),
new PasswordCredentials($pwd),
[
new UserAgentBadge($request),
new SecretAccessBadge($this->userRepository)
]
);
}
/**
* @param PassportInterface $passport
* @param string $firewallName
* @return TokenInterface
*/
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());
}
/**
* @param Request $request
* @param TokenInterface $token
* @param string $firewallName
* @return Response|null
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
/**
* @param Request $request
* @param AuthenticationException $exception
* @return Response|null
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$code = 403;
$message = 'FORBIDDEN';
if ($exception instanceof TooManyLoginAttemptsAuthenticationException) {
$code = Response::HTTP_UNAUTHORIZED;
$message = 'Too many failed login attempts, please try again in a few minutes.';
$MessageData = $exception->getMessageData();
if (is_array($MessageData)) {
if (key_exists('%minutes%', $MessageData)) {
$message = $exception->getMessageKey();
$message = str_replace("%minutes%", $MessageData['%minutes%'], $message);
}
}
} else if ($exception instanceof BadCredentialsException) {
$code = 401;
$message = $exception->getMessage();
} else if ($exception instanceof UserAgentNotAuthorizedException) {
$code = $exception->getCode();
$message = $exception->getMessage();
}
return new JsonResponse(['error' => $message], $code);
}
public function isInteractive(): bool
{
return true;
}
}
Bon là tu sais presque tout soit curieux ouvre le capot de symfony il y a les autres passeport

Si tu es arrivé jusque là bravo et peut être à une prochaine.