Sign-in & Login Facebook / Google / Twitter / Email via Symfony2
Cette article est très vieux et ne devrait pas être utilisé comme support pour votre travail.
Si vous affichez un formulaire de trois kilomètres de long à un utilisateur pour s’inscrire, il va vous faire une syncope tout de suite. C’est terrible, les statistiques le montrent, tout le monde à la flemme et du coup personne ne s’inscrit.
Mais proposez-lui une inscription en deux clics depuis un compte Google ou Facebook (ça tombe bien en plus il est déjà connecté sur son navigateur) et ça sera joie / bonheur / exaltation dans son cœur. Et vous, vous récupérerez un utilisateur de plus. Les fainéants sont légion et il nous faut toujours plus de monde sur nos applis !
Cependant, je vous vois venir avec votre remarque. En effet, certains vont prendre peur en voyant des réseaux sociaux et ne s’inscriront jamais chez vous. On va donc également proposer une alternative email classique.
Pour ce faire on va se baser sur deux bundles, le premier étant FOSUserBundle, poids lourd de la gestion d’utilisateurs dans le milieu symfony2. Le second HWIOAuthBundle permettant une intégration simple du OAuth2 pour les réseaux sociaux.
Le tout va nous permettre un système de sign-in & login comme on les aime.
Un beau dessin
Alors avant de commencer on va déjà se mettre d’accord sur où on va. Pour ce faire au lieu de faire un gros pavé, on va faire un gros dessin.
Il est pas beau mon schéma ? Ho oui c’est de toute beauté! Bon OK, ça ne m’a pas pris un temps fou mais c’est quand même bien sympa !
Alors du coup c’est assez simple, que l’utilisateur veuille passer par les réseaux sociaux ou via le mail classique on le renverra vers un formulaire intermédiaire. À la différence que via les réseaux sociaux il passera par l’API correspondante et on aura déjà les infos !
Installation
Commençons par installer nos chers bundles
composer.son
"require": { ... "friendsofsymfony/user-bundle": "~1.3", "hwi/oauth-bundle": "0.4.*@dev", ... },
Et on met à jour
php composer.phar update
Je ne reviens pas sur l’installation de FOS user, tout est très bien expliqué sur la page officielle !
Move fast and break things
Ho oui comme disait Mark Zuckerberg a une certaine époque, on passe aux choses sérieuses, il y a de nombreux fichiers à configurer pour arriver à nos fins.
Alors dans l’ordre :
Acme/UserBundle/Entity/User.php
<?php namespace Acme\UserBundle\Entity; use FOS\UserBundle\Entity\User as BaseUser; use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; /** * @ORM\Entity * @ORM\Table(name="fos_user") */ class User extends BaseUser { public function __construct() { parent::__construct(); $this->media = new \Doctrine\Common\Collections\ArrayCollection(); } /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @var string * * @ORM\Column(name="facebook_id", type="string", nullable=true) */ protected $facebook_id; /** * @var string * * @ORM\Column(name="google_id", type="string", nullable=true) */ protected $google_id; /** * @var string * * @ORM\Column(name="twitter_id", type="string", nullable=true) */ protected $twitter_id; }
Ici on ajuste notre entité utilisateur à nos besoins. Je rajoute l’id unique et indispensable pour connexion de chaque réseau social. On les met bien évidement en non obligatoire pour nos amis hater des réseaux sociaux pour qu’ils puissent se connecter sans ces derniers. Hé oui on aime tout le monde !
N’oubliez pas de faire un petit : php app/console doctrine:generate:entities AcmeUserBundle pour créer les getters et setters.
App/config/config.yml
fos_user: db_driver: orm firewall_name: main user_class: Acme\UserBundle\Entity\User registration: form: type: acme_user_registration confirmation: enabled: false hwi_oauth: connect: confirmation: true registration_form: fos_user.registration.form firewall_name: main fosub: username_iterations: 30 properties: facebook: facebook_id google: google_id twitter: twitter_id resource_owners: # Facebook Access facebook: type: facebook client_id: [ClientID] client_secret: [ClientIDSecret] scope: "email" options: display: popup # Google Access google: type: google client_id: [ClientID] client_secret: [ClientIDSecret] scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile" options: display: popup # Twitter Access twitter: type: twitter client_id: [ClientID] client_secret: [ClientIDSecret] scope: ""
Le fichier de config !
Ici je me contente de Facebook, Google et Twitter.
Pour accéder aux infos de chaque réseau social, il faudra créer une application concernant votre site sur le réseau social en question. Cette application vous permettra de faire communiquer votre site avec les données de l’utilisateur sur le réseau social. Concernant les deux id : ‘client_id’ et ‘client_secret’ il s’agit des deux clefs d’authentification de votre application.
Voici les liens par réseaux pour créer une application et obtenir les deux clefs d’API nécessaire à la connexion.
Une fois sur ces liens suivez les explications vous allez pouvoir créer vos applications en peu de temps.
App/config/routing.yml
# HWIOAuthBundle hwi_oauth_security: resource: "@HWIOAuthBundle/Resources/config/routing/login.xml" prefix: /connect hwi_oauth_redirect: resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml" prefix: /connect # We override this part of the routing hwi_oauth_connect: resource: "@AcmeAcmeBundle/Resources/config/routing/connect.xml" prefix: /connect facebook_login: pattern: /connect/check-facebook google_login: pattern: /connect/check-google twitter_login: pattern: /connect/check-twitter # FOSUser fos_user_security: resource: "@FOSUserBundle/Resources/config/routing/security.xml" fos_user_register: resource: "@FOSUserBundle/Resources/config/routing/registration.xml" prefix: /register fos_user_resetting: resource: "@FOSUserBundle/Resources/config/routing/resetting.xml" prefix: /resetting fos_user_change_password: resource: "@FOSUserBundle/Resources/config/routing/change_password.xml" prefix: /profile
Le fichier de routing. On rajoute nos routes pour chaque réseau social pour différencier l’appel vers les apis correspondantes, mais surtout on surcharge une partie du routing par notre fichier qui traitera, l’inscription par les réseaux sociaux.
AcmeAcmeBundle/Resources/config/routing/connect.xml
<?xml version="1.0" encoding="UTF-8" ?> <routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd"> <route id="hwi_oauth_connect_service" pattern="/service/{service}"> <default key="_controller">HWIOAuthBundle:Connect:connectService</default> </route> <route id="hwi_oauth_connect_registration" pattern="/registration/{key}"> <default key="_controller">AcmeAcmeBundle:Connect:registration</default> </route> </routes>
Le fichier de route qui nous permettra de faire ce qu’on veut, on aime ça faire ce qu’on veut, lors de l’inscription d’une personne sur notre appli. Je détaillerai le contenu du controller registration plus bas. Mais avant, ENCORE de la config… C’est bientôt fini !
app/config/security.yml
security: encoders: FOS\UserBundle\Model\UserInterface: sha512 role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] providers: fos_userbundle: id: fos_user.user_provider.username_email firewalls: main: pattern: ^/ form_login: provider: fos_userbundle csrf_provider: form.csrf_provider login_path: /login check_path: /login_check oauth: resource_owners: facebook: "/connect/check-facebook" google: "/connect/check-google" twitter: "/connect/check-twitter" login_path: /connect failure_path: /connect oauth_user_provider: service: hwi_oauth.user.provider.fosub_bridge logout: true anonymous: true login: pattern: ^/login$ security: false remember_me: key: "%secret%" lifetime: 31536000 path: / domain: ~ access_control: - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
Le fichier de sécurité indispensable aux connexions.
Ok Google
Overdose de config je suis d’accord mais pas le choix si on veut faire tourner la machine nickel ! Et c’est pas la première, ni la dernière fois que vous configurez un truc hein ! Bref on passe au contrôleur.
Alors on surcharge le contrôleur qui se trouve la HWIOAuthBundle:Connect, on va le chercher et on en fait une copie dans notre bundle à AcmeAcmeBundle:Connect.
Vous l’aurez remarqué on ne surcharge que la méthode registration, cette méthode nous permet d’avoir un contrôle total lors de l’inscription via réseau social, avant et après soumission du formulaire:
src/Acme/AcmeBundle/Controller/ConnectController.php
<?php namespace Acme\AcmeBundle\Controller; use HWI\Bundle\OAuthBundle\OAuth\ResourceOwnerInterface; use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken; use HWI\Bundle\OAuthBundle\Security\Core\Exception\AccountNotLinkedException; use Symfony\Component\DependencyInjection\ContainerAware; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AccountStatusException; use Symfony\Component\Security\Core\SecurityContext; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\SecurityEvents; class ConnectController extends ContainerAware { public function registrationAction(Request $request, $key) { $connect = $this->container->getParameter('hwi_oauth.connect'); if (!$connect) { throw new NotFoundHttpException(); } $hasUser = $this->container->get('security.context')->isGranted('IS_AUTHENTICATED_REMEMBERED'); if ($hasUser) { throw new AccessDeniedException('Cannot connect already registered account.'); } $session = $request->getSession(); $error = $session->get('_hwi_oauth.registration_error.'.$key); $session->remove('_hwi_oauth.registration_error.'.$key); if (!($error instanceof AccountNotLinkedException) || (time() - $key > 300)) { throw new \Exception('Cannot register an account.'); } $userInformation = $this ->getResourceOwnerByName($error->getResourceOwnerName()) ->getUserInformation($error->getRawToken()) ; // enable compatibility with FOSUserBundle 1.3.x and 2.x if (interface_exists('FOS\UserBundle\Form\Factory\FactoryInterface')) { $form = $this->container->get('hwi_oauth.registration.form.factory')->createForm(); } else { $form = $this->container->get('hwi_oauth.registration.form'); } $formHandler = $this->container->get('hwi_oauth.registration.form.handler'); if ($formHandler->process($request, $form, $userInformation)) { // Connect user $this->container->get('hwi_oauth.account.connector')->connect($form->getData(), $userInformation); // Authenticate the user $this->authenticateUser($request, $form->getData(), $error->getResourceOwnerName(), $error->getRawToken()); // Getting user $user = $this->container->get('security.context')->getToken()->getUser(); // Getting social network source $source = $userInformation->getResourceOwner()->getName(); // Updating user by source switch ($source) { case 'facebook': $user = $this->handleFacebookResponse($userInformation, $user); break; case 'google': $user = $this->handleGoogleResponse($userInformation, $user); break; case 'twitter': $user = $this->handleTwitterResponse($userInformation, $user); break; } // Saving User $em = $this->container->get('doctrine.orm.entity_manager'); $em->persist($user); $em->flush(); // Redirect the user to homepage $url = $this->container->get('router')->generate( 'acme_acme_homepage' ); return new RedirectResponse($url); } // reset the error in the session $key = time(); $session->set('_hwi_oauth.registration_error.'.$key, $error); return $this->container->get('templating')->renderResponse('HWIOAuthBundle:Connect:registration.html.' . $this->getTemplatingEngine(), array( 'key' => $key, 'form' => $form->createView(), 'userInformation' => $userInformation, )); } public function handleFacebookResponse($response, $user) { // User is from Facebook : DO STUFF HERE \o/ // All data from Facebook $data = $response->getResponse(); // His profile image : file_get_contents('https://graph.facebook.com/' . $response->getUsername() . '/picture?type=large') return $user; } public function handleGoogleResponse($response, $user) { // User is from Google: DO STUFF HERE \o/ // All data from Google $data = $response->getResponse(); // His profile image : file_get_contents($data['picture']) return $user; } public function handleTwitterResponse($response, $user) { // User is from Twitter: DO STUFF HERE \o/ // All data from Twitter $data = $response->getResponse(); // His profile image : file_get_contents($data['profile_image_url']) return $user; } }
Ici je ne fais que changer la fin du traitement d’inscription, en différenciant les sources de réseaux sociaux pour personnaliser le traitement des informations pour chaque cas.
A noter que si vous voulez changer le formulaire d’inscription, il vous suffira de changer le formulaire d’inscription de FOSUser, il en va de même pour les templates !
Épilogue
Je me contente ici de trois réseaux sociaux, mais si vous en voulez d’autre HWIOAuthBundle en proprose un gros paquet ici.
À vous ensuite d’adapter le code pour chaque réseau social dans les fichiers de configs et dans le contrôleur !
Happy coding !
Bonjour et merci pour cet article, tout fonctionne parfaitement.
Vivement que le AuthBundle soit compatible SF > 2.4 !
Bonjour,
D’abord, merci pour ce tuto.
J’ai essayé de l’implémenter, mais je suis bloqué par 2 erreurs.
1. InvalidArgumentException: The service definition « fos_user.registration.form » does not exist.
2. InvalidArgumentException: Unable to replace alias « fos_user.registration.form » with « hwi_oauth.registration.form ».
Ma configuration est la suivante :
– FOS : 2.0.*@dev
– HWI : 0.3.8
J’ai suivi le tuto et adapté selon mon application.
Il y a cependant un point que je ne suis pas sur d’avoir compris.
J’ai un bundle User qui surcharge FOS.
J’ai créé l’équivalent du fichier src/Acme/AcmeBundle/Controller/ConnectController.php dans mon bundle User mais en fait, je crains qu’il ne soit pas reconnu.
Auriez vous une idée de piste à suivre pour débugger ?
Merci
Bonjour,
il semble que FOSUser ne soit pas chargé dans votre configuration.
Je vois que vous utiliser la version 2.0.*@dev, je vous conseille d’utiliser la dernière version stable, comme dans le tuto, qui est la 1.3 (cela risque d’avoir des effets sur votre projet). Suivez également scrupuleusement la doc pour surcharger le bundle : https://github.com/FriendsOfSymfony/FOSUserBundle/blob/1.3.x/Resources/doc/index.md
Concernant l’autre erreur pourriez vous double checkez le fichier « App/config/routing.yml » et notamment le « hwi_oauth_connect » ?
Bonjour,
J’ai récupéré un site avec une classe User (il n’utilise pas le FOSUserBundle) et jai du mal à comprendre la notion de provider dans ce cas là (je précise que je ne suis pas du tout un dev symfony à la base, donc j’avance un peu dans le noir). Dois-je utiliser un des trois providers proposés par le bundle ?
Merci
Bonjour,
Cela dépend de comment est implémenté la gestion des utilisateurs dans votre appli.
Concernant le provider voila de quoi vous éclairer sur cette partie.
La documentation de HWI en parle et peut vous aider pour ce cas apparemment, vous devriez utiliser le provider EntityUserProvider (service name: hwi_oauth.user.provider.entity) si vos utilisateurs sont bien en base de données. Je n’ai cependant jamais tester ce cas précis.
Hello et merci pour cette publication.
Je découvre OAuth. J’ai lu/entendu que c’était pas top niveau sécurité (en particulier la version 2.0), notamment du fait qu’il s’agit davantage d’un moyen d’autorisation et non d’authentification.
Quels sont les risques en terme de sécurité / vie privée induit par cette méthode ?
Bonjour !
Merci pour votre intérêt.
Concernant votre question, n’étant pas un spécialiste en sécurité, je ne veut pas dire de bêtise alors je vous invite à lire cette article : http://www.bubblecode.net/fr/2013/03/10/comprendre-oauth2/
Bonjour
Merci pour ce tuto !
Je viens de l’installer dans mon site
Je suis en SF 3 donc j’utilise la branche dev-master de hwi/oauth-bundle
J’essaye uniquement la connexion Facebook pour le moment
Quand je clic sur le lien /connect/facebook, il m’envoi bien vers FB
J’autorise mon site à se connecter (l’appli est alors bien ajoutée à mon profil FB)
Mais ensuite je suis redirigée vers un formulaire vide « s’inscrire avec le compte Olivia… » (voir PJ)
C’est bien mon nom mais j’aimerais plutôt atterrir sur mon formulaire d’inscription, avec les champs pré-remplis, ou encore mieux, que le user soit directement connecté et arrive sur la home page !
Mon fichier de config :
hwi_oauth:
connect:
confirmation: true
registration_form: fos_user.registration.form.factory
firewall_names: [main]
fosub:
username_iterations: 30
properties:
facebook: facebook_id
Pour info, j’ai mis fos_user.registration.form.factory au lieu fos_user.registration.form comme indiqué dans la doc, sinon il ne trouve pas le service
Peux-tu m’aider ?
Un grand merci !
Olivia
[EDIT]
En regardant les log, je me suis rendu compte qu’il ne trouvait pas le user en BDD, en cherchant par facebook_id
J’ai donc mis à la main le facebook _ID en base et là ca fonctionne.. 🙂
Par contre, comment faire pour que le facebook_id soit bien sauvegardé en base ??
Une piste ?
merci
Bonjour,
J’ai le même problème que vous, j’aimerai que le user soit directement connecté et arrive sur ma homepage.
Je suis dans les mêmes conditions (symfony 3).
Avez vous trouvé la solution ?
Merci
non toujours pas.. si vous trouvez je suis preneuse !
Bonjour vous deux!
Vous avez trouvé une solution depuis? je planche sur le sujet depuis hier et la plupart des exemples de code que je trouve concernent symfony 2…
core
SMTP
networks