Sécurité : implementation avec JWT

Nous utiliserons des JWT (jetons web JSON) pour mettre en œuvre notre protocole d'identification et d'autorisation.

Un JWT est un document signé numériquement qui contient :

  • un objet JSON non chiffré contenant certains champs standardisés ainsi que des champs personnalisés

  • la version chiffrée de l'objet JSON ci-dessus, chiffrée à l'aide de la clé privée de l'émetteur. Cela crée une signature numérique qui prouve que l'émetteur est la seule personne à pouvoir créer ce JWT.

Les différentes parties sont sérialisées dans un string en base64.

Lors de la connexion, notre API créera un JWT et le signera à l'aide de notre clé privée secrète. Nous le transmettons à l'utilisateur.

Nous attendons de l'utilisateur qu'il nous envoie ce JWT à chaque demande. Comme nous l'avons signé, nous pouvons prouver l'authenticité de la clé transmise en essayant de la décoder à l'aide de la clé publique correspondante. Si le résultat est identique à la partie JSON non chiffrée, nous avons prouvé qu'il s'agit bien d'un JWT que nous avons émis. Dans le cas contraire, la clé est fausse et nous rejetons la demande.

La partie JSON du JWT contient un champ d'expiration, ce qui signifie que le JWT n'est valable que pour une période spécifique. Il devra être réédité régulièrement.

circle-exclamation
circle-exclamation

Flux d'identité

Mettons en place un flux d'identité sans mot de passe.

  • Un utilisateur s'identifie à l'aide de son adresse électronique. Nous aurons besoin d'un point de terminaison tel que POST /auth/login pour cela

  • Ce point de terminaison vérifie la base de données pour cet utilisateur, et si l'utilisateur existe, crée un JWT temporaire qui est envoyé à l'adresse email de l'utilisateur. Le JWT doit expirer après un court laps de temps. De plus, il s'agit d'un JWT spécial qui ne peut pas être utilisé pour accéder à des routes protégées dans notre API !

  • L'utilisateur consulte son adresse électronique. Il y trouve un lien qui le dirige vers une route d'autorisation (GET /auth/authorize?jwt={{JWT}}) qui validera le JWT. Si le JWT est validé et n'a pas expiré, la route renverra un nouveau JWT, cette fois-ci un JWT qui PEUT être utilisé pour autoriser un utilisateur dans notre API (jeton d'accès)

Clés asymétriques

Notre API va émettre et signer des JWT. Pour signer un JWT, nous avons besoin d'une paire de clés cryptographiques asymétriques.

Naviguez dans le dossier config/signing (créez le dossier s'il n'existe pas). C'est là que nous conserverons nos identifiants de signature.

Utilisez les commandes suivantes pour générer une paire de clés à utiliser pour signer les JWT :

Il existe maintenant une paire de clés asymétriques :

  • une clé publique signing.pub

  • une clé privée signing.key : cette clé doit être protégée !

Outil JWT

Ensuite, créons un outil qui créera et validera les JWTs. Tout d'abord, vous aurez besoin de la bibliothèque jsonwebtoken:

Ensuite, dans src/utility/JWT/JWT.ts :

Il y a plusieurs points à noter :

  • nous utilisons d'abord des variables d'environnement pour récupérer nos clés de signature, en revenant à celles stockées localement sur le disque. Cela permet de fournir un jeu unique qui ne sera utilisé qu'en production

  • notre clé utilise le cryptage RS256. Vous pouvez utiliser d'autres algorithmes de chiffrement plus puissants, tels que ES256, mais vous devez générer des clés de signature correspondantes.

  • nous envoyons des erreurs 401 si le JWT n'a pas pu être validé, mais en fournissant plus d'informations pour savoir si le jeton a expiré (token/expired) ou s'il est simplement invalide (token/invalid).

Mailing

Pour notre flux d'identité, nous avons besoin d'un moyen d'envoyer un e-mail à notre utilisateur contenant le JWT.

De nos jours, je vous conseille d'utiliser un service d'envoi tiers qui s'occupe de tous les problèmes de délivrabilité que vous pouvez rencontrer.

Mailjetarrow-up-right, par exemple, fournit ce type de service à l'aide d'une API.

Je vous laisse créer votre compte gratuit. Je vous laisse créer votre compte gratuit et commencer les premières étapes. Vous aurez besoin d'obtenir vos clés API afin d'envoyer un email via leurs API.

Ensuite, créons un outil dans utility qui enverra un email contenant le JWT, dans src/utility/email/Emailer.ts :

Nous enveloppons l'API Mailjet dans ce wrapper, de sorte que si nous changeons de fournisseur de services, il n'y a qu'un seul fichier à modifier.

Constants du JWT

Notre JWT contiendra certaines informations propres à notre API. Notamment dans les champs issuer et audience. Nous les stockerons dans src/utility/JWT/JWTConstants.ts :

Controleur d'identification

Nous pouvons maintenant écrire notre contrôleur qui implémente les routes dont nous avons parlé précédemment.

Tout d'abord, nous allons définir un type pour la charge utile de notre jeton d'accès, dans src/utility/auth/IAccessToken.ts :

Nous créons ensuite notre contrôleur, dans src/controllers/AuthController.ts :

N'oubliez pas d'exécuter le suivant pour mettre à jour les routes :

Testez vos itinéraires avec Postman ! Obtenez-vous un JWT ?

Lorsque vous obtenez un JWT, vous pouvez le déboguer sur jwt.ioarrow-up-right

Sécuriser les endpoints

Au lieu de créer un middleware ad-hoc comme on a fait avant, tsoa préconise un emplacement fixe pour la logique de note notre sécurisation. Cet emplacement est défini dans tsoa.json, notamment la ligne authenticationModule

Il faut donc ajouter un fichier qui s'occupe de l'autorisation, dans /src/utility/auth/authentication.middleware.ts :

Vous remarquerez que tsoa permet de définir différentes stratégies d'autorisation, et contient aussi la logique pour les scopes (qu'on n'utilise pas dans notre exemple).

Ensuite, pour protéger nos routes, il suffit d'ajouter le décorateur @Security devant une classe ou méthode :

Après une recompilation (npm run compile), la route est désormais sécurisée par notre authentification pas JWT.

Essayez la route GET /user après avoir fait le changement ci-dessus. Quel type d'erreur obtenez-vous ? Pouvez-vous configurer postman pour utiliser votre nouveau jeton ?

Vous trouverez de plus amples informations sur la configuration des rôles à l'adresse suivante : Authentication with tsoaarrow-up-right

Renouvellement du jeton d'accès

Notre jeton d'accès a également une date d'expiration. Celle-ci est généralement assez courte, de l'ordre de 5 minutes.

Pourquoi si peu de temps ? Si quelqu'un vole le jeton, ou si nous supprimons l'accès à cet utilisateur, nous aurons limité les dommages possibles à une courte période de temps.

Mais après 5 minutes, que se passe-t-il ?

Nous devons renouveler le jeton, mais si le jeton original est expiré, comment faire ?

Nous utilisons un jeton de rafraîchissement (refresh token). Il s'agit d'un jeton spécial qui ne peut être utilisé que sur un seul endpoint de notre API POST /auth/renew afin d'obtenir un nouveau jeton d'accès.

Le refresh-token est émis en même temps que le jeton d'accès, mais n'est pas transmis à chaque demande (il est donc plus privé, moins de risque d'être volé). Son délai d'expiration est plus long (généralement 1 semaine).

Le endpoint /auth/renew valide le refresh-token, valide (par une requête auprès de la base de données) que l'utilisateur est valide, extrait (par une requête auprès de la base de données) les rôles mis à jour de l'utilisateur, et réémet le jeton d'accès (ainsi qu'un nouveau refresh token au passage).

Remarque : un refresh-token ne peut pas autoriser un utilisateur sur un autre point de terminaison de l'API !

Je vous laisse vous exercer à la mise en œuvre de ce refresh-token.

Mis à jour