L'auteur de l'article, dont nous publions la traduction aujourd'hui, dit que vous pouvez désormais observer la popularité croissante des services d'authentification tels que Google Firebase Authentication, AWS Cognito et Auth0. Les solutions génériques comme passport.js sont devenues la norme de l'industrie. Mais, compte tenu de la situation actuelle, il est devenu courant que les développeurs ne comprennent jamais complètement quels types de mécanismes sont impliqués dans le fonctionnement des systèmes d'authentification.
Ce matériel est consacré au problème de l'organisation de l'authentification des utilisateurs dans Node.js. Dans celui-ci, sur un exemple pratique, l'organisation de l'enregistrement des utilisateurs dans le système et l'organisation de leur entrée dans le système sont considérées. Cela soulèvera des problèmes tels que l'utilisation de la technologie JWT et l'emprunt d'identité des utilisateurs.

Faites également attention à
ce référentiel GitHub, qui contient le code du projet Node.js, dont certains exemples sont donnés dans cet article. Vous pouvez utiliser ce référentiel comme base pour vos propres expériences.
Exigences du projet
Voici les exigences du projet que nous traiterons ici:
- La présence d'une base de données dans laquelle l'adresse e-mail et le mot de passe de l'utilisateur seront stockés, soit clientId et clientSecret, soit une sorte de combinaison de clés privées et publiques.
- Utiliser un algorithme cryptographique puissant et efficace pour crypter un mot de passe.
Au moment où j'écris ce matériel, je crois que le meilleur des algorithmes cryptographiques existants est Argon2. Je vous demande de ne pas utiliser de simples algorithmes cryptographiques comme SHA256, SHA512 ou MD5.
De plus, je vous suggère de jeter un coup d'œil à
ce merveilleux matériel, dans lequel vous pouvez trouver des détails sur le choix d'un algorithme de hachage des mots de passe.
Enregistrement des utilisateurs dans le système
Lorsqu'un nouvel utilisateur est créé dans le système, son mot de passe doit être haché et stocké dans la base de données. Le mot de passe est stocké dans la base de données avec l'adresse e-mail et d'autres informations sur l'utilisateur (par exemple, il peut s'agir du profil utilisateur, de l'heure d'enregistrement, etc.).
import * as argon2 from 'argon2'; class AuthService { public async SignUp(email, password, name): Promise<any> { const passwordHashed = await argon2.hash(password); const userRecord = await UserModel.create({ password: passwordHashed, email, name, }); return {
Les informations du compte utilisateur doivent ressembler à ce qui suit.
Données utilisateur extraites de MongoDB à l'aide de Robo3TConnexion utilisateur
Voici un diagramme des actions effectuées lorsqu'un utilisateur essaie de se connecter.
Connexion utilisateurVoici ce qui se passe lorsqu'un utilisateur se connecte:
- Le client envoie au serveur une combinaison de l'identifiant public et de la clé privée de l'utilisateur. Il s'agit généralement d'une adresse e-mail et d'un mot de passe.
- Le serveur recherche l'utilisateur dans la base de données par adresse e-mail.
- Si l'utilisateur existe dans la base de données, le serveur hache le mot de passe qui lui est envoyé et compare ce qui s'est passé avec le hachage de mot de passe stocké dans la base de données.
- Si la vérification réussit, le serveur génère un soi-disant jeton ou jeton d'authentification - JSON Web Token (JWT).
JWT est une clé temporaire. Le client doit envoyer cette clé au serveur avec chaque demande au point de terminaison authentifié.
import * as argon2 from 'argon2'; class AuthService { public async Login(email, password): Promise<any> { const userRecord = await UserModel.findOne({ email }); if (!userRecord) { throw new Error('User not found') } else { const correctPassword = await argon2.verify(userRecord.password, password); if (!correctPassword) { throw new Error('Incorrect password') return { user: { email: userRecord.email, name: userRecord.name, }, token: this.generateJWT(userRecord), }
La vérification du mot de passe est effectuée à l'aide de la bibliothèque argon2. Il s'agit d'empêcher les soi-disant «
attaques temporelles ». Lors d'une telle attaque, un attaquant tente de déchiffrer le mot de passe par la force brute, sur la base d'une analyse du temps dont le serveur a besoin pour former une réponse.
Voyons maintenant comment générer JWT.
Qu'est-ce qu'un JWT?
JSON Web Token (JWT) est un objet JSON codé sous forme de chaîne. Les jetons peuvent remplacer les cookies, ce qui présente plusieurs avantages par rapport à eux.
Le jeton se compose de trois parties. Il s'agit de l'en-tête, de la charge utile et de la signature. La figure suivante montre son apparence.
JwtLes données de jeton peuvent être décodées côté client sans utiliser de clé ou de signature secrète.
Cela peut être utile pour transférer, par exemple, des métadonnées encodées à l'intérieur du jeton. Ces métadonnées peuvent décrire le rôle de l'utilisateur, son profil, la durée du jeton, etc. Ils peuvent être destinés à être utilisés dans des applications frontales.
Voici à quoi pourrait ressembler un jeton décodé.
Jeton décodéGénération de JWT dans Node.js
Créons la fonction
generateToken
dont nous avons besoin pour terminer le travail sur le service d'authentification des utilisateurs.
Vous pouvez créer JWT à l'aide de la bibliothèque jsonwebtoken. Vous pouvez trouver cette bibliothèque dans npm.
import * as jwt from 'jsonwebtoken' class AuthService { private generateToken(user) { const data = { _id: user._id, name: user.name, email: user.email }; const signature = 'MySuP3R_z3kr3t'; const expiration = '6h'; return jwt.sign({ data, }, signature, { expiresIn: expiration }); }
La chose la plus importante ici est les données encodées. N'envoyez pas d'informations secrètes sur l'utilisateur en jetons.
Une signature (voici la constante de
signature
) est les données secrètes qui sont utilisées pour générer le JWT. Il est très important de s'assurer que la signature ne tombe pas entre de mauvaises mains. Si la signature est compromise, l'attaquant pourra générer des jetons au nom des utilisateurs et voler leurs sessions.
Protection des terminaux et validation JWT
Maintenant, le code client doit envoyer un JWT dans chaque demande à un point de terminaison sécurisé.
Il est recommandé d'inclure JWT dans les en-têtes de demande. Ils sont généralement inclus dans l'en-tête Autorisation.
En-tête d'autorisationMaintenant, sur le serveur, vous devez créer du code qui est un middleware pour les routes express. Mettez ce code dans le fichier
isAuth.ts
:
import * as jwt from 'express-jwt';
Il est utile de pouvoir obtenir des informations complètes sur le compte d'utilisateur à partir de la base de données et de les joindre à la demande. Dans notre cas, cette fonctionnalité est implémentée à l'aide d'un middleware du fichier
attachCurrentUser.ts
. Voici son code simplifié:
export default (req, res, next) => { const decodedTokenData = req.tokenData; const userRecord = await UserModel.findOne({ _id: decodedTokenData._id }) req.currentUser = userRecord; if(!userRecord) { return res.status(401).end('User not found') } else { return next(); }
Après avoir implémenté ce mécanisme, les routes pourront recevoir des informations sur l'utilisateur qui exécute la demande:
import isAuth from '../middlewares/isAuth'; import attachCurrentUser from '../middlewares/attachCurrentUser'; import ItemsModel from '../models/items'; export default (app) => { app.get('/inventory/personal-items', isAuth, attachCurrentUser, (req, res) => { const user = req.currentUser; const userItems = await ItemsModel.find({ owner: user._id }); return res.json(userItems).status(200); })
La route
inventory/personal-items
est désormais protégée. Pour y accéder, l'utilisateur doit disposer d'un JWT valide. Un itinéraire peut en outre utiliser les informations utilisateur pour rechercher dans la base de données les informations dont il a besoin.
Pourquoi les jetons sont-ils protégés contre les intrus?
Après avoir lu sur l'utilisation de JWT, vous pouvez vous poser la question suivante: "Si les données JWT peuvent être décodées côté client, est-il possible de traiter le jeton de manière à changer l'ID utilisateur ou d'autres données?".
Décodage de jetons - l'opération est très simple. Cependant, vous ne pouvez pas "refaire" ce jeton sans avoir cette signature, ces données secrètes qui ont été utilisées lors de la signature du JWT sur le serveur.
C'est pourquoi la protection de ces données sensibles est si importante.
Notre serveur vérifie la signature dans le middleware isAuth. La bibliothèque express-jwt est responsable de la vérification.
Maintenant, après avoir compris comment fonctionne la technologie JWT, parlons de quelques fonctionnalités supplémentaires intéressantes qu'elle nous offre.
Comment se faire passer pour un utilisateur?
L'usurpation d'identité de l'utilisateur est une technique utilisée pour se connecter à un système en tant qu'utilisateur spécifique sans connaître son mot de passe.
Cette fonctionnalité est très utile pour les super administrateurs, les développeurs ou le personnel d'assistance. L'emprunt d'identité leur permet de résoudre des problèmes qui n'apparaissent qu'au cours des utilisateurs travaillant avec le système.
Vous pouvez travailler avec l'application au nom de l'utilisateur sans connaître son mot de passe. Pour ce faire, il suffit de générer un JWT avec la signature correcte et avec les métadonnées nécessaires décrivant l'utilisateur.
Créez un point de terminaison qui peut générer des jetons pour entrer dans le système sous le couvert d'utilisateurs spécifiques. Seul le super-administrateur du système peut utiliser ce point de terminaison.
Pour commencer, nous devons attribuer à cet utilisateur un rôle avec un niveau de privilège plus élevé que les autres utilisateurs. Cela peut se faire de différentes manières. Par exemple, il suffit d'ajouter le champ de
role
aux informations utilisateur stockées dans la base de données.
Il peut ressembler à celui illustré ci-dessous.
Nouveau champ dans les informations utilisateurLa valeur du champ de
role
super-admin
est
super-admin
.
Ensuite, vous devez créer un nouveau middleware qui vérifie le rôle utilisateur:
export default (requiredRole) => { return (req, res, next) => { if(req.currentUser.role === requiredRole) { return next(); } else { return res.status(401).send('Action not allowed'); }
Il doit être placé après isAuth et attachCurrentUser. Créez maintenant le point de terminaison qui génère le JWT pour l'utilisateur au nom duquel le super-administrateur souhaite se connecter:
import isAuth from '../middlewares/isAuth'; import attachCurrentUser from '../middlewares/attachCurrentUser'; import roleRequired from '../middlwares/roleRequired'; import UserModel from '../models/user'; export default (app) => { app.post('/auth/signin-as-user', isAuth, attachCurrentUser, roleRequired('super-admin'), (req, res) => { const userEmail = req.body.email; const userRecord = await UserModel.findOne({ email: userEmail }); if(!userRecord) { return res.status(404).send('User not found'); return res.json({ user: { email: userRecord.email, name: userRecord.name }, jwt: this.generateToken(userRecord) }) .status(200); })
Comme vous pouvez le voir, il n'y a rien de mystérieux. Le super administrateur connaît l'adresse e-mail de l'utilisateur au nom duquel vous souhaitez vous connecter. La logique du code ci-dessus rappelle très bien le fonctionnement du code, fournissant une entrée au système des utilisateurs ordinaires. La principale différence est que le mot de passe n'est pas vérifié ici.
Le mot de passe n'est pas vérifié ici car il n'est tout simplement pas nécessaire ici. La sécurité des terminaux est assurée par un middleware.
Résumé
Il n'y a rien de mal à s'appuyer sur des services d'authentification et des bibliothèques tiers. Cela aide les développeurs à gagner du temps. Mais ils doivent également connaître les principes sur lesquels repose le fonctionnement des systèmes d'authentification et ce qui garantit le fonctionnement de ces systèmes.
Dans cet article, nous avons exploré les possibilités d'authentification JWT, parlé de l'importance de choisir un bon algorithme cryptographique pour hacher les mots de passe. Nous avons examiné la création d'un mécanisme d'usurpation d'identité des utilisateurs.
Faire la même chose avec quelque chose comme passport.js est loin d'être facile. L'authentification est un sujet énorme. Peut-être que nous reviendrons à elle.
Chers lecteurs! Comment créez-vous des systèmes d'authentification pour vos projets Node.js?