How to FOSUser + FOSFacebook + Custom Login

In this article I’m going to teach you how to integrate FOSUser + FOSFacebook to enable your application login by using facebook or your custom login at the same time …

This took me a lot of time, but once you understand it is pretty easy, in sake of brevity I’ll show you only the key points and you should do the rest, ok lets do this!!

Important: this works for Symfony 2.1 only as it depends on chained providers feature, for Symfony 2.0 a somewhat different setting is needed.

Lets keep it as simple as possible so start with a fresh new download of Symfony Standard Vendors package, set it up so you can run the Acme/DemoBundle, we will be using this as a base.

Ready? Ok, you’ll note that on /demo/secured/login the demo provides us a custom login which uses a memory provider. This provider is what you want to replace in order to persist/provide users: e.g. FOSUserBundle provider (checkout their documentation here for further details). The Demo app provided has 3 controllers named DemoController.php SecuredController.php, WelcomeController.php, we are only interested on SecuredController.php. This controller protects /demo/secured/hello action against unauthenticated users. For convenience lets tweak the security.yml a bit so that when we login and logout it takes us back to the login page instead of the welcome page. Replace your “main” firewall with the following

  main:
      pattern:    ^/demo/secured/
      form_login:
          check_path: /demo/secured/login_check
          login_path: /demo/secured/login
          default_target_path: /demo/secured/hello/world
      logout:
          path:   /demo/secured/logout
          target: /demo/secured/login

Install FOSUserBundle and FOSFacebookBundle.

Ok, now install FOSUserBundle and FOSFacebook on your app.

Add the dependencies on composer.json

 "require": {
         ...
        "friendsofsymfony/user-bundle": "*",
        "friendsofsymfony/facebook-bundle": "dev-master",
         ...
}

Download them by running these commands

$ php composer.phar update friendsofsymfony/user-bundle
$ php composer.phar update friendsofsymfony/facebook-bundle

Lastly enable the bundles on appKernel.php

 
    public function registerBundles() { 
        return array( 
            // ... 
            new FOS\UserBundle\FOSUserBundle(), 
            new FOS\FacebookBundle\FOSFacebookBundle(), 
            // ... 
        ); 
    }

Enable Facebook library on your pages.

Lets add the facebook library loader, handlers and the login button. Replace your Acme/DemoBundle/Resources/views/layout.html.twig with the following.

<!DOCTYPE html>
<html lang="en" xmlns:fb="http://www.facebook.com/2008/fbml">
    <head>
        <meta charset="UTF-8" />
        <title>{% block title %}Demo Bundle{% endblock %}</title>
        <link rel="icon" sizes="16x16" href="{{ asset('favicon.ico') }}" />
        <link rel="stylesheet" href="{{ asset('bundles/acmedemo/css/demo.css') }}" />
    </head>
    <body>
        <script>
          function goLogIn(){
              window.location = "{{ path('_fb_security_check') }}";
          }

          function onFbInit() {
              if (typeof(FB) != 'undefined' && FB != null ) {
                  FB.Event.subscribe('auth.statusChange', function(response) {
                      setTimeout(goLogIn, 500);
                  });
              }
              FB.Event.subscribe('auth.logout',
                       function(response) {
                           if (response.status === 'unknown')  {
                              window.location = "{{ path('_demo_logout') }}";
                            }
              });                            
          }
        </script>
        {{ facebook_initialize({'xfbml': true, 'fbAsyncInit': 'onFbInit();'}) }}
        <div id="symfony-wrapper">
            <div id="symfony-header">
                <a href="{{ path('_welcome') }}">
                    <img src="{{ asset('bundles/acmedemo/images/logo.gif') }}" alt="Symfony logo" />
                </a>
                <form id="symfony-search" method="GET" action="http://symfony.com/search">
                    <label for="symfony-search-field"><span>Search on Symfony Website</span></label>
                    <input name="q" id="symfony-search-field" type="search" placeholder="Search on Symfony website" />
                    <input type="submit" value="OK" />
                </form>
            </div>

            {% for flashMessage in app.session.flashbag.get('notice') %}
                <div>
                    <em>Notice</em>: {{ flashMessage }}
                </div>
            {% endfor %}

            {% block content_header %}                
                <ul id="menu">
                    {% block content_header_more %}
                        <li><a href="{{ path('_demo') }}">Demo Home</a></li>
                    {% endblock %}
                </ul>

                <div style="clear: both"></div>
            {% endblock %}

            <div>
                {{ facebook_login_button({'autologoutlink': true}) }}
                {% block content %}
                {% endblock %}
            </div>

            {% if code is defined %}
                <h2>Code behind this page</h2>
                <div>{{ code|raw }}</div>
            {% endif %}
        </div>
    </body>
</html>

One important thing to note on the above file is that the function goLogIn() redirects to _fb_security_check, this route doesn’t need an implementation as the firewall handles it, BUT it has to be defined and also has to be behind the firewall (“main” in our case) so lets add it to our routing.yml

#route has to be different form the other authetication providers
 _fb_security_check:
  pattern: /demo/secured/facebook/login_check

The User Class

In our app when a user log’s in they are persisted into database (if doesn’t exists) so we can build up our users database. Lets create the users Entity

<?php
// Acme/DemoBundle/Entity/User.php
namespace Acme\DemoBundle\Entity;

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

/**
 * @ORM\Entity
 * @ORM\Table(name="user")
 */
class User extends BaseUser
{
    /**
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;

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

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

    /**
     * @var string
     *
     * @ORM\Column(name="facebookId", type="string", length=255)
     */
    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);
    }

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

    /**
     * @param string $firstname
     */
    public function setFirstname($firstname)
    {
        $this->firstname = $firstname;
    }

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

    /**
     * @param string $lastname
     */
    public function setLastname($lastname)
    {
        $this->lastname = $lastname;
    }

    /**
     * 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;
        $this->setUsername($facebookId);
        $this->salt = '';
    }

    /**
     * @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->setLastname($fbdata['last_name']);
        }
        if (isset($fbdata['email'])) {
            $this->setEmail($fbdata['email']);
        }
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }
}

The User provider

We also need a user provider, checkout Symfony documentation on user providers, for now just know that it has the responsibility to retrieve and return a user, if it doesn’t exists then it stores it as well.

<?php
// Acme/DemoBundle/Security/User/Provider/FacebookProvider.php

namespace Acme\DemoBundle\Security\User\Provider;

use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
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;

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

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

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

    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('');
            }

            // TODO use http://developers.facebook.com/docs/api/realtime
            $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)) {
            throw new UsernameNotFoundException('The user is not authenticated on facebook');
        }

        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());
    }
}

Weeoof!! those were a few lines of code don’t they? keep strong the tough part is over.  Now we need to configure FOSUser so it can access the database, lets tell it to use Doctrine to persist and query our users.  Add at the end of config.yml

Tie up everything:

fos_user:
    db_driver: orm
    firewall_name: facebook
    user_class: Acme\DemoBundle\Entity\User

Here we also told it what would our User class be.

Next configure FOSFacebook, Add at the end of config.yml

fos_facebook:
    file:   %kernel.root_dir%/../vendor/facebook/php-sdk/src/base_facebook.php
    alias:  facebook
    app_id: 
    secret: 
    cookie: true
    permissions: [email,user_birthday,user_location]

Now lets tell FOSFacebook to use our user provider

services:
    my.facebook.user:
        class: Acme\DemoBundle\Security\User\Provider\FacebookProvider
        arguments:
            facebook: "@fos_facebook.api"
            userManager: "@fos_user.user_manager"
            validator: "@validator"
            container: "@service_container"

Still here?! great now for the final part lets edit security.yml.  Now the trick part comes here, Symfony 2.1 introduces the concept of chaining providers, that is that you can stack providers and each will be asked if it knows the user, if not then handles the request to the next one (in the order they are defined) until it is authenticated, if not then authentication is denied.

in_memory:
        memory:
            users:
                user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ]}

    my_fos_facebook_provider:
        id: my.facebook.user

Chaining providers provides a cleaner way to implement this than method used in Symfony 2.0

Setup the firewalls.

Finally the firewalls, after this you should be able to test your app and log in by either method

    firewalls:

        login:
            pattern:  ^/demo/secured/login$
            security: false

        main:
            pattern:    ^/demo/secured/
            form_login:
                check_path: /demo/secured/login_check
                login_path: /demo/secured/login
                default_target_path: /demo/secured/hello/world

            fos_facebook:
                app_url: "http://apps.facebook.com/AppName/"
                server_url: "http://localhost/facebookApp"
                login_path: /login
                check_path: /demo/secured/facebook/login_check
                default_target_path: /demo/secured/hello/world

            logout:
                path:   /demo/secured/logout
                target: /demo/secured/login

!Extra: just in case remember to build the database

$ php app/console doctrine:database:create
$ php app/console generate:doctrine:entities Acme
$ php app/console doctrine:schema:create

Well that’s it, I admit it was more complex that I would like, the user provider really helps to achieve loose coupling, but still I feel that too much configuration was needed (I’m new to Symfony, though).

Hope this tutorial helps you on your development.

!Important: If you are testing on your local machine remember that the VirtualHost URL you set must be the same that you setup on the Facebook App in order for facebook connect to work.

References: If you are curious about the internals of authentication Mathias Noback has an excellent series about the topic, check it out here.  Also checkout Symfony’s docs. User provider and User class taken from FOSFacebookBundle documentation.

9 thoughts on “How to FOSUser + FOSFacebook + Custom Login

  1. I have one question regarding the chained authentication.
    Once a user is created an account through the Facebook. Is it possible login through form_login . If Yes how? otherwise any possibility to achieve that ?.

    • Nisam, no facebook users are authenticated by using the facebook API, the bundle checks if the user has an active session and proceeds, but it does create a user on the database,
      as facebook’s password is not accessible, you can ask them to provide one for recovery purposes for example and update the database accordingly, this way users can use either methods to login.
      Hope this helps, thanks for your comment.

      • Is it possible the other way around ?

        I have a user, that created an account with form login. But now he wants to couple his account. How can I do something like that ?

        • Well, in this case you just need to use the Facebook API directly (PHP or Javascript), one solution will be to put the facebook connect button, once the user is logged into facebook (it might be already logged in into your website) you can use the facebookId to update your database records accordingly.

          Hope this helps.
          Cheers!

  2. Hi,

    thanks for the tutorial. I could finally at least login a facebook account to my webpage in symfony2. But how can I save the user data from facebook directly to my db?

    Thanks!

    Gunnar

    • Easy, from the example above user data is already stored on database through FOSUserBundle, if you need to add extra data
      like for example the facebook picture you can modify the User::setFBData() method of your User class.
      Cheers!

  3. Hi!

    Thank you so much for this guide! It was so much more straight forward than the documentation. This guide should be linked to the documentation. You have saved me so much frustration you can’t even imagine. Thanks again, you are my hero!

    -Jason

  4. Hi, very nice explanation.

    My doubt is,

    I have used FOS userBundle and FOSFacebookBundle. I have made made the login as per configuration. Login was working Fine (system session).

    Now I want to logging application through Facebook and done the FB setup and created user in my User table and FB session has created.

    But I want to create my system session using this FB authentication. (like how we did using via form login)

    Please help me on this.

    Will FOSFacebookBundle will create FBsession and system user loggin session?

    • Aravind,
      I do not quite understand what you are trying to do, but I think the best approach is to leave the facebook’s session only for facebook related operations and your app operation with its own session, so you should manually manage it through Symfony’s session component.

      Hope this helps, cheers!

Leave a Reply

Your email address will not be published. Required fields are marked *