Chapitre 926 minIntermédiaire
Authentification JWT, argon2 et rôles dans une API Express
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- hacher et vérifier un mot de passe avec argon2 ;
- émettre un JWT à la connexion et le vérifier à chaque requête ;
- réserver une route à un rôle et contrôler le propriétaire d'une ressource ;
- choisir entre jeton JWT et session selon le client.
4 exercices corrigés1 schéma
Authentifier, c'est vérifier qui envoie la requête ; autoriser, c'est vérifier ce qu'il a le droit de faire. Dans une API Node.js consommée par un front React ou une application mobile, la méthode la plus courante est le jeton JWT : l'utilisateur se connecte une fois avec son identifiant et son mot de passe (stocké haché avec argon2 ou bcrypt), reçoit un jeton signé, et le présente à chaque requête. Ce chapitre code tout le circuit pour AppliFrais : stockage des mots de passe, route de connexion, vérification du jeton, contrôle du rôle et contrôle du propriétaire, puis compare jetons et sessions.
Trois contrôles, trois codes
Les notions d'identification, d'authentification et d'autorisation sont posées dans le cours Authentification et habilitations ; le fonctionnement des jetons Bearer et la structure d'un JWT dans le cours API REST. Ici, on les code. Chaque requête protégée passe trois contrôles successifs, et chaque refus a son code :
| Contrôle | Où | Refus | Exemple |
|---|---|---|---|
| Authentification : le jeton est-il valide ? | Middleware authentifier | 401 Unauthorized | Pas de jeton, jeton expiré ou falsifié |
| Rôle : l'opération est-elle permise à ce rôle ? | Middleware exigerRole | 403 Forbidden | Un visiteur tente de valider une fiche |
| Propriétaire : cette ressource est-elle la sienne ? | Service, avec la donnée en base | 404 Not Found (ou 403) | Lucas demande la fiche de Sarah |
Le troisième contrôle est celui qu'on oublie, et c'est la première vulnérabilité des API selon l'OWASP : le BOLA (Broken Object Level Authorization). Un visiteur connecté qui change 12 en 13 dans l'adresse ne doit pas lire la fiche d'un collègue (sécurité des API).
Stocker les mots de passe : argon2
Un mot de passe ne se stocke jamais en clair, ni chiffré, ni haché avec un algorithme rapide comme MD5 ou SHA-256 : en cas de fuite de la base, un attaquant teste des milliards de mots de passe par seconde contre ces empreintes. On utilise une fonction de hachage lente et salée, conçue pour ça. La recommandation actuelle (OWASP) est Argon2id ; bcrypt reste acceptable. Le principe du sel et du facteur de coût est expliqué dans le cours Cryptographie.
npm install argon2
import argon2 from 'argon2';
const hachage = await argon2.hash('Visiteur#2026');
console.log(hachage);
// $argon2id$v=19$m=65536,p=4,t=3$Dgts2gA9wpYgVIEfwBEhYQ$CPbE0ae6USdangrpcLEhpkWahdkUyvSEQ2WZba188lU
await argon2.verify(hachage, 'Visiteur#2026'); // true
await argon2.verify(hachage, 'visiteur#2026'); // false
La chaîne produite contient tout ce qu'il faut pour vérifier plus tard : l'algorithme (argon2id), sa version, les paramètres de coût (64 Mio de mémoire, 3 passes, 4 fils), le sel aléatoire puis l'empreinte. Hacher deux fois le même mot de passe donne deux chaînes différentes, grâce au sel : impossible de repérer deux utilisateurs qui ont le même mot de passe. C'est pour ça que la colonne mot_de_passe fait 255 caractères, et qu'on ne compare jamais deux hachages entre eux : on appelle verify.
argon2 (argon2) | bcrypt (bcrypt) | |
|---|---|---|
| Recommandation | Premier choix (OWASP) | Acceptable, très répandu |
| Réglage | Mémoire, passes, parallélisme (bons réglages par défaut) | Un facteur de coût : 12 est un bon point de départ |
| Limite | Aucune en pratique | Seuls les 72 premiers octets du mot de passe comptent |
| API | await argon2.hash(mdp), await argon2.verify(hachage, mdp) | await bcrypt.hash(mdp, 12), await bcrypt.compare(mdp, hachage) |
Les deux paquets compilent du code natif à l'installation ; ils fournissent des binaires précompilés pour les systèmes courants. Utilise toujours les fonctions asynchrones : un hachage prend volontairement des dizaines de millisecondes, et la version synchrone bloquerait toute l'API (chapitre 1).
Créer les comptes
Dans AppliFrais, on ne s'inscrit pas : c'est le service informatique de GSB qui crée les comptes des visiteurs et des comptables. Un petit script suffit, lancé par un administrateur :
// scripts/creer-utilisateur.js
// Usage : node --env-file=.env scripts/creer-utilisateur.js lmartin Martin Lucas visiteur
import { randomBytes } from 'node:crypto';
import argon2 from 'argon2';
import { pool } from '../src/db.js';
const [login, nom, prenom, role = 'visiteur'] = process.argv.slice(2);
if (!login || !nom || !prenom || !['visiteur', 'comptable'].includes(role)) {
console.error('Usage : node scripts/creer-utilisateur.js <login> <nom> <prénom> [visiteur|comptable]');
process.exit(1);
}
const provisoire = randomBytes(12).toString('base64url'); // 16 caractères aléatoires
const hachage = await argon2.hash(provisoire); // argon2id, sel aléatoire inclus
await pool.execute(
'INSERT INTO utilisateur (login, nom, prenom, mot_de_passe, role) VALUES (?, ?, ?, ?, ?)',
[login, nom, prenom, hachage, role],
);
console.log(`Compte ${login} créé. Mot de passe provisoire : ${provisoire}`);
await pool.end();
Compte jdupont créé. Mot de passe provisoire : oMRiCdAqTEvLzmxe
Une application complète obligerait l'utilisateur à changer ce mot de passe provisoire à sa première connexion. Les règles de longueur et de complexité recommandées par la CNIL et l'ANSSI sont détaillées dans le cours Authentification.
La route de connexion
// src/controllers/auth.controller.js
import argon2 from 'argon2';
import jwt from 'jsonwebtoken';
import { config } from '../config.js';
import * as utilisateurs from '../depots/utilisateurs.depot.js';
import { ErreurApi } from '../erreurs.js';
// Hachage factice : un login inconnu coûte le même temps qu'un mauvais mot de passe.
const HACHAGE_FACTICE = await argon2.hash('mot-de-passe-factice');
const DUREE_JETON = 15 * 60; // secondes
export async function connexion(req, res) {
const { login, motDePasse } = req.valide.body;
const utilisateur = await utilisateurs.trouverParLogin(login);
const motDePasseCorrect = await argon2.verify(utilisateur?.motDePasse ?? HACHAGE_FACTICE, motDePasse);
if (!utilisateur || !motDePasseCorrect) {
req.log.warn({ login }, 'Échec de connexion');
throw new ErreurApi(401, 'identifiants-invalides', 'Identifiants invalides', 'Login ou mot de passe incorrect.');
}
const accessToken = jwt.sign({ role: utilisateur.role }, config.jwtSecret, {
subject: String(utilisateur.id),
expiresIn: DUREE_JETON,
algorithm: 'HS256',
});
req.log.info({ utilisateurId: utilisateur.id }, 'Connexion réussie');
res.set('Cache-Control', 'no-store').json({ accessToken, tokenType: 'Bearer', expiresIn: DUREE_JETON });
}
// src/routes/auth.routes.js
import { Router } from 'express';
import { rateLimit } from 'express-rate-limit';
import { connexion, moi } from '../controllers/auth.controller.js';
import { ErreurApi } from '../erreurs.js';
import { authentifier } from '../middlewares/authentifier.js';
import { valider } from '../middlewares/valider.js';
import { schemaConnexion } from '../schemas/fiches.schemas.js';
// 10 échecs par quart d'heure et par adresse IP : de quoi se tromper, pas de quoi deviner.
const limiteConnexion = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 10,
skipSuccessfulRequests: true, // seuls les échecs comptent
standardHeaders: 'draft-8',
legacyHeaders: false,
handler: (req, res, next) => next(new ErreurApi(429, 'trop-de-tentatives', 'Trop de tentatives',
'Trop de tentatives de connexion. Réessaie dans quelques minutes.')),
});
export const routeurAuth = Router();
routeurAuth.post('/connexion', limiteConnexion, valider({ body: schemaConnexion }), connexion);
routeurAuth.get('/moi', authentifier, moi);
Chaque détail de cette route protège contre une attaque précise :
- un seul message pour un login inconnu et un mauvais mot de passe : sinon, l'API dit à un attaquant quels logins existent (énumération de comptes) ;
- le hachage factice : sans lui, un login inconnu répondrait en 1 ms et un login existant en 50 ms (le temps du hachage). Cette différence de temps suffirait à deviner les logins existants ; avec lui, les deux cas prennent le même temps ;
- la limitation de débit freine l'attaque par force brute : après dix échecs en quinze minutes depuis la même adresse, la tentative suivante reçoit
429 Too Many Requestsavec un en-têteRetry-After, même si le mot de passe est juste.skipSuccessfulRequestsne compte que les échecs, pour ne pas bloquer un bureau entier derrière la même adresse IP ; Cache-Control: no-store: la réponse qui contient le jeton ne doit être gardée dans aucun cache ;- le journal trace les échecs, avec le login tenté mais jamais le mot de passe (chapitre 11).
Signer et vérifier un JWT
npm install jsonwebtoken
jwt.sign(chargeUtile, secret, options) produit le jeton. Il contient le minimum : l'identifiant de l'utilisateur dans sub (subject), son rôle, et les dates d'émission (iat) et d'expiration (exp), ajoutées par la bibliothèque.
curl -s -X POST http://localhost:3000/v1/auth/connexion \
-H 'Content-Type: application/json' \
-d '{"login": "lmartin", "motDePasse": "Visiteur#2026"}'
{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidmlzaXRldXIiLCJpYXQiOjE3OTM2Mjg2MzgsImV4cCI6MTc5MzYyOTUzOCwic3ViIjoiNyJ9.pdVAZOedEtXlxkDmSRJvQPmhSVJoPWurynFT3Ov8wCM","tokenType":"Bearer","expiresIn":900}
La partie centrale, décodée, donne {"role":"visiteur","iat":1793628638,"exp":1793629538,"sub":"7"} : quinze minutes de validité (1793629538 − 1793628638 = 900 secondes). Cette partie est lisible par tout le monde (c'est du base64url, pas du chiffrement) : n'y mets jamais de donnée confidentielle. Ce qui la protège, c'est la signature : HMAC-SHA256 calculé avec le secret du serveur. Modifier le rôle dans le jeton invalide la signature.
Le secret est une chaîne aléatoire longue, propre à chaque environnement, rangée dans la variable JWT_SECRET. Pour en générer une :
node -e "console.log(require('node:crypto').randomBytes(48).toString('base64url'))"
Elle produit 64 caractères aléatoires. Qui connaît ce secret peut fabriquer un jeton de comptable : il ne s'écrit ni dans le code ni dans Git.
Le middleware authentifier
// src/middlewares/authentifier.js
import jwt from 'jsonwebtoken';
import { config } from '../config.js';
import { ErreurApi } from '../erreurs.js';
// Vérifie le jeton « Authorization: Bearer … » et place l'utilisateur dans req.utilisateur.
export function authentifier(req, res, next) {
const [schema, jeton] = req.get('Authorization')?.split(' ') ?? [];
if (schema !== 'Bearer' || !jeton) {
res.set('WWW-Authenticate', 'Bearer realm="api"');
throw new ErreurApi(401, 'non-authentifie', 'Authentification requise', 'Jeton absent.');
}
try {
const charge = jwt.verify(jeton, config.jwtSecret, { algorithms: ['HS256'] });
req.utilisateur = { id: Number(charge.sub), role: charge.role };
} catch {
res.set('WWW-Authenticate', 'Bearer realm="api", error="invalid_token"');
throw new ErreurApi(401, 'jeton-invalide', 'Jeton invalide', 'Le jeton est invalide ou a expiré.');
}
next();
}
jwt.verify contrôle la signature et l'expiration ; il lève TokenExpiredError pour un jeton expiré et JsonWebTokenError pour une signature invalide. L'option algorithms: ['HS256'] impose l'algorithme attendu : sans elle, certaines bibliothèques acceptaient un jeton qui annonçait lui-même un algorithme plus faible, voire aucun. jsonwebtoken refuse déjà un jeton non signé, mais fixer l'algorithme reste la règle. L'en-tête WWW-Authenticate est celui que prévoit la norme des jetons Bearer (RFC 6750) avec un 401.
On branche le middleware une fois sur le routeur, pour toutes ses routes :
// src/routes/fiches.routes.js
routeurFiches.use(authentifier); // toutes les routes des fiches exigent un jeton
Les rôles : exigerRole
// src/middlewares/autoriser.js
import { ErreurApi } from '../erreurs.js';
// exigerRole('comptable') : à placer après authentifier.
export function exigerRole(...roles) {
return (req, res, next) => {
if (!roles.includes(req.utilisateur?.role)) {
throw new ErreurApi(403, 'acces-refuse', 'Accès refusé', 'Ton rôle ne permet pas cette opération.');
}
next();
};
}
routeurFiches.post('/', exigerRole('visiteur'), valider({ body: schemaCreationFiche }), fiches.creer);
routeurFiches.post('/:id/validations', exigerRole('comptable'),
valider({ params: schemaId, body: schemaValidation }), fiches.valider);
La liste des routes devient la matrice d'habilitations de l'API, lisible ligne par ligne. Une route sans exigerRole est ouverte à tout utilisateur connecté : c'est un choix qui doit être conscient. Le modèle de contrôle par rôle (RBAC) et le principe du moindre privilège sont présentés dans le cours Authentification.
Le propriétaire : un contrôle dans le service
Le rôle ne suffit pas : Lucas et Sarah sont tous deux visiteurs, mais Lucas ne doit voir que ses fiches. Ce contrôle dépend de la donnée (à qui appartient la fiche 13 ?), il se fait donc dans le service, après lecture en base :
// src/services/fiches.service.js
// 404 aussi pour la fiche d'un autre visiteur : on ne révèle pas qu'elle existe.
export async function chargerFiche(utilisateur, id) {
const fiche = await depot.trouverParId(id);
if (!fiche || (utilisateur.role === 'visiteur' && fiche.visiteurId !== utilisateur.id)) {
throw new ErreurApi(404, 'fiche-introuvable', 'Fiche introuvable', `La fiche ${id} n'existe pas.`);
}
return fiche;
}
export async function listerFiches(utilisateur, filtres) {
// Un visiteur ne voit que ses fiches, quel que soit le filtre envoyé.
const visiteurId = utilisateur.role === 'visiteur' ? utilisateur.id : filtres.visiteur;
return depot.lister({ ...filtres, visiteurId });
}
export async function creerFiche(utilisateur, mois) {
const id = await depot.creer(utilisateur.id, mois); // le propriétaire vient du jeton, jamais du corps
return lireFiche(utilisateur, id);
}
Toutes les opérations sur une fiche commencent par chargerFiche. Pourquoi 404 plutôt que 403 pour la fiche d'un autre ? Un 403 confirmerait que la fiche 13 existe ; un 404 ne révèle rien. Les deux se défendent, à condition d'être cohérent. Et listerFiches ignore le filtre ?visiteur=8 pour un visiteur : sinon, Lucas lirait la liste de Sarah en ajoutant un paramètre.
Certaines opérations mêlent rôle et donnée : un visiteur peut clôturer sa fiche, seul un comptable peut la mettre en paiement. Le service le vérifie selon la valeur demandée :
export async function changerEtat(utilisateur, id, nouvelEtat) {
const fiche = await chargerFiche(utilisateur, id);
// Le visiteur clôture sa fiche ; seul le comptable met en paiement.
const roleRequis = nouvelEtat === 'CL' ? 'visiteur' : 'comptable';
if (utilisateur.role !== roleRequis) {
throw new ErreurApi(403, 'acces-refuse', 'Accès refusé', `Seul un ${roleRequis} peut passer une fiche à l'état ${nouvelEtat}.`);
}
verifierTransition(fiche.etat, nouvelEtat);
if (!(await depot.changerEtat(id, fiche.etat, nouvelEtat))) {
throw new ErreurApi(409, 'conflit', 'Conflit', 'La fiche a été modifiée entre-temps : recharge-la.');
}
return lireFiche(utilisateur, id);
}
Tester le circuit avec curl
# 1. Se connecter et garder le jeton dans une variable
JETON=$(curl -s -X POST http://localhost:3000/v1/auth/connexion \
-H 'Content-Type: application/json' \
-d '{"login":"lmartin","motDePasse":"Visiteur#2026"}' | jq -r .accessToken)
# 2. Utiliser le jeton
curl -s http://localhost:3000/v1/auth/moi -H "Authorization: Bearer $JETON"
{"id":7,"login":"lmartin","nom":"Martin","prenom":"Lucas","role":"visiteur"}
| Requête de Lucas (visiteur n° 7) | Réponse |
|---|---|
GET /v1/fiches/12 sans en-tête Authorization | 401, WWW-Authenticate: Bearer realm="api" |
GET /v1/fiches/12 avec un jeton modifié à la main | 401, error="invalid_token" |
GET /v1/fiches/12 avec son jeton | 200, sa fiche d'octobre |
GET /v1/fiches/13 (fiche de Sarah) | 404 |
GET /v1/fiches?visiteur=8 | 200, mais ses fiches seulement |
POST /v1/fiches/12/validations | 403, réservé aux comptables |
PATCH /v1/fiches/12 avec {"etat":"MP"} | 403, la mise en paiement est réservée aux comptables |
jq est un petit outil en ligne de commande qui extrait une valeur d'un JSON. Dans Bruno ou Postman, un script après la requête de connexion range le jeton dans une variable d'environnement (tester une API).
Expiration, déconnexion et révocation
Un JWT est autoporteur : l'API le vérifie sans consulter la base. C'est rapide et simple, mais l'API ne peut pas « oublier » un jeton avant son expiration. D'où trois règles :
- une durée de vie courte pour le jeton d'accès : 15 minutes ici ;
- un jeton de rafraîchissement (refresh token), plus long et stocké en base, pour obtenir un nouveau jeton d'accès sans redemander le mot de passe. Comme il est en base, on peut le révoquer. Son fonctionnement et sa rotation sont décrits dans le cours API REST, et le cours Développement mobile ajoute à cette API la route de rafraîchissement avec rotation, la déconnexion et la clé d'idempotence (l'API Node.js d'une application mobile) ;
- la déconnexion consiste, côté client, à oublier le jeton ; côté serveur, à révoquer le jeton de rafraîchissement.
Pour bloquer immédiatement un compte compromis, on peut aussi vérifier à chaque requête que l'utilisateur existe et est actif, au prix d'une requête en base : la route GET /v1/auth/moi le fait déjà.
Sessions ou JWT ?
L'autre grande méthode est la session : à la connexion, le serveur crée une session (stockée en mémoire, en base ou dans Redis) et envoie au navigateur un cookie qui contient seulement son identifiant. Le navigateur renvoie le cookie automatiquement à chaque requête.
// Avec express-session : à titre de comparaison
import session from 'express-session';
app.use(session({
secret: config.secretSession,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30 * 60 * 1000 },
}));
// À la connexion réussie : req.session.utilisateur = { id: utilisateur.id, role: utilisateur.role };
// À la déconnexion : req.session.destroy(() => res.sendStatus(204));
| Jeton JWT (Bearer) | Session (cookie) | |
|---|---|---|
| Où est l'état | Dans le jeton, chez le client | Sur le serveur |
| Envoi | En-tête Authorization, ajouté par le code du client | Cookie, envoyé automatiquement par le navigateur |
| Révocation immédiate | Difficile (attendre l'expiration, ou liste de révocation) | Facile : supprimer la session |
| Plusieurs serveurs, application mobile | Simple : rien à partager | Il faut un stockage commun des sessions (Redis) |
| Risque principal | Vol du jeton, par exemple par une faille XSS s'il est lisible par JavaScript | Requêtes forgées depuis un autre site (CSRF) : SameSite et un jeton anti-CSRF |
| Cas typique | API consommée par un front séparé et une application mobile | Application web classique, front et back sur le même domaine |
AppliFrais sert un front React et une application mobile : le jeton Bearer est le choix le plus simple. Pour un front web seul sur le même domaine, une session avec un cookie HttpOnly, Secure et SameSite est une excellente solution, souvent plus sûre. Le cours Authentification compare les deux en détail, et le cours React montre où le front garde la session ou le jeton.
Exercices
Exercice 1 sur 4 401, 403 ou 404 ? Réussi À revoir
Sarah Vincent (visiteuse n° 8) et Anne Durand (comptable) utilisent l'API. Donne le code de chaque réponse.
- Sarah, avec un jeton émis il y a 20 minutes :
GET /v1/fiches. - Sarah, jeton valide :
GET /v1/fiches/12(fiche de Lucas). - Sarah, jeton valide :
PATCH /v1/fiches/13avec{"etat":"CL"}(sa fiche, déjà clôturée). - Anne :
POST /v1/fichesavec{"mois":"2026-11"}. - Anne :
POST /v1/fiches/13/validationsavec{"montantValide":567}. - Anne :
GET /v1/fiches/12.
Voir la réponseMasquer la réponse de l'exercice 1
401: le jeton a expiré (validité de 15 minutes).404: la fiche n'appartient pas à Sarah, l'API ne révèle pas son existence.409: la fiche est bien à elle et elle a le bon rôle, maisCL→CLn'est pas une transition permise.403: seuls les visiteurs créent des fiches (exigerRole('visiteur')).201: la fiche 13 est clôturée, Anne est comptable ; la fiche passe àVA. Le total de la fiche 13 est 2 × 110 + 250 × 0,62 + 80 + 4 × 25 + 12 = 567 €.200: un comptable peut lire toutes les fiches.
Tu avais trouvé ?
Exercice 2 sur 4 Revue de code Réussi À revoir
Relève les problèmes de sécurité de cette route de connexion, et classe-les du plus grave au moins grave.
app.post('/login', async (req, res) => {
const [rows] = await pool.query(`SELECT * FROM utilisateur WHERE login = '${req.body.login}'`);
const u = rows[0];
if (!u) return res.status(404).json({ erreur: 'Login inconnu' });
if (u.mot_de_passe !== md5(req.body.motDePasse)) return res.status(401).json({ erreur: 'Mot de passe faux' });
const jeton = jwt.sign({ id: u.id, role: u.role, nom: u.nom, motDePasse: u.mot_de_passe }, 'secret');
console.log(`Connexion de ${u.login} avec ${req.body.motDePasse}`);
res.json({ jeton, utilisateur: u });
});
Voir la réponseMasquer la réponse de l'exercice 2
- Injection SQL : le login est concaténé dans la requête. Requête préparée avec
?. - Secret JWT faible et écrit dans le code (
'secret') : n'importe qui peut fabriquer un jeton de comptable. Un secret long et aléatoire, dansJWT_SECRET. - Mot de passe haché avec MD5, rapide et sans sel : cassé en quelques secondes en cas de fuite. argon2id.
- Le hachage du mot de passe est mis dans le jeton, lisible par tous, et renvoyé dans
utilisateur: u(SELECT *). Charge utile minimale (sub,role), réponse sans données sensibles. - Le mot de passe en clair est écrit dans le journal. Ne jamais journaliser un secret.
- Énumération des comptes :
404 Login inconnuet401 Mot de passe fauxdisent à un attaquant quels logins existent. Un seul message et un seul code (401), et un temps de réponse identique. - Pas d'expiration du jeton (
expiresInabsent) : un jeton volé reste valable pour toujours. AjouteexpiresIn: 900. - Pas de limitation de débit ni de validation des entrées.
L'ordre exact des points 2 à 4 se discute ; l'injection SQL et le secret faible donnent chacun un accès complet, ce sont les plus graves.
Tu avais trouvé ?
Exercice 3 sur 4 Le contrôle oublié Réussi À revoir
Un camarade a écrit cette route pour supprimer un frais hors forfait. Les tests avec son propre compte passent. Quelle faille contient-elle ? Comment la prouverais-tu, puis la corrigerais-tu ?
routeurFrais.delete('/:id', authentifier, async (req, res) => {
await pool.execute('DELETE FROM frais_hors_forfait WHERE id = ?', [req.params.id]);
res.sendStatus(204);
});
Voir la réponseMasquer la réponse de l'exercice 3
La route vérifie que l'utilisateur est connecté, mais pas que le frais lui appartient : c'est un BOLA. N'importe quel utilisateur connecté supprime le frais de n'importe qui en changeant l'identifiant. Elle ne vérifie pas non plus que la fiche est en cours, ni que l'utilisateur est un visiteur.
Preuve : un test qui se connecte en Lucas et supprime le frais 59, qui appartient à Sarah. Avec ce code, il reçoit 204 ; il devrait recevoir 404. Ce test reste ensuite dans la suite de tests, pour que la faille ne revienne jamais (chapitre 12).
Correction : passer par un service qui charge le frais avec sa fiche, vérifie frais.visiteurId === req.utilisateur.id (sinon 404) et etatFiche === 'CR' (sinon 409), avant de supprimer ; et ajouter exigerRole('visiteur'). C'est le frais.service.js de l'API.
Tu avais trouvé ?
Exercice 4 sur 4 Choisir pour son projet Réussi À revoir
Pour chaque projet, choisis entre jeton JWT et session, et justifie en deux phrases.
- Un intranet de réservation de salles, en pages HTML générées par le serveur.
- Une API utilisée par une application mobile de terrain et par un front React hébergé sur un autre domaine.
Voir la réponseMasquer la réponse de l'exercice 4
- Session : les pages et le serveur sont sur le même domaine, le navigateur gère le cookie tout seul, et la déconnexion ou la désactivation d'un compte prennent effet immédiatement. Avec un cookie
HttpOnly,SecureetSameSite, le jeton n'est pas accessible au JavaScript de la page. - Jeton JWT (Bearer), avec un jeton de rafraîchissement : une application mobile ne gère pas les cookies comme un navigateur, et le front est sur un autre domaine. L'API vérifie le jeton sans stocker de session, ce qui fonctionne aussi avec plusieurs serveurs.
Tu avais trouvé ?