Last Updated: April 28, 2017
·
36.26K
· vinceve

FULL guide for integrating facebook with fos userbundle in symfony 2.1

Overview

  1. Introduction
  2. Assumptions
  3. Configuring FOS User Bundle
  4. Configuring FOS Facebook Bundle

Introduction

This took me a while to implement these 3 bundles together. Configuring the user bundle was not the problem but integrating the Facebook and Twitter bundles was a pain in the ass. Especially the Twitter one, this one was a bit deprecated and I needed to add a setter in the library itself to create dynamic callback URLS.

This tutorial covers the following aspects of the integration:

  • Create account
  • Connect to an account

Assumptions

For this tutorial I assume you have setup some things:

  • Installed Symfony 2 (and got it fully running)
  • Setup a database
  • Can run commands on the commandline
  • Setup composer
  • You use Doctrine to save your data

Configuring FOS User Bundle

So let's start integrating. The first thing what we want to do is installing the FOS Userbundle. This bundle is going to be our base user provider. It's easier to start from this bundle because it has the most features that just need extension from the other 2 bundles.

The first thing we are going to do is to add the composer references:

{
    "require": {
    "friendsofsymfony/user-bundle": "*"
    }
}

Now run the composer tool to update your dependencies and compose will download the latest version of the FOS User bundle to your vendors folder.
The next thing we want to do is enabling the bundle in our kernel.

<?php

// app/AppKernel.php

public function registerBundles()
{
       $bundles = array(
           // ...
           new FOS\UserBundle\FOSUserBundle(),
       );
 }

If this is correct, do not try to go to the website yet, as we didn't configured our bundle yet, we will get several exceptions. What we now have to do is to configure a user entity in a newly created bundle. To create a bundle run the generate:bundle command

The user entity

For this tutorial I am going to take the example of the user entity class of the fos userbundle:

<?php
// src/My/UserBundle/Entity/User.php

namespace Acme\UserBundle\Entity;

use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="fos_user")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    public function __construct()
    {
        parent::__construct();
        // your own logic
    }
}

Because we extend from a base user we have already some fields defined (roles, username, ...) so we just add an ID to the class to save our user in the database. After we done this we move on to the configuration.

Configuration

To configure our entity we need to define this in config.yml. Open up the config.yml (in app/config/config.yml) and add this configuration:

# Fos userbundle configuration.
fos_user:
    db_driver: orm
    firewall_name: main
    user_class: My\UserBundle\Entity\User

As you see in our configuration, we need to add a new firewall called main. To do this we need to open the security.yml (app/config/security.yml)
If you have opened this file, the first thing we are going to do is configuring a user provider. This is needed to define a source were our users come from.
To do this add this value underneath providers:

providers:
    fos_userbundle:
        id: fos_user.user_provider.username

We also need to define an encoder to hash our passwords:

encoders:
    FOS\UserBundle\Model\UserInterface: sha512

Now that we have done this, we can configure our actual firewall.

main:
            pattern: ^/
            form_login:
                  provider: fos_userbundle
                  csrf_provider: form.csrf_provider
                  login_path:     /user/login
                  use_forward:    false
                  check_path:     /user/login_check
                  failure_path:   null
            logout:
                path: /user/logout
            anonymous: ~
            remember_me:
                  key:      mySuperDuperKey
                  lifetime: 4147200
                  path:     /
                  domain:   ~

Now we have configured our firewall! That's that, but were not quite ready yet. At the moment we didn't have any access rules yet. So let's configure those:

access_control:
    - { path: ^/user/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/secured, role: ROLE_USER }

In these access rules we configured that the login must be open for anonymous users and that the path /secured only can be accessed by people who are logged in. The secured is just a standard page that you can create and put your routing to that address.

Good now it's time to import the routes of the fos user bundle. Open the app/routing.yml and add this (it could be that the generate:bundle already added this routing to this file.):

my_user_management:
    resource: "@MyUserManagementBundle/Resources/config/routing.yml"

At this point we have done all of the basic steps to configure the bundle. Now we move on to customize the routing a bit.

First of all we are going to make sure our bundle can inherit from the original sources by opening up src/My/UserBundle/MyUserBundle.php. Add this line of code:

public function getParent()
{
        return 'FOSUserBundle';
}

Now create a controller in this bundle and name it SecurityController.

<?php

namespace My\UserBundle\Controller;

use \FOS\UserBundle\Controller\SecurityController as BaseSecurityController;
use Symfony\Component\Security\Core\SecurityContext;

class SecurityController extends BaseSecurityController
{
}

After doing this open the Resources/config folder and add this to the routing:

fos_user_security_login:
    defaults: { _controller: MyUserBundle:Security:login }
    pattern: /user/login

fos_user_security_check:
    pattern: /user/login_check
    defaults: { _controller: MyUserBundle:Security:check }

fos_user_security_logout:
    pattern: /user/logout
    defaults: { _controller: MyUserBundle:Security:logout }

fos_user_profile:
    resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
    prefix: /user

fos_user_register:
    resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
    prefix: /registration

fos_user_resetting:
    resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
    prefix: /account

fos_user_change_password:
    resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
    prefix: /account

By giving the routes prefixes we can nicely separate the different pages from each other. You can leave it as is or customize it to your needs.

After doing this we finally have configured the fos user bundle! To test this I created a user with fixtures. I'm not going into detail for this but here is an example on to how to create a user:

function load(ObjectManager $manager)
    {
        $userManager = $this->container->get('fos_user.user_manager');

        $user1 = $userManager->createUser();

        $user1->setUsername("user");
        $user1->setEmail("my@email.com");
        $user1->setEnabled(true);

        $encoder = $this->container->get('security.encoder_factory')->getEncoder($user1);
        $encodedPass = $encoder->encodePassword('userpass', $user1->getSalt());

        $user1->setPassword($encodedPass);

        $userManager->updateUser($user1);
}

Configuring FOS Facebook Bundle

Now that we configured the fos user bundle it should only get easier. To install the fos facebook Bundle we are going to add some references in the composer.json file:

"facebook/php-sdk": "3.2.0",
"friendsofsymfony/facebook-bundle": "dev-master",

After we added these lines we run the composer update command again and we have our fresh libraries. The first thing we are going to do is to initiate our library in the kernel. Open up the app/appKernel.php and add this line:

$bundles = array(
    // other bundles ...
    new FOS\FacebookBundle\FOSFacebookBundle(),
);

If we done this, it's time to do some basic configuration. Open up the app/config/config.yml and add these lines:

# Facebook configuration
fos_facebook:
  alias:  facebook
  app_id: xxxxxxxxxxx
  secret: xxxxxxxxxxx
  cookie: true
  permissions: [email, user_hometown, user_location]

With this done, we are going to open the security.yml(app/config/security.yml) again. Because we have multiple providers at the moment we are going to chain these. To do this, we need to add a chainprovider and our facebook provider:

providers:
    chainprovider:
                  chain:
                      providers: [fos_userbundle, fos_facebook_provider]
    fos_facebook_provider:
        id: fos_facebook.user.login

Now under the main firewall we are going to define our facebook rules. In this section we define where our app is located and we also define our callback url here.

main:
        fos_facebook:
              app_url: "http://apps.facebook.com/this-is-my-awesome-app"
              server_url: "http://mywebsite.com"
              login_path: /user/login
              check_path: /facebook/login_check
              provider: fos_facebook_provider
              default_target_path: /

The firewall section should look like this then:

firewalls:
        main:
            pattern: ^/
            fos_facebook:
                  app_url: "http://apps.facebook.com/this-is-my-awesome-app"
                  server_url: "http://mywebsite.com"
                  login_path: /user/login
                  check_path: /facebook/login_check
                  provider: fos_facebook_provider
                  default_target_path: /
            form_login:
                  provider: fos_userbundle
                  csrf_provider: form.csrf_provider
                  login_path:     /user/login
                  use_forward:    false
                  check_path:     /user/login_check
                  failure_path:   null
            logout:
                path: /user/logout
            anonymous: ~
            remember_me:
                  key:      mySuperDuperKey
                  lifetime: 45146
                  path:     /
                  domain:   ~

As you can see in the provider section, the fosfacebookprovider contains an ID. This ID references to a service that we need to define. To do this (I assume you load the services from your bundle) open up your src/My/UserBundle/Resources/config/services.yml.

There add these lines of yaml:

services:
    fos_facebook.user.login:
        class: My\UserBundle\Security\User\Provider\FacebookProvider
        arguments:
            facebook: "@fos_facebook.api"
            userManager: "@fos_user.user_manager"
            validator: "@validator"
            container: "@service_container"

By doing this we are defining our own facebookprovider. Now that we defined it, we should create it too! To do this create a folder structure: src/MyUserBundle/Security/User/Provider and create a php file in this directory called FacebookProvider. In this class we are going to add this code:

<?php

namespace My\UserBundle\Security\User\Provider;

use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Facebook;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use \BaseFacebook;
use \FacebookApiException;

class FacebookProvider implements UserProviderInterface
{
    /**
     * @var \Facebook
     */
    protected $facebook;
    protected $userManager;
    protected $validator;
    protected $container;

    public function __construct(BaseFacebook $facebook, $userManager, $validator, $container)
    {
        $this->facebook = $facebook;

        // Add this to not have the error "the ssl certificate is invalid."
        Facebook::$CURL_OPTS[CURLOPT_SSL_VERIFYPEER] = false;
        Facebook::$CURL_OPTS[CURLOPT_SSL_VERIFYHOST] = 2;

        $this->userManager = $userManager;
        $this->validator = $validator;
        $this->container = $container;
    }

    public function supportsClass($class)
    {
        return $this->userManager->supportsClass($class);
    }

    public function findUserByFbId($fbId)
    {
        return $this->userManager->findUserBy(array('facebookId' => $fbId));
    }

    public function findUserByUsername($username)
    {
        return $this->userManager->findUserBy(array('username' => $username));
    }

    public function connectExistingAccount()
    {

        try {
            $fbdata = $this->facebook->api('/me');
        } catch (FacebookApiException $e) {
            $fbdata = null;
            return false;
        }

        $alreadyExistingAccount = $this->findUserByFbId($fbdata['id']);

        if (!empty($alreadyExistingAccount)) {
            return false;
        }

        if (!empty($fbdata)) {

            $currentUserObj = $this->container->get('security.context')->getToken()->getUser();

            $user = $this->findUserByUsername($currentUserObj->getUsername());

            if (empty($user)) {
                return false;
            }

            $user->setFBData($fbdata);

            if (count($this->validator->validate($user, 'Facebook'))) {
                // TODO: the user was found obviously, but doesnt match our expectations, do something smart
                throw new UsernameNotFoundException('The facebook user could not be stored');
            }
            $this->userManager->updateUser($user);

            return true;
        }

        return false;

    }

    public function loadUserByUsername($username)
    {
        $user = $this->findUserByFbId($username);

        try {
            $fbdata = $this->facebook->api('/me');
        } catch (FacebookApiException $e) {
            $fbdata = null;
        }

        if (!empty($fbdata)) {
            if (empty($user)) {
                $user = $this->userManager->createUser();
                $user->setEnabled(true);
                $user->setPassword('');
            }

            if($user->getUsername() == '' || $user->getUsername() == null)
            {
                $user->setUsername($username . '@facebook.com');
            }

            $user->setFBData($fbdata);

            if (count($this->validator->validate($user, 'Facebook'))) {
                // TODO: the user was found obviously, but doesnt match our expectations, do something smart
                throw new UsernameNotFoundException('The facebook user could not be stored');
            }
            $this->userManager->updateUser($user);
        }

        if (empty($user)) {

            // TODO: the user was found obviously, but doesnt match our expectations, do something smart
            throw new UsernameNotFoundException('The facebook user could not be stored');

        }

        return $user;
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$this->supportsClass(get_class($user)) || !$user->getFacebookId()) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }

        return $this->loadUserByUsername($user->getFacebookId());
    }
}

As you can see in our code we need to add some variables to our user entity to connect facebook. So open up the user entity and add these variables and methods:

/**
     * @var string
     *
     * @ORM\Column(name="facebookId", type="string", length=255, nullable=true)
     */
    protected $facebookId;

public function serialize()
    {
        return serialize(array($this->facebookId, parent::serialize()));
    }

    public function unserialize($data)
    {
        list($this->facebookId, $parentData) = unserialize($data);
        parent::unserialize($parentData);
    }

    /**
     * Get the full name of the user (first + last name)
     * @return string
     */
    public function getFullName()
    {
        return $this->getFirstname() . ' ' . $this->getLastname();
    }

    /**
     * @param string $facebookId
     * @return void
     */
    public function setFacebookId($facebookId)
    {
        $this->facebookId = $facebookId;
    }

    /**
     * @return string
     */
    public function getFacebookId()
    {
        return $this->facebookId;
    }

    /**
     * @param Array
     */
    public function setFBData($fbdata)
    {
        if (isset($fbdata['id'])) {
            $this->setFacebookId($fbdata['id']);
            $this->addRole('ROLE_FACEBOOK');
        }
        if (isset($fbdata['first_name'])) {
            $this->setFirstname($fbdata['first_name']);
        }
        if (isset($fbdata['last_name'])) {
            $this->setSurname($fbdata['last_name']);
        }
        if (isset($fbdata['email'])) {
            $this->setEmail($fbdata['email']);
        }
    }

Now that we have added this, we need to rebuild our database and we are ready to add our views.

If you didn't create an app on facebook, that now is the moment to create it. Creating an app should be straight forward, that's why I didn't cover it in this tutorial.

If we created our app, it's time to add some links to our views. To initiate a facebook login I added {{ facebook_initialize({'xfbml': true, 'fbAsyncInit': 'onFbInit();'}) }} between the head tags of my twig view. After that I added the facebook login button on the place that I want it with: {{ facebook_login_button() }}

As you see in our tag we still need a javascript function that does the redirect to our login_check. To do this I added this to the bottom of my page.

<script type="text/javascript">

        var authurl = '{{ path('_security_check_facebook') }}';

        function goLogIn(){
            window.location = authurl;
        }

        function onFbInit() {
            if (typeof(FB) != 'undefined' && FB != null ) {
                FB.Event.subscribe('auth.statusChange', function(response) {
                    setTimeout(goLogIn, 500);
                });
            }
        }

</script>

Now our button is setup too. We still need to do 2 things, adding some controller methods and our routes.

To add our routes, we are going to open up our routing.yml in our bundle and add these routes:

_security_check_facebook:
    pattern:   /facebook/login_check
    defaults:  { _controller: MyUserBundle:User:loginFb }

_security_facebook_logout:
  pattern:  /facebook/logout

user_couple_fb_with_account:
    pattern: /user/couple/facebook
    defaults: { _controller: MyUserBundle:User:connectFacebookWithAccount }

These routes are needed to define our flow with facebook. The one with couple_fb in the name, can be used if the user has already an account and he wants to couple his facebook to his profile.

the last thing that we are going to do is to add our controller methods.

I created a UserController in the controller folder of my bundle. In that controller I added this:

<?php

namespace My\UserBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class UserController extends Controller
{

    public function connectFacebookWithAccountAction()
    {
        $fbService = $this->get('fos_facebook.user.login');
        //todo: check if service is successfully connected.
        $fbService->connectExistingAccount();
        return $this->redirect($this->generateUrl('fos_user_profile_edit'));
    }

    public function loginFbAction() {
        return $this->redirect($this->generateUrl("_homepage"));
    }

}

** And we're ready! **

21 Responses
Add your response

Pretty cool, thanks for sharing.

over 1 year ago ·

you don't need to redefine the root to prefix them. Simply put a prefix when importing the resource: http://symfony.com/doc/current/book/routing.html#prefixing-imported-routes

And instead of using FOSFacebookBundle and FOSTwitterBundle, I would recommend using https://github.com/hwi/HWIOAuthBundle. It is better implemented and maintained, and allow you to connect to any OAuth (1 or 2) provider, which includes Twitter and Facebook. Thus, you only need 1 integration with FOSUserBundle, not 2

over 1 year ago ·

@stof I'll give you that. But a lot of people are already using FOS Userbundle in there symfony installations. I noticed you almost need to do a complete rewrite to integrate HWIOAuth and the fos Userbundle. Because the HWIOAuth is the "base" bundle and the userbundle is a service defined for the HWIOAuth bundle. If you have an existing website and you just want to extend your website with some extra providers, I don't think it's necessary to completely change your backend.

And for the routes ... I'm not using the same prefix everywhere. For example I have an account/manage and a profile/[username] :)

over 1 year ago ·

this is awesome, thanks so much. i was pulling my hair out for a while there trying to get this sorted.

over 1 year ago ·

Any update regarding Twitter?

over 1 year ago ·

Veryyyy goood ! :) thank u so much, only one thing that u forgot is the methods setFirstname, SetSurname and getFirstname in User.php :)

over 1 year ago ·

Thank you so much.

I have an app that doesn't use FOSUser and have a custom authentication implementation (use cookie instead of session). May I get some tips on how should I integrate it with my custom authentication method?

over 1 year ago ·

@stof the best way to do it

over 1 year ago ·

any update for FosTwitter

over 1 year ago ·

Thank you so much!!!

over 1 year ago ·

Thanks for the tutorial! You say you added the redirect javascript for the login_check at the bottom of your page which didn't work for me I did it like the GitHub doc says "Note that we need to include this code before the initialization of the Facebook Javascript SDK Initialization in order to have the onFbInit() event listener correctly triggered (in this case between the beginning of the 'body' tag and the templating helper provided by this bundle)".

over 1 year ago ·

@abenbachir: I think I'm going not going to write it (going to edit the tutorial) because I didn't work now for a couple of months in symfony. I'm sorry about this.

@albertstill You could try to debug your request with console.log(). Also check the console if there goes something wrong. Sometimes it can have something to do with requests from localhosts. (try to surf to your ip, instead of localhost)

over 1 year ago ·

Hi!!! thanks for the article!!! I have just one question: what about accepting tos or privacy policy BEFORE storing fbdata in the db?? how could we do it? thank you!!

over 1 year ago ·

@xavifuefer I guess you could show a checkbox first, and if the user ticks the box, show your connect facebook button. That way you implicitly know the user has accepted your tos.

over 1 year ago ·

so simple, so brilliant... thanks!!! Sometimes the simplest answer is the most appropriate.

over 1 year ago ·

@stof
i found problem to install hwioauthbundle ;
i have this problem when i run php composer.phar update hwi/oauth-bundle:
"The child node "firewallname" at path "hwioauth" must be configured."
How can i put my buttom of facebook and twitter?

over 1 year ago ·

@stof any docummentation or exemple of implementation of hwioauthbundle ?
please help

over 1 year ago ·

I have a question about confirm pages after creating accounts.

Currently I use FOS UserBundle able to create accounts. And it shows a confirm page after the user is finished creating their account.

I'm trying to do the same with FOS FacebookBundle, where if a user logs in and does not have a valid user object it will create a user for them. However by doing this I am unable to get to the confirmation page and am redirected strait to the dashboard. So I'm if there's any suggestions on what I should do or anything that I should look at.

over 1 year ago ·

I want to upload the profile picture to amazon s3. For this I need the service. But I cannot inject a servicecontainer into the FOSUBUserProvider.php. How is it possible to access the servicecontainer from this file?

over 1 year ago ·

I had to use:

myusermanagement:
type : rest
resource: "@Bundle/Resources/config/routing.yml"

in app/config/routing.yml. Notice the 'type: rest'. Without that, I get the error:

he routing file "/vagrant/src/Bundle/Resources/config/routing.yml" contains unsupported keys for "twitter": "na

me_prefix". Expected one of: "resource", "type", "prefix", "pattern", "path", "host", "schemes", "methods", "defaults", "requirem

ents", "options", "condition" in /vagrant/src/Bundle/Resources/config/routing.yml (which is being imported from "/vagrant

/app/config/routing.yml").

over 1 year ago ·

HWIOauth bundle solved your problems.

over 1 year ago ·