Angular and SSO (Single Sign On), client side.

Authentication

When you create a complex WEB site based on a database, the question of authentication is very quickly addressed and it quickly becomes necessary to identify the users of your application in order to grant them rights.

With this in mind, I'm going to share with you my experience with SSO (Single Sign On) authentication and social networks, which I implemented in Javacript and the AngularJS framework, I'm in no way trying to instruct you on the use of these different tools.

Connect to your site via social networks

First of all, why not develop your own authentication mechanism?

  • The operation is complex and has to ensure optimal safety. It takes a lot of time to work on the subject.

  • Visitors to your site will be more wary of creating a new account, they will be more comfortable if they can log in via an account they already have such as Facebook, Google, etc...

If you have already experienced it, you may have noticed that using a third-party application to facilitate the connection is a fairly common practice in the mobile application field, especially in games, but it also applies very well in the WEB sector.

Everything leads us to believe that this solution is the miracle cure for the difficulty of managing the user's connection.

Nevertheless, it is important to note that authenticating via a third-party application does carry risks that should not be overlooked.

If the application on which your site relies is no longer working (Facebook, Google, LinkedIn, etc.), the user will no longer be able to log in, and unfortunately you will not be able to do anything about it as you have no control over it. (Believe me, this happens more often than you might think). ) This was the case with Twitter, for example, which decided to remove the Javascript API it provided when it moved from version 1.0 to version 1.1 (for security reasons), forcing all API users to update their sites.

My experience and needs

In my experience, I have had to deal with the following problems:

  • Client-side authentication in Javascript via Facebook, Google and LinkedIn

  • Customise Facebook, Google and LinkedIn buttons

  • Saving my site's users in a database

In this example, I used Javascript with the AngularJS framework on the client side. The server side will only be mentioned a little later.

I will thus use the Javascript APIs offered by these three social networks.

Here are the specifications of how my "News' Innov" application (for Facebook) works:

User-side SSO authentication process

A little theory...

In order to facilitate maintainability and code reuse, it is important to structure your application well. If you look for more information about authentication via Facebook, Google and LinkedIn, you will find that there are many ways to do it. Our goal here is to homogenise these three different APIs, in order to make their use more intuitive to the programmer.

There are 2 phases:

  • Loading APIs

  • Use of APIs

Here is the architecture I used to manage client-side authentication, considering the following points:

  • Single page application, the page is partially refreshed as the user navigates between the different features

  • Client-side development on AngularJS

  • Little processing done on the server (only storage of user information and management of tokens)

SSO Authentication Service

You will note that whatever our social network, the operation is the same. Thus, it becomes simple to add, if you need it, a new API.

I chose to architect this authentication module according to the MVC (model-view-controller) model.

Without further ado, I will explain in detail how each of these parts work.

Loading APIs

Before we get started, we need to get the API keys to use the services offered by Facebook, Google and LinkedIn.

I leave it to you to do the necessary research to obtain these keys, you will find some information here:

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

The first step is to load the APIs offered by our different social networks.

We will retrieve them asynchronously so as not to block the loading of the page.

Facebook

Drawing on the literature :

Load the SDK asynchronously

(function(){

If the SDK is already installed, it's OK!

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

Creating the Facebook root 'fb-root' in the 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 of the Facebook JS SDK

facebookJS.src =connect.facebook.net/en_FR/all.js';

Inserting the Facebook JS SDK into the 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 of the LinkedIn JS SDK

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



Adding the onLoad parameter

var keys = document.createTextNode(

" n" +

"onLoad: linkedInAsyncInit+

" n"

);

linkedInJS.appendChild(keys);



Insert the LinkedIn JS SDK into the DOM

firstScriptElement.parentNode.insertBefore(linkedInJS, firstScriptElement);

}());

If you are using AngularJS, you can include the script definition in a :

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

directive(‘googlePlus’,function(){

     return{

       restrict:‘A’,

       scope:true,

       controller:function($scope, $attrs){

          Ajouter le code ci-dessus

       }

     }

   });

Don't forget to include the scripts and inject the various modules created into your 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.

In our case, these are the functions that will be called at the end of the loading of each script:

Facebook: By default, the fbAsyncInit

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

LinkedIn: linkedInAsyncInit - defined in the body of the onLoad script: linkedInAsyncInit

I encountered a bug: the "linkedInAsyncInit" callback is not called on Internet Explorer, if you encounter the same problem, you will have to add this piece of code in the body of the asynchronous function.

if(linkedInJS.readyState){

   linkedInJS.onreadystatechange=function(){

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

           linkedInJS.readyState==« complete »){

           linkedInJS.onreadystatechange=null;

           linkedInAsyncInit(); On lance manuellement notre callback

}

};

}

linkedInJS.readyState allows us to test "manually" if we are on Internet Explorer, if so, we implement a listener that will call the callback once the script is loaded.

The body of these callbacks will be defined later.

We are finally ready to use the FB, gapi and IN objects, which allow us to communicate with our suppliers!

User creation

We need to store all the user information in an object. This will need to be available on all pages of our site, so we can store it as an AngularJS service (it is also possible to use an AngularJS factory).

It is up to you to choose which user information to store. In my case, I need to retrieve a token that will allow me to maintain and secure my connection with my provider. I also save my data in a cookie that will be reusable the next time the user visits my site. The validity of the cookie will be ensured by the 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’);

   }



});

In order to use the AngularJS $cookieStore service, don't forget to inject the "ng-cookies" module into your application.

Definition of Facebook, Google and LinkedIn services

Writing the structure of our services

I created 3 AngularJS factory services for each provider.

Each of these services will implement an API initialisation function, a connection function and a disconnection function.

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

           }

       };

   });

Initialization of APIs

We have previously defined how to load our different APIs, let's move on to initializing them.

Our initialization function takes as a parameter the API key retrieved from our provider that will be needed to allow a connection. It will contain the callback that will be called when the script is fully loaded. You can set the initialization options as you wish, these are described in the documentation for each API.

The behaviour of the APIs is quite different, so I will describe them case by case.

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

       });

   }

}

Remember, the callback called by Facebook is "fbAsyncInit".

Google

init:function(clientId){

   window.googlePlusAsyncInit=function(){

This function allows you to bypass popup blockers

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

We store the parameters of the connection in a 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

       };

   }

}

The callback called by Google is "googlePlusAsyncInit", the name has been arbitrarily defined before.

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 »

       });

   };

}

The callback called by LinkedIn is "linkedInPlusAsyncInit", the name was arbitrarily defined earlier.

LinkedIn works in a special way because it implements the mechanism of events (listeners). As a result, events must be loaded only once. I have defined them in the "loadLinkedIn" callback.

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.

   });

}

Why so many callbacks?

As you have probably realised, the use of callbacks is not very elegant because the functions are defined globally in the whole application. For Facebook and Google, it is possible to provide a function as a parameter to the "onLoad", but this is not the case with LinkedIn, which takes the name of the function as a parameter. Thus, LinkedIn looks for a function defined globally in the window.

Recovery of user data

This is the most important phase of our authentication module.

When the user clicks on our button, we will have to call the corresponding API to open a login popup. Don't worry, this popup is fully managed by the provider, you just have to call the function to display it and get the user information.

Facebook

login:function(){

   FB.getLoginStatus(function(response){

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

We get the temporary token that we store in a variable

accessToken = response.authResponse.accessToken;

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

           getProfile();

       }else{

If the user is not already connected to Facebook, the popup is opened

openPopup();

}

});

}

 

Fonction d’ouverture de la popup

function openPopup(){

Opening the popup

FB.login(function(response){

Tests if the user is now logged in

if(response.status=='connected'){

We get the temporary token that we store in a 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

}

Profile retrieval function

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

       });

}

In the case of LinkedIn, the user may not have defined a picture. The "pictureUrl" field is then "undefined". If you want to retrieve the default LinkedIn picture, you can find it via this link: https: s.c.lnkd.licdn.com/scds/common/u/images/themes/katy/ghosts/person/ghost_person_200x200_v1.png

Disconnection

In my case, when the user clicks on the "Logout" button, they will only log out of my application and not the social network.

In all three cases, my disconnect function is as follows

logout:function(){

   UserService.logout();

}

If you want to log out of Facebook, for example, you can add:

FB.logout();

Global authentication service

All our functions are now implemented, we just need to call them!

We will gather all our calls into one 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();

           }

       }

   });

Controller

The controller acts as a mediator between the service and the view.

We'll initialise all our APIs, and then request login/logout when the user clicks a button.

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;

           }

       };

   });

Creating the view

Let's move on to the HTML. We need to add our 3 login buttons:

<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>

and our logout button:

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

You can add any style you like using CSS.

With AngularJS, directives are called by adding them as attributes or tags (depending on how you define your directive).

If you are not using AngularJS, you can replace ng-click with on-click.

Small point on the tokens and validation by the server

So far, the tokens recovered are tokens with a limited lifespan. It is then necessary to obtain validated tokens.

In my case, I only use social networks for authentication. The client will send the temporary token to the server, and this server will communicate a second time with the provider in order to validate this token (Remember, Javascript is not known for its security). If the token is valid, the application is allowed to use the blocked site features.

For more information on the token mechanism, I invite you to visit the Facebook Developers site, which offers several architectures depending on your needs:

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

To conclude

In this long study, I spent a lot of time selecting the architecture that I felt was best suited for my type of application. If you are looking to build an application with similar functionality, it will be useful to spend some time on the different APIs documentation to know the specific settings to use. You will probably find another solution more suited to your case.

In collaborative development, it is essential to be clear about the code in order to save others a lot of time reading your code.

As Alain Rémond said so well, "To leave is to tidy up a bit"...

Are you interested in this topic?

CONTACT US