4 minutes

Authorisation Avec JWT et Node

Dernière mise à jour: 25 avril, 2022

La gestion de l'authentification et de l'autorisation peut être très intimidante, surtout si vous êtes débutant. Je sais que c'était pour moi. Avec cet article, vous pouvez obtenir une compréhension de base du processus d'authentification jwt avec passeport.js. Nous n'utiliserons aucun framework frontal, nous allons le garder agréable et simple.

§Les Porfaits dont Nous Aurons Besoin

Nous allons hacher (hasing) notre mot de passe et créer un jeton (token) utilisateur à stocker dans le stockage local (local storage). L'utilisation d'une base de données sera également nécessaire pour ce petit projet, mon choix personnel est MongoDB Atlas. Il est très facile à utiliser et fait partie de ma stack préférée. J'ai utilisé la mongoose pour interagir avec ma base de données, c'est vraiment facile. Avec "nodemon", vous n'avez pas besoin d'arrêter et de démarrer votre projet après chaque changement.

Lancez un package npm et ignorez simplement les questions avec "-y":

npm init -y

Allez-y également et installez les packages nécessaires à notre projet :

npm i bcrypt crypto dotenv express fs jsonwebtoken mongoose passport nodemon

N'oubliez pas d'ajouter un script pour nodemon, si vous ne voulez pas écrire la commande sur la console encore et encore, le mien est "dev": "nodemon server.js".

Fichier package.json complet ici :

{ "name": "auth-project-post", "version": "1.0.0", "description": "", "main": "server.js", "scripts": { "dev": "nodemon server.js", "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "bcrypt": "^5.0.1", "crypto": "^1.0.1", "dotenv": "^16.0.0", "express": "^4.17.3", "fs": "^0.0.1-security", "jsonwebtoken": "^8.5.1", "mongoose": "^6.3.0", "passport": "^0.5.2" }, "devDependencies": { "nodemon": "^2.0.15" } }

§Configuration du Fichier Serveur

Notre commande "npm run dev" indiquera à nodemon d'exécuter le fichier server.js à la racine (root) du projet, ce qui est assez basique. Les commentaires expliqueront la logique.

const express = require('express'); // Nous allons obtenir notre URL pour la connexion // à la base de données à partir du fichier .env require('dotenv').config(); const mongoose = require('mongoose'); // Il est toujours bon de séparer votre code const routes = require('./routes/index.js') const secretRoutes = require('./secret-routes/index.js') const app = express(); // Nous utiliserons le corps (body) pour envoyer des données app.use(express.json()) app.use(express.urlencoded({"extended":true})) // Dites à express où se trouvent vos fichiers statiques app.use('/', express.static(process.cwd() + '/public')); app.use('/', routes); app.use('/secret', secretRoutes); const port = process.env.PORT || 5000; // Pour ne rien risquer, nous connectons puis exécutons la commande listen mongoose.connect(process.env.URI, { useNewUrlParser:true, useUnifiedTopology: true, } ) .then(() => app.listen(port, () => console.log(`L'application écoute sur le port: ${port}`))) .catch((error) => console.log(error))

§Gestion de La Connexion à La Base de Données et du Schéma Utilisateur

La base de données que j'utilise pour ce projet est Mongodb Atlas. Il offre un plan gratuit que vous pouvez utiliser pour vos projets. Vous pouvez trouver un guide pour créer votre propre base de données ici. Le package Mongoose facilite grandement la connexion et l'interaction avec votre base de données. J'utiliserai la mongoose dans ce projet. Je conserve mon URL Mongodb dans un fichier .env à la racine de mon projet.

Vous pouvez tester votre connexion en exécutant la commande "npm run dev" dans la console. Si tout va bien, vous devriez voir "L'application écoute sur le port : 5000".

Ok, créons votre schéma utilisateur maintenant. Considérez le schéma comme un moyen de faire savoir à la base de données comment vous voulez interagir avec elle. Dans la racine de votre projet, créez un dossier "models", dans ce dossier, nous pouvons créer un modèle. Pour notre projet, nous avons besoin d'un fichier "user.js". Le code est ici :

const mongoose = require('mongoose'); const { Schema } = mongoose; const UserSchema = new Schema({ email: { type: String, required: true }, password: { type: String, required: true } }) const User = mongoose.model("User", UserSchema); module.exports = User

§Création de Nos Clés

À la racine, le projet crée un fichier pour générer les clés publiques et privées. La clé privée (private key) sera utilisée pour signer les jetons (tokens). Votre client ne devrait pas connaître cette clé. La clé publique (public key) sera utilisée pour vérifier l'utilisateur pour les activités d'authentification ultérieures. Alors allez-y et créez votre fichier, j'ai nommé le mien genKeyPair.js. Ceci est le code complet :

const fs = require('fs'); const crypto = require('crypto'); const genKeyPair = () => { const keyPair = crypto.generateKeyPairSync('rsa', { modulusLength: 4096, publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, privateKeyEncoding: { type: 'pkcs1', format: 'pem' } }); // Les clés seront des fichiers séparés fs.writeFileSync(__dirname + '/id_rsa_pub.pem', keyPair.publicKey); fs.writeFileSync(__dirname + '/id_rsa_priv.pem', keyPair.privateKey) } genKeyPair();

Ok maintenant exécutez:

node genKeyPair.js

commande pour créer les clés.

§Gestion de L'Inscription

Nos fichiers seront très basiques bien que vous puissiez apporter toutes les améliorations que vous souhaitez. Créez simplement un dossier "public" dans la racine de votre projet et nous pouvons commencer. Dans le dossier public, n'oubliez pas de créer un dossier js et un fichier style.css. Ils seront nécessaires pour gérer la logique et rendre notre application un peu meilleure. Il y a beaucoup de place pour l'amélioration à chaque étape, donc si vous sentez que vous pouvez faire une amélioration, allez-y. Je partagerai avec vous tout le code, alors ne vous inquiétez pas.

Dans notre home.html n'oubliez pas de référencer les fichiers js et css nécessaires, le code complet est ici :

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="style.css"> <title>Document</title> </head> <body> <nav> <a href="/">HOME</a> <ul> <li><a href="register">Register</a></li> <li><a href="/login">Login</a></li> <li><a href="/secret">Secret Page</a></li> </ul> </nav> <div class="home-wrapper"> <h1>Welcome to he home page of our awesome website</h1> <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Sint eum et quam excepturi corrupti, ipsa illum eligendi aut! Labore molestiae fuga incidunt. Recusandae, consequatur consectetur? Aliquam eligendi deserunt doloribus voluptatum. Eligendi rem repellendus dolores laboriosam dolorum quia vero nihil perferendis id odio, quas molestiae, vitae exercitationem autem voluptates! Eius illum aliquid tenetur sapiente minus quo, consequatur magni repudiandae. Quaerat, necessitatibus! Ea soluta repellendus modi ab officia quae porro dolorem cum nulla. Aspernatur dolore tenetur corporis deserunt consequuntur omnis voluptate tempora eos facilis ipsum. Aspernatur ad laudantium, harum dolor autem eius. Reprehenderit, aliquam distinctio. Vel tenetur dolore praesentium fugit nam iste totam amet nulla voluptatibus. Eaque reiciendis reprehenderit, eos suscipit omnis id at vel accusantium earum explicabo, delectus dignissimos possimus incidunt! Aperiam optio perferendis natus, vel tempore quaerat assumenda maxime voluptate voluptates rerum suscipit? Asperiores, ea deleniti! Amet similique, provident ipsam error ad nostrum corporis, eligendi ea dignissimos dicta, quia aperiam?</p> </div> </body> </html>

Le CSS de ce projet est également basique, code complet:

* { margin: 0; padding: 0; box-sizing: border-box; } body { display: flex; flex-direction: column; align-items: center; background: rgb(94, 139, 162); color: rgb(255, 255, 255); } p { line-height: 1.7; padding-top: 20px; } .home-wrapper { width: 80%; padding: 50px; background: rgba(0,0,0,0.7); margin: 10px; border-radius: 20px; } nav { background: olivedrab; display: flex; flex-direction: row; align-items: flex-start; width: 100%; padding: 20px; } ul { margin-left: auto; display: flex; flex-direction: row; list-style: none; } li { margin-left: 10px; } a { color: azure; font-size: 1.2rem; text-decoration: none; } .login-wrapper, .register-wrapper { margin-top: 20px; background: rgba(0,0,0,0.7); padding: 20px; border-radius: 20px; } .login-form, .registery-form { display: flex; flex-direction: column; min-width: 600px; } .login-form > input, .registery-form > input { padding: 20px; font-size: 1.6rem; border-radius: 20px; } .login-submit-btn, .registry-submit-btn { margin-top: 10px; border:none; background: rgba(234, 97, 170, 0.7); transition: 0.4s; } .login-submit-btn:hover, .registry-submit-btn:hover { cursor: pointer; background: rgba(234, 97, 170, 1); } .secret-wrapper { width: 80%; margin-top: 20px; background: rgba(0,0,0,0.7); padding: 50px; border-radius: 20px; }

Maintenant, pour créer notre page de registre et la logique pour créer un utilisateur. La majeure partie de la logique sera gérée dans nos fichiers js pour la création et la connexion d'un utilisateur:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="style.css"> <script defer src="./js/registry.js"></script> <title>Document</title> </head> <body> <nav> <a href="/">HOME</a> <ul> <li><a href="register">Register</a></li> <li><a href="/login">Login</a></li> <li><a href="/secret">Secret Page</a></li> </ul> </nav> <div class="register-wrapper"> <h1>Create an account</h1> <p class="register-error"></p> <form class="registery-form"> <p>Email:</p> <input placeholder="example@gmail.com" name="email" id="femail" type="email" > <p>Password:</p> <input id="fpassword" name="password" type="password" > <p>Rewrite Password:</p> <input id="fpassword2" name="password2" type="password" > <input class="registry-submit-btn" type="submit" value="SUBMIT"> </form> </div> </body> </html>

La logique de gestion du formulaire de création de l'utilisateur:

const regForm = document.querySelector('.registery-form'); const password = document.querySelector('#fpassword'); const password2 = document.querySelector('#fpassword2'); const email = document.querySelector('#femail'); regForm.addEventListener('submit', async (event) => { event.preventDefault() // arrêter le rafraîchissement de la page try { // envoyer les informations de l'utilisateur // dans le corps à notre route const response = await fetch('/register', { method: 'POST', body: JSON.stringify({ email: email.value, password: password.value, password2: password2.value }), headers: { 'Content-Type': 'application/json' }, }); const data = await response.json(); // Obtenez le jeton créé const { expiresIn } = await data; const { token } = await data // Supprimez le jeton existant localStorage.removeItem("id_token"); localStorage.removeItem("expires_at"); // Le jeton expirera dans 24 heures const expiresAt = new Date.now() + Number.parseInt(expiresIn) * 86400 * 1000; // day // Nous allons enregistrer notre jeton dans le stockage local localStorage.setItem('id_token', token); localStorage.setItem('expires_at', JSON.stringify(expiresAt.valueOf())); // et si tout va bien rediriger l'utilisateur vers notre page d'accueil if(data){ window.location = '/'} } catch (error) { // attrapez une erreur avant de casser l'application console.log("erreur dans le registre ==>",error) } })

Maintenant que nous avons vu le côté HTML de l'enregistrement, nous pouvons aller dans le backend pour voir comment nous traitons les informations utilisateur. Les informations utilisateur envoyées seront utilisées ici pour créer notre jeton à utiliser plus tard dans l'authentification de l'utilisateur.

À la racine de votre projet, créez un dossier routes et dans ce dossier, créez index.js pour gérer la logique backend, c'est là que la magie opère:

const express = require('express'); const jsonwebtoken = require('jsonwebtoken'); const router = express.Router(); const path = require('path'); const bcrypt = require('bcrypt'); const fs = require('fs'); const passport = require('passport'); const User = require('../models/user.js') // La clé privée (public key) est nécessaire pour émettre (issuing) le jwt const pathToKey = path.join(__dirname, "../", 'id_rsa_priv.pem') const PRIV_KEY = fs.readFileSync(pathToKey, 'utf8'); // Cette fonction sert à émettre notre jwt const issueJWT = (user) => { const _id = user._id; const expiresIn = '1'; const payload = { sub: _id, iat: Date.now() }; const signedToken = jsonwebtoken.sign(payload, PRIV_KEY, { expiresIn: expiresIn, algorithm: 'RS256' }); return { token: "Bearer " + signedToken, expires: expiresIn } // c'est simple } // Cette fonction sera utilisée pour hacher (hashing) // le mot de passe en clair donné const genPassword = async (plaintextPassword) => { const saltRounds = 10; const hash = await bcrypt.hash(plaintextPassword, saltRounds) return hash }; // eh bien, nous devons servir la page d'accueil // pour créer notre barre de navigation router.get('/', (req, res) => { res.sendFile(path.dirname(__dirname) + "/public/home.html"); }) // rendez le formulaire router.get('/register', (req, res) => { res.sendFile(path.dirname(__dirname) + "/public/register.html"); }) // Notre demande de publication sera traitée ici router.post('/register', (req, res) => { const { email, password, password2 } = req.body; // Vous pouvez vérifier les informations envoyées ici console.log("ceux-ci sont reçus", email, password, password2) // Si aucune information utilisateur n'est // envoyée pour une raison quelconque if (!req.body.email) { console.log("Aucune information envoyée") return } // Si les mots de passe ne correspondent pas if (password !== password2) { res.status(401).json({ error: "Les mots de passe ne sont pas identiques !" }) return } // Maintenant, nous recherchons l'utilisateur // dans notre base de données par la propriété email User.findOne({ email: email}, async (err, data) => { if(err) { return res.status(400).json({ error: `une erreur s'est produite: ${err}` }) } else if(data) { // Si l'email existe déjà dans notre base de données return res.status(400).json({ error: "L'utilisateur existe dans la base de données" }) } else { // S'il n'y a pas un tel e-mail dans la base de données // nous ne pouvons pas simplement enregistrer le mot // de passe donné par l'utilisateur, nous devons // le hacher en utilisant notre fonction ci-dessus const passwordToSave = await genPassword(password); // Créez l'utilisateur const newUser = new User({ email: email, password: passwordToSave }); // Et enfin l'enregistrer dans notre base de données newUser.save() .then(async user => { console.log("Nouvel utilisateur enregistré") // Émettez le jwt avec l'identifiant de l'utilisateur créé const jwt = await issueJWT(user) // Renvoyez toutes les informations à notre interface res.status(201).json( { user: { email: user.email, }, token: jwt.token, expiresIn: jwt.expires } ).catch(console.log) // en cas d'erreur }) } }) }) module.exports = router;

On y va ! Nous créons notre utilisateur et le connectons également après la création.

§Maintenant, Pour Prendre Soin de La Connexion de L'Utilisateur

Nous devons être en mesure de connecter notre utilisateur bien sûr. Nous allons commencer avec le fichier HTML et créer ensuite notre backend:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="style.css"> <script defer src="./js/login.js"></script> <title>Document</title> </head> <body> <nav> <a href="/">HOME</a> <ul> <li><a href="register">Register</a></li> <li><a href="/login">Login</a></li> <li><a href="/secret">Secret Page</a></li> </ul> </nav> <div class="login-wrapper"> <h1>Login</h1> <p class="login-error"></p> <form class="login-form"> <p>Email:</p> <input type="email" id="l-email" name="email" placeholder="example@gmail.com" > <p>Password:</p> <input type="password" id="l-password" name="password" > <input class="login-submit-btn" type="submit" value="LOGIN"> </form> </div> </body> </html>

Et la logique pour gérer le formulaire de connexion soumis:

const loginForm = document.querySelector('.login-form'); const email = document.querySelector('#l-email'); const password = document.querySelector('#l-password'); loginForm.addEventListener('submit', async (event) => { event.preventDefault(); try { const response = await fetch('/login', { method: 'POST', body: JSON.stringify({ email: email.value, password: password.value }), headers: { 'Content-Type': 'application/json' }, }) const data = await response.json(); const { expiresIn } = await data; const { token } = await data localStorage.removeItem("id_token"); localStorage.removeItem("expires_at"); const expiresAt = Date.now() + Number.parseInt(expiresIn) * 86400 * 1000; // jour localStorage.setItem('id_token', token); localStorage.setItem('expires_at', JSON.stringify(expiresAt.valueOf())); if(data){ window.location = '/'} } catch (error) { console.log("erreur de connexion", error) } })

Dans notre fichier routes/index.js, nous devons servir les fichiers HTML et gérer la demande de publication. Nous pouvons donc simplement ajouter les lignes suivantes sous les lignes précédentes que nous avons écrites pour le registre des utilisateurs :

// servir notre fichier html router.get('/login', (req, res) => { res.sendFile(path.dirname(__dirname) + "/public/login.html"); }) router.post('/login', (req, res) => { // Récupérez les valeurs que nous voulons dans le corps (body) de la requête const { email,password } = req.body; if(!email) { // Vous pouvez créer un message d'erreur pour les // utilisateurs pour l'instant nous allons // simplement enregistrer un message console.log("Aucune information envoyée") return } // Recherchez l'utilisateur dans la base de données User.findOne({ email: email}, async (err, user) => { if(err){ return res.status(400).json({ error: `une erreur s'est produite: ${err}`}) } else if (!user) { // S'il n'y a pas d'utilisateur res.status(400).json({ error: "Nom d'utilisateur inconnu"}) } else { // Trouvé l'utilisateur, comparer les mots de passe const valid = await bcrypt.compare(password, user.password); // Si les mots de passe ne correspondent pas if (!valid) { console.log("Ce n'est pas valide") res.status(400).json({ error: "Mauvais mot de passe"}) } else { // tout fonctionne jusqu'à présent const tokenObj = issueJWT(user) res.status(200).json({ user: { username: user.username, email: user.email, createdAt: user.createdAt }, token: tokenObj.token, expiresIn: tokenObj.expires } ); } } }) });

Nous pouvons maintenant créer et connecter un utilisateur. Nous devrions pouvoir faire quelque chose avec toutes les fonctionnalités que nous avons créées jusqu'à présent.

§Une Page Cecrète Pour Les Utilisateurs Connectés

Nous pouvons continuer et créer une page secrète pour nos utilisateurs. Ce que j'ai fait était très basique, juste un message indiquant que l'utilisateur est connecté.

Nous allons obtenir les informations du stockage local et vérifier la validation. N'hésitez pas à ajouter la logique que vous voulez ici :

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="style.css"> <script defer src="./js/profile.js"></script> <title>Document</title> </head> <body> <nav> <a href="/">HOME</a> <ul> <li><a href="register">Register</a></li> <li><a href="/login">Login</a></li> <li><a href="/secret">Secret Page</a></li> </ul> </nav> <div class="secret-wrapper"> <div id="user-email"></div> </div> </body> </html>

Et bien sûr le puissant javascript pour faire bouger les choses :

const profileEmail = document.querySelector("#user-email"); const idToken = localStorage.getItem("id_token"); const expiresAt = localStorage.getItem("expires_at"); // Vérifiez si le jeton a expiré if(idToken && expiresAt >= Date.now()){ const text = "Le jeton existe et est toujours valide, n'hésitez pas à l'utiliser avec la clé publique !" const h1 = document.createElement("h1") h1.innerText = text profileEmail.appendChild(h1) }

Ok maintenant créez un dossier secret-routes à la racine de notre projet et dans ce dossier créez un fichier index.js.

Ici, je ne renvoie que le fichier .html. Il y a tellement de choses que vous pouvez faire ici en utilisant notre jwt.

Vous pouvez gérer l'autorisation ici en utilisant le paquet de passeport.Vous pouvez modifier l'application pour que le client envoie le jeton dans le header d'autorisation et dans le backend, vérifiez la validité à l'aide de la clé publique que nous avons créée. Je n'inclus pas cette partie ici parce que je sais par expérience que nous n'apprenons qu'après avoir lutté pour comprendre la logique derrière les technologies que nous utilisons. Jetez un coup d'œil à cette. Cela ne prendra pas longtemps et croyez-moi, vous me remercierez après.

§Conclusion et Prochaines Étapes

Jusqu'à présent, nous avons vu comment créer un utilisateur, hacher le mot de passe, créer des clés publiques et privées pour les opérations jwt. C'est beaucoup de travail, félicitations !

Voir comment toutes ces choses fonctionnent ensemble fera de vous un développeur meilleur et plus confiant. C'était définitivement le cas pour moi. Même s'il existe de nombreux services prenant en charge les processus d'authentification à des prix très raisonnables, il est toujours bon de savoir ce qui se passe dans les coulisses.

Ilker Akbiyik