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.
Tous les champs d'un JWT sont en texte clair et peuvent être lus par n'importe qui ! N'envoyez jamais d'informations sensibles via un JWT !
Étant donné que le JWT est envoyé avec chaque demande, faites attention à la quantité d'informations que vous y mettez ! En général, vous n'incluez que l'identifiant de l'utilisateur et éventuellement son rôle.
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 celaCe 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.
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 ?
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 ?
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.
Dernière mise à jour