Tutoriel sur les jetons Web JSON : un exemple dans Laravel et AngularJS

Publié: 2022-03-11

Avec la popularité croissante des applications à page unique, des applications mobiles et des services d'API RESTful, la façon dont les développeurs Web écrivent le code back-end a considérablement changé. Avec des technologies comme AngularJS et BackboneJS, nous ne passons plus beaucoup de temps à créer du balisage, nous construisons plutôt des API que nos applications frontales consomment. Notre back-end concerne davantage la logique métier et les données, tandis que la logique de présentation est déplacée exclusivement vers les applications front-end ou mobiles. Ces changements ont conduit à de nouvelles façons de mettre en œuvre l'authentification dans les applications modernes.

L'authentification est l'une des parties les plus importantes de toute application Web. Pendant des décennies, les cookies et l'authentification basée sur le serveur ont été la solution la plus simple. Cependant, la gestion de l'authentification dans les applications mobiles et à page unique modernes peut être délicate et exiger une meilleure approche. Les solutions les plus connues aux problèmes d'authentification pour les API sont OAuth 2.0 et le JSON Web Token (JWT).

Avant d'entrer dans ce didacticiel sur les jetons Web JSON, qu'est-ce qu'un JWT ?

Qu'est-ce qu'un jeton Web JSON ?

Un jeton Web JSON est utilisé pour envoyer des informations qui peuvent être vérifiées et approuvées au moyen d'une signature numérique. Il comprend un objet JSON compact et sécurisé pour les URL, qui est signé de manière cryptographique pour vérifier son authenticité, et qui peut également être chiffré si la charge utile contient des informations sensibles.

En raison de sa structure compacte, JWT est généralement utilisé dans les en-têtes d' Authorization HTTP ou les paramètres de requête d'URL.

Structure d'un jeton Web JSON

Un JWT est représenté comme une séquence de valeurs encodées en base64url séparées par des points.

Exemple de jeton Web JSON dans laravel et angularjs

Voici un exemple de jeton JWT :

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0 . yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw

Entête

L'en-tête contient les métadonnées du jeton et contient au minimum le type de signature et l'algorithme de chiffrement. (Vous pouvez utiliser un outil de formatage JSON pour embellir l'objet JSON.)

Exemple d'en-tête

 { "alg": "HS256", "typ": "JWT" }

Cet exemple d'en-tête JWT déclare que l'objet encodé est un jeton Web JSON et qu'il est signé à l'aide de l'algorithme HMAC SHA-256.

Une fois celui-ci encodé en base64, nous avons la première partie de notre JWT.

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Charge utile (réclamations)

Dans le contexte de JWT, une revendication peut être définie comme une déclaration sur une entité (généralement, l'utilisateur), ainsi que des métadonnées supplémentaires sur le jeton lui-même. La revendication contient les informations que nous souhaitons transmettre et que le serveur peut utiliser pour gérer correctement l'authentification JSON Web Token. Il y a plusieurs réclamations que nous pouvons fournir ; ceux-ci incluent les noms de revendications enregistrées, les noms de revendications publiques et les noms de revendications privées.

Revendications JWT enregistrées

Ce sont les revendications qui sont enregistrées dans le registre des revendications de jeton Web IANA JSON. Ces revendications JWT ne sont pas destinées à être obligatoires, mais plutôt à fournir un point de départ pour un ensemble de revendications utiles et interopérables.

Ceux-ci inclus:

  • iss : L'émetteur du jeton
  • sub : le sujet du jeton
  • aud : L'audience du jeton
  • exp : heure d'expiration JWT définie en heure Unix
  • nbf : heure "Pas avant" qui identifie l'heure avant laquelle le JWT ne doit pas être accepté pour traitement
  • iat : heure "Issued at", en heure Unix, à laquelle le jeton a été émis
  • jti : la demande d'ID JWT fournit un identifiant unique pour le JWT

Revendications publiques

Les revendications publiques doivent avoir des noms résistants aux collisions. En faisant du nom un URI ou un URN, les collisions de nommage sont évitées pour les JWT où l'expéditeur et le destinataire ne font pas partie d'un réseau fermé.

Un exemple de nom de revendication public pourrait être : https://www.toptal.com/jwt_claims/is_admin , et la meilleure pratique consiste à placer un fichier à cet emplacement décrivant la revendication afin qu'elle puisse être déréférencée pour la documentation.

Revendications privées

Les noms de revendication privés peuvent être utilisés dans des endroits où les jetons JWT ne sont échangés que dans un environnement fermé entre des systèmes connus, comme à l'intérieur d'une entreprise. Ce sont des revendications que nous pouvons définir nous-mêmes, comme les identifiants d'utilisateur, les rôles d'utilisateur ou toute autre information.

L'utilisation de noms de revendication qui pourraient avoir des significations sémantiques conflictuelles en dehors d'un système fermé ou privé est sujette à collision, utilisez-les donc avec prudence.

Il est important de noter que nous voulons garder un jeton Web aussi petit que possible, donc n'utilisez que les données nécessaires dans les revendications publiques et privées.

Exemple de charge utile JWT

 { "iss": "toptal.com", "exp": 1426420800, "https://www.toptal.com/jwt_claims/is_admin": true, "company": "Toptal", "awesome": true }

Cet exemple de charge utile a deux revendications enregistrées, une revendication publique et deux revendications privées. Une fois encodé en base64, nous avons la deuxième partie de notre JWT.

 eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0

Signature

La norme JWT suit la spécification JSON Web Signature (JWS) pour générer le jeton signé final. Il est généré en combinant l'en-tête JWT encodé et la charge utile JWT encodée, et en le signant à l'aide d'un algorithme de chiffrement fort, tel que HMAC SHA-256. La clé secrète de la signature est détenue par le serveur afin qu'il puisse vérifier les jetons existants et en signer de nouveaux.

 $encodedContent = base64UrlEncode(header) + "." + base64UrlEncode(payload); $signature = hashHmacSHA256($encodedContent);

Cela nous donne la dernière partie de notre JWT.

 yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw

Sécurité et chiffrement JWT

Il est essentiel d'utiliser TLS/SSL en conjonction avec JWT, pour empêcher les attaques de type "man-in-the-middle". Dans la plupart des cas, cela suffira à chiffrer la charge utile JWT si elle contient des informations sensibles. Cependant, si nous voulons ajouter une couche de protection supplémentaire, nous pouvons chiffrer la charge utile JWT elle-même à l'aide de la spécification JSON Web Encryption (JWE).

Bien sûr, si nous voulons éviter les frais supplémentaires liés à l'utilisation de JWE, une autre option consiste simplement à conserver les informations sensibles dans notre base de données et à utiliser notre jeton pour des appels d'API supplémentaires vers le serveur chaque fois que nous avons besoin d'accéder à des données sensibles.

Pourquoi le besoin de jetons Web ?

Avant de pouvoir voir tous les avantages de l'utilisation de l'authentification JWT, nous devons examiner la manière dont l'authentification a été effectuée dans le passé.

Authentification basée sur le serveur

Authentification basée sur le serveur

Étant donné que le protocole HTTP est sans état, il doit exister un mécanisme pour stocker les informations utilisateur et un moyen d'authentifier l'utilisateur à chaque demande ultérieure après la connexion. La plupart des sites Web utilisent des cookies pour stocker l'identifiant de session de l'utilisateur.

Comment ça fonctionne

Le navigateur fait une requête POST au serveur qui contient l'identification et le mot de passe de l'utilisateur. Le serveur répond avec un cookie, qui est défini sur le navigateur de l'utilisateur, et inclut un identifiant de session pour identifier l'utilisateur.

À chaque demande ultérieure, le serveur doit trouver cette session et la désérialiser, car les données utilisateur sont stockées sur le serveur.

Inconvénients de l'authentification basée sur le serveur

  • Difficile à mettre à l'échelle : le serveur doit créer une session pour un utilisateur et la conserver quelque part sur le serveur. Cela peut être fait en mémoire ou dans une base de données. Si nous avons un système distribué, nous devons nous assurer que nous utilisons un stockage de session séparé qui n'est pas couplé au serveur d'application.

  • Partage de requêtes cross-origin (CORS) : lors de l'utilisation d'appels AJAX pour récupérer une ressource d'un autre domaine ("cross-origin"), nous pourrions rencontrer des problèmes avec des requêtes interdites car, par défaut, les requêtes HTTP n'incluent pas de cookies sur cross-origin. demandes d'origine.

  • Couplage avec le framework web : Lors de l'utilisation de l'authentification basée sur le serveur, nous sommes liés au schéma d'authentification de notre framework. Il est vraiment difficile, voire impossible, de partager des données de session entre différents frameworks Web écrits dans différents langages de programmation.

Authentification basée sur des jetons

Authentification basée sur des jetons

L'authentification basée sur les jetons/JWT est sans état, il n'est donc pas nécessaire de stocker les informations de l'utilisateur dans la session. Cela nous donne la possibilité de faire évoluer notre application sans nous soucier de l'endroit où l'utilisateur s'est connecté. Nous pouvons facilement utiliser le même jeton pour récupérer une ressource sécurisée à partir d'un domaine autre que celui auquel nous sommes connectés.

Fonctionnement des jetons Web JSON

Un navigateur ou un client mobile adresse une requête au serveur d'authentification contenant les informations de connexion de l'utilisateur. Le serveur d'authentification génère un nouveau jeton d'accès JWT et le renvoie au client. À chaque demande adressée à une ressource restreinte, le client envoie le jeton d'accès dans la chaîne de requête ou l'en-tête d' Authorization . Le serveur valide ensuite le jeton et, s'il est valide, renvoie la ressource sécurisée au client.

Le serveur d'authentification peut signer le jeton à l'aide de n'importe quelle méthode de signature sécurisée. Par exemple, un algorithme à clé symétrique tel que HMAC SHA-256 peut être utilisé s'il existe un canal sécurisé pour partager la clé secrète entre toutes les parties. Alternativement, un système à clé publique asymétrique, tel que RSA, peut également être utilisé, éliminant ainsi la nécessité d'un partage de clé supplémentaire.

Avantages de l'authentification basée sur les jetons

Sans état, plus facile à mettre à l'échelle : Le jeton contient toutes les informations permettant d'identifier l'utilisateur, éliminant ainsi le besoin de connaître l'état de la session. Si nous utilisons un équilibreur de charge, nous pouvons transmettre l'utilisateur à n'importe quel serveur, au lieu d'être lié au même serveur sur lequel nous nous sommes connectés.

Réutilisabilité : nous pouvons avoir de nombreux serveurs distincts, fonctionnant sur plusieurs plates-formes et domaines, réutilisant le même jeton pour authentifier l'utilisateur. Il est facile de créer une application qui partage des autorisations avec une autre application.

Sécurité JWT : puisque nous n'utilisons pas de cookies, nous n'avons pas à nous protéger contre les attaques de falsification de requête intersite (CSRF). Nous devons toujours chiffrer nos jetons à l'aide de JWE si nous devons y mettre des informations sensibles et transmettre nos jetons via HTTPS pour empêcher les attaques de l'homme du milieu.

Performance : Il n'y a pas de recherche côté serveur pour trouver et désérialiser la session à chaque requête. La seule chose que nous devons faire est de calculer le HMAC SHA-256 pour valider le jeton et analyser son contenu.

Un exemple de jeton Web JSON utilisant Laravel 5 et AngularJS

Dans ce didacticiel JWT, je vais montrer comment implémenter l'authentification de base à l'aide de jetons Web JSON dans deux technologies Web populaires : Laravel 5 pour le code backend et AngularJS pour l'exemple d'application à page unique (SPA) frontal. (Vous pouvez trouver la démo complète ici et le code source dans ce référentiel GitHub afin que vous puissiez suivre le didacticiel.)

Cet exemple de jeton Web JSON n'utilisera aucun type de cryptage pour garantir la confidentialité des informations transmises dans les revendications. En pratique, cela convient souvent, car TLS/SSL crypte la requête. Cependant, si le jeton doit contenir des informations sensibles, telles que le numéro de sécurité sociale de l'utilisateur, il doit également être chiffré à l'aide de JWE.

Exemple de backend Laravel

Nous utiliserons Laravel pour gérer l'enregistrement des utilisateurs, conserver les données utilisateur dans une base de données et fournir des données restreintes nécessitant une authentification pour que l'application Angular puisse être consommée. Nous allons créer un exemple de sous-domaine d'API pour simuler également le partage de ressources cross-origin (CORS).

Installation et démarrage du projet

Pour utiliser Laravel, nous devons installer le gestionnaire de packages Composer sur notre machine. Lors du développement à Laravel, je recommande d'utiliser la "boîte" pré-emballée Laravel Homestead de Vagrant. Il nous fournit un environnement de développement complet quel que soit notre système d'exploitation.

Le moyen le plus simple de démarrer notre application JWT Laravel consiste à utiliser un package Composer Laravel Installer.

 composer global require "laravel/installer=~1.1"

Nous sommes maintenant tous prêts à créer un nouveau projet Laravel en exécutant laravel new jwt .

Pour toute question concernant ce processus, veuillez vous référer à la documentation officielle de Laravel.

Après avoir créé l'application de base Laravel 5, nous devons configurer notre Homestead.yaml , qui configurera les mappages de dossiers et la configuration des domaines pour notre environnement local.

Exemple de fichier Homestead.yaml :

 --- ip: "192.168.10.10" memory: 2048 cpus: 1 authorize: /Users/ttkalec/.ssh/public.psk keys: - /Users/ttkalec/.ssh/private.ppk folders: - map: /coding/jwt to: /home/vagrant/coding/jwt sites: - map: jwt.dev to: /home/vagrant/coding/jwt/public - map: api.jwt.dev to: /home/vagrant/coding/jwt/public variables: - key: APP_ENV value: local

Une fois que nous avons démarré notre boîte Vagrant avec la commande vagrant up et que nous nous y sommes connectés à l'aide de vagrant ssh , nous naviguons vers le répertoire de projet précédemment défini. Dans l'exemple ci-dessus, ce serait /home/vagrant/coding/jwt . Nous pouvons maintenant exécuter la commande php artisan migrate afin de créer les tables utilisateur nécessaires dans notre base de données.

Installation des dépendances Composer

Heureusement, il existe une communauté de développeurs travaillant sur Laravel et gérant de nombreux packages de qualité avec lesquels nous pouvons réutiliser et étendre notre application. Dans cet exemple, nous utiliserons tymon/jwt-auth , de Sean Tymon, pour gérer les jetons côté serveur, et barryvdh/laravel-cors , de Barry vd. Heuvel, pour la gestion du CORS.

jwt-auth

Exigez le tymon/jwt-auth dans notre composer.json et mettez à jour nos dépendances.

 composer require tymon/jwt-auth 0.5.*

Ajoutez le JWTAuthServiceProvider à notre tableau de fournisseurs app/config/app.php .

 'Tymon\JWTAuth\Providers\JWTAuthServiceProvider'

Ensuite, dans le fichier app/config/app.php , sous le tableau des aliases , nous ajoutons la façade JWTAuth .

 'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth'

Enfin, nous voudrons publier la config du package à l'aide de la commande suivante : php artisan config:publish tymon/jwt-auth

Les jetons Web JSON sont chiffrés à l'aide d'une clé secrète. Nous pouvons générer cette clé en utilisant la commande php artisan jwt:generate . Il sera placé dans notre fichier config/jwt.php . Dans l'environnement de production, cependant, nous ne voulons jamais avoir nos mots de passe ou clés API dans les fichiers de configuration. Au lieu de cela, nous devrions les placer dans les variables d'environnement du serveur et les référencer dans le fichier de configuration avec la fonction env . Par exemple:

 'secret' => env('JWT_SECRET')

Nous pouvons en savoir plus sur ce package et tous ses paramètres de configuration sur Github.

laravel-cors

Exigez le barryvdh/laravel-cors dans notre composer.json et mettez à jour nos dépendances.

 composer require barryvdh/laravel-cors 0.4.x@dev

Ajoutez le CorsServiceProvider à notre tableau de fournisseurs app/config/app.php .

 'Barryvdh\Cors\CorsServiceProvider'

Ajoutez ensuite le middleware à notre app/Http/Kernel.php .

 'Barryvdh\Cors\Middleware\HandleCors'

Publiez la configuration dans un fichier config/cors.php local à l'aide de la commande php artisan vendor:publish .

Exemple de configuration de fichier cors.php :

 return [ 'defaults' => [ 'supportsCredentials' => false, 'allowedOrigins' => [], 'allowedHeaders' => [], 'allowedMethods' => [], 'exposedHeaders' => [], 'maxAge' => 0, 'hosts' => [], ], 'paths' => [ 'v1/*' => [ 'allowedOrigins' => ['*'], 'allowedHeaders' => ['*'], 'allowedMethods' => ['*'], 'maxAge' => 3600, ], ], ];

Routage et gestion des requêtes HTTP

Par souci de brièveté, je mettrai tout mon code dans le fichier routes.php qui est responsable du routage Laravel et de la délégation des requêtes aux contrôleurs. Nous créons généralement des contrôleurs dédiés pour gérer toutes nos requêtes HTTP et gardons notre code modulaire et propre.

Nous allons charger notre vue AngularJS SPA en utilisant

 Route::get('/', function () { return view('spa'); });

Enregistrement de l'utilisateur

Lorsque nous faisons une requête POST à /signup avec un nom d'utilisateur et un mot de passe, nous essayons de créer un nouvel utilisateur et de l'enregistrer dans la base de données. Une fois l'utilisateur créé, un JWT est créé et renvoyé via une réponse JSON.

 Route::post('/signup', function () { $credentials = Input::only('email', 'password'); try { $user = User::create($credentials); } catch (Exception $e) { return Response::json(['error' => 'User already exists.'], HttpResponse::HTTP_CONFLICT); } $token = JWTAuth::fromUser($user); return Response::json(compact('token')); });

Connexion de l'utilisateur

Lorsque nous faisons une requête POST à /signin avec un nom d'utilisateur et un mot de passe, nous vérifions que l'utilisateur existe et renvoyons un JWT via la réponse JSON.

 Route::post('/signin', function () { $credentials = Input::only('email', 'password'); if ( ! $token = JWTAuth::attempt($credentials)) { return Response::json(false, HttpResponse::HTTP_UNAUTHORIZED); } return Response::json(compact('token')); });

Récupération d'une ressource restreinte sur le même domaine

Une fois que l'utilisateur est connecté, nous pouvons récupérer la ressource restreinte. J'ai créé une route /restricted qui simule une ressource nécessitant un utilisateur authentifié. Pour ce faire, l'en-tête d' Authorization de la demande ou la chaîne de requête doit fournir le JWT à vérifier par le backend.

 Route::get('/restricted', [ 'before' => 'jwt-auth', function () { $token = JWTAuth::getToken(); $user = JWTAuth::toUser($token); return Response::json([ 'data' => [ 'email' => $user->email, 'registered_at' => $user->created_at->toDateTimeString() ] ]); } ]);

Dans cet exemple, j'utilise le middleware jwt-auth fourni dans le jwt-auth en utilisant 'before' => 'jwt-auth' . Ce middleware est utilisé pour filtrer la requête et valider le jeton JWT. Si le jeton est invalide, absent ou expiré, le middleware lèvera une exception que nous pourrons intercepter.

Dans Laravel 5, nous pouvons intercepter des exceptions à l'aide du fichier app/Exceptions/Handler.php . En utilisant la fonction de render , nous pouvons créer des réponses HTTP basées sur l'exception levée.

 public function render($request, Exception $e) { if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException) { return response(['Token is invalid'], 401); } if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException) { return response(['Token has expired'], 401); } return parent::render($request, $e); }

Si l'utilisateur est authentifié et que le jeton est valide, nous pouvons renvoyer en toute sécurité les données restreintes à l'interface via JSON.

Récupération des ressources restreintes du sous-domaine de l'API

Dans le prochain exemple de jeton Web JSON, nous adopterons une approche différente pour la validation de jeton. Au lieu d'utiliser le middleware jwt-auth , nous gérerons les exceptions manuellement. Lorsque nous faisons une requête POST à ​​un serveur API api.jwt.dev/v1/restricted , nous faisons une requête cross-origin et devons activer CORS sur le backend. Heureusement, nous avons déjà configuré CORS dans le fichier config/cors.php .

 Route::group(['domain' => 'api.jwt.dev', 'prefix' => 'v1'], function () { Route::get('/restricted', function () { try { JWTAuth::parseToken()->toUser(); } catch (Exception $e) { return Response::json(['error' => $e->getMessage()], HttpResponse::HTTP_UNAUTHORIZED); } return ['data' => 'This has come from a dedicated API subdomain with restricted access.']; }); });

Exemple d'interface AngularJS

Nous utilisons AngularJS comme frontal, en nous appuyant sur les appels d'API vers le serveur d'authentification back-end Laravel pour l'authentification des utilisateurs et les exemples de données, ainsi que sur le serveur API pour les exemples de données d'origine croisée. Une fois sur la page d'accueil de notre projet, le backend servira la resources/views/spa.blade.php qui démarrera l'application Angular.

Voici la structure des dossiers de l'application Angular :

 public/ |-- css/ `-- bootstrap.superhero.min.css |-- lib/ |-- loading-bar.css |-- loading-bar.js `-- ngStorage.js |-- partials/ |-- home.html |-- restricted.html |-- signin.html `-- signup.html `-- scripts/ |-- app.js |-- controllers.js `-- services.js

Amorcer l'application angulaire

spa.blade.php contient le strict nécessaire pour exécuter l'application. Nous utiliserons Twitter Bootstrap pour le style, ainsi qu'un thème personnalisé de Bootswatch. Pour avoir un retour visuel lors d'un appel AJAX, nous utiliserons le script angular-loading-bar, qui intercepte les requêtes XHR et crée une barre de chargement. Dans la section d'en-tête, nous avons les feuilles de style suivantes :

 <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/bootstrap.superhero.min.css"> <link rel="stylesheet" href="/lib/loading-bar.css">

Le pied de page de notre balisage contient des références aux bibliothèques, ainsi que nos scripts personnalisés pour les modules, contrôleurs et services Angular.

 <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular-route.min.js"></script> <script src="/lib/ngStorage.js"></script> <script src="/lib/loading-bar.js"></script> <script src="/scripts/app.js"></script> <script src="/scripts/controllers.js"></script> <script src="/scripts/services.js"></script> </body>

Nous utilisons la bibliothèque ngStorage pour AngularJS, pour enregistrer les jetons dans le stockage local du navigateur, afin que nous puissions l'envoyer à chaque demande via l'en-tête Authorization .

Dans l'environnement de production, bien sûr, nous réduirions et combinerions tous nos fichiers de script et feuilles de style afin d'améliorer les performances.

J'ai créé une barre de navigation à l'aide de Bootstrap qui modifiera la visibilité des liens appropriés, en fonction du statut de connexion de l'utilisateur. L'état de connexion est déterminé par la présence d'une variable de token dans la portée du contrôleur.

 <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">JWT Angular example</a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav navbar-right"> <li data-ng-show="token"><a ng-href="#/restricted">Restricted area</a></li> <li data-ng-hide="token"><a ng-href="#/signin">Sign in</a></li> <li data-ng-hide="token"><a ng-href="#/signup">Sign up</a></li> <li data-ng-show="token"><a ng-click="logout()">Logout</a></li> </ul> </div>

Routage

Nous avons un fichier nommé app.js qui est responsable de la configuration de toutes nos routes frontales.

 angular.module('app', [ 'ngStorage', 'ngRoute', 'angular-loading-bar' ]) .constant('urls', { BASE: 'http://jwt.dev:8000', BASE_API: 'http://api.jwt.dev:8000/v1' }) .config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) { $routeProvider. when('/', { templateUrl: 'partials/home.html', controller: 'HomeController' }). when('/signin', { templateUrl: 'partials/signin.html', controller: 'HomeController' }). when('/signup', { templateUrl: 'partials/signup.html', controller: 'HomeController' }). when('/restricted', { templateUrl: 'partials/restricted.html', controller: 'RestrictedController' }). otherwise({ redirectTo: '/' });

Ici, nous pouvons voir que nous avons défini quatre routes qui sont gérées par HomeController ou RestrictedController . Chaque route correspond à une vue HTML partielle. Nous avons également défini deux constantes qui contiennent des URL pour nos requêtes HTTP au backend.

Intercepteur de demande

Le service $http d'AngularJS nous permet de communiquer avec le backend et de faire des requêtes HTTP. Dans notre cas, nous voulons intercepter chaque requête HTTP et lui injecter un en-tête d' Authorization contenant notre JWT si l'utilisateur est authentifié. Nous pouvons également utiliser un intercepteur pour créer un gestionnaire d'erreurs HTTP global. Voici un exemple de notre intercepteur qui injecte un jeton s'il est disponible dans le stockage local du navigateur.

 $httpProvider.interceptors.push(['$q', '$location', '$localStorage', function ($q, $location, $localStorage) { return { 'request': function (config) { config.headers = config.headers || {}; if ($localStorage.token) { config.headers.Authorization = 'Bearer ' + $localStorage.token; } return config; }, 'responseError': function (response) { if (response.status === 401 || response.status === 403) { $location.path('/signin'); } return $q.reject(response); } }; }]);

Contrôleurs

Dans le fichier controllers.js , nous avons défini deux contrôleurs pour notre application : HomeController et RestrictedController . HomeController gère les fonctionnalités de connexion, d'inscription et de déconnexion. Il transmet les données de nom d'utilisateur et de mot de passe des formulaires de connexion et d'inscription au service Auth , qui envoie des requêtes HTTP au backend. Il enregistre ensuite le jeton dans le stockage local ou affiche un message d'erreur, en fonction de la réponse du backend.

 angular.module('app') .controller('HomeController', ['$rootScope', '$scope', '$location', '$localStorage', 'Auth', function ($rootScope, $scope, $location, $localStorage, Auth) { function successAuth(res) { $localStorage.token = res.token; window.location = "/"; } $scope.signin = function () { var formData = { email: $scope.email, password: $scope.password }; Auth.signin(formData, successAuth, function () { $rootScope.error = 'Invalid credentials.'; }) }; $scope.signup = function () { var formData = { email: $scope.email, password: $scope.password }; Auth.signup(formData, successAuth, function () { $rootScope.error = 'Failed to signup'; }) }; $scope.logout = function () { Auth.logout(function () { window.location = "/" }); }; $scope.token = $localStorage.token; $scope.tokenClaims = Auth.getTokenClaims(); }])

RestrictedController se comporte de la même manière, sauf qu'il récupère les données à l'aide des fonctions getRestrictedData et getApiData sur le service Data .

 .controller('RestrictedController', ['$rootScope', '$scope', 'Data', function ($rootScope, $scope, Data) { Data.getRestrictedData(function (res) { $scope.data = res.data; }, function () { $rootScope.error = 'Failed to fetch restricted content.'; }); Data.getApiData(function (res) { $scope.api = res.data; }, function () { $rootScope.error = 'Failed to fetch restricted API content.'; }); }]);

Le backend est chargé de servir les données restreintes uniquement si l'utilisateur est authentifié. Cela signifie que pour répondre avec les données restreintes, la demande de ces données doit contenir un JWT valide dans son en-tête d' Authorization ou sa chaîne de requête. Si ce n'est pas le cas, le serveur répondra avec un code d'état d'erreur 401 non autorisé.

Service d'authentification

Le service Auth est responsable de la connexion et de l'inscription des requêtes HTTP au backend. Si la demande aboutit, la réponse contient le jeton signé, qui est ensuite décodé en base64, et les informations de demande de jeton jointes sont enregistrées dans une variable tokenClaims . Ceci est transmis au contrôleur via la fonction getTokenClaims .

 angular.module('app') .factory('Auth', ['$http', '$localStorage', 'urls', function ($http, $localStorage, urls) { function urlBase64Decode(str) { var output = str.replace('-', '+').replace('_', '/'); switch (output.length % 4) { case 0: break; case 2: output += '=='; break; case 3: output += '='; break; default: throw 'Illegal base64url string!'; } return window.atob(output); } function getClaimsFromToken() { var token = $localStorage.token; var user = {}; if (typeof token !== 'undefined') { var encoded = token.split('.')[1]; user = JSON.parse(urlBase64Decode(encoded)); } return user; } var tokenClaims = getClaimsFromToken(); return { signup: function (data, success, error) { $http.post(urls.BASE + '/signup', data).success(success).error(error) }, signin: function (data, success, error) { $http.post(urls.BASE + '/signin', data).success(success).error(error) }, logout: function (success) { tokenClaims = {}; delete $localStorage.token; success(); }, getTokenClaims: function () { return tokenClaims; } }; } ]);

Services de données

Il s'agit d'un service simple qui envoie des demandes au serveur d'authentification ainsi qu'au serveur d'API pour certaines données restreintes factices. Il fait la demande et délègue les rappels de réussite et d'erreur au contrôleur.

 angular.module('app') .factory('Data', ['$http', 'urls', function ($http, urls) { return { getRestrictedData: function (success, error) { $http.get(urls.BASE + '/restricted').success(success).error(error) }, getApiData: function (success, error) { $http.get(urls.BASE_API + '/restricted').success(success).error(error) } }; } ]);

Au-delà de ce didacticiel sur les jetons Web JSON

L'authentification basée sur les jetons nous permet de construire des systèmes découplés qui ne sont pas liés à un schéma d'authentification particulier. Le jeton peut être généré n'importe où et consommé sur n'importe quel système qui utilise la même clé secrète pour signer le jeton. Ils sont prêts pour les mobiles et ne nous obligent pas à utiliser des cookies.

Les jetons Web JSON fonctionnent dans tous les langages de programmation populaires et gagnent rapidement en popularité. Ils sont soutenus par des sociétés comme Google, Microsoft et Zendesk. Leur spécification standard par Internet Engineering Task Force (IETF) est toujours dans la version préliminaire et peut changer légèrement à l'avenir.

Il y a encore beaucoup à couvrir sur les JWT, comme la gestion des détails de sécurité et l'actualisation des jetons lorsqu'ils expirent, mais le didacticiel JSON Web Token devrait démontrer l'utilisation de base et, plus important encore, les avantages de l'utilisation des JWT.

Lectures complémentaires sur le blog Toptal Engineering :

  • Création d'une API REST Node.js/TypeScript, partie 3 : MongoDB, authentification et tests automatisés