Binding Facebook Connect with HWIOAuthBundle in Symfony2

Jun Rikson picture Jun Rikson · May 9, 2014 · Viewed 7.4k times · Source

I've read documentations and examples from few sources but still can't get this HWIOAuthBundle works. I've login form using user ID and password which added manually in Admin section that already works fine.

I want to add Binding Facebook Button in User Area after they login successfully, so they can login with normal ID/Password either Facebook Login in future. I've searching about this HWIOAuthBundle but can't find any similar case like this.

My Security :

security:
    encoders:
        Sifo\UserBundle\Entity\Student: plaintext

    providers:
        user_area:
            entity: { class: SifoUserBundle:Student, property: code }

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false
            anonymous: true
        login:
            pattern:  ^/user/login$
            security: false
    user_area:
        pattern: ^/user
        anonymous: false
        provider: user_area
        form_login: 
            check_path: /user/login_check
            login_path: /user/login
        logout:
            path:   /user/logout
            target: /user
    user_area_socials:
        anonymous: false
        oauth:
            resource_owners:
                facebook:  "/login/check-facebook"
            login_path:    /login
            use_forward:   false
            failure_path:  /login

            oauth_user_provider:
                service: hwi_oauth.user.provider.entity


    access_control:
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/user/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/user/connect, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/user/, roles: ROLE_USER }

Student Entity :

<?php

namespace Sifo\UserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Student
 */
class Student implements UserInterface, \Serializable
{
    /**
     * @var integer
     */
    private $id;

    /**
     * @var string
     */
    private $email;

    /**
     * @var string
     */
    private $code;

    /**
     * @var string
     */
    private $facebookId;

    /**
     * @var string
     */
    private $facebookAccessToken;

app/config/routing.yml

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

hwi_oauth_login:
    resource: "@HWIOAuthBundle/Resources/config/routing/login.xml"
    prefix:   /login

facebook_login:
    pattern: /login/check-facebook

app/config/config.yml

hwi_oauth:
    # name of the firewall in which this bundle is active, this setting MUST be set
    firewall_name: user_area_socials
    resource_owners:
        facebook:
            type:                facebook
            client_id:           XXXXX73105XXXXX
            client_secret:       XXXXX5534ce8d0c50893fbb9c45XXXXX
            scope:               "email"
services:
    hwi_oauth.user.provider.entity:
        class: HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider

Login Form (login.html.twig) :

  <form class="form-signin" action="{{ path('user_login_check') }}" method="post">
    <h2 class="form-signin-heading">sign in now</h2>
    <div class="login-wrap">
        <input type="text" class="form-control" placeholder="User ID" autofocus id="username" name="_username" />
        <input type="password" class="form-control" placeholder="Password" id="password" name="_password" />
        <label class="checkbox">
            <input type="checkbox" value="remember-me"> Remember me
            <span class="pull-right">
                <a href="{{ path('public_default') }}"><i class="icon-home"></i> Back to home</a>
            </span>
        </label>
        <button class="btn btn-lg btn-login btn-block" type="submit">Sign in</button>
        <p>or you can sign in via social network</p>
        <div class="login-social-link">
            <a href="{{ path('user_facebook_login') }}" class="facebook">
                <i class="icon-facebook"></i>
                Facebook
            </a>
            <a href="{{ path('user_twitter_login') }}" class="twitter">
                <i class="icon-twitter"></i>
                Twitter
            </a>
        </div>         
    </div>
  </form>

Routing for user_facebook_login :

user_facebook_login:
    pattern:  /login_facebook
    defaults: { _controller: "SifoUserBundle:Default:facebook" }

My Controller :

<?php

namespace Sifo\UserBundle\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

use Sifo\UserBundle\Form\DefaultType;

class DefaultController extends Controller
{    
    public function facebookAction()
    {
        return $this->render('SifoUserBundle:Default:facebook.html.twig');
    }

    public function loginAction()
    {
        $request = $this->getRequest();
        $session = $request->getSession();

        // get the login error if there is one
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        } else {
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
            $session->remove(SecurityContext::AUTHENTICATION_ERROR);
        }

        return $this->render('SifoUserBundle:Default:login.html.twig', array(
            // last username entered by the user
            'last_username' => $session->get(SecurityContext::LAST_USERNAME),
            'error'         => $error,
        ));
    }
}

facebook.html.twig :

<!DOCTYPE html>
<html lang="en">
<head>
</head>
  <body class="login-body">
<script>
  // This is called with the results from from FB.getLoginStatus().
  function statusChangeCallback(response) {
    console.log('statusChangeCallback');
    console.log(response);
    // The response object is returned with a status field that lets the
    // app know the current login status of the person.
    // Full docs on the response object can be found in the documentation
    // for FB.getLoginStatus().
    if (response.status === 'connected') {
        // connected
        document.location = "{{ url("hwi_oauth_service_redirect", {service: "facebook"}) }}";
    } else if (response.status === 'not_authorized') {
        // not_authorized
        FB.login(function(response) {
            if (response.authResponse) {
                document.location = "{{ url("hwi_oauth_service_redirect", {service: "facebook"}) }}";
            } else {
                alert('Cancelled.');
            }
        }, {scope: 'email'});
    } else {
      // The person is not logged into Facebook, so we're not sure if
      // they are logged into this app or not.
      document.getElementById('status').innerHTML = 'Please log ' +
        'into Facebook.';
    }
  }

  // This function is called when someone finishes with the Login
  // Button.  See the onlogin handler attached to it in the sample
  // code below.
  function checkLoginState() {
    FB.getLoginStatus(function(response) {
      statusChangeCallback(response);
    });
  }

  window.fbAsyncInit = function() {
  FB.init({
    appId      : 'XXXXX73105XXXXX',
    cookie     : true,  // enable cookies to allow the server to access 
                        // the session
    xfbml      : true,  // parse social plugins on this page
    version    : 'v2.0' // use version 2.0
  });

  // Now that we've initialized the JavaScript SDK, we call 
  // FB.getLoginStatus().  This function gets the state of the
  // person visiting this page and can return one of three states to
  // the callback you provide.  They can be:
  //
  // 1. Logged into your app ('connected')
  // 2. Logged into Facebook, but not your app ('not_authorized')
  // 3. Not logged into Facebook and can't tell if they are logged into
  //    your app or not.
  //
  // These three cases are handled in the callback function.

  FB.getLoginStatus(function(response) {
    statusChangeCallback(response);
  });

  };

  // 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/sdk.js";
    fjs.parentNode.insertBefore(js, fjs);
  }(document, 'script', 'facebook-jssdk'));

  // Here we run a very simple test of the Graph API after login is
  // successful.  See statusChangeCallback() for when this call is made.
  function testAPI() {
    console.log('Welcome!  Fetching your information.... ');
    FB.api('/me', function(response) {
      console.log('Successful login for: ' + response.name);
      document.getElementById('status').innerHTML =
        'Thanks for logging in, ' + response.name + '!';
    });
  }
</script>
  </body>
</html>
  1. How to get Facebook ID and Access token and saved into database (for binding Facebook)
  2. How to check in database for Facebook ID. If Exist, give ROLE_USER like normal loginAction in my Controller. If not exist throw exception.
  3. If this Bundle can't accommodate my problems, is there any better Bundle or function which fit for my needs?

Answer

isomoar picture isomoar · Nov 5, 2014

You need to create your own provider, which implements OAuthAwareUserProviderInterface. In loadUserByOAuthUserResponse method from $response parameter you can get any info about Facebook-user. Example:

class MyOAuthProvider implements UserProviderInterface, OAuthAwareUserProviderInterface
{
    public function loadUserByOAuthUserResponse(UserResponseInterface $response)
    {
        $token = $response->getAccessToken();
        $facebookId =  $response->getUsername(); // Facebook ID, e.g. 537091253102004
        $username = $response->getRealName();
        $email = $response->getEmail();

        // search user in database
        $result = $this->em->getRepository('AppUserBundle:User')->findOneBy(
            array(
                'facebookId' => $facebookId
            )
        );

        if(!$result) {
            $user = new User();
            $user->setEmail($email);
            $user->setUsername($username);
            $user->setFacebookId($facebookId);

            // ..
            // save to database instructions
            // ..
        }

        return $this->loadUserByUsername($username);
    }

    // other methods
}

Don't forget specify your custom provider in app/config/config.yml

services:
    app.user.provider:
        class: App\UserBundle\Entity\MyOAuthProvider

and in app/config/security.yml

firewalls:
        ...
        user_area_socials:
            oauth:
                oauth_user_provider:
                    service: app.user.provider

Read more in docs: How to Create a custom User Provider