Blog

L’authentification avec AngularJS

Avertissement : Cet article date de 2014, et contient des informations obsolétes. Voici un lien vers un repository Github qui présente un exemple fonctionnel (et maintenu) : fnakstad/angular-client-side-auth

Un framework front-end comme AngularJS ne peut pas être utilisé seul pour gérer l’authentification de manière sécurisée. En effet AngularJS étant entièrement instancié dans le navigateur, l’utilisateur peut modifier absolument tout le code et les données que vous lui fournissez.
Nous devons donc mettre en place l’authentification du côté serveur de notre application, ce qui entrainera généralement la création d’une session serveur qui contient les informations de l’utilisateur connecté.

Et là vous vous demandez surement comment AngularJS va-t’il récupérer ces informations ?

C’est là en effet tout le problème : gérer une authentification dans la partie backend de  notre application, c’est une partie de plaisir avec les frameworks actuels, mais comment faire passer l’information à notre partie cliente ? Plusieurs solutions s’offrent à nous :

Le cookie
C’est la méthode traditionnelle, elle consiste à mettre en place un cookie généré en backend qui contient les informations de l’utilisateur.
Ensuite, notre partie front-end l’utilise afin d’authentifier l’utilisateur. Le cookie est mis à jour par le serveur dès que nécessaire.
Le token
Cette solution authentifie notre utilisateur à l’aide d’un token signé qui est envoyé dans toutes les requêtes adressées à notre serveur.
Le token est d’abord généré par le backend qui l’envoie lors de l’authentification d’un utilisateur, pour chaque requête, le serveur va comparer le token qu’il a généré avec celui qu’il reçoit généralement à l’aide d’un middleware afin de s’assurer de l’identité de l’utilisateur.
L’utilisation d’une variable navigateur
Ici, une version simplifiée de l’objet user généré côté serveur à notre client qui va l’encapsuler dans un service AngularJS, pour sécuriser les routes qui nécessitent une authentification, une requête AJAX est effectuée auprès du serveur pour s’assurer que l’utilisateur est bien connecté.
Vous l’aurez deviné c’est la solution que l’on va utiliser ici!

Récupérer les informations utilisateur

Pour commencer, nous avons besoin que notre backend envoie les données de notre utilisateur à chaque rafraîchissement (avouez qu’il serait bête de perdre nos si précieuses informations d’authentification à chaque actualisation de la page), pour cela il faut mettre en place un catch-all qui fournira l’objet user pour toutes les requêtes reçues par notre serveur qui ne correspondent ni à une API ni à notre système d’authentification. Ici le backend est un serveur nodejs, mais cela n’a pas d’importance pour cet exemple, tout framework est en mesure de faire le nécessaire.

app.get('*', function(req, res) {
    res.render('index.html', {
        user: req.user ? JSON.stringify(req.user.username) : 'null'
    });
});

Comme vous l’avez remarqué, j’utilise ici l’objet user stocké dans la session serveur, ici je ne fournis à ma partie client que le nom d’utilisateur, si besoin, on peut fournir toutes les informations nécessaires.

Et maintenant place (enfin) au code nécessaire dans la partie cliente afin de récupérer nos informations utilisateurs:

<script type="text/javascript">
    window.user = {{ user|safe }};
</script>

Et oui c’est aussi bête que ça ! Lors du rendu de la vue, l’objet user va y être injecté et nous pourrons désormais y accéder au travers de la variable JavaScript window.user.

Une authentification à notre service

La difficulté va maintenant être de partager cet objet user au travers de notre application et de lui permettre de se mettre à jour. Pour cela, nous allons créer un service qui permet d’interagir avec l’objet user.

angular.module('app.services.auth', []).service('Auth', function() {
    var user = window.user;
    return user;
});

Ce service nous permet d’accéder à notre objet user dans toute l’application AngularJS, nous allons néanmoins l’améliorer un peu afin d’encapsuler la variable user et permettre de la modifier plus tard.

angular.module('app.services.auth', []).service('Auth', function() {
    var user = window.user;
    return {
        getUser: function() {
            return user;
        },
        setUser: function(newUser) {
            user = newUser;
        },
        isConnected: function() {
            return !!user;
        }
    };
});

Ce service encapsule l’objet user afin que notre application n’y accède pas directement. Maintenant que notre service est créé, nous pouvons l’injecter dans les contrôleurs qui ont besoin de connaître l’utilisateur. Voici le contrôleur responsable de la connexion :

angular.module('app.controllers.auth', []).controller('AuthController', ['$scope', '$location', '$http', 'Auth', 
    function($scope, $location, $http, Auth) {
    $scope.userData = {};
        $scope.loginError = '';

    $scope.submitLogin = function() {
        $http.post('/login', this.userData)
        .success(function(user) {
            Auth.setUser(user);
            $location.url('/');
        })
        .error(function(data) {
            $scope.loginError = data.loginError;
        });
    };
}]);

Ce contrôleur permet de connecter l’utilisateur en appelant la méthode login de notre serveur, celle-ci est responsable de la création de l’objet user dans la session serveur et doit également nous renvoyer les informations qui le concerne en cas de succès. Avec ces informations nous n’avons plus qu’à mettre à jour notre service d’authentification.

Nous pouvons utiliser ce service partout, voici un exemple d’utilisation dans une entête afin de mettre à jour la barre de navigation en fonction de l’état de la connexion de l’utilisateur :

angular.module('app.controllers.layout', []).controller('HeaderController', ['$scope', 'Auth', 
    function($scope, Auth) {
    $scope.user = Auth;
}]);

Nous n’avons ensuite plus qu’à gérer l’affichage de la barre de navigation en fonction de la variable $scope.user :

<div class="container-fluid" data-ng-controller="HeaderController">
    <div class="navbar-header">
        <a class="navbar-brand" href="#!/">AngularJSAuth</a>
    </div>
    <ul class="nav navbar-nav navbar-right" data-ng-hide="user.isConnected()">
        <li><a href="#!/login">Login</a></li>
        <li><a href="#!/signup">Signup</a></li>
    </ul>
    <ul class="nav navbar-nav navbar-right" data-ng-show="user.isConnected()">
        <li><a href="#!">{{ user.getUser() }}</a></li>
        <li><a href="/logout">Signout</a></li>
    </ul>
</div>

Et voila le travail ! Nous avons mis en place une authentification simple avec AngularJS.

Une route VIP

Il nous faut maintenant mettre en place un système sécurisé qui nous permette de restreindre des routes aux utilisateurs connectés uniquement. (donc non dépendant de notre service d’authentification qui est instancié dans le navigateur du client)

Voici les routes que j’utilise avec AngularJS pour ma petite application de test :

$stateProvider
        .state('home', {
            url: '/',
            templateUrl: 'views/home.html'
        })
        .state('signup', {
            url: '/signup',
            templateUrl: 'views/signup.html'
        })
        .state('login', {
            url: '/login',
            templateUrl: 'views/login.html'
        })
        .state('restricted', {
            url: '/restricted',
            templateUrl: 'views/restricted.html',
            resolve: {
                connected: checkIsConnected
            }
        });

J’utilise ici ui-router pour définir mes routes mais cela doit également fonctionner avec le module ngRoute d’AngularJS. Nous allons ici nous intéresser à la route restricted.

On peut déjà remarquer qu’elle a un petit quelque chose en plus, c’est partie resolve. Que se soit chez ui-router ou ngRoute, resolve a le même comportement, ces dépendances vont être transmises au contrôleur.

La partie intéressante, c’est que si l’une des dépendances est une promesse, elle va être résolue et convertie en valeur avant que l’événement $routeChangeSuccess soit lancé.

Cela signifie que si nous vérifions la connexion de l’utilisateur et que l’on obtient une réponse sous la forme d’une promesse, nous pourrons intercepter un utilisateur qui tente d’accéder à une route sécurisée avant que celle-ci ne soit activée.

Pour vérifier qu’un utilisateur est connecté, nous n’avons pas le choix il faut faire une requête à notre serveur, pour cela nous allons utiliser le service $http d’AngularJS qui, je vous le donne en mille, retourne une promesse. Voici un exemple d’implémentation possible :

var checkIsConnected = function($q, $timeout, $http, $location) {
    var deferred = $q.defer();

    $http.get('/loggedin').success(function(user) {
        if (user !== '-1') {
            $timeout(deferred.resolve, 0);
        } else {
            $timeout(deferred.reject, 0);
            $location.url('/login');
        }
    });

    return deferred.promise;
};

Cette fonction fait appel au serveur qui renvoie le nom d’utilisateur si il est connecté ou ‘-1’ dans le cas contraire. Si l’utilisateur est connecté, la promesse est résolue sinon elle est rejetée et l’utilisateur est redirigé vers la page de connexion.

Nous voilà arrivés au bout de nos peines, vos utilisateurs peuvent désormais se connecter sur votre application AngularJS. Bien entendu cet exemple est relativement simple et facilement améliorable, on pourrait par exemple ajouter la notion de rôle et mettre en place une inscription. À vos claviers et amusez-vous bien avec AngularJS !