:::: MENU ::::

Sécuriser une SPA partie2, le client sous AngularJs

Cet article est le dernier d’une série consacrée à l’authentification d’une application Web :

Les bases de l’authentification web

Sécuriser une SPA partie1, le serveur avec expressJS

Sécuriser une SPA partie2, le client sous AngularJs

Dans la deuxième partie, nous avons vu comment configurer le serveur, mettre en place des routes sécurisées et connecter un utilisateur à l’aide de passportJs. Passons maintenant au client.

Rappel du Workflow

WorkFlow SPA On veut pouvoir proposer au client l’accès à une API composée d’une partie publique ouverte et d’une autre protégée nécessitant un compte sur le serveur. A la différence d’une application standard, notre application web va devoir réagir à des codes HTTP et modifier son apparence en conséquence. Afin de rester le plus propre et réutilisable possible, on va utiliser différentes techniques tel que le routage, l’injection de services et au cœur de l’authentification, un intercepteur.

Architecture

Notre projet se découpe de cette manière :

/public                     ---> Répertoire de l'application cliente
    /authentification       ---> Répertoire du module qui gère 
                                 l'authentification
        auth-services.js    ---> Module d'authentification
        login-template.html ---> Template du formulaire de connexion

    controller.js           ---> Module qui regroupe les contrôleurs de
                                 l'application
    services.js             ---> Services de l'application
    app.js                  ---> Fichier principal de l'application,
                                 configuration et routage

Tout ce qui concerne la connexion de l’utilisateur est regroupé dans le module d’authentification, l’appel à l’api se fait par les services de l’application.

On pourrait découper autrement, en séparant la gestion de l’utilisateur par exemple, suivant la complexité de l’application. Cet exemple illustre le principe d’authentification, je vous laisse l’adapter et le compléter au besoin.

Voici un schéma résumant l’idée générale du système d’authentification :

L’application contient un routeur permettant de gérer les différents états ainsi qu’un intercepteur qui va s’occuper d’identifier les besoin d’authentification.

Principe de l’interception

Comme son nom l’indique, l’intercepteur va …intercepter ! C’est un service que l’on va greffer à $httpProvider . Toutes les requêtes qui passent par là pourront être traitées avant leurs callback de résolution . Pour notre cas, on doit écouter et réagir lorsque le serveur retourne une erreur 401 ( Non autorisé ). Cela signifie que l’on essaye d’accéder à une ressource protégée et donc que notre utilisateur n’est pas enregistré. Dans ce cas, on proposera à l’utilisateur un formulaire de connexion en le routant vers /login .

On peut aussi créer notre intercepteur dans un service.

    $httpProvider.interceptors.push(function($q, $location, httpBufferService) {

        return {

            "responseError": function(response) {
                var deferred = $q.defer();

                if (response.status === 401) {

                    $location.path("/login");

                    httpBufferService.storeRequest({
                        config: response.config,
                        deferred: deferred
                    });
                }
                return deferred.promise;
            }
        };
    });

On stocke notre requête sous forme de promesse dans un service qui fera le rôle de buffer. De cette manière, lorsque l’utilisateur aura remplit le formulaire, on pourra relancer sa dernière demande automatiquement .


    .factory("httpBufferService", function($injector) {

        var $http;
        var buffer = {};

        return {
            storeRequest: function(request) {
                buffer = request;
            },
            retryLastRequest: function() {

                function successCallback(response) {
                    buffer.deferred.resolve(response);
                }

                function errorCallback(response) {
                    buffer.deferred.reject(response);
                }
                $http = $http || $injector.get("$http");
                $http(buffer.config).then(successCallback, errorCallback);
            }
        };
    });

Enregistrement des sessions

Une fois enregistré, on sauvegarde l’état de l’utilisateur. De cette manière, si on ferme notre application et que la session sur le serveur est toujours valide, on peut continuer sans avoir besoin de se re connecter. On l’a vu dans l’article précédent, il est possible de stocker des informations sous forme de cookie, c’est d’ailleurs comme ça que notre serveur reconnait l’utilisateur. Pour varier les plaisirs ( et se laisser plus de souplesse pour l’avenir) , on va utiliser le localStorage pour enregistrer nos données de session. modularize-all-the-things

    .service("SessionService", function() {
        this.setValue = function(key, value) {
            localStorage.setItem(key, value);
        };
        this.getValue = function(key) {
            return localStorage.getItem(key);
        };
        this.destroyItem = function(key) {
            localStorage.removeItem(key);
        };
    })

Ce service étant très générique, on pourra tout à fait changer la méthode d’enregistrement avec sessionStorage par exemple.

La différence entre localStorage et sessionStorage est la persistence. Avec localStorage, ce qui est enregistré est concervé même après la fermeture du navigateur alors que sessionStorage conserve les données  uniquement dans l’onglet courant et jusqu’à sa fermeture.

Gestion de l’utilisateur

C’est finalement ici que la logique de l’authentification est organisée autour de nos précédents services.

    .service("UserService", function($http, $location, SessionService, httpBufferService) {

        this.currentUser = {
            name: SessionService.getValue("session.name") || "",
            isLoggedIn: (SessionService.getValue("session.name") ? true : false)
        };

        this.login = function(user) {
            var _this = this;
            return $http.post("/login", {
                "username": user.name,
                "password": user.pass
            }).success(function(response) {

                _this.currentUser.name = response.username;
                _this.currentUser.isLoggedIn = true;
                SessionService.setValue("session.name", response.username);
                $location.path("/");
                httpBufferService.retryLastRequest();

            });
        };
        this.logout = function() {
            var _this = this;
            return $http.post("/logout").success(function() {
                _this.currentUser.isLoggedIn = false;
                SessionService.destroyItem("session.name");
            });
        };

        this.loginShowing = false;

        this.setLoginState = function(state) {
            this.loginShowing = state;
        };
    })

On distingue les phases de login et de logout, loginState permet de récupérer l’état de d’enregistrement de l’application sans avoir besoin d’analyser l’url, ce sera utile à la vue pour afficher le formulaire de login par exemple.

Ici on pourrait rajouter une gestion plus poussée des droits, la création de compte, un « remember me », un chiffrage du mot de passe etc..

Appel des services

Le plus dur est fait, il ne reste plus qu’à effectuer l’appel aux services de connexion en lui passant un couple user / pass . La gestion de l’API au niveau sécurité est transparent ici puisque c’est l’intercepteur qui s’en charge.

factory("DataService",function($http){
    return {
        getPublicData : function(){
            return $http.get("/api/data");
        },
        getPrivateData : function() {
            return $http.get("/api/secure/data");
        }
    };
});

Difficile de faire plus simple !

Formulaire de connexion

On va utiliser ui-router pour afficher notre formulaire lorsque l’on se place sur l’url /login.

    $stateProvider
        .state("Main", {
            url: "/"
        })
        .state("login", {
            url: "/login",
            onEnter: function(UserService) {
                UserService.setLoginState(true);
            },
            onExit: function(UserService) {
                UserService.setLoginState(false);
            },
            views: {
                "login": {
                    templateUrl: "authentification/login-template.html",
                    controller: "LoginCtrl"
                }
            }
        });

Pour le reste, on se contente de masquer des éléments suivant si l’utilisateur est enregistré.

</pre>
<header>
<div class="pull-right clearfix login">
<div><a>Sign in</a>
<div>{{user.name}} <button>Logout</button></div>
</div>
<div></div>
</div>
</header>
<pre>

Un mot sur les Tokens

Même si l’authentification par jetons est un peu différente, sur le principe, la gestion de l’application est similaire et on peut toujours se baser sur le système précédent, à savoir un intercepteur et le routeur.

  • On affiche l’application
  • L’utilisateur s’enregistre et envoie ses identifiants
  • Le serveur vérifie et retourne un jeton unique
  • Le client enregistre le jeton et l’envoie pour chaque requête

Pour gérer la fabrication du jeton, trois techniques possibles :

  • Directement dans l’url, mais ce n’est pas des plus élégants :
    $http.get('/api/secure/'+'?access_token='+token) ... 
  • Un header personnalisé du type X-Access-Token
  • Rester sur le standard avec le header HTTP Authorization.

Et pour modifier le header à la volée sans toucher aux requêtes applicatives, penser aux intercepteurs !

En résumé, on remplace le cookie par une entrée dans le sessionStorage. L’attribut cookie du header http devient un attribut d’Authorisation contenant un jeton.

 

Conclusion

Nous voila arrivé au bout de cette série sur l’authentification d’une application web de type Single Page Application. En utilisant des outils, tel que expressJs et angularJs, il est assez facile de mettre en place un système bien organisé. La possibilité de créer des modules qui se chargent de traiter l’authentification de manière assez transparente permet de se concentrer un maximum sur les fonctionnalités de l’application plutôt que des problèmes techniques . Vous retrouverez le code de cet article ainsi que du précédant sur Github .

git clone https://github.com/maxdow/expressjs-angularjs-authentification.git && cd expressjs-angularjs-authentification && npm install && bower install && node app.js
Login/Pass : joe

Ressources pour aller plus loin

Se protéger contre les failles CSRF

Un boilerplate pour construire une application sécurisée à base de passportJs

Un exemple complet server client sur les memes principes mais avec les jetons

Une explication complete sur le protocole Oauth2 en français