Symfony 6 : S’authentifier avec Facebook

S

Dans un précédent article, j’avais déjà évoqué ce sujet en prenant comme exemple une authentification Google, on va faire de même avec Facebook. (Sachant que le plus compliqué c’est de récupérer les clé API Facebook)

Un des intérêts de se connecter à l’aide du compte Facebook c’est d’éviter à saisir un énième mot de passe.

Comme d’habitude on va partir de 0 et on va commencer par créer le projet :

symfony new sffacebook

Et y ajouter toutes les librairies que l’on va avoir besoin :

composer req twig
composer req --dev symfony/maker-bundle
composer req security
composer req orm
composer req --dev debug

Et en particulier celles qui vont nous permettre d’utiliser Facebook pour s’authentifier :

composer require knpuniversity/oauth2-client-bundle
composer require league/oauth2-facebook

On va faire mettre de côté Symfony pour récupérer nos clés Facebook (ce qui implique que vous ayez un compte Facebook).

On va d’abord se connecter au portail développeur : https://developers.facebook.com, puis dans le menu : « Mes applications » et ensuite « Créer une app ».

Comme type d’application, on sélectionnera « aucun » :

Et enfin saisir le nom de votre appli (qui ne doit pas contenir les mots « Facebook », « FB », …), pour ma part ce sera SymfonyAuth.

Ensuite il faut ajouter le produit « Facebook Login » (Le produit de connexion social numéro un au monde…)

Ensuite dans le menu de gauche « Facebook Login », cliquer sur « Paramètres » :

Saisir l’URL de redirection OAuth valides : (votre nom de domaine puis /connect/facebook/check, on créera cette route plus tard dans Symfony) :

Ensuite « Paramètres -> Général » et récupérer votre identifiant et la clé secrète, que vous pouvez déjà mettre dans votre fichier .env.local :

OAUTH_FACEBOOK_CLIENT_ID=app_id
OAUTH_FACEBOOK_CLIENT_SECRET=app_secret

A partir de là on a déjà fait le plus gros du travail ! On va maintenant revenir sur Symfony et créer l’entity User à l’aide du makerBundle :

bin/console make:entity

On envoie le tout dans la base de données :

bin/console make:migration
bin/console doctrine:migration:migrate

Ensuite c’est un peu de paramétrage, tout d’abord dans le fichier config/packages/knpu_oauth2_client.yaml :

knpu_oauth2_client:
    clients:
        # will create service: "knpu.oauth2.client.facebook"
        # an instance of: KnpU\OAuth2ClientBundle\Client\Provider\FacebookClient
        # composer require league/oauth2-facebook
        facebook:
            # must be "facebook" - it activates that type!
            type: facebook
            # add and set these environment variables in your .env files
            client_id: '%env(OAUTH_FACEBOOK_CLIENT_ID)%'
            client_secret: '%env(OAUTH_FACEBOOK_CLIENT_SECRET)%'
            # a route name you'll create
            redirect_route: connect_facebook_check
            redirect_params: {}
            graph_api_version: v2.12
            # whether to check OAuth2 "state": defaults to true
            # use_state: true

On va maintenant créer un controller :

Et y mettre ce code : (rien d’extraordinaire, public_profile et email sont les scopes auxquels on aura accès), ce controller va nous permettre de nous logger/deloguer (route app_login et app_logout), puis récupérer les informations utilisateur ainsi que le token de Facebook :

<?php

namespace App\Controller;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class FacebookController extends AbstractController
{
    #[Route('/login', name: 'app_login')]
    public function index(): Response
    {
        return $this->render('facebook/index.html.twig');
    }

    #[Route('/logout', name: 'app_logout')]
    public function logout()
    {
        throw new \Exception('Don\'t forget to activate logout in security.yaml');
    }

    #[Route('/connect/facebook', name: 'connect_facebook')]
    public function connectAction(ClientRegistry $clientRegistry): RedirectResponse
    {
        //Redirect to facebook
        return $clientRegistry->getClient('facebook')->redirect(['public_profile', 'email'], []);
    }

    /**
     * After going to facebook, you're redirected back here
     * because this is the "redirect_route" you configured
     * in config/packages/knpu_oauth2_client.yaml
     */
    #[Route('/connect/facebook/check', name: 'connect_facebook_check')]
    public function connectCheckAction(Request $request)
    {
        // ** if you want to *authenticate* the user, then
        // leave this method blank and create a Guard authenticator
    }
}

Et dans le twig (facebook/index.html.twig), un joli bouton pour se connecter via Facebook :

{% extends 'base.html.twig' %}

{% block body %}

    <a href="{{ path('connect_facebook') }}">
        <img class="img" src="/facebooklogo.png" alt="">
    </a>

{% endblock %}

On va maintenant créer le GuardAuthenticator qui va nous permettre de nous connecter à notre application , c’est un poil plus compliqué :

<?php
# src/Security/FacebookAuthenticator.php
namespace App\Security;

use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use League\OAuth2\Client\Provider\FacebookUser;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

class FacebookAuthenticator extends OAuth2Authenticator
{

    public function __construct(
        readonly ClientRegistry $clientRegistry,
        readonly EntityManagerInterface $entityManager,
        readonly RouterInterface $router)
    {
    }

    public function supports(Request $request): ?bool
    {
        // continue ONLY if the current ROUTE matches the check ROUTE
        return $request->attributes->get('_route') === 'connect_facebook_check';
    }

    public function authenticate(Request $request): Passport
    {

        $client = $this->clientRegistry->getClient('facebook');
        $accessToken = $this->fetchAccessToken($client);

        return new SelfValidatingPassport(
            new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) {
                /** @var FacebookUser $facebookUser */
                $facebookUser = $client->fetchUserFromToken($accessToken);

                $email = $facebookUser->getEmail();

                // have they logged in with Facebook before? Easy!
                $existingUser = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $email]);

                //User doesnt exist, we create it !
                if (!$existingUser) {
                    $existingUser = new User();
                    $existingUser->setEmail($email);
                    $this->entityManager->persist($existingUser);
                }
                $this->entityManager->flush();

                return $existingUser;
            })
        );
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {

        // change "app_dashboard" to some route in your app
        return new RedirectResponse(
            $this->router->generate('app_default')
        );

        // or, on success, let the request continue to be handled by the controller
        //return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        $message = strtr($exception->getMessageKey(), $exception->getMessageData());

        return new Response($message, Response::HTTP_FORBIDDEN);
    }

Et enfin last but not least, il faut que l’on dise à Symfony d’utiliser ce guard, et ça ça se fait dans le fichier config/packages/security.yaml :

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider

            form_login:
                login_path: app_login
                check_path: app_login

            logout:
                path: /logout
                target: /login

            custom_authenticators:
                - App\Security\FacebookAuthenticator

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        - { path: ^/login, roles: PUBLIC_ACCESS }
        - { path: ^/connect, roles: PUBLIC_ACCESS }
        - { path: ^/, roles: ROLE_USER }

Vous pouvez maintenant vous connecter à votre site, vous serez redirigé vers la page /login, et vous n’aurez plus à cliquer sur le bouton et hop :

Derniere astuce, pour vous deconnecter il suffit de se rendre sur /logout.

Vous pourrez retrouver toutes les sources sur le github : https://github.com/gponty/sffacebook

A propos de l'auteur

Ajouter un commentaire

Guillaume

Get in touch

Je suis un développeur web passionné par les technologies Symfony et ChatGPT. J'aime partager mes connaissances et mon expérience à travers des articles que j'écris sur ces sujets. Avec une solide expertise en développement web, je m'efforce de fournir des contenus utiles et instructifs pour aider les développeurs à créer des applications web de qualité supérieure.