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:
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)
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.
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);
}());
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);
}());
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.
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".
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.
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.
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
}
});
}
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
…
}
);
});
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?