4 minutes
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.
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"
}
}
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))
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
À 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.
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.
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.
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.
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 AkbiyikTable Des Matières