<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Antoine lamirault on Medium]]></title>
        <description><![CDATA[Stories by Antoine lamirault on Medium]]></description>
        <link>https://medium.com/@alamirault?source=rss-cebacd5f419e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/2*4Zz1inev6m_dMBeJHtKAzw.jpeg</url>
            <title>Stories by Antoine lamirault on Medium</title>
            <link>https://medium.com/@alamirault?source=rss-cebacd5f419e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 20 Jun 2026 05:46:57 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@alamirault/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Symfony: Protéger l’accès de certaines pages avec un OTP]]></title>
            <link>https://alamirault.medium.com/symfony-prot%C3%A9ger-lacc%C3%A8s-de-certaines-pages-avec-un-otp-4d72458e3d08?source=rss-cebacd5f419e------2</link>
            <guid isPermaLink="false">https://medium.com/p/4d72458e3d08</guid>
            <category><![CDATA[symfony]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[php]]></category>
            <dc:creator><![CDATA[Antoine lamirault]]></dc:creator>
            <pubDate>Sun, 06 Dec 2020 18:12:16 GMT</pubDate>
            <atom:updated>2020-12-06T18:12:16.331Z</atom:updated>
            <content:encoded><![CDATA[<p>Symfony permet de sécuriser l’accès à nos pages très facilement juste avec de la configuration (firewall, access_control, isGranted).</p><p>En revanche , il n’est pas prévu nativement avec symfony de demander confirmation au moment d’accéder à une page spécifique. Cela peut être très utile sur des pages sensibles de demander confirmation via un OTP(email, sms) ou un code TOTP (Google Authenticator).</p><p>Nous allons voir ici, comment mettre en place ce système de re-confirmation d’accès. L’ensemble du code est disponible ici: <a href="https://github.com/alamirault/protected-area">https://github.com/alamirault/protected-area</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/805/1*gqax2AwzUPiZ-m5BgmhyWA.png" /><figcaption>Demande d’OTP avant d’accèder à la page</figcaption></figure><p>Pour proteger une page nous voulons uniquement ajouter une annotation à une action ou a un controlleur.</p><pre><em>// </em>src/Controller/AccountController.php</pre><pre><em>/**<br> * </em><strong><em>@ProtectedArea</em></strong><em>(name=&quot;critical-access&quot;)<br> * </em><strong><em>@Route</em></strong><em>(&quot;/account&quot;, name=&quot;account&quot;)<br> */<br></em>public function account(): Response<br>{<br>    return $this-&gt;render(&#39;account/index.html.twig&#39;);<br>}</pre><pre><em>//</em> src/Annotation/ProtectedArea.php</pre><pre><em>/**<br> * </em><strong><em>@Annotation<br> </em></strong><em>*/<br></em>class ProtectedArea<br>{<br>    public string $name;<br>}</pre><p>Au moment de se rendre sur les pages qui ont une annotation de protection, nous devons vérifier que les conditions sont remplies (OTP valide). Si ce n’est pas le cas, nous devons les demander tant que ce n’est pas valide. Nous allons donc faire cela grâce à un subscriber.</p><pre>// src/EventSubscriber/ProtectedAreaSubscriber.php<br></pre><pre>// On écoute l&#39;évènement &#39;kernel.controller&#39;<br>public static function getSubscribedEvents()<br>{<br>    return [<br>        &#39;kernel.controller&#39; =&gt; &#39;onKernelController&#39;,<br>    ];<br>}</pre><pre>public function onKernelController(ControllerEvent $event)<br>{<br>    // On vérifie qu&#39;il y a l&#39;annotation ProtectedArea<br>    // On verifie que la zone protégée n&#39;est pas déja validée<br>    // Si ce n&#39;est pas le cas on redirige vers le formulaire d&#39;OTP</pre><pre>if (!is_array($controllers = $event-&gt;getController())) {<br>    return;<br>}<br>$request = $event-&gt;getRequest();<br><br>list($controller, $methodName) = $controllers;<br><br>$protectedAreaAnnotation = $this-&gt;getAnnotation($controller, $methodName);<br>if(!$protectedAreaAnnotation){<br>    return;<br>}<br><br>$sessionKey = $this-&gt;protectedAreaSessionManager-&gt;getSessionKey($protectedAreaAnnotation);<br><br>// If no protected area process, or process is not finished<br>if (!$request-&gt;getSession()-&gt;get($sessionKey) || $request-&gt;getSession()-&gt;get($sessionKey)[&quot;status&quot;] != ProtectedAreaSessionManager::<em>OK</em>) {<br>    $this-&gt;protectedAreaSessionManager-&gt;setRequestedProtectedAreaInSession($protectedAreaAnnotation, $request);<br><br>    //Internal redirect to form otp when user has not secured area in session.<br>    $event-&gt;setController(function () use ($request, $protectedAreaAnnotation) {<br>        return $this-&gt;forward($request, &#39;App\\Controller\\ProtectedAreaController::form&#39;, [<br>            &#39;protected-area&#39; =&gt; $protectedAreaAnnotation-&gt;name,<br>        ]);<br>    });<br>}</pre><pre>}</pre><p><a href="https://github.com/alamirault/protected-area/blob/master/src/EventSubscriber/ProtectedAreaSubscriber.php">https://github.com/alamirault/protected-area/blob/master/src/EventSubscriber/ProtectedAreaSubscriber.php</a></p><p>Il nous faut maintenant créer le formulaire qui demande l’OTP et le valider. S’il est valide alors l’accès à la zone protégée est accepté.</p><pre><em>/**<br> * </em><strong><em>@Route</em></strong><em>(&quot;/protected-area&quot;)<br> */<br></em>public function form(Request $request, ProtectedAreaSessionManager $protectedAreaSessionManager): Response<br>{<br>    $protectedAreaName = $request-&gt;query-&gt;get(&quot;protected-area&quot;);<br><br>    $protectedAreaData = $request-&gt;getSession()-&gt;get($protectedAreaSessionManager-&gt;getSessionKeyFromString($protectedAreaName));<br><br>    //Here send otp how you cant<br>    $otpSent = &#39;ABC-DEF&#39;;<br><br>    $form = $this-&gt;createForm(OtpType::class, null, [<br>        &quot;otpSent&quot; =&gt; $otpSent,<br>    ]);<br>    $form-&gt;handleRequest($request);<br><br>    if ($form-&gt;isSubmitted() &amp;&amp; $form-&gt;isValid()) {<br>        $data = $protectedAreaSessionManager-&gt;setAuthorizedProtectedAreaInSession($protectedAreaName, $request);<br><br>        //Return to original requested url<br>        return $this-&gt;redirect($data[&quot;protected_url&quot;]);<br>    }<br>    return $this-&gt;render(&#39;protected_area/index.html.twig&#39;, [<br>        &#39;form&#39; =&gt; $form-&gt;createView(),<br>        &#39;cancelUrl&#39; =&gt; $protectedAreaData[&quot;cancel_url&quot;],<br>        &#39;protectedArea&#39; =&gt; $protectedAreaName,<br>    ]);<br>}</pre><p>La validation de l’OTP se fait dans l’OtpType. Quand le formulaire est valide, il suffit de mettre en session le fait que l’accès est autorisé et nous redirigons sur la page initiallement demandée. Le subscriber va vérifier les conditions, elles sont remplies, la page est affichée !</p><p>L’ajout de zone protégée peut rendre les tests fonctionnels plus complexes. Heureusement nous pouvons outre-passer toute cette partie en passant directement en sessions cette vérification.</p><pre>protected function passProtectedArea(string $name = &quot;<em>critical-access</em>&quot;)<br>{<br>    $session = static::<em>$kernel</em>-&gt;getContainer()-&gt;get(&quot;session&quot;);<br>    $session-&gt;set(&quot;protected-area-&quot; . $name, [<br>        &quot;status&quot; =&gt; &quot;OK&quot;,<br>    ]);<br>}</pre><pre>public function testAccountListing(): void {</pre><pre>    $this-&gt;passProtectedArea();<br>    // Test normal</pre><pre>}</pre><p>L’ensemble du code est disponible ici: <a href="https://github.com/alamirault/protected-area">https://github.com/alamirault/protected-area</a></p><p>Si vous avez des remarques ou des commentaires n’hésitez pas. Vous pouvez également me contacter sur twitter : <a href="https://twitter.com/a_lamirault">a_lamirault</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4d72458e3d08" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[EnumType Symfony]]></title>
            <link>https://alamirault.medium.com/enumtype-symfony-cf7dc32ca2f2?source=rss-cebacd5f419e------2</link>
            <guid isPermaLink="false">https://medium.com/p/cf7dc32ca2f2</guid>
            <category><![CDATA[symfony]]></category>
            <dc:creator><![CDATA[Antoine lamirault]]></dc:creator>
            <pubDate>Thu, 03 Dec 2020 21:44:14 GMT</pubDate>
            <atom:updated>2020-12-03T21:44:14.242Z</atom:updated>
            <content:encoded><![CDATA[<p>Really useful for available choices in an API</p><pre>&lt;?php<br><br>namespace App\Form\Common;<br><br>use Symfony\Component\Form\Extension\Core\Type\ChoiceType;<br>use Symfony\Component\OptionsResolver\Options;<br>use Symfony\Component\OptionsResolver\OptionsResolver;<br><br>class EnumType extends ChoiceType<br>{<br>    public function configureOptions(OptionsResolver $resolver): void<br>    {<br>        parent::configureOptions($resolver);<br><br>        $invalidMessageParametersNormalizer = static function(Options $options) {<br>            return [<br>                &#39;{{ permissibleValues }}&#39; =&gt; implode(&#39;, &#39;, $options[&#39;choices&#39;]),<br>            ];<br>        };<br><br>        $resolver-&gt;setDefaults([<br>            &#39;invalid_message&#39; =&gt; &quot;The value &#39;{{ value }}&#39; is not allowed. Permissible values: {{ permissibleValues }}&quot;,<br>            &#39;invalid_message_parameters&#39; =&gt; [],<br>        ]);<br><br>        $resolver-&gt;setNormalizer(&#39;invalid_message_parameters&#39;, $invalidMessageParametersNormalizer);<br>    }<br>}</pre><p><a href="https://gist.github.com/alamirault/b9701cb832899e920f918636f81643a9">https://gist.github.com/alamirault/b9701cb832899e920f918636f81643a9</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cf7dc32ca2f2" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to marry Symfony forms and ElasticSearch ?]]></title>
            <link>https://alamirault.medium.com/how-to-marry-symfony-forms-and-elasticsearch-24a9ccefa185?source=rss-cebacd5f419e------2</link>
            <guid isPermaLink="false">https://medium.com/p/24a9ccefa185</guid>
            <dc:creator><![CDATA[Antoine lamirault]]></dc:creator>
            <pubDate>Tue, 03 Sep 2019 06:02:57 GMT</pubDate>
            <atom:updated>2019-09-03T06:02:57.376Z</atom:updated>
            <content:encoded><![CDATA[<h3>How to marry Symfony forms and ElasticSearch ?</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8T6p_2a8TW1EWwREI8gRCw.png" /></figure><p>Most of the app in PHP are linked with relational database. They have advantages and drawbacks but today I will not confront this model with NoSql database. Why? Because they don’t have the same goal. In my case I must store app logs (changes on entities, actions..) and must have the possibility to search it. ElasticSearch was the best solution and will talk about it.</p><p>Elasticsearch is easy to consume (Api REST), have a powerful full-text search mechanism and have a great scalabilty.</p><p>There is a lot of libraries in PHP in order to wrap http calls. Most famous are elasticsearch/elasticsearch-php (official), ruflin/elastica (my choice), friendsofsymfony/elastica-bundle…</p><p>Why I choose <a href="https://github.com/ruflin/Elastica">Elastica</a> ? All searches are made with objects not with deep array! It’s very easy to compose and maintain code.</p><p>Elastica is amazing, but not enough. We have to manage our mapping, indexes, searches…</p><p>I decided to make <a href="https://github.com/alamirault/elasticsearch-form-bundle">my own bundle</a> `alamirault/elasticsearch-form-bundle` in order to fix these problems. Mapping are defined by yaml, indexes are created automatically and we can <strong>manage forms!</strong></p><p>Manage form is for me the most important. We want edit Elasticsearch search when user choose a value in a select, filter by date, by action.. With doctrine and SQL, the easiest way I found is <a href="https://packagist.org/packages/lexik/form-filter-bundle">lexik/form-filter-bundle</a>. I was inspired by this bundle to works with elasticsearch.</p><p>Principle is very easy, when bundle is registered a new option `elastic_filter_condition` is available in FormTypes.</p><pre>public function buildForm(FormBuilderInterface $builder, array $options)<br>        {<br>                    $builder-&gt;add(&#39;uuid&#39;, TextType::class, [<br>                        &#39;elastic_filter_condition&#39; =&gt; function (?string $value) {<br>                            if (is_null($value)) {<br>                                return;<br>                            }<br>        <br>                            return new Match(&#39;uuid&#39;, $value);<br>                        },<br>                    ])<br>        }</pre><p>When value is filled, we want filtering documents with an uuid perfect match. On each form fields you have to implements the return of this option. Query parts are aggregated in a global query (MUST).</p><p>FormType is very easy but how manage it in my controller ? In all pages of my website search system is the same: create form, manage pagination query, sort direction, query search, submission etc…</p><pre>public function indexAction(Request $request, FormFactoryInterface $formFactory, EntityManagerInterface $entityManager,<br>                                TranslatorInterface $translator, BoLogEntryDenormalizer $denormalizer)<br>    {<br>        $form = $formFactory-&gt;create(ExampleFilterType::class);<br><br>        $sortableChanges = $this-&gt;elasticsearchMaker-&gt;manageForm($form, $request, $this-&gt;indexRegistry,<br>            &quot;changes&quot;, $denormalizer);<br><br>        return $this-&gt;render(&#39;AlamiraultLogsBundle:Changes:index.html.twig&#39;,<br>            [<br>                &quot;form&quot; =&gt; $form-&gt;createView(),<br>                &quot;sortableChanges&quot; =&gt; $sortableChanges,<br>            ]<br>        );<br>    }</pre><p>The class ElasticsearchMaker make all the job with method manageForm. It takes search form with options elastic_filter_condition, request in order to submit form and also extract pagination settings, in which Index make search and a denormalizer in order to work with object. (I really hate works with arrays).</p><p>And that all ! This method returns an object SortableItems with only the N (defined by pagination settings) documents, sort fields, sort directions and the current page.</p><p>Of course is possible to compose your own logic calling only sort method, pagination or extract query of form.</p><p>It was a quick overview of my way to store data and especially have a powerful system allowing complex searches without pain !</p><p>If you have questions or if you have any comments feel free to contact me here or on twitter: <a href="https://twitter.com/a_lamirault">a_lamirault</a></p><p>Have a good day !</p><p>Inspiration: <a href="https://jolicode.github.io/elasticsearch-php-conf/slides/#/">https://jolicode.github.io/elasticsearch-php-conf/slides/#/</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=24a9ccefa185" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Symfony: Authentification double facteur]]></title>
            <link>https://alamirault.medium.com/symfony-authentification-double-facteur-a2be5d405420?source=rss-cebacd5f419e------2</link>
            <guid isPermaLink="false">https://medium.com/p/a2be5d405420</guid>
            <category><![CDATA[php]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[symfony]]></category>
            <category><![CDATA[google]]></category>
            <category><![CDATA[2fa]]></category>
            <dc:creator><![CDATA[Antoine lamirault]]></dc:creator>
            <pubDate>Sun, 07 Jul 2019 10:53:20 GMT</pubDate>
            <atom:updated>2020-02-03T18:39:53.952Z</atom:updated>
            <content:encoded><![CDATA[<p>Il est de plus en plus courant aujourd’hui de vouloir sécuriser l’accès à une zone sécurisée d’une application avec un OTP. L’authentification par couple login, mot de passe n’étant pas suffisante pour s’assurer que c’est bien le bon individu qui essaye de se connecter à son compte.</p><p>En ajoutant un facteur d’authentification nous assurons la protection de l’accès à un compte. Dans cet article j’ai fait le choix d’utiliser Google Authenticator en second facteur mais sachez qu’il y a d’<a href="https://medium.com/@renansdias/the-5-factors-of-authentication-bcb79d354c13">autres principes</a> plus ou moins compliqués à mettre en oeuvre.</p><p>Vous pouvez retrouver l’intégralité du code sur <a href="https://github.com/alamirault/sf-two-factor">mon gihub,</a> le tout sans bundle additionnel !</p><h3>Authentification “Simple”</h3><p>L’objectif est de limiter l’accès du dashboard aux utilisateurs qui n’ont pas rempli les deux facteurs.</p><p>src/Controller/DashboardController.php:</p><pre>&lt;?php<br><br>namespace App\Controller;<br><br>use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;<br>use Symfony\Component\Routing\Annotation\Route;<br><br>class DashboardController extends AbstractController<br>{<br>    /**<br>     * @Route(&quot;/dashboard&quot;, name=&quot;dashboard&quot;)<br>     */<br>    public function index()<br>    {<br>        return $this-&gt;render(&#39;dashboard/index.html.twig&#39;);<br>    }<br>}</pre><p>config/packages/security.yaml:</p><pre>security:<br><br>  providers :<br>    app_users:<br>      id: App\Security\CustomerProvider<br><br>  firewalls:<br>    dev:<br>      pattern: ^/(_(profiler|wdt)|css|images|js)/<br>      security: false<br><br>    main:<br>      anonymous: ~<br>      pattern: ^/<br>      guard:<br>          authenticators:<br>              - App\Security\CustomerAuthenticator<br>      logout:<br>        path:   app_logout<br>        target: app_login<br><br>  access_control:<br>    - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }<br>    - { path: ^/dashboard, roles: ROLE_USER }</pre><p>Pour accéder à n’importe quelle page de notre application il faut passer le guard CustomerAuthenticator. La page login est accessible en ayant le rôle IS_AUTHENTICATED_ANONYMOUSLY (Non connecté).</p><p>src/Security/CustomerAuthenticator.php:</p><pre><br>&lt;?php<br><br>namespace App\Security;<br><br>use Symfony\Component\HttpFoundation\RedirectResponse;<br>use Symfony\Component\HttpFoundation\Request;<br>use Symfony\Component\Routing\RouterInterface;<br>use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;<br>use Symfony\Component\Security\Core\Exception\AuthenticationException;<br>use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;<br>use Symfony\Component\Security\Core\Security;<br>use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;<br>use Symfony\Component\Security\Core\User\UserInterface;<br>use Symfony\Component\Security\Core\User\UserProviderInterface;<br>use Symfony\Component\Security\Csrf\CsrfToken;<br>use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;<br>use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;<br>use Symfony\Component\Security\Http\Util\TargetPathTrait;<br><br>class CustomerAuthenticator extends AbstractGuardAuthenticator<br>{<br><br>    use TargetPathTrait;<br><br>    /**<br>     * @var RouterInterface<br>     */<br>    private $router;<br>    /**<br>     * @var CsrfTokenManagerInterface<br>     */<br>    private $csrfTokenManager;<br>    /**<br>     * @var PasswordEncoder<br>     */<br>    private $passwordEncoder;<br>    /**<br>     * @var CustomerProvider<br>     */<br>    private $customerProvider;<br><br><br>    /**<br>     * DatabaseAuthenticator constructor.<br>     * @param RouterInterface $router<br>     * @param CsrfTokenManagerInterface $csrfTokenManager<br>     * @param UserPasswordEncoderInterface $passwordEncoder<br>     */<br>    public function __construct(RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager,<br>                                UserPasswordEncoderInterface $passwordEncoder, CustomerProvider $customerProvider)<br>    {<br>        $this-&gt;router = $router;<br>        $this-&gt;csrfTokenManager = $csrfTokenManager;<br>        $this-&gt;passwordEncoder = $passwordEncoder;<br>        $this-&gt;customerProvider = $customerProvider;<br>    }<br><br>    public function supports(Request $request)<br>    {<br>        return &#39;app_login&#39; === $request-&gt;attributes-&gt;get(&#39;_route&#39;) &amp;&amp; $request-&gt;isMethod(&#39;POST&#39;);<br>    }<br><br>    public function start(Request $request, AuthenticationException $authException = null)<br>    {<br>        $url = $this-&gt;router-&gt;generate(&#39;app_login&#39;);<br><br>        return new RedirectResponse($url);<br>    }<br><br>    public function getCredentials(Request $request)<br>    {<br>        $credentials = [<br>            &#39;login&#39; =&gt; $request-&gt;request-&gt;get(&#39;login&#39;),<br>            &#39;password&#39; =&gt; $request-&gt;request-&gt;get(&#39;password&#39;),<br>            &#39;csrf_token&#39; =&gt; $request-&gt;request-&gt;get(&#39;_csrf_token&#39;),<br>        ];<br>        $request-&gt;getSession()-&gt;set(<br>            Security::LAST_USERNAME,<br>            $credentials[&#39;login&#39;]<br>        );<br><br>        return $credentials;<br>    }<br><br>    public function getUser($credentials, UserProviderInterface $userProvider)<br>    {<br>        $token = new CsrfToken(&#39;authenticate&#39;, $credentials[&#39;csrf_token&#39;]);<br>        if (!$this-&gt;csrfTokenManager-&gt;isTokenValid($token)) {<br>            throw new InvalidCsrfTokenException();<br>        }<br><br>        return $this-&gt;customerProvider-&gt;loadUserByUsername($credentials[&#39;login&#39;]);<br>    }<br><br>    public function checkCredentials($credentials, UserInterface $user)<br>    {<br>        return true; // Here you must check credentials with PasswordEncoder or by ldap connexion<br>    }<br><br>    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)<br>    {<br>        $request-&gt;getSession()-&gt;set(Security::AUTHENTICATION_ERROR, $exception);<br>    }<br><br>    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)<br>    {<br>        $url = $this-&gt;router-&gt;generate(&#39;dashboard&#39;);<br>        if ($targetUrl = $this-&gt;getTargetPath($request-&gt;getSession(), $providerKey)) {<br>            $url = $targetUrl;<br>        }<br><br>        return new RedirectResponse($url);<br>    }<br><br>    public function supportsRememberMe()<br>    {<br>        return true;<br>    }<br>}</pre><p>La méthode <em>supports()</em> indique que le CustomerAuthenticator sera exécuté si l’on est sur la page de login et que l’on vient de valider le formulaire (POST). Si cette condition est remplie alors la méthode <em>getCredentials </em>est appelée. Cela permet de formater les données à partir de la requête. Survient ensuite la récupération de l’utilisateur avec la méthode <em>getUser. </em>On vérifie dans un premier temps que le token CSRF est valide puis on récupère le user par son login grâce au CustomerProvider. Enfin on vérifie qu’il a le bon mot de passe dans la méthode <em>checkCredentials</em>. Dans cet exemple il n’y a pas de vérification. (Tout mot de passe est considéré comme valide). S’il y a la moindre exception <em>AuthenticationException </em>de levée elle est mise en session pour être affichée à l’utilisateur. Si ce n’est pas le cas et que tout est ok l’utilisateur est redirigé sur la page qu’il souhaitait accéder.</p><p>L’utilisateur peut maintenant se connecter à notre site avec son login et mot de passe. C’est la <a href="https://symfony.com/doc/current/security/form_login_setup.html">méthode préconisée par Symony</a>.</p><h3>Mise en place de Google Authenticator</h3><p>Le principe de Google Authenticator est assez simple. Chaque utilisateur de votre site a un code secret. Il permet de générer un QrCode qu’il doit scanner dans l’application afin de générer des TOTP. L’utilisateur doit juste saisir ce code sur votre site et on doit s’assurer qu’il est valide avec le code secret.</p><p>Pour gérer la création des codes secrets et leurs vérifications j’ai choisi la library <em>pragmarx/google2fa.</em> Pour le QRCode<em> bacon/bacon-qr-code. </em>Vous pouvez utiliser bien entendu n’importe quelles autres paquets.</p><p>Il faut prévoir dans notre entité Customer un nouveau champ pour stocker le code secret. Celui-ci peut être null et sera enregistré uniquement quand l’utilisateur aura validé son code pour la première fois.</p><pre>/**<br> * @ORM\Column(type=&quot;string&quot;, length=16, nullable=true)<br> */<br>private $googleAuthenticatorSecret;</pre><pre>public function getGoogleAuthenticatorSecret(): ?string<br>    {<br>        return $this-&gt;googleAuthenticatorSecret;<br>    }</pre><pre>public function setGoogleAuthenticatorSecret(?string $googleAuthenticatorSecret): self<br>    {<br>        $this-&gt;googleAuthenticatorSecret = $googleAuthenticatorSecret;<br><br>        return $this;<br>    }</pre><p>Pour forcer l’utilisateur à rentrer son code Google Authenticator, nous allons rajouter un Subscriber qui redirigera sur une seconde page de login tant que l’utilisateur n’aura pas le role <em>2FA_SUCCEED.</em></p><pre>&lt;?php<br><br>namespace App\EventSubscriber;<br><br>use Symfony\Component\EventDispatcher\EventSubscriberInterface;<br>use Symfony\Component\HttpFoundation\RedirectResponse;<br>use Symfony\Component\HttpKernel\Event\GetResponseEvent;<br>use Symfony\Component\HttpKernel\KernelEvents;<br>use Symfony\Component\Routing\RouterInterface;<br>use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;<br>use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;<br>use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;<br><br>class TwoFactorAuthenticationSubscriber implements EventSubscriberInterface<br>{<br>    const ROLE_2FA_SUCCEED = &quot;2FA_SUCCEED&quot;;<br><br>    const FIREWALL_NAME = &quot;main&quot;;<br>    const ROUTE_FOR_2FA = &quot;two-factor&quot;;<br>    /**<br>     * @var TokenStorageInterface<br>     */<br>    private $tokenStorage;<br>    /**<br>     * @var RouterInterface<br>     */<br>    private $router;<br><br>    /**<br>     * TwoFactorAuthenticationSubscriber constructor.<br>     * @param TokenStorageInterface $tokenStorage<br>     * @param RouterInterface $router<br>     */<br>    public function __construct(TokenStorageInterface $tokenStorage, RouterInterface $router)<br>    {<br>        $this-&gt;tokenStorage = $tokenStorage;<br>        $this-&gt;router = $router;<br>    }<br><br><br>    public function onKernelRequest(GetResponseEvent $event)<br>    {<br>        if (!$event-&gt;isMasterRequest()) {<br>            return;<br>        }<br><br>        if (in_array($event-&gt;getRequest()-&gt;attributes-&gt;get(&#39;_route&#39;), [&quot;app_login&quot;, self::ROUTE_FOR_2FA])) {<br>            return;<br>        }<br><br><br>        if (($currentToken = $this-&gt;tokenStorage-&gt;getToken()) &amp;&amp; $currentToken instanceof PostAuthenticationGuardToken) {<br>            if ($currentToken-&gt;getProviderKey() === self::FIREWALL_NAME) {<br>                if (!$this-&gt;hasRole($currentToken, self::ROLE_2FA_SUCCEED)) {<br>                    $response = new RedirectResponse($this-&gt;router-&gt;generate(self::ROUTE_FOR_2FA));<br>                    $event-&gt;setResponse($response);<br>                }<br>            }<br>        }<br>    }<br><br>    public static function getSubscribedEvents()<br>    {<br>        return [<br>            KernelEvents::REQUEST =&gt; [&#39;onKernelRequest&#39;, -10],<br>        ];<br>    }<br><br>    private function hasRole(TokenInterface $token, string $role): bool<br>    {<br>        foreach ($token-&gt;getRoles() as $userRole) {<br>            if ($userRole-&gt;getRole() === $role) {<br>                return true;<br>            }<br>        }<br>        return false;<br>    }<br>}</pre><p>Cette page affiche un formulaire pour rentrer le code OTP ainsi que le QrCode s’il n’est pas encore enregistré dans l’entité Customer. Le secret est mis en session pour éviter d’être régénéré à chaque chargement de la page.</p><pre>class TwoFactorAuthenticationController extends AbstractController<br>{<br>    /**<br>     * @Route(&quot;/two-factor&quot;, name=&quot;two-factor&quot;)<br>     * @param Request $request<br>     * @param TokenStorageInterface $tokenStorage<br>     * @param SessionInterface $session<br>     * @param EntityManagerInterface $entityManager<br>     * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response<br>     * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException<br>     * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException<br>     * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException<br>     */<br>    public function twoFactorAction(Request $request, TokenStorageInterface $tokenStorage, SessionInterface $session,<br>                                    EntityManagerInterface $entityManager)<br>    {<br>        $form = $this-&gt;createForm(GoogleAuthenticatorType::class);<br><br>        $form-&gt;handleRequest($request);<br>        $google2fa = new Google2FA();<br><br>        $svg = null;<br><br>        /** @var Customer $customer */<br>        $customer = $this-&gt;getUser();<br>        if (!$customer-&gt;getGoogleAuthenticatorSecret()) {<br>            if ($session-&gt;get(&#39;2fa_secret&#39;)) {<br>                $secret = $session-&gt;get(&#39;2fa_secret&#39;);<br>            } else {<br>                $secret = $google2fa-&gt;generateSecretKey();<br>                $request-&gt;getSession()-&gt;set(&#39;2fa_secret&#39;, $secret);<br>            }<br><br>            $svg = $this-&gt;generateSvgForUser($google2fa, $customer, $secret);<br>        } else {<br>            $secret = $customer-&gt;getGoogleAuthenticatorSecret();<br>        }<br><br>        if ($form-&gt;isSubmitted() &amp;&amp; $form-&gt;isValid()) {<br>            $code = $form-&gt;getData()[&quot;code&quot;];<br>            $codeIsValid = $google2fa-&gt;verifyKey($secret, $code, 4);<br>            if ($codeIsValid) {<br>                if (!$customer-&gt;getGoogleAuthenticatorSecret()) {<br>                      $customer-&gt;setGoogleAuthenticatorSecret($secret);<br>                    $entityManager-&gt;persist($customer);<br>                    $entityManager-&gt;flush();<br>                }<br><br>                $this-&gt;addRoleTwoFA($tokenStorage, $session);<br><br>                return $this-&gt;redirectToRoute(&quot;dashboard&quot;);<br>            }<br>            $this-&gt;addFlash(&quot;error&quot;, &quot;Invalid verification code&quot;);<br>        }<br><br>        return $this-&gt;render(&quot;security/two-factor.html.twig&quot;, [<br>            &quot;svg&quot; =&gt; $svg,<br>            &quot;form&quot; =&gt; $form-&gt;createView(),<br>        ]);<br>    }<br>}</pre><p>La génération de l’url de QrCode prend le secret mais aussi le nom de site et le login utilisateur. Cela permettra à votre utilisateur de retrouver le bon code généré facilement parmi ses autres codes.</p><pre>private function generateSvgForUser(Google2FA $google2FA, Customer $customer, string $secret): string<br>    {<br>        $g2faUrl = $google2FA-&gt;getQRCodeUrl(<br>            &quot;My website&quot;,<br>            $customer-&gt;getLogin(),<br>            $secret<br>        );<br><br>        $writer = new Writer(<br>            new ImageRenderer(<br>                new RendererStyle(400),<br>                new SvgImageBackEnd() // can also user new ImagickImageBackEnd() in order to generate png<br>            )<br>        );<br><br>        return $writer-&gt;writeString($g2faUrl);<br>    }</pre><p>Le plus important est l’ajout de rôle <em>2FA_SUCCEED </em>à l’utilisateur quand le bon code est rentré. Pour cela on duplique le token actuel et on merge ce rôle avec les rôles existants de l’utilisateur. Il faut remplacer le token à la fois dans le TokenStorage et en session. Durant la redirection, le Subscriber remarque le nouveau rôle et n’intercepte pas la requête.</p><pre>private function addRoleTwoFA(TokenStorageInterface $tokenStorage, SessionInterface $session): void<br>    {<br>        /** @var PostAuthenticationGuardToken $currentToken */<br>        $currentToken = $tokenStorage-&gt;getToken();<br>        $roles = array_merge($currentToken-&gt;getRoles(), [TwoFactorAuthenticationSubscriber::ROLE_2FA_SUCCEED]);<br>        $newToken = new PostAuthenticationGuardToken($currentToken-&gt;getUser(), $currentToken-&gt;getProviderKey(), $roles);<br>        $tokenStorage-&gt;setToken($newToken);<br>        $session-&gt;set(&#39;_security_&#39; . $currentToken-&gt;getProviderKey(), serialize($newToken));<br>    }</pre><p>Nous devons également modifier notre utilisateur pour qu’il implémente l’interface Symfony\Component\Security\Core\User\EquatableInterface. Cette interface permet à symfony de vérifier que notre utilisateur présent en session est identique à l’utilisateur qui a était rechargé après la redirection.</p><pre>use Symfony\Component\Security\Core\User\EquatableInterface;<br>class User implements UserInterface, EquatableInterface<br>{<br>  ...<br>  public function isEqualTo(UserInterface $user){<br>      return $this-&gt;getEmail() === $user-&gt;getEmail()</pre><pre>  }</pre><pre>} </pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/674/1*PEHf18aeL0gAEZJCIKfkmg.png" /><figcaption>Affichage du QrCode</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NR62tQaZNcJK_nUWHBrJgQ.png" /><figcaption>GoogleAuthenticator après avoir scanné le qrCode</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/533/1*MVYFV6ZkylqcNNHtZxPMyQ.png" /><figcaption>Quand l’utilisateur a déjà validé son QrCode</figcaption></figure><p>Si vous avez des remarques ou des commentaires n’hésitez pas. Vous pouvez également me contacter sur twitter : <a href="https://twitter.com/a_lamirault">a_lamirault</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dYtcUYES1J_0hiuAIGqRPQ.jpeg" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a2be5d405420" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>