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
4 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
    2 años ago · Responder

    Gracias Diego por el aporte.

Responder a Diego Cancelar la 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.