Nouvelle-Techno.fr

Symfony 5.3.6 authentification notions avancée / PHP8

Par Nouvelle-Techno.fr le 25 août 2021 - Catégories : symfony5

Lire l'article sur le site d'origine

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: 


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. 
 

#Applications Mobiles #API #php8 #apache #symfony5 #authentification #administration