// Backend

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 comme exp (date d’expiration) ou iat (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 exp reste 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