Symfony2: Facebook Connect with HWIOAuthBundle and SonataAdminBundle

I have a project with SonataAdminBundle for admin side and FosUserBundle for user management.
I override user management part in act to customize user entity, user login form ecc.
In my case override files are in src/Application/UserBundle. 

I had some difficulties to set properly HWIOAuthBundle, so i wrote what i did, and I hope it can help someone.

Step 1: Setting up bundles

Step 2: Setting configuration: app/config.yml

hwi_oauth:
    connect:
      account_connector: wannaplay_user_provider
    firewall_names:        [main]
    resource_owners:
      facebook:
        type:          facebook
        client_id:     123456789101112
        client_secret: 8db9a0as2cfdde6863a9e0698273159
        scope:         "email"
        options:
          display: popup #dialog is optimized for popup window
    fosub:
      # try 30 times to check if a username is available (foo, foo1, foo2 etc)
      username_iterations: 30

      # mapping between resource owners (see below) and properties
      properties:
          facebook: facebook_id

services:
    wannaplay_user_provider:
        class: Application\Sonata\UserBundle\Security\Core\User\FOSUBUserProvider
        #this is the place where the properties are passed to the UserProvider - see config.yml
        arguments: [@fos_user.user_manager,{facebook: facebook_id}]

 

Step 3: Setting security.yml

security:
    encoders:
        FOS\UserBundle\Model\UserInterface: sha512
        
    role_hierarchy:
        ROLE_ADMIN:       [ROLE_USER, ROLE_SONATA_ADMIN]
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
        SONATA:
            - ROLE_SONATA_PAGE_ADMIN_PAGE_EDIT  # if you are using acl then this line must be commented

    providers:
        fos_userbundle:
            id: fos_user.user_manager

    firewalls:
        
        # -> custom firewall for the admin area of the URL
        admin:
            pattern:            /admin(.*)
            context:            user
            form_login:
                provider:       fos_userbundle
                login_path:     /admin/login
                use_forward:    false
                check_path:     /admin/login_check
                failure_path:   null
            logout:
                path:           /admin/logout
            anonymous:          true
        # -> end custom configuration

        # default login area for standard users

        # This firewall is used to handle the public login area
        # This part is handled by the FOS User Bundle
        main:
            pattern:             .*
            context:             user
            oauth:
              resource_owners:
                  facebook:      /login_facebook
              login_path:        /login
              use_forward:       false
              failure_path:      null
              oauth_user_provider:
                #it's the custom user provider. It will be registered as service
                service: wannaplay_user_provider 

            form_login:
                provider:       fos_userbundle
                login_path:     /login
                use_forward:    false
                check_path:     /login_check
                failure_path:   null
                always_use_default_target_path : true
                default_target_path: /event/list
                use_referer : true
            logout:
              path:   /logout
              target: /login
            anonymous:          true
            
    access_control:
        # URL of FOSUserBundle which need to be available to anonymous users
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }

        # Admin login page needs to be access without credential
        - { path: ^/admin/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/logout$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/login_check$, role: IS_AUTHENTICATED_ANONYMOUSLY }

        # Secured part of the site
        # This config requires being logged for the whole site and having the admin role for the admin part.
        # Change these rules to adapt them to your needs
        - { path: ^/admin/, role: [ROLE_ADMIN, ROLE_SONATA_ADMIN] }
        - { path: ^/.*, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api, roles: [ IS_AUTHENTICATED_FULLY ] }

Step 4: Update routing

admin:
    resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml'
    prefix: /admin

_sonata_admin:
    resource: .
    type: sonata_admin
    prefix: /admin
    
sonata_user_security:
    resource: "@SonataUserBundle/Resources/config/routing/sonata_security_1.xml"

sonata_user_resetting:
    resource: "@SonataUserBundle/Resources/config/routing/sonata_resetting_1.xml"
    prefix: /resetting

sonata_user_profile:
    resource: "@SonataUserBundle/Resources/config/routing/sonata_profile_1.xml"
    prefix: /profile

sonata_user_register:
    resource: "@SonataUserBundle/Resources/config/routing/sonata_registration_1.xml"
    prefix: /register

sonata_user_change_password:
    resource: "@SonataUserBundle/Resources/config/routing/sonata_change_password_1.xml"
    prefix: /profile

sonata_user:
    resource: '@SonataUserBundle/Resources/config/routing/admin_security.xml'
    prefix: /admin

fos_message:
    resource: "@FOSMessageBundle/Resources/config/routing.xml"
    prefix: /massages

fos_oauth_server_token:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml"

fos_oauth_server_authorize:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/authorize.xml"

hwi_oauth_redirect:
    resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
    prefix:   /connect

facebook_login:
    path: /login_facebook

Step 5: Customize user provider

User provider is registered as service, extended from FOSUBUserProvider. This is the one that actually does the User registration in YOUR database with data from PROVIDERS (Facebook, Google, etc.) and in responsible for connecting already logged in users with accounts from PROVIDERS. It does this by overwriting 2 functions (connect(UserInterface $user, UserResponseInterface $response) and loadUserByOAuthUserResponse(UserResponseInterface $response)). See code below.

//Application\Sonata\UserBundle\Security\Core\User\FOSUBUserProvider.php

<?php

namespace Application\Sonata\UserBundle\Security\Core\User;

use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\FOSUBUserProvider as BaseClass;
use Symfony\Component\Security\Core\User\UserInterface;

class FOSUBUserProvider extends BaseClass
{
    /**
     * {@inheritDoc}
     */
    public function connect(UserInterface $user, UserResponseInterface $response)
    {
        $property = $this->getProperty($response);
        $username = $response->getUsername();
        //on connect - get the access token and the user ID
        $service = $response->getResourceOwner()->getName();
        $setter = 'set'.ucfirst($service);
        $setter_id = $setter.'Id';
        $setter_token = $setter.'AccessToken';
        //we "disconnect" previously connected users
        if (null !== $previousUser = $this->userManager->findUserBy(array($property => $username))) {
            $previousUser->$setter_id(null);
            $previousUser->$setter_token(null);
            $this->userManager->updateUser($previousUser);
        }
        //we connect current user
        $user->$setter_id($username);
        $user->$setter_token($response->getAccessToken());
        $this->userManager->updateUser($user);
    }
    /**
     * {@inheritdoc}
     */
    public function loadUserByOAuthUserResponse(UserResponseInterface $response)
    {
        $username = $response->getUsername();
        $user = $this->userManager->findUserBy(array($this->getProperty($response) => $username));
        
        //If user not exist create user and return it
        if (null === $user) {

            $service = $response->getResourceOwner()->getName();
            $setter = 'set'.ucfirst($service);
            $setter_id = $setter.'Id';
            $setter_token = $setter.'AccessToken';

            // create new user here
            $user = $this->userManager->createUser();
            $user->$setter_id($username);
            $user->$setter_token($response->getAccessToken());
            $user->setUsername( $response->getRealName());
            $user->setEmail($response->getEmail());
            $user->setPassword($username);
            $user->setEnabled(true);
            $this->userManager->updateUser($user);

            return $user;
        }

        //if user exists - go with the HWIOAuth way
        $user = parent::loadUserByOAuthUserResponse($response);
        $serviceName = $response->getResourceOwner()->getName();
        $setter = 'set' . ucfirst($serviceName) . 'AccessToken';

        //update access token
        $user->$setter($response->getAccessToken());
        return $user;
    }
}

 

Step 5: Register user provider as service

services:
    wannaplay_user_provider:
        class: Application\Sonata\UserBundle\Security\Core\User\FOSUBUserProvider
        #this is the place where the properties are passed to the UserProvider - see config.yml
        arguments: [@fos_user.user_manager,{facebook: facebook_id}]

 

Step 6: Update your User Entity

We add facebook_id and facebook_access_token field in entity and into table of DB

<?php

namespace Application\Sonata\UserBundle\Entity;

use Sonata\UserBundle\Entity\BaseUser as BaseUser;
use FOS\MessageBundle\Model\ParticipantInterface;

use Doctrine\ORM\Mapping as ORM;

use Symfony\Component\HttpFoundation\File\UploadedFile;


/**
 * @ORM\Entity 
 * @ORM\Table(name="fos_user")
 */
class User extends BaseUser implements ParticipantInterface
{
     /**
      * @var integer $id
      */
     protected $id;

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

    /** @ORM\Column(name="facebook_access_token", type="string", length=255, nullable=true) */
    protected $facebook_access_token;
    
    .......................
    .......................
    
    public function getFacebookId()
    {
        return $this->facebook_id;
    }

    public function setFacebookId($facebookId)
    {
        $this->facebook_id = $facebookId;
    }

    public function getFacebookAccessToken()
    {
        return $this->facebook_access_token;
    }

   public function setFacebookAccessToken($facebookAccessToken)
   {
      $this->facebook_access_token = $facebookAccessToken;
   }
   ....................... 
   .......................

I use Sonata Admin so I must override the mapping entity file for add correctly new fields. In my case I just added two lines inside the entity object:

//src/Application/Sonata/UserBundle/Resources/config/doctrine/User.orm.xml

<entity name="Application\Sonata\UserBundle\Entity\User" table="fos_user">
    
   .......

   <field name="facebook_id" type="string" column="facebook_id" length="255" nullable="true"/>
   <field name="facebook_access_token" type="string" column="facebook_access_token" length="255" nullable="true"/>
</entity>

Step 7: Customize login form – Adding the Facebook Login Button

//src/Application/Sonata/UserBundle/Resources/view/Security/base_login.html.twig
<div id="fb-root"></div>
    <script>
        window.fbAsyncInit = function() {
            // init the FB JS SDK
            FB.init({
                appId      : '12345678910',                        // App ID from the app dashboard
                channelUrl : '//yourdomain.com/channel.html',      // Channel file for x-domain comms
                status     : true,                                 // Check Facebook Login status
                xfbml      : true                                  // Look for social plugins on the page
            });
        };

        // Load the SDK asynchronously
        (function(d, s, id){
            var js, fjs = d.getElementsByTagName(s)[0];
            if (d.getElementById(id)) {return;}
            js = d.createElement(s); js.id = id;
            js.src = "//connect.facebook.net/en_US/all.js";
            fjs.parentNode.insertBefore(js, fjs);
        }(document, 'script', 'facebook-jssdk'));

        function fb_login() {
            FB.getLoginStatus(function(response) {
                if (response.status === 'connected') {
                    // connected
                    alert('Already connected, redirect to login page to create token.');
                    document.location = "{{ url("hwi_oauth_service_redirect", {service: "facebook"}) }}";
                } else {
                    // not_authorized
                    FB.login(function(response) {
                        if (response.authResponse) {
                            document.location = "{{ url("hwi_oauth_service_redirect", {service: "facebook"}) }}";
                        } else {
                            alert('Cancelled.');
                        }
                    }, {scope: 'email'});
                }
            });
        }
       
        .................
        .................

       <form action="{{ path("fos_user_security_check") }}" method="post" role="form" class="form col-md-12 center-block">
        .................
        {% render(controller('ApplicationSonataUserBundle:Connect:connect')) %}
       ................
       </form>

Extra

I used “render controller” for show facebook button. But in this moment button is just a link, so i want customize link for show properly the facebook connect button. So i must override controller and view.

Step 8: Customize Connect Controller

// src/Application/Sonata/UserBundle/Controller/ConnectController
<?php


namespace Application\Sonata\UserBundle\Controller; 

.............

class ConnectController extends Controller
{
    /**
     * Action that handles the login 'form'. If connecting is enabled the
     * user will be redirected to the appropriate login urls or registration forms.
     *
     * @param Request $request
     *
     * @return Response
     */
    public function connectAction(Request $request)
    {
        .......................

        return $this->render('@ApplicationSonataUser/Security/connect_login.html.twig', array(
            'error' => $error,
        ));
    }

    ...........................
}

Step 9: Customize view

{# src/Application/Sonata/UserBundle/Resurces/views/Security #}

{% extends 'HWIOAuthBundle::layout.html.twig' %}

{% block hwi_oauth_content %}
    {% if error is defined and error %}
        <span>{{ error }}</span>
    {% endif %}
    {% for owner in hwi_oauth_resource_owners() %}
        {% if owner == 'facebook' %}
            <a class="btn btn-primary btn-lg btn-block" role="button" href="{{ hwi_oauth_login_url(owner) }}">
                <i class="fa fa-facebook-official" aria-hidden="true"></i> &nbsp;{{ 'Continue with Facebook' | trans({}, 'HWIOAuthBundle') }}
            </a> <br/>
        {% else %}
            <a href="{{ hwi_oauth_login_url(owner) }}">{{ owner | trans({}, 'HWIOAuthBundle') }}</a> <br/>
        {% endif %}
    {% endfor %}
{% endblock hwi_oauth_content %}

 

Final result:

 

References:

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s