Angular et SSO (Single Sign On), côté client.

L’authentification

Lorsque vous créez un site WEB complexe reposant sur une base de données, la question de l’authentification est très vite abordée et il devient rapidement nécessaire d’identifier les utilisateurs de votre application afin de leur accorder des droits.

Dans cette optique, je vais vous faire part de mon expérience avec l’authentification SSO (Single Sign On) et les réseaux sociaux, que j’ai implémenté en Javacript et le framework AngularJS, je ne cherche en aucun cas à vous instruire l’utilisation de ces différents outils.

Se connecter sur votre site via les réseaux sociaux

Tout d’abord, pourquoi ne pas développer son propre mécanisme d’authentification ?

  • Le fonctionnement est complexe et doit garantir une sécurité optimale. Se pencher sur le sujet prend beaucoup de temps.

  • Les visiteurs de votre site seront plus méfiants à l’idée de se créer un nouveau compte, ils seront plus à l’aise s’ils peuvent se connecter via un compte qu’ils possèdent déjà tels que Facebook, Google, etc…

Si vous l’avez déjà expérimenté, vous avez peut être remarqué que l’utilisation d’une application tierce pour se faciliter la connexion est une pratique assez répandue dans le domaine des applications mobiles, plus particulièrement dans les jeux, mais elle s’applique aussi très bien dans le secteur du WEB.

Tout nous amène à croire que cette solution est le remède miracle contre la difficulté de la gestion de la connexion de l’utilisateur.

Néanmoins, il est important de noter que s’authentifier via une application tierce comporte tout de même des risques à ne pas négliger.

Si l’application sur laquelle votre site repose ne fonctionne plus (Facebook, Google, LinkedIn, ect…), l’utilisateur ne pourra plus se connecter, et vous ne pourrez malheureusement rien y faire puisque vous n’y avez aucun contrôle. (Croyez-moi, cela arrive beaucoup plus de fois qu’on pourrait le penser). Ce fût par exemple le cas avec Twitter qui a décidé de supprimer l’API Javascript qu’il mettait à disposition lors de son passage de la version 1.0 à la version 1.1 (pour des raisons de sécurité), forçant tous les utilisateurs de l’API à mettre à jour leurs sites.

Mon expérience et besoin

Dans le cadre de mon expérience, j’ai eu à traiter les problèmes suivants :

  • Authentification côté client en Javascript via Facebook, Google et LinkedIn

  • Personnaliser les boutons Facebook, Google et LinkedIn

  • Sauvegarde des utilisateurs de mon site dans une base

Dans cet exemple, j’ai utilisé du Javascript avec le framework AngularJS fonctionnant côté client. Le serveur ne sera que très peu évoqué par la suite.

Je vais ainsi utiliser les APIs Javascript proposées par ces trois réseaux sociaux.

Voici les spécifications du fonctionnement de mon application “News’ Innov” (pour Facebook) :

Processus d'authentification SSO côté utilisateur

Un peu de théorie…

Afin de faciliter la maintenabilité et la réutilisation du code, il est important de bien structurer son application. Si vous cherchez plus d’informations concernant l’authentification via Facebook, Google et LinkedIn, vous vous apercevrez qu’il existe bien des manières de la réaliser. Notre but ici est d’homogénéiser ces trois APIs différentes, afin de rendre leur utilisation plus intuitive aux yeux du programmeur.

On distingue 2 phases :

  • Chargement des APIs

  • Utilisation des APIs

Voici l’architecture gérant l’authentification côté client que j’ai utilisé en considérant les points suivants :

  • Application single page, la page est partiellement rafraîchie lorsque l’utilisateur navigue entre les différentes fonctionnalités

  • Développement côté client sur AngularJS

  • Peu de traitements effectués sur le serveur (uniquement le stockage des informations utilisateurs, et la gestion des tokens)

Service d'Authentification SSO

Vous noterez que quelque soit notre réseau social, le fonctionnement est le même. Ainsi, il devient simple d’ajouter, si vous en avez le besoin, une nouvelle API.

J’ai choisi d’architecturer ce module d’authentification conformément au modèle MVC (modèle-vue-contrôleur).

Passons sans plus tarder à la pratique, je vous expliquerai en détail le fonctionnement de chacune de ces parties.

Chargement des APIs

Avant de se lancer, nous devons récupérer les clés d’API nous permettant d’utiliser les services proposés par Facebook, Google et LinkedIn.

Je vous laisse le soin d’effectuer les recherches nécessaires afin d’obtenir ces clés, vous trouverez quelques informations ici :

Facebook : https://ftutorials.com/facebook-api-key/

Google : https://developers.google.com/maps/documentation/javascript/tutorial?hl=fr#api_key

LinkedIn : https://developer.linkedin.com/documents/authentication

La 1ère étape consiste à charger les APIs proposées par nos différents réseaux sociaux.

Nous allons les récupérer de manière asynchrone afin de ne pas bloquer le chargement de la page.

Facebook

En s’inspirant de la documentation :

 // Charge le SDK de manière asynchrone

 (function(){

    // Si le SDK est déjà installé, c’est bon !

    if(document.getElementById(facebook-jssdk)){return;}

   // Création de la racine Facebook ‘fb-root dans le DOM

    var fbroot = document.getElementById(fb-root);

    if(!fbroot){

        fbroot    = document.createElement(‘div’);

        fbroot.id=fb-root;

        document.body.insertBefore(fbroot, document.body.childNodes[0]);

    }

    // On récupère la première balise <script>

    var firstScriptElement = document.getElementsByTagName(‘script’)[0];

    // Création du <script> Facebook

    var facebookJS = document.createElement(‘script’);

    facebookJS.id=facebook-jssdk;

    // Source du Facebook JS SDK

    facebookJS.src =‘//connect.facebook.net/fr_FR/all.js’;

    // Insertion du Facebook JS SDK dans le DOM

    firstScriptElement.parentNode.insertBefore(facebookJS, firstScriptElement);

  }());

Google

// Charge le SDK de manière asynchrone

 (function(){

    // On récupère la première balise <script>

    var firstScriptElement = document.getElementsByTagName(‘script’)[0];



    // Création du <script> Google

    var googleJS = document.createElement(‘script’);

    googleJS.type=‘text/javascript’;

    googleJS.async=true;



    // Source du Google JS SDK

    googleJS.src=https://apis.google.com/js/client:plusone.js?onload=googlePlusAsyncInit;



    // Insertion du Google JS SDK dans le DOM

    firstScriptElement.parentNode.insertBefore(googleJS, firstScriptElement);

  }());

LinkedIn

// Charge le SDK de manière asynchrone

 (function(){

    // On récupère la première balise <script>

    var firstScriptElement = document.getElementsByTagName(‘script’)[0];



    // Création du <script> LinkedIn

    var linkedInJS = document.createElement(‘script’);

    linkedInJS.type=‘text/javascript’;

    linkedInJS.async=true;

    // Source du LinkedIn JS SDK

    linkedInJS.src=https://platform.linkedin.com/in.js;



    // Ajout du paramètre onLoad

    var keys = document.createTextNode(

        « n« +

        « onLoad: linkedInAsyncInit »+

        « n« 

    );

    linkedInJS.appendChild(keys);



    // Insertion du LinkedIn JS SDK dans le DOM

    firstScriptElement.parentNode.insertBefore(linkedInJS, firstScriptElement);

  }());

Si vous utilisez AngularJS, vous pouvez inclure la définition des scripts dans une directive :

angular.module(‘GooglePlus’,[]).

directive(‘googlePlus’,function(){

     return{

       restrict:‘A’,

       scope:true,

       controller:function($scope, $attrs){

         // Ajouter le code ci-dessus

       }

     }

   });

N’oubliez pas d’inclure les scripts et d’injecter les différents modules créés dans votre application.

Les codes ci-dessus vont créer les balises <script> permettant de récupérer le contenu des scripts chez nos fournisseurs. Une fois ces scripts chargés, chacun va faire appel à une fonction callback, qui va nous avertir.

Dans notre cas, voici les fonctions qui seront appelées à la fin du chargement de chaque script :

Facebook : Par défaut, la fonction fbAsyncInit

Google : googlePlusAsyncInit – défini en paramètre ?onload=googlePlusAsyncInit

LinkedIn : linkedInAsyncInit – défini dans le corps du script onLoad: linkedInAsyncInit

J’ai rencontré un bug : la callback “linkedInAsyncInit” n’est pas appelée sur Internet Explorer, si vous rencontrez le même problème, vous devrez ajouter ce bout de code dans le corps de la fonction asynchrone.

if(linkedInJS.readyState){

   linkedInJS.onreadystatechange=function(){

       if(linkedInJS.readyState==« loaded »||

           linkedInJS.readyState==« complete »){

           linkedInJS.onreadystatechange=null;

           linkedInAsyncInit(); // On lance manuellement notre callback

       }

   };

}

linkedInJS.readyState nous permet de tester “manuellement” si nous sommes sur Internet Explorer, si c’est le cas, on implémente un listener qui appellera la callback une fois le script chargé.

Le corps de ces callbacks sera défini plus tard.

Nous voilà enfin prêts à utiliser les objets FB, gapi et IN, qui nous autorisent à communiquer avec nos fournisseurs !

Création de l’utilisateur

Nous avons besoin de stocker toutes les informations relatives à l’utilisateur dans un objet. Celui-ci devra être disponible sur toutes les pages de notre site, de ce fait, nous pouvons le stocker sous la forme d’un service AngularJS (il est aussi possible d’utiliser une factory AngularJS).

A vous de choisir quelles informations utilisateur stocker. Dans mon cas, je dois récupérer un token qui me permettra de maintenir et de sécuriser ma connexion avec mon fournisseur. Je sauvegarde aussi mes données dans un cookie qui sera réutilisable lors de la prochaine visite de l’utilisateur sur mon site. La validité du cookie sera assurée par le token.

angular.module(‘myApp’)

   .service(‘UserService’,function($cookieStore){



   var user ={

       accessToken:«  »,

       id:«  »,

       isLogged:false,

       firstName:«  »,

       lastName:«  »,

       email:«  »,

       socialNetwork:«  »,

       image:«  »

   };



   // Fonctions utilisateurs

   this.setUser=function(userData){

       user.isLogged=true;

       // … Affecter les différents champs de notre utilisateur

       $cookieStore.put(‘user’, user);// Ajouter dans les cookies

       sendToServer();

   }



   // Ajoutez d’autres fonctions …

   this.sendToServer  =function(userData){

       // …

   }

 

   this.logout=function(){

       // Vider tous les champs

       user.isLogged=false;

       $cookieStore.remove(‘user’);

   }



});

Afin d’utiliser le service $cookieStore d’AngularJS, n’oubliez pas d’injecter le module “ng-cookies” dans votre application.

Définition des services Facebook, Google et LinkedIn

Écriture de la structure de nos services

J’ai créé 3 services de type factory AngularJS correspondant à chaque fournisseur.

Chacun de ces services implémentera une fonction d’initialisation de l’API, une fonction de connexion et une fonction de déconnexion.

angular.module(‘Authentication’)

   .factory(‘GooglePlusService’,function(UserService){

       return{

           init:function(clientId){

               // Code d’initialisation

           },

           login:function(){

               // Code de connexion

           },

           logout:function(){

               // Code de déconnexion

           }

       };

   });

Initialisation des APIs

Nous avons précédemment défini comment charger nos différentes API, passons à leur initialisation.

Notre fonction d’initialisation prend en paramètre la clé d’API récupérée auprès de notre fournisseur qui sera nécessaire afin d’autoriser une connexion. Elle contiendra la callback qui sera appelée lorsque le script sera complètement chargé. Vous pouvez paramétrer les options d’initialisation comme vous le désirez, celles-ci sont décrits dans la documentation de chacune des APIs.

Le comportement des APIs est assez différent, je vais alors vous les décrire au cas par cas.

Facebook

init:function(apiKey){

   // Cette fonction est appelée lorsque le script sera chargé

   window.fbAsyncInit=function(){

       FB.init({

           appId: apiKey, // La clé d’API Facebook

           cookie:true,

           status:true,

           xfbml:true

       });

   }

}

Souvenez-vous, la callback appelée par Facebook est “fbAsyncInit”.

Google

init:function(clientId){

   window.googlePlusAsyncInit=function(){

// Cette fonction permet de contourner les bloqueurs de popup

       window.setTimeout(gapi.auth.init,1);

       // On stocke les paramètres de la connexion dans une variable

// Ces paramètres seront utilisés

       params ={

           client_id: clientId +« .apps.googleusercontent.com », // La clé d’API Google

           immediate:false,

           scope:« https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/userinfo.email » // Les permissions

       };

   }

}

La callback appelée par Google est “googlePlusAsyncInit”, le nom a été défini arbitrairement auparavant.

LinkedIn

init:function(apiKey){

   window.linkedInAsyncInit=function(){

       IN.init({

           onLoad:« loadLinkedIn »,

           api_key: apiKey, // La clé d’API LinkedIn

           authorize:true,

           credentials_cookie:true,

           lang:« fr_FR »

       });

   };

}

La callback appelée par LinkedIn est “linkedInPlusAsyncInit”, le nom a été défini arbitrairement auparavant.

Le fonctionnement de LinkedIn est particulier car implémente le mécanisme des événements (listeners). De ce fait, les événements doivent être chargés qu’une seule fois. Je les ai définis dans la callback “loadLinkedIn”.

window.loadLinkedIn=function(){

   // Evénement qui sera lancé lorsque l’utilisateur sera authentifié

   IN.Event.on(IN,« auth »,function(){

       login(); // On récupère les données utilisateur.

   });

}

Pourquoi tant de callbacks ?

Vous vous en êtes probablement rendu compte, l’utilisation de callbacks n’est pas très élégant car les fonctions sont définies globalement dans toute l’application. Pour Facebook et Google, il est possible de fournir en paramètre du “onLoad” une fonction, mais ce n’est pas le cas avec LinkedIn, qui prend en paramètre le nom de la fonction. Ainsi, LinkedIn recherche une fonction définie globalement dans le window.

Récupération des données utilisateur

Nous voici dans la phase la plus importante de notre module d’authentification.

Lorsque l’utilisateur va cliquer sur notre bouton, il faudra alors faire appel à l’API correspondante afin d’ouvrir une popup de connexion. Rassurez-vous, cette popup est entièrement gérée par le fournisseur, vous n’avez juste qu’à faire appel à la fonction permettant de l’afficher et récupérer les informations utilisateur.

Facebook

login:function(){

   FB.getLoginStatus(function(response){

       if(response.status===‘connected’){

           // On récupère le token temporaire qu’on stocke dans une variable

           accessToken = response.authResponse.accessToken;

    // Si l’utilisateur est déjà connecté à Facebook, on récupère son profil

           getProfile();

       }else{

           // Si l’utilisateur n’est déjà connecté à Facebook, on ouvre la popup

           openPopup();

       }

   });

}

 

// Fonction d’ouverture de la popup

function openPopup(){

   // Ouverture de la popup

   FB.login(function(response){

       // Teste si l’utilisateur est désormais connecté

       if(response.status==‘connected’){

           // On récupère le token temporaire qu’on stocke dans une variable

           accessToken = response.authResponse.accessToken;

           // On récupère le profil utilisateur

           getProfile();

       }elseif(angular.isDefined(response.error)){

           // Une erreur s’est produite           

}

   },{scope:’email’}); // Paramétrage pour récupérer l’email

}

// Fonction de récupération du profil

function getProfile(){

   FB.api(‘/me’,function(response){

       if(angular.isUndefined(response.error)){

           // On remplit notre objet utilisateur

           UserService.set(response, accessToken /* Ajoutez les champs nécessaires */ );

       }else{

           // Une erreur s’est produite

       }

   });

}

Google

login:function(){

   // Ouvre la popup

   // Prend en paramètre les paramètres utilisés lors de l’initialisation

   // et la callback qui va traiter la réponse envoyée par Google

   gapi.auth.authorize(params, loginFinishedCallback);

}

 

function loginFinishedCallback(authResult){

   if(authResult[‘access_token’]&& authResult[‘g-oauth-window’]&&

       angular.isUndefined(authResult[‘error’])){



       // On affecte le token à notre variable gapi

       // (Utile pour récupérer l’adresse email)

       gapi.auth.setToken(authResult);



       // On récupère le token temporaire

       var accessToken = authResult[‘access_token’];



       // On récupère les informations utilisateur

       gapi.client.load(‘plus’,‘v1’,function(){

           var request = gapi.client.plus.people.get({‘userId’:‘me’});

           request.execute(

               function(profile){

                   // On remplit notre objet utilisateur

                   UserService.set(profile, accessToken /* Ajoutez les champs nécessaires */ );

               }

           );

       });

   }elseif(authResult[‘error’]){

       // Une erreur s’est produite

   }else{

       // authResult est vide, l’utilisateur a abandonné la connexion

   }

}

 

// Si on désire récupérer l’adresse mail, il faut réaliser cette requête supplémentaire

// gapi.auth.setToken doit avoir été appelé auparavant

gapi.client.load(‘oauth2’,‘v2’,function(){

   var request = gapi.client.oauth2.userinfo.get();

   request.execute(

       function(profile){

           email = profile.email;// On récupère l’email

           // …

       }

   );

});

LinkedIn

login:function(){

   // On affiche la popup

   var isAlreadyLogged = IN.User.authorize();

   // Si l’utilisateur s’est connecté, on récupère son profil

   if(isAlreadyLogged){

       getProfile();

   }else{

       // L’utilisateur a abandonné la connexion

   }

}

function getProfile(){

   IN.API.Profile(« me »).fields([« id »,« firstName »,« lastName »,« pictureUrl »,« publicProfileUrl »,« emailAddress »])/* Ajoutez les champs désirés */

       .result(function(result){



           var profile ={};

           // Le résultat envoyé par LinkedIn est très complet, nous récupérons seulement le profil

           angular.copy(result.values[0], profile);



           // Récupération du token temporaire

           var accessToken = IN.ENV.auth.oauth_token;

           UserService.set(profile, accessToken /* Ajoutez les champs nécessaires */);



       }).error(function(err){

           // Une erreur s’est produite

       });

}

Dans le cas de LinkedIn, il se peut que l’utilisateur n’ait pas défini de photo. Le champ “pictureUrl” est alors “undefined”. Si vous voulez récupérer la photo par défaut de LinkedIn, vous la trouverez via ce lien : https://s.c.lnkd.licdn.com/scds/common/u/images/themes/katy/ghosts/person/ghost_person_200x200_v1.png

La déconnexion

Dans mon cas, lorsque l’utilisateur cliquera sur le bouton “Se déconnecter”, il se déconnectera uniquement de mon application et non du réseau social.

Dans les trois cas, ma fonction de déconnexion est la suivante

logout:function(){

   UserService.logout();

}

Si on veut fermer toute session de Facebook, par exemple, on peut ajouter:

FB.logout();

Service d’authentification global

Toutes nos fonctions sont désormais implémentées, il ne nous reste plus qu’à les appeler !

Nous allons réunir tous nos appels dans un service:

angular.module(‘Authentication’)

   .factory(‘AuthenticationService’,

   function(UserService, FacebookService, GooglePlusService, LinkedInService){

       var FACEBOOK_API_KEY =« Votre_cle_Facebook »;

       var GOOGLE_CLIENT_ID =« Votre_cle_Google »;

       var LINKED_IN_API_KEY =« Votre_cle_LinkedIn »;



       return{

           // Pour récupérer les données de l’utilisateur

           getUser:function(){

               return UserService.getUser();

           },

           // Fonctions d’initialisation

           initFacebook:function(){

               FacebookService.init(FACEBOOK_API_KEY);

           },

           initGooglePlus:function(){

               GooglePlusService.init(GOOGLE_CLIENT_ID);

           },

           initLinkedIn:function(){

               LinkedInService.init(LINKED_IN_API_KEY);

           },

           // Fonctions de connexion

           connectFacebook:function(){

               FacebookService.login();

           },

           connectGooglePlus:function(){

               GooglePlusService.login();

           },

           connectLinkedIn:function(){

               LinkedInService.login();

           },

           // Fonctions de déconnexion

           disconnectFacebook:function(){

               FacebookService.logout();

           },

           disconnectGooglePlus:function(){

               GooglePlusService.logout();

           },

           disconnectLinkedIn:function(){

               LinkedInService.logout();

           }

       }

   });

Contrôleur

Le contrôleur joue le rôle de médiateur entre le service et la vue.

On va initialiser toutes nos APIs, puis demander la connexion/déconnexion lorsque l’utilisateur va cliquer sur un bouton.

angular.module(‘Authentication’)

   .controller(‘AuthenticationCtrl’,function($scope, $location, SourcesCache, AuthenticationService){



       // On initialise toutes les APIs

       AuthenticationService.initFacebook();

       AuthenticationService.initLinkedIn();

       AuthenticationService.initGooglePlus();



       // On récupère notre utilisateur, pour par exemple l’afficher sur notre vue

       $scope.user= AuthenticationService.getUser();



       // Les fonctions de connexion, lancées par un clic



       $scope.connectFacebook=function(){

           AuthenticationService.connectFacebook();

       };



       $scope.connectTwitter=function(){

           AuthenticationService.connectTwitter();

       };



       $scope.connectGooglePlus=function(){

           AuthenticationService.connectGooglePlus();

       };



       $scope.connectLinkedIn=function(){

           AuthenticationService.connectLinkedIn();

       };



       // La déconnexion est la même pour chaque réseau social dans mon cas

       // J’ai affecté un champ « socialNetwork » qui renseigne quel réseau social

       // a été utilisé

       $scope.disconnectUser=function(){

           switch($scope.user.socialNetwork){

               case(« facebook »):

                   AuthenticationService.disconnectFacebook();

                   break;

               case(« google »):

                   AuthenticationService.disconnectGooglePlus();

                   break;

               case(« linkedin »):

                   AuthenticationService.disconnectLinkedIn();

                   break;

           }

       };

   });

Création de la vue

Passons dorénavant au HTML. Nous devons ajouter nos 3 boutons de connexion :

<a facebookclass=« facebook »ng-click=« connectFacebook() »>Se connecter avec Facebook</a>

<a google-plusclass=« google »ng-click=« connectGooglePlus() »>Se connecter avec Google</a>

<a linkedinclass=« linkedin »ng-click=« connectLinkedIn() »>Se connecter avec LinkedIn</a>

et notre bouton de déconnexion :

<a ng-click=« disconnectUser() »>Se déconnecter</a>

Vous pouvez ajouter le style que vous souhaitez grâce au CSS.

Avec AngularJS, on fait appel aux directives en les ajoutant en attribut ou en balise (cela dépend de la façon dont vous avez défini votre directive).

Si vous n’utilisez pas AngularJS, vous pouvez remplacer ng-click par on-click.

Petit point sur les tokens et validation par le serveur

Jusqu’ici, les tokens récupérés sont des tokens ayant une durée de vie limitée. Il faut alors se procurer des tokens validés.

Dans mon cas, je n’utilise uniquement les réseaux sociaux pour l’authentification. Le client va envoyer le token temporaire au serveur, et ce serveur va communiquer une seconde fois avec le fournisseur afin de valider ce token (Rappelons-le, le Javascript n’est pas réputé pour sa sécurité). Si le token est bien valide, l’application est autorisée à utiliser les fonctionnalités du site bloquées.

Pour plus d’informations sur le mécanisme de tokens, je vous invite à vous rendre sur le site de Facebook Developers qui vous propose plusieurs architectures selon vos besoins :

https://developers.facebook.com/docs/facebook-login/access-tokens/#architecture

Pour conclure

Dans cette longue étude, j’ai passé beaucoup de temps à sélectionner l’architecture qui me semblait la plus adaptée pour mon type d’application. Si vous cherchez à réaliser une application aux fonctionnalités similaires, il vous sera utile de passer du temps sur les différentes documentations des APIs afin de connaître les paramétrages spécifiques à utiliser. Vous trouverez sans doute une autre solution plus adaptée à votre cas.

En développement collaboratif, il est primordial d’être clair sur le code afin de faire gagner beaucoup de temps aux autres à la lecture de votre code.

Comme l’a si bien dit Alain Rémond, “Partir, c’est ranger un peu”…

Ce sujet vous intéresse ?

NOUS CONTACTER