Rodrigo Borrego Bernabé - Software Developer




Autenticando con Facebook en Symfony 4

Category : Programación, Symfony · by Sep 12th, 2019

Vamos a repasar rápidamente cómo integrar la autenticación con Facebook en una aplicación Symfony (4.3.4 en el momento de escribir este artículo).

Requerimientos iniciales

Dependemos de dos componentes que vamos a instalar usando composer de la manera habitual (en consola):

  • Cliente OAuth (knpuniversity/oauth2-client-bundle)
  • Biblioteca cliente de Facebook (league/oauth2-facebook), necesitaremos configurar un cliente para cada servidor OAuth con el que queramos autenticarnos.
$ composer require knpuniversity/oauth2-client-bundle league/oauth2-facebook

También debemos haber registrado nuestra aplicación en Facebook (https://developers.facebook.com) y obtener el APP_ID y la clave (SECRET).

Configuración

Como estamos utilizando las recetas de autoconfiguración de Symfony 4 y Flex no necesitaremos hacer prácticamente nada, salvo incorporar los datos particulares de nuestra aplicación:

Añade al fichero .env y .env.local los parámetros (en el segundo fichero con los datos reales obtenidos de Facebook):

OAUTH_FACEBOOK_ID=app_id
OAUTH_FACEBOOK_SECRET=app_secret

Y dentro del fichero config/packages/knpu_oauth2_client.yaml que se habrá creado en la instalación del bundle incluye los datos del cliente:

knpu_oauth2_client:
    clients:
        # configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration

      # the key "facebook" can be anything, it
      # will create a service: "knpu.oauth2.client.facebook"
      facebook:
        type: facebook
        client_id: '%env(OAUTH_FACEBOOK_ID)%'
        client_secret: '%env(OAUTH_FACEBOOK_SECRET)%'
        # the route that you're redirected to after
        redirect_route: oauth_facebook_check
        redirect_params: {}
        graph_api_version: v4.0

Creando las operaciones de autenticación

Debemos crear dos rutas y sus correspondientes métodos en el controlador de seguridad (o en otro específico para la autenticación en Facebook).

# config/routes/public.yaml

# [...]

oauth_facebook:
  path: /connect/facebook
  controller: App\Controller\SecurityController:facebookConnect

oauth_facebook_check:
  path: /connect/facebook/check
  controller: App\Controller\SecurityController:facebookConnectionCheck
<?php // src/Controller/SecurityController.php

namespace App\Controller;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class SecurityController extends AbstractController
{
    // [...]

    public function facebookConnect(ClientRegistry $clientRegistry)
    {
        return $clientRegistry
            ->getClient('facebook')
            ->redirect([
                'public_profile', 'email' // the scopes you want to access
            ])
            ;
    }

    public function facebookConnectionCheck(Request $request, ClientRegistry $clientRegistry)
    {
        return $this->redirectToRoute('home'); // or any other route
    }

    // [...]

}

Autenticación en Symfony

Y ahora el último paso. Una vez realizada la autenticación en el servidor OAuth y recuperada la información del usuario y el token de acceso tenemos que efectivamente autenticar en nuestra aplicación.

El método de conseguir esto varía según el mecanismo interno de autenticación de cada aplicación. Yo lo voy a describir para la autenticación básica que se describe en este artículo, utilizando el Maker de Symfony (https://symfony.com/blog/new-in-makerbundle-1-8-instant-user-login-form-commands).

Básicamente consiste (para no incorporar el código en el controlador) en crear un nuevo Authenticator que podemos basar en el SocialAuthenticator.

<?php // src/Security/FacebookAuthenticator.php

namespace App\Security;

use App\Entity\User; //User entity
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use KnpU\OAuth2ClientBundle\Client\Provider\FacebookClient;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
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\Core\User\UserProviderInterface;

class FacebookAuthenticator extends SocialAuthenticator
{
    private $clientRegistry;
    private $em;
    private $router;

    public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router)
    {
        $this->clientRegistry = $clientRegistry;
        $this->em = $em;
        $this->router = $router;
    }

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

    public function getCredentials(Request $request)
    {
        // this method is only called if supports() returns true

        return $this->fetchAccessToken($this->getFacebookClient());
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        /** @var FacebookUser $facebookUser */
        $facebookUser = $this->getFacebookClient()->fetchUserFromToken($credentials);

        $email = $facebookUser->getEmail();

        // 1) have they logged in with Facebook before? Easy!
        $existingUser = $this->em->getRepository(User::class)
            ->findOneBy([
                'origin' => User::ORIGIN_FACEBOOK,
                'externalId' => $facebookUser->getId()
            ]);
        if ($existingUser) {
            return $existingUser;
        }

        $user = new User();
        $user->setEmail($facebookUser->getEmail());
        $user->setPassword('NO_PASSWORD');
        $user->setOrigin(User::ORIGIN_FACEBOOK);
        $user->setExternalId($facebookUser->getId());
        //TODO: Fill profile

        $this->em->persist($user);
        $this->em->flush();

        return $user;
    }

    /**
     * @return FacebookClient
     */
    private function getFacebookClient()
    {
        // "facebook" is the key used in config/packages/knpu_oauth2_client.yaml
        return $this->clientRegistry->getClient('facebook');
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        return new RedirectResponse($this->router->generate('home'));

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

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

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

    /**
     * Called when authentication is needed, but it's not sent.
     * This redirects to the 'login'.
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new RedirectResponse(
            $this->router->generate('app_login'), // might be the site, where users choose their oauth provider
            Response::HTTP_TEMPORARY_REDIRECT
        );
    }
}

Y tenemos que decirle en la configuración que lo utilice. Ahora bien, si seguimos teniendo la autenticación por usuario/contraseña existente tendremos dos clases para la misma funcionalidad, múltiples autenticadores y tenemos obligatoriamente que especificar cuál se utilizará por defecto: el entry point.

# config/packages/security.yaml

security:
  # ...
    firewalls:
        main:
            anonymous: true
            guard:
                authenticators:
                    - App\Security\LoginFormAuthenticator
                    - App\Security\FacebookAuthenticator
                entry_point: App\Security\LoginFormAuthenticator

Referencias

SHARE :

(2) comments

Diego
2 años ago · Responder

Rodri!, interesante entrada, muy completa, lo único que haría falta es la clase `User`, ya que se ve que tiene algunas propiedades que no viene por defecto al crear el usuario con Symfony.

$user = new User();
...
$user->setOrigin(User::ORIGIN_FACEBOOK);
$user->setExternalId($facebookUser->getId());
...
//TODO: Fill profile

saludos!

    admin.rodrinet
    7 meses ago · Responder

    Gracias Diego por el aporte.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.