Howto Facebook Connect in Symfony2

To implement Facebook Connect leveraging awesome Symfony2 Auth system I had a little bit difficult, mostly because I could not found good guide how to do it in documentation, nor somewhere on the Internet. So that I have finally have figured this out, here is brief of what I have exactly done. I am not confident 100% about this solution, maybe there could be better and more elegant one, but at least this works. But if someone has something to add, I would really like to know it.

Symfony2 distribution contains demo application, with an example of username/password authentication. We will build our application on top of that, additionally providing a way to login using Facebook.

First add Facebook Connect button: First of all we need to define our Facebook Application credentials (get them by registering new app in Facebook), wee will use them everywhere.

app/config/parameters.ini:

facebookAppId=your-app-id
facebookAppSecret=your-app-secret

Then we need to pass them to the output of our application, therefore we will make DemoController::indexAction to return AppId, that in turn will be set to template.

src/Acme/DemoBundle/Controllers/DemoController.php

// ...
public function indexAction()
{
    return array('facebookAppId' =>
    $this->container->getParameter('facebookAppId'));
}
// ...

And in template add Facebook Connect button

src/Acme/DemoBundle/Resources/views/Demo/index.html.twig:

<fb:login-button v="2" size="xlarge" onlogin="onFacebookConnect();">
    Login with Facebook
</fb:login-button>
<div id="fb-root"></div>


<script type="text/javascript">
window.fbAsyncInit = function() {
    FB.init({
        appId      : '{{ facebookAppId }}',
        status     : true,
        cookie     : true,
        xfbml      : true
    });

    FB.Event.subscribe('auth.login', function(response) {
        window.location.reload();
    });
};

function onFacebookConnect() {
    window.location.reload();
}

(function(d){
    var js, id = 'facebook-jssdk'; if (d.getElementById(id)) {return;}
    js = d.createElement('script'); js.id = id; js.async = true;
    js.src = "//connect.facebook.net/en_US/all.js";
    d.getElementsByTagName('head')[0].appendChild(js);
}(document));
</script>

And do not forget add required namespaces to html element:

src/Acme/DemoBundle/Resources/views/layout.html.twig:

<html xmlns:fb="http://www.facebook.com/2008/fbml"
      xmlns:og="http://opengraphprotocol.org/schema/">

This is all ok Facebook should now authenticate users. And to set cookie to our application. Next we are going to extract user data from Facebook using the cookie, And authenticate user to our application if everything goes correct.

First let us model data structure for our user:

src/Acme/DemoBundle/Facebook/FacebookUser.php

<?php
namespace Acme\DemoBundle\Facebook;
use Symfony\Component\Security\Core\User\UserInterface;
class FacebookUser
{
    public function __construct($data)
    {
        $this->id = $data->id;
        $this->username = $data->first_name;
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function getId()
    {
        return $this->id;
    }

    public function __toString()
    {
        return serialize($this);
    }
}

Make changes to our configuration

app/config/security.yml:

security:
    ...
    firewalls:
        ...
        secured_area:
            pattern:    ^/demo/secured/
            form_login:
                check_path: /demo/secured/login_check
                login_path: /demo/secured/login
            logout:
                path:   /demo/secured/logout
                target: /demo/
            # Add this to call facebook authentication listener
            # in this secured area
            facebook: ~
...

New factory wil be responsible for creation of objects for Facebook Authentication

src/Acme/DemoBundle/Resources/config/security_factories.xml:

<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services
                               http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <service id="security.authentication.factory.facebook"
                 class="Acme\DemoBundle\Facebook\FacebookFactory"
                 public="false">
            <tag name="security.listener.factory" />
        </service>
    </services>
</container>

So we have to create Factory class:

src/Acme/DemoBundle/Facebook/FacebookFactory.php:

<?php
namespace Acme\DemoBundle\Facebook;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory;

/**
 * The factory is where you hook into the security component,
 * telling it the name of your provider and any configuration
 * options available
 * for it.
 * /
class FacebookFactory implements SecurityFactoryInterface
{
    public function create(ContainerBuilder $container, $id, $config,
                           $userProvider, $defaultEntryPoint)
    {
        $providerId = 'security.authentication.provider.facebook.'.$id;
        $providerDD = new DefinitionDecorator(
                          'facebook.security.authentication.provider');

        $container->setDefinition($providerId, $providerDD)
                ->replaceArgument(0, new Reference($userProvider));

        $listenerId = 'security.authentication.listener.facebook.'.$id;
        $listenerDD = new DefinitionDecorator(
                              'facebook.security.authentication.listener');
        $listener = $container->setDefinition($listenerId, $listenerDD);

        return array($providerId, $listenerId, $defaultEntryPoint);
    }

    public function getPosition()
    {
        return 'pre_auth';
    }

    public function getKey()
    {
        return 'facebook';
    }

    public function addConfiguration(NodeDefinition $node)
    {
    }
}

And we can see services used by this factory: facebook.security.authentication.listener and facebook.security.authentication.provider So we must inject them to our dependency container them too:

src/Acme/DemoBundle/Resources/config/services.xml:

<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
                        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="twig.extension.acme.demo"
                 class="Acme\DemoBundle\Twig\Extension\DemoExtension"
                 public="false">
            <tag name="twig.extension" />
            <argument type="service" id="twig.loader" />
        </service>

        <service id="acme.demo.listener"
                 class="Acme\DemoBundle\ControllerListener">
            <tag name="kernel.event_listener"
                 event="kernel.controller"
                 method="onKernelController" />
            <argument type="service" id="twig.extension.acme.demo" />
        </service>

        <service id="facebook.security.authentication.provider" class="Symfony\Component\Security\Core\Authentication\Provider\AnonymousAuthenticationProvider" public="false">
            <argument />
        </service>

        <service id="facebook.security.authentication.listener"
          class=”Acme\DemoBundle\Facebook\FacebookListener" public="false">
            <argument type="service" id="security.context"/>
            <argument type="service" id="security.authentication.manager" />
            <argument>%facebookAppId%</argument>
            <argument>%facebookAppSecret%</argument>
        </service>
    </services>
</container>

To comply with Auth framework we need to define authentication token.

src/Acme/DemoBundle/Facebook/FacebookUserToken.php:

<?php
namespace Acme\DemoBundle\Facebook;

use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;

/**
 * A token represents the user authentication data present in the request.
 * Once a request is authenticated, the token retains the user`s data,
 * and delivers this data across the security context.
 * /
class FacebookUserToken extends AbstractToken
{
    public $accessToken;

    public function setAccessToken($accessToken)
    {
        $this->accessToken = $accessToken;
    }

    public function getAccessToken()
    {
        return $this->accessToken;
    }

    public function getCredentials()
    {
        return '';
    }
}

This token will be used by listener. And here is the listener itself

src/Acme/DemoBundle/Facebook/FacebookListener.php:

<?php
namespace Acme\DemoBundle\Facebook;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;

/**
 * The listener is responsible for fielding requests to the firewall
 * and calling the authentication provider.
 * /
class FacebookListener implements ListenerInterface
{
    protected $securityContext;
    protected $authenticationManager;

    public function __construct(SecurityContextInterface $securityContext,
                                AuthenticationManagerInterface $authenticationManager,
                                $appId, $appSecret)
    {
        $this->securityContext = $securityContext;
        $this->authenticationManager = $authenticationManager;
        $this->appId = $appId;
        $this->appSecret = $appSecret;
    }

    public function handle(GetResponseEvent $event)
    {
        $request = $event->getRequest();
        if (null !== $this->securityContext->getToken()) {
            return;
        }

    $cookie = $this->getFacebookCookie();
        if ($cookie) {
            $token = new FacebookUserToken();
            $token->setAccessToken($cookie['access_token']);


            $content = @file_get_contents(
                    'https://graph.facebook.com/me?access_token=' .
                    $token->getAccessToken());
            if($content) {
                $userData = json_decode($content);
                $user = new FacebookUser($userData);
                $token->setUser($user);
                $this->securityContext->setToken($token);
            }
        }
    }

    private function getFacebookCookie() {
        $args = array();
        if (!isset($_COOKIE['fbs_' . $this->appId])) {
            return;
        }
        parse_str(trim($_COOKIE['fbs_' . $this->appId], '\\"'), $args);
        ksort($args);
        $payload = '';
        foreach ($args as $key => $value) {
            if ($key != 'sig') {
                $payload .= $key . '=' . $value;
            }
        }
        if (md5($payload . $this->appSecret) != $args['sig']) {
            return null;
        }
        return $args;
    }
}

Now you should be able to authenticate to demo app with facebook, and you should see this in secured are when authenticated by Facebook:

image0

Comments !

tag cloud

social