How to configure and use the HWIOAuthBundle with FOSUserBundle (Social Login) in Symfony 3

How to configure and use the HWIOAuthBundle with FOSUserBundle (Social Login) in Symfony 3

A lot of people suffer today the famous "Password Fatigue", even probably you. The visitors of a website are always looking for a simple way to do the things, and when we talk about logging into sites, the Social Login are the best option because the users can use their existing social accounts like Facebook, Google+, Twitter etc. This advantage, can boost conversions on your website by improving their User Experience. Many websites report that their target consumers prefer using Social Login instead of creating a new account on their site and that for good reason. For a Symfony Project, the HWIOAuthBundle is the most famous and best solution to achieve this task.

In this article you will learn how to allow your user to login into your app using a Social Network. Although you can configure in the same way any social network, we'll explain in this case with Github, Facebook, Google Plus and Stack Exchange.

Note

You can follow the same process if you want to add other Social Networks like Twitter etc. Or if you don't need a social network from the example, just skip it.

Requirements

Before proceeding, you will need to create in the fos_user table of your database 2 columns of type string (Varchar 255) for every Social Network (resource owner) that you want to add. We'll set this step as a requirement, because you are the one that decides how to add the fields to the database. Some developers handle the database with some manager like PHPMyAdmin or other follows the symfony way by modifying the user.orm.yml file and then building the database using php bin/console doctrine:schema:update --force.

The 2 fields for every resource owners follow the next nomenclature: <social-network-name>_id and <social-network-name>_access_token. For example, with our 4 mentioned social networks we would have 8 new columns in the fos_user table namely:

github_id
github_access_token

facebook
facebook_access_token

googleplus_id
googleplus_access_token

stackexchange_id
stackexchange_access_token

Once the columns exists, you will need obviously add the getters and setters of the field in the User class of your application:

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

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

/** @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;

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

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

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

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

public function setGithubId($githubId) {
    $this->github_id = $githubId;

    return $this;
}

public function getGithubId() {
    return $this->github_id;
}

public function setGithubAccessToken($githubAccessToken) {
    $this->github_access_token = $githubAccessToken;

    return $this;
}

public function getGithubAccessToken() {
    return $this->github_access_token;
}

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

    return $this;
}

public function getFacebookId() {
    return $this->facebook_id;
}

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

    return $this;
}

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

public function setGoogleplusId($googlePlusId) {
    $this->googleplus_id = $googlePlusId;

    return $this;
}

public function getGoogleplusId() {
    return $this->googleplus_id;
}

public function setGoogleplusAccessToken($googleplusAccessToken) {
    $this->googleplus_access_token = $googleplusAccessToken;

    return $this;
}

public function getGoogleplusAccessToken() {
    return $this->googleplus_access_token;
}


public function setStackexchangeId($stackExchangeId) {
    $this->stackexchange_id = $stackExchangeId;

    return $this;
}

public function getStackexchangeId() {
    return $this->stackexchange_id;
}

public function setStackexchangeAccessToken($stackExchangeAccessToken) {
    $this->stackexchange_access_token = $stackExchangeAccessToken;

    return $this;
}

public function getStackexchangeAccessToken() {
    return $this->stackexchange_access_token;
}

If you set everything in order, you will be able to retrieve those properties from the User object and you can proceed to configure the HWIOAuthBundle.

Note

Remember that you need to clear the cache and logout from the current user when you modify the user class, otherwise when you add new fields, they won't be updated till the user logins again.

1. Install and enable HWIOAuthBundle

The first you need to do is to install the HWIOAuthBundle with composer using the following command:

composer require hwi/oauth-bundle

Alternatively, you can modify manually your composer.json and set the bundle as a dependency:

{
    "require": {
        "hwi/oauth-bundle": "^0.5.3",
    }
}

And finally install it using composer install. Once the installation of the bundle finishes, don't forget to enable it in the AppKernel.php file of your symfony app:

<?php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            // .. 
            new HWI\Bundle\OAuthBundle\HWIOAuthBundle(),
            // .. 
        ];

        // .. 
    }
}

2. Create Developer Accounts on Social Networks

The most important point to allow that your user can login into your application with a Social Network, is that the third party service (Social Network) allow you to do it as well. For it, most of the services require that your application is registered in order to manage requests permissions etc.

For example, for Github you can register your application here, for Facebook here, for Stack Exchange here and for Google Plus Here. Once your application is registered, they will provide you the required OAuth tokens that will allow you to create request to their servers. The Github manager looks like:

OAuth Manager Github

Note

Besides in the settings of your custom application, you will need to provide the Authorization Callback URL that we'll define in the step #4. So create your application with this option empty and don't forget to update it once you are done with this tutorial.

In this case the URL will follow a pattern like https://yourwebsite/connect/check-<resource-owner-name>., however you are free to change the callback URL as you follow this tutorial.

Almost all of the Resource Owner have at least 2 parameters namely client_id and secret. In this example, we are going to use the 4 previously mentioned Social Networks, so we'll need to register the following parameters in your app/config/config.yml file. Is recommended to store your tokens within double quotes too e.g "your-token-here":

# app/config/config.yml
parameters:
    # For Github you'll need the client_id and secret   
    github_client_id: <replace-with-your-github-client-id>
    github_secret: <replace-with-your-github-secret>
    
    # For Facebook you'll need the client_id and secret   
    facebook_client_id: <replace-with-your-facebook-client-id>
    facebook_secret: <replace-with-your-facebook-secret>
    
    # For Google+ you'll need the client_id and secret   
    googleplus_client_id: <replace-with-your-googleplus-client-id>
    googleplus_secret: <replace-with-your-googleplus-secret>
    
    # For Stack Exchange you'll need the client_id, secret and key
    stackexchange_client_id: <replace-with-your-stackexchange-client-id>
    stackexchange_secret: <replace-with-your-stackexchange-secret>
    stackexchange_key: <replace-with-your-stackexchange-key>

These tokens will be used by the Resource Owners in the next step.

3. Configure HWIO and Resource Owner

Now that you have the rights to create requests to the desired Social Network servers, you need to create the local Resource Owners of every Social Network. Go to the config.yml file of your Symfony Application and set the configuration (hwi_oauth) of HWIOAuthBundle. Here is where you register new social networks with their respective access tokens:

# app/config/config.yml
hwi_oauth:
    # Define which firewalls will be used for oauth
    # Usually, its only the main, but you can add it if you have a custom one
    firewall_names: ["main"]
    fosub:
        username_iterations: 30
        # Define in which columns of the fos_user table will be stored
        # the access token of every resource_owner
        properties:
            github: github_id
            facebook: facebook_id
            googleplus: googleplus_id
            stackexchange: stackexchange_id
    # Define the resource_owners that your user can use to login into your app
    # Note that the client_id and client_secret and key values are symfony parameters
    # stored too in the config.yml from the previous step !
    resource_owners:
        github:
            type:           github
            client_id:      "%github_client_id%"
            client_secret:  "%github_secret%"
            scope: 'user:email,public_repo'
        facebook:
            type:           facebook
            client_id:      "%facebook_client_id%"
            client_secret:  "%facebook_secret%"
            infos_url:     "https://graph.facebook.com/me?fields=id,name,email"
        googleplus:
            type:           google
            client_id:      "%googleplus_client_id%"
            client_secret:  "%googleplus_secret%"
            scope:  "email profile"
        stackexchange:
            type:           stack_exchange
            client_id:      "%stackexchange_client_id%"
            client_secret:  "%stackexchange_secret%"
            options:
                key: "%stackexchange_key%"

For more information about how the configuration of every resource owner works, refer to the official docs of HWIOAuthBundle here.

4. Configure Resource Owners Routes

Your user will need to access a route that identifies with which social network he wants to login. Go to the app/config/security.yml file of your application and add the oauth configuration for your firewall:

# app/config/security.yml
security:
    # Modify firewalls
    firewalls:
        # Set the config on your firewall
        main:
            oauth:
                # Declare the OAuth Callback URLs for every resource owner
                # They will be added in the routing.yml file too later
                resource_owners:
                    github: "/connect/check-github"
                    facebook: "/connect/check-facebook"
                    googleplus: "/connect/check-googleplus"
                    stackexchange: "/connect/check-stackexchange"
                ## Provide the original login path of your application (fosuserroute)
                ## and the failure route when the authentication fails.
                login_path:     /user/login
                failure_path:   /user/login
                # Inject a service that will be created in the step #6
                oauth_user_provider:
                    service: app.fos_user.oauth_provider

As next, proceed to add the routes of HWIOBundle and the resource owners to your app/config/routing.yml file:

Note

As mentioned in the step 2, in the OAuth account manager of apps like Facebook, Github etc, you will need to provide the OAuth Callback URL. You can use the routes of this step to provide a route to the third party services e.g http://yoursite.com/connect/check-facebook.

# app/config/routing.yml

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

hwi_oauth_login:
    resource: "@HWIOAuthBundle/Resources/config/routing/login.xml"
    prefix:   /login
    
github_login:
    path: /connect/check-github
    
facebook_login:
    path: /connect/check-facebook
    
googleplus_login:
    path: /connect/check-googleplus

stackexchange_login:
    path: /connect/check-stackexchange

In this way, if the user wants to sign in to your application using his Facebook Account, you would only need to redirect him to the route http://yoursite.com/connect/check-facebook

5. Create Login and Register Manager

Of someway your application needs to receive information from the Social Network to register or to login. That's the function of the following FOSUBUserProvider class. By default it works without needing any modification. Once the user access the check route, the loadUserByOAuthUserResponse function comes into action. If the user isn't registered with the Social Network Account on your application, it will create a new row on the fos_user table by default with a random username e.g 12345_<name-of-social-network> and signs him automatically. If the user already exists, it will search the user by the field <social-network>_id and will send the access token to retrieve information.

You are free to modify the class to set the fields you need, to set the username you need etc. In this case, we stored the class in the Entity folder of the AppBundle, but you can save it wherever you want:

<?php

// Change the namespace according to your project.
namespace AppBundle\Entity;

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


// Source: https://gist.github.com/danvbe/4476697

class FOSUBUserProvider extends BaseClass {

    public function connect(UserInterface $user, UserResponseInterface $response) {
        $property = $this->getProperty($response);
        
        $username = $response->getUsername();
        
        // On connect, retrieve the access token and the user id
        $service = $response->getResourceOwner()->getName();
        
        $setter = 'set' . ucfirst($service);
        $setter_id = $setter . 'Id';
        $setter_token = $setter . 'AccessToken';
        
        // 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);
        }
        
        // Connect using the current user
        $user->$setter_id($username);
        $user->$setter_token($response->getAccessToken());
        $this->userManager->updateUser($user);
    }

    public function loadUserByOAuthUserResponse(UserResponseInterface $response) {
        $data = $response->getResponse();
        $username = $response->getUsername();
        $email = $response->getEmail() ? $response->getEmail() : $username;
        $user = $this->userManager->findUserBy(array($this->getProperty($response) => $username));
        
        // If the user is new
        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());
            
            //I have set all requested data with the user's username
            //modify here with relevant data
            $user->setUsername($this->generateRandomUsername($username, $response->getResourceOwner()->getName()));
            $user->setEmail($email);
            $user->setPassword($username);
            $user->setEnabled(true);
            $this->userManager->updateUser($user);
            return $user;
        }
        
        // If the user exists, use the HWIOAuth
        $user = parent::loadUserByOAuthUserResponse($response);
        
        $serviceName = $response->getResourceOwner()->getName();
        
        $setter = 'set' . ucfirst($serviceName) . 'AccessToken';
        
        // Update the access token
        $user->$setter($response->getAccessToken());
        
        return $user;
    }
    
    /**
     * Generates a random username with the given 
     * e.g 12345_github, 12345_facebook
     * 
     * @param string $username
     * @param type $serviceName
     * @return type
     */
    private function generateRandomUsername($username, $serviceName){
        if(!$username){
            $username = "user". uniqid((rand()), true) . $serviceName;
        }
        
        return $username. "_" . $serviceName;
    }
}

6. Create the fos_user.oauth_provider service

In the security.yml we have defined the oauth_user_provider option with the app.fos_user.oauth_provider service, that till now doesn't exist, therefore you need to create it. The service returns the FOSUBUserProvider class and as arguments the user manager of FOSUserBundle and the Resource Owners created in the step 3:

# app/config/services.yml
services:
    app.fos_user.oauth_provider:
        # Change the class according to the location of the FOSUBUserProvider class
        class: AppBundle\Entity\FOSUBUserProvider
        arguments:
            # Inject as first argument the user_manager of FOSUserBundle
            user_manager: "@fos_user.user_manager"
            # An object/array with the registered Social Media from config.yml
            user_response:
                github: github_id
                facebook: facebook_id
                googleplus: googleplus_id 
                stackexchange: stackexchange_id

7. Test it !

If everything was configured correctly (and following the default configuration) you will able to access (login or register) via a Social Network accessing the following routes:

<!-- Remove app_dev.php from the URL if you aren't in DEV mode -->

<a href="/app_dev.php/connect/github">
    Login with Github 
</a>

<a href="/app_dev.php/connect/stackexchange">
    Login with Stack Exchange 
</a>

<a href="/app_dev.php/connect/facebook">
    Login with Facebook 
</a>

<a href="/app_dev.php/connect/googleplus">
    Login with Google+ 
</a>

Here the user will be redirected to the Social Network grant page that asks the user if he really want to use his account to sign into another application. It's worth to say that you need to clear the cache and sign out from the current account to prevent any error.

Happy coding !

Become a more social person