L'authentification JWT expliquée (JSON Web Token)
Dès qu’une application possède des comptes utilisateurs, une question se pose : comment le serveur sait-il, à chaque requête, qui est connecté ? Le JWT — pour JSON Web Token — est aujourd’hui l’une des réponses les plus répandues, en particulier pour les API consommées par des applications web ou mobiles. Ce guide explique ce qu’est un JWT, comment se déroule le flux d’authentification, comment on signe et vérifie un jeton, et surtout quelles précautions de sécurité ne jamais négliger.
La structure d’un JWT
Un JSON Web Token est une chaîne de caractères composée de trois parties séparées par des points : header.payload.signature. Chaque partie est un objet JSON encodé en Base64URL. Un jeton ressemble à ceci :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5vbSI6IkNhbWlsbGUifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Décomposons les trois segments :
- Le header décrit le type de jeton et l’algorithme de signature utilisé, par exemple
{ "alg": "HS256", "typ": "JWT" }. - Le payload contient les claims, c’est-à-dire les informations sur l’utilisateur : son identifiant (
sub), éventuellement son rôle, et des champs standard commeexp(date d’expiration) ouiat(date d’émission). Par exemple{ "sub": "42", "nom": "Camille", "exp": 1739700000 }. - La signature est calculée à partir du header, du payload et d’une clé secrète connue du seul serveur. C’est elle qui garantit que le jeton n’a pas été altéré.
Point crucial pour un débutant : le payload est encodé, pas chiffré. N’importe qui peut le décoder et lire son contenu. On ne place donc jamais d’information sensible (mot de passe, numéro de carte) dans un JWT. Ce que la signature garantit, ce n’est pas la confidentialité, c’est l’intégrité : impossible de modifier le payload sans invalider la signature, faute de connaître le secret.
Le flux d’authentification
Le principe du JWT repose sur un modèle sans état (stateless) : le serveur ne conserve pas de session en mémoire. Le déroulé typique compte trois temps.
D’abord, l’utilisateur se connecte en envoyant ses identifiants (email et mot de passe) à une route de login. Le serveur les vérifie, et s’ils sont valides, il génère un JWT signé avec sa clé secrète, puis le renvoie au client.
Ensuite, à chaque requête vers une route protégée, le client renvoie ce jeton, généralement dans l’en-tête HTTP Authorization :
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5c...
Enfin, le serveur vérifie la signature du jeton reçu. Si elle est valide et le jeton non expiré, il en déduit l’identité de l’utilisateur — sans jamais consulter de base de données de sessions. C’est cette absence d’état qui fait la force du JWT pour les API distribuées : n’importe quel serveur connaissant le secret peut valider le jeton. Ce mécanisme s’intègre naturellement dans une API REST, où chaque requête est indépendante.
Signer un jeton à la connexion
En pratique, on n’implémente pas la cryptographie à la main : on utilise une bibliothèque éprouvée comme jsonwebtoken dans l’écosystème Node.js. Voici une route de login avec Express :
import express from "express";
import jwt from "jsonwebtoken";
const app = express();
app.use(express.json());
const SECRET = process.env.JWT_SECRET; // jamais en dur dans le code
app.post("/login", async (req, res) => {
const { email, motDePasse } = req.body;
const utilisateur = await trouverUtilisateur(email);
if (!utilisateur || !(await verifierMotDePasse(motDePasse, utilisateur.hash))) {
return res.status(401).json({ erreur: "Identifiants invalides" });
}
const token = jwt.sign(
{ sub: utilisateur.id, nom: utilisateur.nom },
SECRET,
{ expiresIn: "15m" }
);
res.json({ token });
});
Trois éléments méritent d’être soulignés. Le mot de passe est comparé à un hash stocké en base — jamais au mot de passe en clair, car on ne stocke jamais de mots de passe en clair. Le secret provient d’une variable d’environnement, pas du code source. Et l’option expiresIn: "15m" fixe une durée de vie courte au jeton, un choix de sécurité que nous justifierons plus bas.
Vérifier un jeton sur les routes protégées
Côté serveur, la vérification se factorise dans un middleware appliqué à toutes les routes qui exigent une authentification :
function authentifier(req, res, next) {
const entete = req.headers.authorization;
if (!entete || !entete.startsWith("Bearer ")) {
return res.status(401).json({ erreur: "Jeton manquant" });
}
const token = entete.slice(7); // retire "Bearer "
try {
const donnees = jwt.verify(token, SECRET);
req.utilisateur = donnees; // { sub, nom, iat, exp }
next();
} catch (e) {
return res.status(401).json({ erreur: "Jeton invalide ou expiré" });
}
}
// route protégée
app.get("/profil", authentifier, (req, res) => {
res.json({ id: req.utilisateur.sub, nom: req.utilisateur.nom });
});
La fonction jwt.verify fait tout le travail : elle recalcule la signature et la compare, vérifie que le jeton n’est pas expiré, et lève une exception dans le cas contraire. Si tout est valide, on attache les informations de l’utilisateur à req.utilisateur, accessibles ensuite dans la route.
Où stocker le jeton côté client ?
C’est l’une des questions les plus débattues, car elle engage la sécurité. Deux options principales s’offrent au client web.
Le localStorage est simple : le jeton s’y range et se lit en JavaScript pour l’ajouter à l’en-tête Authorization. Son défaut majeur est la vulnérabilité aux attaques XSS (injection de script) : un script malveillant injecté dans la page peut lire le localStorage et voler le jeton.
Le cookie — idéalement avec les attributs HttpOnly, Secure et SameSite — est plus sûr contre le XSS, car un cookie HttpOnly est inaccessible au JavaScript. En contrepartie, il expose potentiellement aux attaques CSRF, qu’on contre avec l’attribut SameSite et des jetons anti-CSRF.
res.cookie("token", token, {
httpOnly: true, // inaccessible au JavaScript du navigateur
secure: true, // transmis uniquement en HTTPS
sameSite: "strict",
maxAge: 15 * 60 * 1000 // 15 minutes
});
Il n’y a pas de réponse universelle, mais pour une application web classique, le cookie HttpOnly + Secure + SameSite offre le meilleur compromis. Le localStorage se justifie surtout quand le client n’est pas un navigateur (application mobile, service à service).
Le refresh token
Nous avons donné au jeton une durée de vie de 15 minutes. C’est volontairement court : si un jeton est volé, la fenêtre d’exploitation reste limitée. Mais reconnecter l’utilisateur toutes les 15 minutes serait pénible. La solution est le couple access token / refresh token.
L’access token est ce JWT à courte durée de vie, envoyé à chaque requête. Le refresh token, lui, a une durée de vie longue (plusieurs jours) et sert uniquement à obtenir un nouvel access token quand celui-ci expire, sans redemander le mot de passe. On le stocke de façon plus protégée (cookie HttpOnly) et, contrairement à l’access token, on le conserve côté serveur pour pouvoir le révoquer — par exemple lors d’une déconnexion ou d’un vol suspecté.
app.post("/refresh", async (req, res) => {
const { refreshToken } = req.cookies;
const valide = await refreshTokenExisteEnBase(refreshToken);
if (!valide) return res.status(401).json({ erreur: "Refresh invalide" });
const donnees = jwt.verify(refreshToken, SECRET_REFRESH);
const nouveauToken = jwt.sign({ sub: donnees.sub }, SECRET, { expiresIn: "15m" });
res.json({ token: nouveauToken });
});
Ce mécanisme concilie confort d’usage et sécurité : jetons de session courts et jetables, jeton de renouvellement long mais révocable.
Les règles de sécurité à ne jamais oublier
Le JWT est puissant mais impardonnable en cas de négligence. Quelques principes non négociables :
- Garder le secret secret. La clé de signature ne doit jamais apparaître dans le code source ni dans un dépôt Git. Elle vit dans une variable d’environnement et doit être longue et aléatoire.
- Toujours fixer une expiration. Un jeton sans
expreste valide éternellement. Une durée courte pour l’access token est la meilleure défense contre le vol. - Servir en HTTPS uniquement. Sans chiffrement du transport, un jeton peut être intercepté sur le réseau.
- Ne rien mettre de sensible dans le payload. Il est lisible par quiconque possède le jeton.
- Prévoir la révocation. Comme un JWT est sans état, on ne peut pas l’invalider unilatéralement avant son expiration ; d’où l’intérêt des refresh tokens stockés et révocables côté serveur.
En résumé
Le JWT est un jeton signé en trois parties — header, payload, signature — qui permet une authentification sans état : le serveur émet le jeton à la connexion, le client le renvoie à chaque requête, et le serveur en vérifie simplement la signature. Sa simplicité et son adéquation aux API en ont fait un standard, mais il exige de la rigueur : secret bien gardé, expiration courte, HTTPS, stockage réfléchi et refresh tokens pour la révocation. Bien maîtrisé, il sécurise élégamment n’importe quelle API moderne.
À lire ensuite
- Créer une API REST avec Express.js — le cadre dans lequel s’intègrent les middlewares d’authentification.
- API REST : principes et bonnes pratiques — pour situer l’authentification dans une architecture REST.
- Le CRUD expliqué : Create, Read, Update, Delete — les opérations que vos routes protégées vont exposer.