Express 08 - đïž Authentification avec JWT
â ïž Avant de commencer cette quĂȘte, on te recommande de jeter un coup d'Ćil Ă celle-ci :
Dans les quĂȘtes prĂ©cĂ©dentes, nous avons vu comment crĂ©er des utilisateurs et des films.
De plus, nous avons appris à attribuer un mot de passe haché à un utilisateur.
Mais comment faire si nous voulons limiter des fonctionnalités aux utilisateurs authentifiés uniquement ? (par exemple, sur les réseaux sociaux, tu dois te connecter pour pouvoir publier quelque chose). Nous avons besoin d'un moyen pour que notre application nous reconnaisse.
C'est parti đ
đ€ Dans cette quĂȘte, tu vas apprendre :
â Comment mettre en place un flux d'authentification â Qu'est-ce qu'un JWT â Comment crĂ©er et utiliser des JWT
đ OĂč nous allons
Nous allons ajouter une route de connexion Ă notre application : la route vĂ©rifiera les identifiants dans le corps de la requĂȘte, et renverra une preuve d'authentification si tout va bien (appelons cette preuve un token, ou jeton en français : c'est l'Ă©tape 1 dans l'image ci-dessous). Sinon, la route rĂ©pondra avec le statut "Unauthorized" (401).
Ensuite, nous protĂ©gerons la route POST /api/movies. Si la requĂȘte POST ne contient aucun jeton, l'application n'acceptera plus de crĂ©er le film (Ă©tape 2a). La crĂ©ation fonctionnera comme d'habitude si le jeton est prĂ©sent (Ă©tape 2b).

Allons-y une Ă©tape Ă la fois đȘ
âšïž Login
Tu as déjà utilisé un formulaire de connexion sur un site internet :
- Tu remplis le formulaire avec ton email/pseudo/id et ton mot de passe
- Tu cliques sur le bouton "envoyer"
- Tu as maintenant accÚs à tes paramÚtres privés
Maintenant que tu as acquis plus d'expĂ©rience, tu comprends ce que signifie soumettre un formulaire : tu envoies une requĂȘte HTTP au serveur.
Si nous avions une route POST /api/login dans notre application express, une requĂȘte de connexion Ă notre serveur ressemblerait Ă ceci :
POST http://localhost:5000/api/login
Content-type: application/json
{
"email": "dwight@theoffice.com",
"password": "123456"
}
C'est par lĂ que tu vas commencer : crĂ©ez une route POST /api/login. RĂ©ponds "Credentials are valid" si le corps de la requĂȘte contient "dwight@theoffice.com" et "123456" comme email et mot de passe. Sinon, envoie le statut 401. Ce ne sera pas le code dĂ©finitif, juste une mise en place ;)
Afficher la solution|||javascript|||Tu as des doutes sur comment faire ?|||0|||Masquer
// in app.js
const isItDwight = (req, res) => {
if (req.body.email === "dwight@theoffice.com" && req.body.password === "123456") {
res.send("Credentials are valid");
} else {
res.sendStatus(401);
}
};
app.post("/api/login", isItDwight);
Avant d'aller plus loin, tu devrais créer un utilisateur avec un mot de passe dont tu te souviendras, afin d'avoir un email et un mot de passe pour tester ta connexion.
Maintenant que ta route existe, nous devons penser au processus de connexion pour remplacer le middleware isItDwight :
- Tu vas d'abord rĂ©cupĂ©rer dans la base de donnĂ©es le mot de passe hachĂ© de l'utilisateur pour l'e-mail envoyĂ© dans la requĂȘte.
- Ensuite, vous tu vas vĂ©rifier ce mot de passe hachĂ© par rapport au mot de passe en clair envoyĂ© dans la requĂȘte.
Une bonne pratique ici serait de découper ce processus en plusieurs middlewares. Quelque chose comme ca :
app.post(
"/api/login",
userHandlers.getUserByEmailWithPasswordAndPassToNext,
verifyPassword
);
Tu trouves peut ĂȘtre le nom getUserByEmailWithPasswordAndPassToNext compliquĂ©, mais prends le temps d'y penser :
- La partie
getUserByEmailest simple : c'est un miroir du middlewaregetUserById. - Nous avons dit prĂ©cĂ©demment que nos mĂ©thodes "get" ne devaient pas sĂ©lectionner le mot de passe pour des raisons de sĂ©curitĂ©. Sauf qu'ici nous devons sĂ©lectionner le mot de passe : cela doit ĂȘtre explicite dans le nom, d'oĂč la partie
WithPassword. - Jusqu'à présent, nos méthodes "get" renvoyaient la réponse au client aprÚs avoir récupérer les informations de la base de données. Sauf qu'ici, nous voulons passer ces informations au middleware suivant (
verifyPassword), d'oĂč la partieAndPassToNext.
Cela fait un nom trÚs long, mais nous ne pouvons pas nous tromper sur ce que ce middleware fera, et sur ses spécificités par rapport aux autres middlewares.
Avec verifyPassword prototypé comme ceci dans app.js :
// ...
const userHandlers = require("./userHandlers");
// ...
const verifyPassword = (req, res) => {
res.send(req.user);
}
app.post(
"/api/login",
userHandlers.getUserByEmailWithPasswordAndPassToNext,
verifyPassword
);
// ...
Essaie de créer getUserByEmailWithPasswordAndPassToNext dans userHandlers.js. Tu peux t'inspirer de getUserById :
const getUserById = (req, res) => {
const id = parseInt(req.params.id);
database
.query("select id, firstname, lastname, email, city, language from users where id = ?", [id])
.then(([users]) => {
if (users[0] != null) {
res.json(users[0]);
} else {
res.status(404).send("Not Found");
}
})
.catch((err) => {
console.error(err);
res.status(500).send("Error retrieving data from database");
});
};
Sauf que :
- Nous voulons sĂ©lectionner par un e-mail, en utilisant celui du corps de la requĂȘte.
- N'oublie pas de sélectionner également
hashedPassworddans la tableusers. - En cas de succÚs, ne renvoie pas de réponse : remplis
req.useravec l'utilisateur sélectionné, et passe au middleware suivant. - Si tu ne trouves pas d'utilisateur pour l'e-mail donné, envoie le statut
401sans aucun message.
Afficher la solution|||javascript|||Tu as des doutes sur comment faire ?|||0|||Masquer
// ...
const getUserByEmailWithPasswordAndPassToNext = (req, res, next) => {
const { email } = req.body;
database
.query("select * from users where email = ?", [email])
.then(([users]) => {
if (users[0] != null) {
req.user = users[0];
next();
} else {
res.sendStatus(401);
}
})
.catch((err) => {
console.error(err);
res.status(500).send("Error retrieving data from database");
});
};
module.exports = {
// ...
getUserByEmailWithPasswordAndPassToNext, // don't forget to export
};
Essaie ta route en utilisant Postman ou un autre outil : un email et un mot de passe valides doivent te retourner les données de l'utilisateur avec le mot de passe haché.
Continuons avec verifyPassword. Tu devrais d'abord le déplacer dans auth.js, avec le middleware hashPassword :
// in auth.js
const verifyPassword = (req, res) => {
res.send(req.user);
}
module.exports = {
hashPassword,
verifyPassword, // don't forget to export
};
// in app.js
const userHandlers = require("./userHandlers");
const { hashPassword, verifyPassword } = require("./auth");
app.post(
"/api/login",
userHandlers.getUserByEmailWithPasswordAndPassToNext,
verifyPassword
);
// ...
Tu ne peux pas "dé-hacher" le mot de passe haché. C'est là que argon2.verify peut t'aider : en lui donnant un mot de passe haché et un mot de passe en clair, il répondra s'ils correspondent ou non.
Sans rentrer dans les dĂ©tails, il hachera le mot de passe en clair avec la mĂȘme configuration que le mot de passe hachĂ©. Il se retrouvera donc avec 2 hachages compatibles et les comparera.
Regarde la documentation d'argon2, et comment tu as utilisé les promesses dans hashPassword :
const hashPassword = (req, res, next) => {
argon2
.hash(req.body.password, hashingOptions)
.then((hashedPassword) => {
// do something with the result
})
.catch((err) => {
console.error(err);
res.sendStatus(500);
});
};
(Tu peux également essayer la version async/wait si tu es d'humeur aventureuse)
Essaie maintenant d'utiliser argon2.verify dans verifyPassword. N'oublie pas : nous avons stocké les données de l'utilisateur dans req.user, tu peux donc accéder au mot de passe haché avec req.user.hashedPassword.
Si argon2 peut vérifier le mot de passe, réponds par "Credentials are valid" pour cette étape. Sinon, envoie le statut 401.
Afficher la solution|||javascript|||Tu as des doutes sur comment faire ?|||0|||Masquer
const verifyPassword = (req, res) => {
argon2
.verify(req.user.hashedPassword, req.body.password)
.then((isVerified) => {
if (isVerified) {
res.send("Credentials are valid");
} else {
res.sendStatus(401);
}
})
.catch((err) => {
console.error(err);
res.sendStatus(500);
});
};
Test ta route : maintenant, ton application est capable de vérifier un e-mail et un mot de passe par rapport aux données stockées dans la base de données lors de l'inscription.
Prochaine Ă©tape : renvoyer un token comme preuve d'authentification đȘ
đ§Ÿ JWT
Habituellement sur un site web, tu n'envoies pas tes identifiants pour chaque page. Une fois le login passĂ©, le serveur renvoie une preuve d'authentification. Cette preuve d'authentification est ensuite stockĂ©e par le client (ton navigateur) et envoyĂ©e au serveur dans les requĂȘtes suivantes.
Cela peut ĂȘtre un cookie avec un identifiant de session par exemple. Mais l'utilisation d'une session signifie que le serveur stocke des donnĂ©es pour chaque personne connectĂ©e. Un principe des APIs Rest est qu'une API doit ĂȘtre sans Ă©tat, ce qui signifie que le serveur ne doit stocker aucune information sur qui fait les requĂȘtes. Au lieu de cela, il doit fournir une chaĂźne de caractĂšres unique (chaque personne doit avoir une chaĂźne diffĂ©rente) pour identifier ses clients. Cet identifiant unique est appelĂ© un token (ou jeton en français).
Nous devons créer une chaßne unique (une par client) qui peut contenir des informations sur la personne connectée (comme son identifiant), et que seul notre serveur peut créer.
Heureusement, il existe une norme qui couvre cela. Elle s'appelle JWT.
Tout d'abord, voici Ă quoi ressemble un "JSON Web Token"Â :

Comme tu peux le voir, il y a 3 parties encodées, séparées par des points et des couleurs, dans le token :

- Header : inclut le type de jeton et l'algorithme d'encodage (pour JWT, nous utiliserons HS256, qui est plus sûr).
- Payload : comprend la version encodée des informations de l'utilisateur (ou toute autre information).
- Signature : une version hachĂ©e de l'en-tĂȘte + payload. Cette partie garantira que le token n'a pas Ă©tĂ© altĂ©rĂ© : si le header ou le payload sont modifiĂ©s par un hacker, il devra gĂ©nĂ©rer un nouveau hash pour la signature, ce qui nĂ©cessitera un code secret (
ton code secret doit rester secret ).
Description des Jetons JWT
Maintenant, comment créer ce JWT sur ton serveur ?
Tout d'abord, tu dois installer le paquet jsonwebtoken :
npm install jsonwebtoken
Ensuite, tu dois importer la bibliothĂšque et utiliser la fonction sign. Elle prend 2 arguments obligatoires :
- Le payload Ă encoder.
- Le code secret Ă utiliser. Nous avons besoin de ce code secret pour "signer" notre encodage et nous assurer que personne ne pourra crĂ©er le mĂȘme jeton. Sans ce code secret, le jeton n'est pas valide.
En utilisant un troisiÚme argument optionnel, tu peux ajouter des options comme un délai d'expiration par exemple.
à partir de la documentation de jsonwebtoken, essaie de compléter ton middleware verifyPassword avec les instructions suivantes :
- Utilise la version synchrone pour plus de simplicité.
- Le payload doit contenir l'identifiant de l'utilisateur sous le claim
sub(subsignifie "sujet", l'utilisateur pour lequel le jeton est créé). - Le jeton doit ĂȘtre signĂ© Ă l'aide d'une chaĂźne secrĂšte stockĂ©e dans ton fichier
.env(tu peux appeler la variableJWT_SECRET: n'oublie pas de mettre à jour ton fichier.env.sample). - Le jeton doit expirer 1 heure aprÚs sa création.
- Envoie le jeton dans la réponse. Tu peux également envoyer les données de l'utilisateur pour plus de commodité, mais n'envoie pas de mot de passe !
Afficher la solution|||javascript|||Tu as des doutes sur comment faire ?|||0|||Masquer
// ...
const jwt = require("jsonwebtoken"); // don't forget to import
// ...
const verifyPassword = (req, res) => {
argon2
.verify(req.user.hashedPassword, req.body.password)
.then((isVerified) => {
if (isVerified) {
const payload = { sub: req.user.id };
const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: "1h",
});
delete req.user.hashedPassword;
res.send({ token, user: req.user });
} else {
res.sendStatus(401);
}
})
.catch((err) => {
console.error(err);
res.sendStatus(500);
});
};
Essaie de te connecter et d'obtenir un token de ton serveur.
Et voici la meilleure partie de JWT : N'importe qui peut lire le contenu des jetons, mais SEUL ton serveur peut les créer.
Pour vérifier cela, rends toi sur jwt.io et copie/colle un de tes tokens :

Comme tu peux le voir, ton payload peut ĂȘtre entiĂšrement dĂ©codĂ©e (mais pas la signature).
đĄïž ProtĂšge tes routes
Tu peux maintenant obtenir une preuve d'authentification de ton serveur : il est temps de protéger tes routes. Nous allons commencer par POST /api/movies.
Ă l'heure actuelle, n'importe qui peut publier un nouveau film sur le serveur en utilisant une requĂȘte comme celle-ci :
POST http://localhost:5000/api/movies
Content-type: application/json
{
"title": "Citizen Kane",
"director": "Orson Wells",
"year": "1941",
"color": "0",
"duration": 120
}
Ton objectif est de rejeter une telle demande Ă moins qu'elle ne contienne un en-tĂȘte "Authorization" avec le schĂ©ma d'authentification "Bearer" suivi par un jeton valide. Quelque chose comme ca :
POST http://localhost:5000/api/movies
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjcsImlhdCI6MTY2MDIwMjczMywiZXhwIjoxNjYwMjA2MzMzfQ._5PYuoPoBfDtzq6fMsUn_8YGhfqtt_OaBVMHhe-FHlU
Content-type: application/json
{
"title": "Citizen Kane",
"director": "Orson Wells",
"year": "1941",
"color": "0",
"duration": 120
}
Crée un nouveau middleware verifyToken dans auth.js comme ceci :
// ...
const verifyToken = (req, res, next) => {
try {
next();
} catch (err) {
console.error(err);
res.sendStatus(401);
}
};
module.exports = {
hashPassword,
verifyPassword,
verifyToken, // don't forget to export
};
Pour l'instant, verifyToken ne vérifie rien : il se contente de passer au middleware suivant.
Comme d'habitude, il est prĂ©fĂ©rable de commencer par une implĂ©mentation minimale et testable (mĂȘme si elle est incomplĂšte) puis de construire Ă©tape par Ă©tape.
Utilise verifyToken dans app.js :
// ...
const { hashPassword, verifyPassword, verifyToken } = require("./auth"); // don't forget to import
// ...
app.post("/api/movies", verifyToken, movieHandlers.postMovie);
// ...
C'est à toi maintenant ! Compléte le bloc try dans verifyToken en suivant ces étapes :
- RĂ©cupĂšre l'en-tĂȘte "Authorization" de la requĂȘte : gĂ©nĂšre une erreur si l'en-tĂȘte est manquant.
- Split l'en-tĂȘte en 2 sous-chaines
typeettoken: génÚre une erreur si le type n'est pas"Bearer". - Vérifie le jeton de maniÚre synchrone : s'il est valide, remplis
req.payloadavec le payload décodé. - Passe au suivant -- tu as déjà celle-ci ;)
Afficher la solution|||javascript|||Tu as des doutes sur comment faire ?|||0|||Masquer
// ...
const verifyToken = (req, res, next) => {
try {
const authorizationHeader = req.get("Authorization");
if (authorizationHeader == null) {
throw new Error("Authorization header is missing");
}
const [type, token] = authorizationHeader.split(" ");
if (type !== "Bearer") {
throw new Error("Authorization header has not the 'Bearer' type");
}
req.payload = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
console.error(err);
res.sendStatus(401);
}
};
module.exports = {
hashPassword,
verifyPassword,
verifyToken, // don't forget to export
};
Essaie ta route POST /api/movies avec et sans jeton. Maintenant, ta route est protégée par authentification.
đ§± Le mur d'authentification
Une derniĂšre astuce. Ajouter le middleware verifyToken sur chaque route que tu souhaites protĂ©ger peut ĂȘtre fastidieux :
// ...
app.post("/api/movies", verifyToken, movieHandlers.postMovie);
app.put("/api/movies/:id", verifyToken, movieHandlers.updateMovie);
app.delete("/api/movies/:id", verifyToken, movieHandlers.deleteMovie);
// ...
Un moyen pratique consiste Ă d'abord rĂ©partir tes routes entre celles publiques et celles qui doivent ĂȘtre protĂ©gĂ©es :
// the public routes
app.get("/api/movies", movieHandlers.getMovies);
app.get("/api/movies/:id", movieHandlers.getMovieById);
app.post(
"/api/login",
userHandlers.getUserByEmailWithPasswordAndPassToNext,
verifyPassword
); // /!\ login should be a public route
// then the routes to protect
app.post("/api/movies", movieHandlers.postMovie);
app.put("/api/movies/:id", movieHandlers.updateMovie);
app.delete("/api/movies/:id", movieHandlers.deleteMovie);
// ...
Ensuite, tu vas construire un "mur d'authentification" entre les 2 parties de ton fichier : chaque route avant le mur restera inchangée, mais chaque route aprÚs le mur va nécessiter un jeton pour fonctionner.
Tu peux construire un tel mur en activant le middleware verifyToken avec app.use sans aucun path : le middleware sera exĂ©cutĂ© pour toutes les requĂȘtes Ă l'application, mais uniquement pour les routes dĂ©clarĂ©es aprĂšs l'appel Ă app.use. Quelque chose comme ca :
// the public routes
app.get("/api/movies", movieHandlers.getMovies);
app.get("/api/movies/:id", movieHandlers.getMovieById);
app.post(
"/api/login",
userHandlers.getUserByEmailWithPasswordAndPassToNext,
verifyPassword
); // /!\ login should be a public route
// then the routes to protect
app.use(verifyToken); // authentication wall : verifyToken is activated for each route after this line
app.post("/api/movies", movieHandlers.postMovie);
app.put("/api/movies/:id", movieHandlers.updateMovie);
app.delete("/api/movies/:id", movieHandlers.deleteMovie);
// ...
đȘ Challenge:
Réorganise ton fichier app.js :
- Toutes les routes GET doivent ĂȘtre publiques.
- Les routes "register" (POST
/api/users) et "login" (POST/api/login) doivent ĂȘtre publiques. - Toutes les autres routes POST, PUT et DELETE doivent ĂȘtre protĂ©gĂ©es.
Héberge ton application améliorée sur GitHub et partage l'URL comme solution.
Bonus:
- PUT
/api/users/:idet DELETE/api/users/:idne devraient fonctionner que si l'id correspond à celui du payload du token (req.payload.sub). Si ce n'est pas le cas, réponds avec un statut403("Forbidden").
đ§ CritĂšres de validation
- [ ] L'application génÚre un JWT lorsque l'utilisateur se connecte.
- [ ] Les routes GET, la route "register" et la route "login" sont publiques.
- [ ] Toutes les autres routes sont protégées.
==$==
Ce que tu devrais avoir dans :
require("dotenv").config();
const express = require("express");
const app = express();
app.use(express.json());
const port = process.env.APP_PORT ?? 5000;
const welcome = (req, res) => {
res.send("Welcome to my favourite movie list");
};
app.get("/", welcome);
const movieHandlers = require("./movieHandlers");
const userHandlers = require("./userHandlers");
const { hashPassword, verifyPassword, verifyToken } = require("./auth");
app.get("/api/movies", movieHandlers.getMovies);
app.get("/api/movies/:id", movieHandlers.getMovieById);
app.get("/api/users", userHandlers.getUsers);
app.get("/api/users/:id", userHandlers.getUserById);
app.post("/api/users", hashPassword, userHandlers.postUser);
app.post(
"/api/login",
userHandlers.getUserByEmailWithPasswordAndPassToNext,
verifyPassword
);
app.use(verifyToken);
app.post("/api/movies", movieHandlers.postMovie);
app.put("/api/movies/:id", movieHandlers.updateMovie);
app.delete("/api/movies/:id", movieHandlers.deleteMovie);
app.put("/api/users/:id", hashPassword, userHandlers.updateUser);
app.delete("/api/users/:id", userHandlers.deleteUser);
app.listen(port, (err) => {
if (err) {
console.error("Something bad happened");
} else {
console.log(`Server is listening on ${port}`);
}
});
const argon2 = require("argon2");
const jwt = require("jsonwebtoken");
const hashingOptions = {
type: argon2.argon2id,
memoryCost: 2 ** 16,
timeCost: 5,
parallelism: 1,
};
const hashPassword = (req, res, next) => {
argon2
.hash(req.body.password, hashingOptions)
.then((hashedPassword) => {
req.body.hashedPassword = hashedPassword;
delete req.body.password;
next();
})
.catch((err) => {
console.error(err);
res.sendStatus(500);
});
};
const verifyPassword = (req, res) => {
argon2
.verify(req.user.hashedPassword, req.body.password)
.then((isVerified) => {
if (isVerified) {
const payload = { sub: req.user.id };
const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: "1h",
});
delete req.user.hashedPassword;
res.send({ token, user: req.user });
} else {
res.sendStatus(401);
}
})
.catch((err) => {
console.error(err);
res.sendStatus(500);
});
};
const verifyToken = (req, res, next) => {
try {
const authorizationHeader = req.get("Authorization");
if (authorizationHeader == null) {
throw new Error("Authorization header is missing");
}
const [type, token] = authorizationHeader.split(" ");
if (type !== "Bearer") {
throw new Error("Authorization header has not the 'Bearer' type");
}
req.payload = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
console.error(err);
res.sendStatus(401);
}
};
module.exports = {
hashPassword,
verifyPassword,
verifyToken,
};
// ...
const updateUser = (req, res) => {
const id = parseInt(req.params.id);
if (id !== req.payload.sub) {
return res.sendStatus(403);
}
const { firstname, lastname, email, city, language, hashedPassword } = req.body;
database
.query(
"update users set firstname = ?, lastname = ?, email = ?, city = ?, language = ?, hashedPassword = ? where id = ?",
[firstname, lastname, email, city, language, hashedPassword, id]
)
.then(([result]) => {
if (result.affectedRows === 0) {
res.status(404).send("Not Found");
} else {
res.sendStatus(204);
}
})
.catch((err) => {
console.error(err);
res.status(500).send("Error editing the user");
});
};
const deleteUser = (req, res) => {
const id = parseInt(req.params.id);
if (id !== req.payload.sub) {
return res.sendStatus(403);
}
database
.query("delete from users where id = ?", [id])
.then(([result]) => {
if (result.affectedRows === 0) {
res.status(404).send("Not Found");
} else {
res.sendStatus(204);
}
})
.catch((err) => {
console.error(err);
res.status(500).send("Error deleting the user");
});
};
const getUserByEmailWithPasswordAndPassToNext = (req, res, next) => {
const { email } = req.body;
database
.query("select * from users where email = ?", [email])
.then(([users]) => {
if (users[0] != null) {
req.user = users[0];
next();
} else {
res.sendStatus(401);
}
})
.catch((err) => {
console.error(err);
res.status(500).send("Error retrieving data from database");
});
};
module.exports = {
getUsers,
getUserById,
postUser,
updateUser,
deleteUser,
getUserByEmailWithPasswordAndPassToNext,
};
Ressources partagĂ©es sur cette quĂȘte
- Short guide to get a better idea of JWT validation, to complement the quest.https://www.freecodecamp.org/news/how-to-sign-and-validate-json-web-tokens/
- Présentation du JWT par Graphikarthttps://www.youtube.com/watch?v=S-xBAo47W58