Chapitre 518 minIntermédiaire
Sessions, cookies et JWT : rester connecté
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- sécuriser une session côté serveur et son cookie ;
- lire un JWT et expliquer le rôle de sa signature ;
- vérifier un jeton correctement et éviter les erreurs classiques ;
- combiner un jeton d'accès court et un jeton de rafraîchissement ;
- choisir entre session côté serveur et JWT.
4 exercices corrigés1 schéma
Une session est le mécanisme qui permet à une application de se souvenir qu'un utilisateur s'est authentifié, pour ne pas lui redemander son mot de passe à chaque page. Elle repose soit sur un identifiant de session aléatoire stocké dans un cookie, l'état restant sur le serveur, soit sur un jeton signé comme le JWT (JSON Web Token), qui transporte lui-même l'identité de l'utilisateur. Ce chapitre explique les deux approches, leurs failles classiques et les réglages qui les protègent.
Pourquoi il faut une session
Le protocole HTTP est sans état : chaque requête arrive seule, le serveur ne sait pas qu'elle vient de la même personne que la précédente. Après l'authentification, l'application remet donc au navigateur une preuve qu'il renverra à chaque requête. Qui vole cette preuve devient l'utilisateur, sans connaître son mot de passe et sans passer la MFA : protéger la session est aussi important que protéger la connexion.
La session côté serveur et son cookie
C'est la méthode historique, celle de PHP par défaut :
- après une authentification réussie, le serveur crée un identifiant de session long et aléatoire, et range les données de la session (identifiant de l'utilisateur, rôle, heure de connexion) dans son stockage ;
- il envoie l'identifiant au navigateur dans un cookie (
PHPSESSIDen PHP) ; - le navigateur renvoie ce cookie avec chaque requête ; le serveur retrouve la session correspondante ;
- à la déconnexion, le serveur détruit la session : l'identifiant ne vaut plus rien.
L'état est sur le serveur : on peut fermer une session à distance (après un changement de mot de passe, un licenciement, un vol de portable), il suffit de la supprimer.
Les attributs du cookie sont des réglages de sécurité à part entière :
| Attribut | Effet | Ce qu'il bloque |
|---|---|---|
Secure | Le cookie n'est envoyé qu'en HTTPS | L'interception sur un réseau non chiffré |
HttpOnly | Le cookie est invisible pour le JavaScript de la page | Le vol du cookie par une faille XSS (document.cookie) |
SameSite=Lax ou Strict | Le cookie n'est pas envoyé (ou moins) quand la requête vient d'un autre site | Une bonne partie des attaques CSRF |
Pas de Expires ni Max-Age | Cookie de session, supprimé à la fermeture du navigateur | La session qui reste ouverte sur un poste partagé |
Path, Domain | Limitent les pages et sous-domaines qui reçoivent le cookie | La fuite vers un autre sous-domaine moins protégé |
En PHP :
<?php
// Avant session_start() : un cookie de session sûr
session_set_cookie_params([
'lifetime' => 0, // supprimé à la fermeture du navigateur
'path' => '/',
'secure' => true, // HTTPS seulement
'httponly' => true, // inaccessible au JavaScript
'samesite' => 'Lax',
]);
ini_set('session.use_strict_mode', '1'); // refuse un identifiant inventé par le client
session_start();
// Juste après avoir vérifié le mot de passe (et le second facteur)
session_regenerate_id(true); // nouvel identifiant, l'ancien est détruit
$_SESSION['id_utilisateur'] = $utilisateur['id'];
$_SESSION['role'] = $utilisateur['role'];
$_SESSION['derniere_activite'] = time();
// À chaque requête : expiration après 15 minutes d'inactivité
if (time() - $_SESSION['derniere_activite'] > 900) {
$_SESSION = [];
session_destroy();
header('Location: /connexion');
exit;
}
$_SESSION['derniere_activite'] = time();
Deux délais d'expiration
Une session doit expirer de deux façons :
- après une inactivité : 15 à 30 minutes pour une application qui manipule des données sensibles, moins pour une interface d'administration ;
- après une durée absolue : même active, une session est fermée au bout de quelques heures (une journée de travail, par exemple), pour qu'un identifiant volé ne serve pas indéfiniment.
La déconnexion doit détruire la session côté serveur, pas seulement effacer le cookie dans le navigateur. Et un changement de mot de passe doit fermer toutes les autres sessions de l'utilisateur.
Le JWT : un jeton qui transporte l'identité
Un JWT (JSON Web Token, RFC 7519) est une chaîne en trois parties séparées par des points : en-tête.charge.signature. Les deux premières sont des objets JSON encodés en base64url ; la troisième est une signature calculée par le serveur sur les deux premières.
Voici un jeton réel, signé avec HS256 (secret de démonstration : secret-de-demonstration-a-ne-jamais-utiliser, à ne jamais réutiliser) :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMSIsInJvbGUiOiJ0ZWNobmljaWVuIiwiaWF0IjoxNzkzNjA2NDAwLCJleHAiOjE3OTM2MDczMDB9.MV2YtuXskv9vdJYZ9ur9BLwlPx7e_3NbloTlVhZPSZQ
Colle le jeton et ce secret dans un débogueur JWT, comme celui de jwt.io : la signature est valide. Remplace ensuite la deuxième partie par la même charge où "technicien" devient "admin", en gardant l'ancienne signature :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc5MzYwNjQwMCwiZXhwIjoxNzkzNjA3MzAwfQ.MV2YtuXskv9vdJYZ9ur9BLwlPx7e_3NbloTlVhZPSZQ
La signature ne correspond plus à la charge : le débogueur la déclare invalide, et un serveur qui vérifie les signatures refuse le jeton. Pour fabriquer une signature valide, il faudrait connaître le secret.
Les champs de la charge s'appellent des revendications (claims). Les plus courants sont normalisés :
| Revendication | Sens | Dans l'exemple |
|---|---|---|
sub (subject) | L'identifiant de l'utilisateur | "11" : Chloé Lambert, technicienne |
iat (issued at) | L'heure d'émission, en secondes depuis le 1er janvier 1970 (heure Unix) | 1793606400 : le 2 novembre 2026 à 8 h 00 UTC (9 h à Paris) |
exp (expiration) | L'heure après laquelle le jeton doit être refusé | 1793607300 : 900 secondes plus tard, soit 15 minutes |
iss (issuer) | Qui a émis le jeton | Le serveur d'authentification |
aud (audience) | À qui le jeton est destiné | L'API qui doit l'accepter |
role n'est pas normalisé : c'est une revendication propre à l'application.
Signer et vérifier un JWT
Deux familles d'algorithmes coexistent :
- HS256 (HMAC avec SHA-256) : un secret partagé sert à signer et à vérifier. Simple quand le même serveur émet et vérifie. Le secret doit être long et aléatoire (au moins 256 bits), stocké hors du code source ;
- RS256 ou ES256 (signature asymétrique) : le serveur d'authentification signe avec sa clé privée, et chaque API vérifie avec la clé publique. C'est le cas d'OpenID Connect (chapitre 6), où plusieurs applications vérifient les jetons d'un même fournisseur d'identité sans connaître de secret.
En Node.js avec Express et la bibliothèque jsonwebtoken, une connexion qui émet un jeton puis un middleware qui le vérifie :
import jwt from 'jsonwebtoken';
// Connexion réussie (mot de passe vérifié avec bcrypt) : on émet un jeton de 15 minutes
const jeton = jwt.sign(
{ sub: String(utilisateur.id), role: utilisateur.role },
process.env.JWT_SECRET,
{ algorithm: 'HS256', expiresIn: '15m' },
);
// Middleware : vérifie le jeton sur chaque route protégée
export function proteger(req, res, next) {
const [type, valeur] = (req.headers.authorization ?? '').split(' ');
if (type !== 'Bearer' || !valeur) {
return res.status(401).json({ erreur: 'Authentification requise' });
}
try {
// verify() contrôle la signature ET l'expiration ; on impose l'algorithme attendu
const charge = jwt.verify(valeur, process.env.JWT_SECRET, { algorithms: ['HS256'] });
req.utilisateur = { id: charge.sub, role: charge.role };
next();
} catch {
return res.status(401).json({ erreur: 'Jeton invalide ou expiré' });
}
}
Jeton d'accès court, jeton de rafraîchissement
Un JWT ne se révoque pas facilement : tant qu'il n'a pas expiré, sa signature reste valable, même si l'utilisateur s'est déconnecté ou a été licencié. La parade est de lui donner une durée de vie courte et d'utiliser deux jetons :
| Jeton | Durée de vie typique | Rôle | Stockage |
|---|---|---|---|
| Jeton d'accès (access token) | 5 à 15 minutes | Présenté à chaque appel d'API | En mémoire de l'application, ou cookie HttpOnly |
| Jeton de rafraîchissement (refresh token) | Quelques heures à quelques jours | Échangé contre un nouveau jeton d'accès quand l'ancien expire | Cookie HttpOnly, Secure, SameSite=Strict ; le serveur en garde l'empreinte pour pouvoir le révoquer |
À chaque rafraîchissement, le serveur émet un nouveau jeton de rafraîchissement et invalide l'ancien (rotation) : si un jeton volé est réutilisé, le serveur détecte le doublon et ferme la session.
C'est exactement la situation du sujet SLAM de la session 2024 (U6) : une développeuse obtient un JWT auprès du serveur d'authentification unique, fait une pause de 15 minutes, puis interroge l'API avec le même jeton et reçoit "error": "invalid_token", "error_description": "Token verification failed". L'hypothèse la plus probable : le jeton a expiré (sa revendication exp est dépassée). La question suivante demandait de compléter une méthode qui obtient un nouveau jeton à l'aide du jeton de rafraîchissement.
Où stocker le jeton dans le navigateur
| Emplacement | Avantage | Risque |
|---|---|---|
localStorage | Simple à utiliser en JavaScript | Lisible par tout script de la page : une faille XSS vole le jeton |
Cookie HttpOnly, Secure, SameSite | Invisible au JavaScript | Envoyé automatiquement : il faut se protéger du CSRF (SameSite, jeton anti-CSRF) |
| Mémoire de l'application | Disparaît à la fermeture de l'onglet | Il faut un jeton de rafraîchissement en cookie pour ne pas reconnecter à chaque rechargement |
Pour une application web classique, la recommandation la plus sûre est le cookie HttpOnly. Les failles XSS et CSRF sont détaillées dans le cours Sécurité des applications.
Session serveur ou JWT : comment choisir
| Critère | Session côté serveur | JWT |
|---|---|---|
| Où est l'état | Sur le serveur | Dans le jeton, chez le client |
| Révocation immédiate | Oui, on supprime la session | Non, il faut attendre l'expiration ou tenir une liste de révocation |
| Plusieurs serveurs ou API | Il faut partager le stockage des sessions | Chaque API vérifie la signature seule |
| Taille transmise | Un identifiant court | Un jeton de plusieurs centaines d'octets |
| Usage typique | Site web monolithique (PHP, Symfony) | API REST, applications mobiles, authentification unique |
Aucune n'est « plus sécurisée » par nature : chacune est sûre si elle est bien réglée. Les deux se combinent souvent : une session web pour l'interface, des jetons d'accès courts pour les API.
Côté SISR : les sessions des postes et des serveurs
La notion de session existe aussi sur l'infrastructure. Dans le sujet de 2023 (cas Lama Zoo), un poste était resté allumé avec une session ouverte sans personne devant : quiconque passait pouvait agir sous l'identité du salarié. Les réglages correspondants :
- verrouillage automatique des postes après quelques minutes d'inactivité, imposé par une stratégie de groupe (paramètre Ouverture de session interactive : limite d'inactivité de l'ordinateur) ;
- délais des sessions Bureau à distance : déconnexion des sessions inactives et fermeture des sessions déconnectées après un délai, réglés par GPO sur les serveurs RDS ;
- SSH :
ClientAliveIntervaletClientAliveCountMaxdans/etc/ssh/sshd_configferment les connexions mortes ; - VPN : durée maximale de session et réauthentification périodique sur le pare-feu.
Les tickets Kerberos d'un domaine Active Directory jouent eux aussi le rôle d'une session, avec une durée de vie limitée (chapitre 7).
Comment l'E7 l'interroge
- SLAM : émettre une hypothèse sur une erreur
invalid_token; compléter la méthode qui rafraîchit un jeton ; expliquer pourquoi un jeton est rejeté après modification de sa charge ; corriger un code qui utilisedecodeau lieu deverify; justifier les attributs d'un cookie ; régler l'expiration d'une session. - SISR : proposer une mesure contre les sessions laissées ouvertes ; configurer le verrouillage par GPO ; régler la durée des sessions VPN ou RDS.
- Tronc commun : expliquer qu'un jeton volé permet d'usurper une identité sans connaître le mot de passe.
Pour aller plus loin. La signature d'un JWT est un HMAC ou une signature asymétrique (HMAC et signature numérique), et les failles classiques des cookies de session (vol par XSS, CSRF) sont démontées dans les chapitres Faille XSS et Faille CSRF du cours Sécurité applicative.
Exercices
Exercice 1 sur 4 Lire un JWT Réussi À revoir
La charge d'un jeton se décode ainsi :
{"sub":"14","nom":"Paul Martin","role":"comptable","iat":1793712600,"exp":1793716200}
- Quelle est la durée de validité du jeton ?
- L'heure Unix 1 793 712 600 correspond au 3 novembre 2026 à 13 h 30 UTC. Jusqu'à quelle heure UTC le jeton est-il valable ?
- Paul, qui a reçu ce jeton, peut-il le modifier pour devenir
admin? Explique. - Cette charge peut-elle contenir le numéro de sécurité sociale de Paul ?
Voir la réponseMasquer la réponse de l'exercice 1
exp − iat= 1 793 716 200 − 1 793 712 600 = 3 600 secondes, soit 1 heure.- 13 h 30 + 1 h = 14 h 30 UTC (15 h 30 à Paris en novembre).
- Il peut réécrire la charge et la réencoder, mais il ne peut pas recalculer la signature sans le secret du serveur. Si le serveur vérifie la signature avec
verifyet l'algorithme imposé, le jeton modifié est rejeté (401). - Non. La charge est seulement encodée en base64url : toute personne qui intercepte le jeton (ou qui lit le stockage du navigateur) la lit en clair. Un JWT ne transporte que l'identifiant et les informations nécessaires à l'autorisation.
Tu avais trouvé ?
Exercice 2 sur 4 Repérer les failles Réussi À revoir
Voici l'extrait d'une API Express :
app.delete('/factures/:id', (req, res) => {
const charge = jwt.decode(req.headers.authorization.split(' ')[1]);
if (req.body.role === 'comptable') {
supprimerFacture(req.params.id);
return res.status(200).end();
}
res.status(403).end();
});
Relève deux failles et corrige le code.
Voir la réponseMasquer la réponse de l'exercice 2
jwt.decode()ne vérifie ni la signature ni l'expiration : un jeton fabriqué à la main est accepté.- Le rôle est lu dans
req.body.role, que le client choisit librement : il suffit d'envoyer{"role":"comptable"}. La variablechargen'est même pas utilisée.
Correction, avec le middleware proteger du chapitre (qui appelle jwt.verify et pose req.utilisateur) :
app.delete('/factures/:id', proteger, (req, res) => {
if (req.utilisateur.role !== 'comptable') {
return res.status(403).json({ erreur: 'Accès interdit' });
}
supprimerFacture(req.params.id);
res.status(204).end();
});
Sans jeton valide, proteger répond 401 ; authentifié mais pas comptable, la route répond 403 ; sinon la suppression a lieu et la réponse 204 indique un succès sans contenu.
Tu avais trouvé ?
Exercice 3 sur 4 Les attributs du cookie Réussi À revoir
Un site envoie l'en-tête Set-Cookie: session=8f2c1a…; Path=/. Propose les attributs à ajouter et l'attaque que chacun bloque.
Voir la réponseMasquer la réponse de l'exercice 3
Set-Cookie: session=8f2c1a…; Path=/; Secure; HttpOnly; SameSite=Lax
Secure: le cookie n'est jamais envoyé en HTTP clair, il ne peut pas être intercepté sur un Wi-Fi public.HttpOnly: un script injecté par une faille XSS ne peut pas lire le cookie.SameSite=Lax: le cookie n'accompagne pas les requêtes de modification (POST) venues d'un autre site, ce qui bloque une grande partie des attaques CSRF.
L'absence d'Expires et de Max-Age en fait un cookie de session, supprimé à la fermeture du navigateur : c'est le bon choix pour un identifiant de session.
Tu avais trouvé ?
Exercice 4 sur 4 Déconnexion et licenciement Réussi À revoir
Une application utilise des JWT d'accès valables 24 heures, sans jeton de rafraîchissement. Un salarié est licencié à 10 h ; son compte est supprimé à 10 h 05.
- Pourquoi peut-il encore utiliser l'API ?
- Propose une architecture qui corrige le problème.
Voir la réponseMasquer la réponse de l'exercice 4
- Le serveur ne vérifie que la signature et la date d'expiration du jeton, sans consulter la base des comptes. Le jeton émis le matin reste valable jusqu'à son expiration, au plus tard le lendemain matin, même si le compte n'existe plus.
- Des jetons d'accès courts (5 à 15 minutes) et un jeton de rafraîchissement stocké en cookie
HttpOnly, dont le serveur garde l'empreinte. Au licenciement, on révoque les jetons de rafraîchissement du compte : au plus tard 15 minutes après, le salarié ne peut plus obtenir de jeton d'accès. Pour une coupure immédiate sur les actions sensibles, le serveur peut aussi vérifier à chaque appel que le compte est toujours actif, ou tenir une liste de révocation.
Tu avais trouvé ?