Symfony2 : FOSUserBundle et Connexion OAuth (facebook / twitter / google / github / etc…)

2014, May 08    

Dans un précédent article, « Intégrer facilement Facebook Connect et Twitter Connect avec FosUserBundle pour Symfony 2.1″, je vous parlais de l’intégration d’un Facebook Connect et d’un Twitter Connect. Le dit-billet étant un peu passé d’actualité désormais, je vais vous présentez une autre solution. Cette solution s’appuie sur un bundle plus générique et ne se limitant pas qu’à Facebook/Twitter.

Ce tutoriel est pour Symfony2 version >= 2.3, et utilise les bundles « FOSUserBundle » et « HWIOAuthBundle ».

 

FOSUserBundle

Pour ce passage, référez vous à l’article cité plus haut. Cela vous expliquera comment récupérer et configurer le bundle FOSUser.

Si vous êtes en train de lire cet article, c’est que vous voulez implémenter d’autres authentification et inscription que la « basique » user / password. Si vous doutez encore de l’utilité, je suis tombé dernièrement sur une infographie assez sympa. D’ailleurs, je ne pensais pas que le Google Login était autant utilisé. Cependant, ça tombe bien, le bundle que je vais vous présenter et vous aider à configurer réponds tout à fait à ce besoin aussi !

infographie-social-login-gigya

 

HWIOAuthBundle

OAuthC’est parti pour voir comment nous allons implémenter dans notre appli Symfony2 les différents logins. Dans cet exemple, je vais vous montrer le Facebook login. Vous ne devriez pas avoir de soucis d’ajouter les autres par la suite. Si vous rencontrez des soucis, n’hésitez pas à regarder le repo Github du projet ou à laisser un commentaire avec votre question.

Adresse du repo du bundle : https://github.com/hwi/HWIOAuthBundle

Les logins supportées : 37signals, Amazon, Bitbucket, Bitly, Box, Dailymotion, DeviantArt, Disqus, Dropbox, Facebook, Flickr, Foursquare, GitHub, Google, Instagram, JIRA, LinkedIn, Mail.ru Odnoklassniki, QQ, Salesforce, Sensio Connect, Sina, Weibo, Stack Exchange, Stereomood, Twitch, Twitter, VKontakte, Windows Live, WordPress, Yahoo, Yandex.

Autant vous dire, que vous avez l’embarras du choix. Bien entendu, je ne vous conseille pas de tous les mettre ! Choisissez ceux adaptés à votre cible et essayez de vous limiter.

Bref, commençons. Tout d’abord, comme d’hab, il faut ajouter dans votre composer.json / require :

{
    "require": {
        "hwi/oauth-bundle": "0.4.*@dev"
    }
}

Une fois ceci fait, ajouter dans votre app/AppKernel.php :

public function registerBundles()
{
    $bundles = array(
        // ...
        new HWI\Bundle\OAuthBundle\HWIOAuthBundle(),
    );
}

Jusqu’ici rien de compliqué, on est dans la démarche habituelle d’installation d’un nouveau bundle.

Une fois ceci fait, ajoutez la propriété facebookId à votre entité User. Puis générez les getters/setters via la console.

protected $facebookId;
php app/console doctrine:generate:entities Acme/UserBundle/Entity/User

Nous allons nous attaquer à la configuration  du security.yml. Rien de bien compliqué à nouveau.
Tout d’abord, dans votre parameters.yml ajoutez l’id de votre application Facebook ainsi que la clé secrète.

oauth.facebook.id: "id_app"
oauth.facebook.secret: "secret_key_app"

Puis mettez à jour votre fichier de config, en ajoutant ceci :

hwi_oauth:
  firewall_name: main
  resource_owners:
    facebook:
      type: facebook
      client_id: %oauth.facebook.id%
      client_secret: %oauth.facebook.secret%

Vous voila avec une configuration propre. Vous pouvez désormais configurer votre fichier security.yml de la sorte :``

security:
  firewalls:
    main:
      context: user
      pattern: /.*
      oauth:
        resource_owners:
          facebook: "/login/check-facebook"
        login_path: /connect
        failure_path: /connect
        oauth_user_provider:
          service: rkueny.oauth.user_provider
      logout: true
      anonymous: true

Pour le fichier complet, n’oubliez pas de vous référer au premier billet sur l’installation du bundle FOSUserBundle.

Vous ne devriez pas avoir rencontré de problèmes jusqu’ici. Avant de créer le service rkueny.oauth.user_provider nous allons configurer (et oui encore de la configuration !), le fichier de routing.

Il vous suffit d’importer les routes du plugin et d’ajouter la route Facebook configurée. Pour éviter certains bugs (d’après la doc officielle), l’import des routes du plugin doit être avant les votres. Ainsi nous avons dans le fichier de routing :

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

facebook_login:
    pattern: /login/check-facebook

Créons à présent le service rkueny.oauth.user_provider. Pour cela créez le fichier src/RKueny/UserBundle/UserProvider.php et mettez y ce qui suit :

<?php

namespace RKUENY\UserBundle\OAuth;

use FOS\UserBundle\Model\UserInterface as FOSUserInterface;

use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\FOSUBUserProvider;

use RKUENY\UserBundle\Entity\User;

use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Loading and ad-hoc creation of a user by an OAuth sign-in provider account.
 *
 * @author Fabian Kiss <fabian.kiss@ymc.ch>
 */
class UserProvider extends FOSUBUserProvider
{
	/**
	 * {@inheritDoc}
	 */
	public function loadUserByOAuthUserResponse(UserResponseInterface $response)
	{
		try {
			return parent::loadUserByOAuthUserResponse($response);
		} catch (UsernameNotFoundException $e) {
			if (null === $user = $this->userManager->findUserByEmail($response->getEmail())) {
				return $this->createUserByOAuthUserResponse($response);
			}

			return $this->updateUserByOAuthUserResponse($user, $response);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public function connect(UserInterface $user, UserResponseInterface $response)
	{
		$providerName = $response->getResourceOwner()->getName();
		$uniqueId = $response->getUsername();
		$user->addOAuthAccount($providerName, $uniqueId);

		$this->userManager->updateUser($user);
	}

	/**
	 * Ad-hoc creation of user
	 *
	 * @param UserResponseInterface $response
	 *
	 * @return User
	 */
	protected function createUserByOAuthUserResponse(UserResponseInterface $response)
	{
		$user = $this->userManager->createUser();
		$this->updateUserByOAuthUserResponse($user, $response);

		// set default values taken from OAuth sign-in provider account
		if (null !== $email = $response->getEmail()) {
			$user->setEmail($email);
		}

		if (null === $this->userManager->findUserByUsername($response->getNickname())) {
			$user->setUsername($response->getNickname());
		}

		$user->setEnabled(true);

		return $user;
	}

	/**
	 * Attach OAuth sign-in provider account to existing user
	 *
	 * @param FOSUserInterface      $user
	 * @param UserResponseInterface $response
	 *
	 * @return FOSUserInterface
	 */
	protected function updateUserByOAuthUserResponse(FOSUserInterface $user, UserResponseInterface $response)
	{
		$providerName = $response->getResourceOwner()->getName();
		$providerNameSetter = 'set'.ucfirst($providerName).'Id';
		$user->$providerNameSetter($response->getUsername());

		if(!$user->getPassword()) {
			// generate unique token
			$secret = md5(uniqid(rand(), true));
			$user->setPassword($secret);
		}

		return $user;
	}
}

Je vous conseille de vous attarder un peu sur ce fichier pour bien le comprendre et le maitriser. La seule chose « bizarre » que j’ai réalisée est dans la méthode « updateUserByOAuthUserResponse ». C’est lorsqu’un utilisateur utilise le Facebook Connect je lui génère un mot de passe. Il ne l’utilise en fait pas par la suite. Si vous voulez faire un Facebook Connect qui ne remplit que les infos il ne faudra bien sûr pas faire comme cela.

Après la création de ce fichier, il vous faut le déclarer comme service. Ce n’est pas compliqué à faire, il y a plusieurs façons voici la mienne, dans le fichier src/RKueny/UserBundle/Resources/config/services.xml :

<?xml version="1.0" encoding="UTF-8"?>

<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services
                               http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="rkueny.oauth.user_provider.class">RKueny\UserBundle\OAuth\UserProvider</parameter>
    </parameters>

    <services>
        <service id="rkueny.oauth.user_provider" class="%rkueny.oauth.user_provider.class%">
            <argument type="service" id="fos_user.user_manager" />
            <argument type="collection">
                <argument key="facebook">facebookId</argument>
            </argument>
        </service>
    </services>

</container>

Nous avons fini ici la configuration du plugin. Vous pouvez ajouter les logins à votre site via le render du plugin :

{% if is_granted('IS_AUTHENTICATED_FULLY') %}
    Hello {{ app.user.username }}

    
        Logout
    
{% else %}
    {{ render(url('hwi_oauth_connect')) }}
{% endif %}

En ajoutant bien entendu la route qui va bien avec :

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

Vous avez désormais un site acceptant les logins de différents services :) N’hésitez pas si vous avez des questions, je suis peut être passé trop vite sur certains points.