­

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.
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ composer require knpuniversity/oauth2-client-bundle league/oauth2-facebook
$ composer require knpuniversity/oauth2-client-bundle league/oauth2-facebook
$ 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):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
OAUTH_FACEBOOK_ID=app_id
OAUTH_FACEBOOK_SECRET=app_secret
OAUTH_FACEBOOK_ID=app_id OAUTH_FACEBOOK_SECRET=app_secret
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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
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).

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 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
# 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
# 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
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?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
}
// [...]
}
<?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 } // [...] }
<?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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?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
);
}
}
<?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 ); } }
<?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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# config/packages/security.yaml
security:
# ...
firewalls:
main:
anonymous: true
guard:
authenticators:
- App\Security\LoginFormAuthenticator
- App\Security\FacebookAuthenticator
entry_point: App\Security\LoginFormAuthenticator
# config/packages/security.yaml security: # ... firewalls: main: anonymous: true guard: authenticators: - App\Security\LoginFormAuthenticator - App\Security\FacebookAuthenticator entry_point: App\Security\LoginFormAuthenticator
# config/packages/security.yaml

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

Referencias

SHARE :