FOSOAuthServerBundle – implémentation de l’OAuth2 entre deux apps

Objectif

Un projet m’a demandé à implémenter l’OAuth2 entre deux plates-formes à savoir une application Symfony2 (avec FOSOAuthServerBundle) et un site WordPress. L’objectif étant qu’un visiteur sur le site WordPress (déjà utilisateur ou non) puisse se logguer à son administration avec le mécanisme d’authentification présent sur l’app Symfony2.

Dans ce tutoriel, je présente donc une mise en application du FOSOAuthServerBundle couplé particulièrement avec le FOSUserBundle.

Mettons par exemple que nous avons ces deux applications distinctes :

Symfony : http://symfonyapp.com
Wordpress : http://wordpressapp.com

Symfony2OAuth2 Wordpress

Bundles et plugins utilisés

  • FOSUserBundle : gestion des utilisateurs sur l’app Symfony
  • FOSRestBundle : gestion de l’API sur l’app Symfony
  • FOSOAuthServerBundle : support de l’OAuth2 sur l’app Symfony
  • Le plugin WP-Oauth a été choisi pour les tests côté WordPress
Dans cet article je n’aborderai pas certains fondamentaux tels que la connaissance du protocole OAuth2 (qu’il me faut moi-même bien assimiler encore), la création d’un formulaire d’authentification avec le FOSUserBundle ou encore la création d’un compte utilisateur WordPress.

Configuration côté application Symfony

Installation et configuration du FOSOAuthServerBundle

Je vous invite à consulter sa documentation, je vais apporter ici ma configuration, très légèrement différente.

config.yml

On notera la présence du UserManager de notre FOSUserBundle ainsi que les scopes supportés par l’OAuth. Ici je me suis limité au plus petit scope « user »

routing.yml

J’ai importé les deux routes requises par le FOSOAuthServerBundle (CF Documentation officielle). J’ai ensuite ajouté une ressource « myapp_oauth_server_security : » qui embarque deux routes propres à mon process d’authentification déjà en place (FOSUserBundle), cf son contenu ci-dessous

@MyApiBundle/Resources/config/security.yml

Les deux routes ci-dessus sont importantes car elles sont reportées et définies dans le firewall « oauth_authorize»  du FOSOAuthServerBundle (dans notre security.yml principal, voir ci-dessous). Leur nommage est libre mais par soucis de clarté, on reprend le path du authorize.xml : /oauth/v2/auth (qui sera la première route appelée dans le process OAuth2). On lui ajoute un fragment /login qui pointera sur notre action de contrôleur affichant le formulaire de connexion. On ajoute aussi le fragment /login_check qui lui pointera sur l’action de contrôle de l’authentification. Ces deux routes pointent ici vers mon MyUserBundle qui étend le FOSUserBundle.

security.yml

Firewall et sécurité

Gadini / Pixabay

Ici il faut veiller à une chose importante : positionner les deux firewalls du FOSOAuthServerBundle en amont de nos firewalls déjà présents. En effet ils doivent être traités prioritairement.

oauth_authorize : J’ai donc ajouté ici mon process d’authentification (form_login) avec mes deux routes précédemment créées. J’ai précisé le provider d’utilisateurs utilisé (le fos_userbundle) qui, plus haut, permet l’authentification par e-mail ou username. Notons que ce firewall est accessible en anonyme.

oauth_token : recopie de la doc officielle

api :

  • fos_oauth = true  : toutes les urls du pattern ^/api seront sous couvert de l’OAuth2
  • stateless = true : aucune requêtes sur ces urls ne générera de session, il sera nécessaire de passer le token pour chacune.
  • anonymous = false : les urls ne sont pas accessibles sans authentification
Jusque là rien de bien compliqué mais il faut cependant bien comprendre cette configuration

Création de notre premier client à savoir notre application WordPress

Je suppose ici que vous avez bien suivi la documentation d’installation du FOSOAuthServerBundle. Au sein de votre Bundle gérant l’authentification, ou bien peut-être au sein de votre Bundle gérant l’API (c’est mon cas), vous avez logiquement créé ces entités et mis à jour votre schéma :

  • MyApp\MyApiBundle\Entity\Client
  • MyApp\MyApiBundle\Entity\AccessToken
  • MyApp\MyApiBundle\Entity\RefreshToken
  • MyApp\MyApiBundle\Entity\AuthCode

Commande Symfony pour créer des clients

J’ai copié cette commande sur un site googlé ici ou là.
Dans mon contexte, c’est mon Bundle gérant l’API qui s’occupe aussi de l’OAuth2 (je n’ai pas découplé).

./src/MyApp/MyApiBundle/Command/CreateOAuthClientCommand.php
Lancement de la commande pour créer notre client (l’app WordPress)
Commande de création du client

venturaartist / Pixabay

J’ai ajouté ici tous les droits existants pour l’OAuth2 en sachant que dans ce tutoriel, je ne parlerai que du mode « authorization_code » c’est-à-dire le process d’échange d’un code d’authorization (déjà reçu par wordpress) contre un token d’accès (émis en retour par symfony).

Ce qui nous retourne notre client_id avec son secret utilisable côté WordPress.

Note importante : l’identifiant de ce nouveau client est la concaténation de son « id » avec son « random_id » dont voici le format : <id>_<random_id>

Simple test de bon fonctionnement

…du moins, on peut déjà tenter de demander un token en passant par une authentification « client_credentials » car on a autorisé ce droit.
Sans encore passer par wordpress, vous pouvez tester le bon fonctionnement du FOSOAuthServerBundle avec un outil tel que Postman (plugin firefox que j’utilise).

On reçoit un joli token en retour et on notera la présence du scope « user » ainsi que d’un délai d’expiration

Vers un test plus complet en mode « authorization_code »

Pour pousser la machine un peu plus loin et utiliser le mécanisme plus complet d’échange entre un code d’authorisation et un access_token, on va travailler directement avec WordPress et installer le plugin : WP_OAuth

Poussons la machine un peu plus loin

SpaceX-Imagery / Pixabay

Configuration côté WordPress

Installation du plugin WP-OAuth et création d’un nouveau connecteur vers notre application Symfony.

Je ne vais pas trop entrer dans les détails. J’ai créé un nouveau connecteur que j’appelle « sfapp » pour mon application Symfony. Voici le déroulé :

Ajouter « login-sfapp.php » (traitement dédié)

On commence par créer un nouveau connecteur. Ici j’ai simplement recopié le fichier « login-google.php » en « login-sfapp.php » puisque le process OAuth2 inscrit pour Google semblait correspondre à ce que je recherchais (et coup de chance…c’était le cas).

Il faut bien sûr modifier son paramétrage :

Ici j’ai simplement remplacé les valeurs « google » par un petit nom arbitraire pour mon app symfony « sfapp ». J’ai ensuite apporté les 3 Urls impératives :

  • URL_AUTH : pour la phase « authorize » (OAuth2) qui retournera le fameux code d’authorisation
  • URL_TOKEN : pour la phase « token » (OAuth2) qui « échangera » le code d’autorisation contre un access_token
  • URL_USER : L’URL que nous avons préparée pour WordPress côté Symfony. C’est une route Symfony managée par notre FOSRestBundle et qui retourne du JSON.
    En l’occurrence, dans mon contexte, elle va retourner les informations de compte de notre utilisateur (qui se sera loggué ou inscrit sur symfonyapp.com). Mais pour cela, cette URL doit recevoir en paramètre l’access_token bien sûr. En retour de ces informations, WordPress va créer/mettre à jour le compte utilisateur dans sa base et authentifier automatiquement l’utilisateur.

Ajouter le formulaire propre à notre application « sfapp » au plugin wp-oauth (backoffice)

On ouvre « wp-oauth-settings.php » et on copie-colle ensuite le formulaire pour google, on ajuste les noms de propriétés pour « sfapp » et ça roule.
Désormais, dans le backoffice de wp-oauth, on a un nouveau formulaire pour « sfapp » avec ses champs : client, secret, enabled.

Finalement, ajouter le bouton « Login with sfapp » sur la page de login WordPress

Pour avoir le bouton « Login with sfapp » sur la page de login WordPress, il suffit d’ouvrir le fichier « wp-oauth.php » et d’y ajouter une nouvelle ligne. Les boutons sont  définis vers la ligne 126 environ.

Remplir notre client ID et Secret

On rafraichi éventuellement la page WP-OAuth qui va nous afficher notre nouveau formulaire pour « sfapp » et on vient y saisir les champs « Client ID » et « Client Secret » avec les informations de notre client créé sur Symfony.
On active bien sûr le connecteur en cochant la case Enabled.

THE BIG TEST

Nous allons maintenant tester intégralement le mécanisme par un « authorization_code ».

On constate bien sur la page de login wordpress notre bouton de connexion vers notre application Symfony.

En cliquant dessus, le plugin wp-oauth va nous générer une requête et l’appeler :

Attention : Cette requête pointe sur la route « authorize » de notre FOSOAuthServerBundle (=URL_AUTH de login-sfapp.php). Cependant comme nous ne sommes pas encore loggués encore, l’action du controller va nous rediriger vers le process d’authentification que nous avons défini sur le firewall oauth_authorize, rappelez-vous (voir security.yml) :

Nous arrivons donc ici : /oauth/v1/auth/login (app Symfony) et n’avons donc pas le temps de voir la requête précédente 🙂

Mon dilemne à été de savoir comment récupérer les informations de la requête initiale. Coup de chance, Symfony a stocké toute la requête pour moi en session et dans cette clé :

Modification de notre UserBundle

Je n’ai pas souhaité créer de end_point pour l’authorize, à savoir un autre formulaire de connexion ainsi qu’une page d’acceptation de la connexion entre les apps. Dans mon projet, l’objectif est que les utilisateurs soient immédiatement connectés à leur WordPress en passant par notre base d’utilisateurs existantes sur Symfony. Voici comment j’ai procédé et attention…c’est tout encore chaud…il y a peut-être des failles.

Login ou inscription ?

Une fois arrivé sur ma page de login Symfony, j’ai la possibilité de m’inscrire ou de me logguer (dans mon projet pris en exemple). Donc, j’ai deux traitements à modifier de telle sorte que l’OAuth2 suive son cours.

Cas d’un login : LoginSuccessHandler.php

Dans cet Handler, l’utilisateur est déjà loggué et je viens récupérer l’URL d’origine qui est stockée en session. Je valide immédiatement la demande d’autorisation, la réponse retourne un « code d’autorisation » vers la redicte_uri.

En conclusion ici, je n’ai pas trouvé comment faire autrement !
Je régénère donc mon objet Request depuis l’URL d’origine (authorize), j’extrait le scope requis pour terminer la demande d’autorisation que je valide enfin avec la méthode « finishClientAuthorization », veillant à lui passer le $user connecté bien sûr.

Cas d’une inscription : RegistrationConfirmListener.php

Pour l’instant je n’ai pas encore créé d’événement ou de service pour ce traitement particulier. J’ai copié-collé ce précédent traitement au sein du RegistrationConfirmListener.php afin de vérifier le bon déroulement dans le cadre d’une inscription. Pour ce faire, j’ai positionné le traitement dans la méthode onRegistrationconfirm (event) :

L’idéal est de positionner ce traitement au sein d’un événement. Oh tiens ! Justement le FOSOAuthServerBundle embarque des événements 😉 (CF Documentation officielle)

En conclusion, dans les 2 cas (login ou inscription), je retourne bien vers mon process OAuth2 avec une Response vers WordPress donc.

Que se passe-t-il ensuite ?

  1. Symfony2 retourne à la redirect_uri en passant un « code » d’autorisation pour le client.
    Attention, pendant une fraction de seconde, nous avons bien un code d’autorisation dans notre base de données, mais il va être immédiatement « consommé » en l’échange d’un access_token
  2. WordPress génère et appelle la requête de demande de token. Dans cette requête il passe le code d’autorisation qui sera contrôlé par SF.
  3. Symfony2 constate que le code existe bien, il génère un Token + RefreshToken et les retourne à WordPress, puis il supprime immédiatement le code d’autorisation. (je n’ai pas le temps de le voir)
  4. WordPress dispose d’un access_token, il appelle l’API de symfony pour récupérer le compte d’utilisateur venant de se logguer ou de s’inscrire sur Symfony.
  5. Au sein de login-sfapp.php j’ai ajouté les petits traitements permettant de créer un compte WordPress à réception des informations utilisateurs transmises par le FOSRestBundle (JSON). Je ne détaille pas cette partie.

Améliorations du process global

Plusieurs améliorations sont envisageables :

  1. D’abord je dois apporter un contrôle du client avant d’appeler la méthode « finishClientAuthorization() ». En effet, pour le moment je fais une validation d’office étant donné que je connais le client mais la sécurité…la sécurité…
  2. Eviter de passer par wp-oauth côté wordpress et lui préférer, dans sa version 4.7 notamment, son API REST V2. Je ne suis pas fan du développement de wp-oauth qui gagnerait à être amélioré mais il m’a permi de tester l’OAuth2, merci à lui 🙂
  3. Enfin, il faut exploiter les Events du FOSOAuthServerBundle qui sont là en respect des bons principes du développement sous Symfony (et ailleurs…).

Une suite à ce tutoriel ?

Je vais être amené à améliorer cet article qui a été rédigé assez rapidement, aussi je m’excuse par avance pour les fautes. Notamment je souhaite apporter plus de précisions au sujet du FOSOAuthServerBundle.

Je suis preneur de toute remarque corrective ou évolutive mais constructive.

Enfin j’aimerais proposer une démo.

FOSOAuthServerBundle et FOSUserBundle – OAuth2
5 (100%) 1 vote

Partagez cet articleShare on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInDigg thisShare on TumblrShare on RedditEmail this to someone