Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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.

C'est la méthode historique, celle de PHP par défaut :

  1. 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 ;
  2. il envoie l'identifiant au navigateur dans un cookie (PHPSESSID en PHP) ;
  3. le navigateur renvoie ce cookie avec chaque requête ; le serveur retrouve la session correspondante ;
  4. à 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 :

AttributEffetCe qu'il bloque
SecureLe cookie n'est envoyé qu'en HTTPSL'interception sur un réseau non chiffré
HttpOnlyLe cookie est invisible pour le JavaScript de la pageLe vol du cookie par une faille XSS (document.cookie)
SameSite=Lax ou StrictLe cookie n'est pas envoyé (ou moins) quand la requête vient d'un autre siteUne bonne partie des attaques CSRF
Pas de Expires ni Max-AgeCookie de session, supprimé à la fermeture du navigateurLa session qui reste ouverte sur un poste partagé
Path, DomainLimitent les pages et sous-domaines qui reçoivent le cookieLa fuite vers un autre sous-domaine moins protégé

En PHP :

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) :

text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMSIsInJvbGUiOiJ0ZWNobmljaWVuIiwiaWF0IjoxNzkzNjA2NDAwLCJleHAiOjE3OTM2MDczMDB9.MV2YtuXskv9vdJYZ9ur9BLwlPx7e_3NbloTlVhZPSZQ
eyJhbGciOiJIUzI1….eyJzdWIiOiIxMSIs….MV2YtuXskv9vdJYZ…En-tête{"alg": "HS256","typ": "JWT"}algorithme de signatureCharge utile{"sub": "11","role": "technicien","iat": 1793606400,"exp": 1793607300}lisible par tousSignatureHMAC-SHA256(en-tête.charge,secret)seul le serveur saitla calculerLe base64url n'est pas un chiffrement : la signature protège l'intégrité,pas la confidentialité.
Un JWT : deux parties lisibles par tous, une signature que seul le serveur sait calculer

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 :

text
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 :

RevendicationSensDans 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 jetonLe 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 :

js
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 :

JetonDurée de vie typiqueRôleStockage
Jeton d'accès (access token)5 à 15 minutesPrésenté à chaque appel d'APIEn 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 expireCookie 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

EmplacementAvantageRisque
localStorageSimple à utiliser en JavaScriptLisible par tout script de la page : une faille XSS vole le jeton
Cookie HttpOnly, Secure, SameSiteInvisible au JavaScriptEnvoyé automatiquement : il faut se protéger du CSRF (SameSite, jeton anti-CSRF)
Mémoire de l'applicationDisparaît à la fermeture de l'ongletIl 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èreSession côté serveurJWT
Où est l'étatSur le serveurDans le jeton, chez le client
Révocation immédiateOui, on supprime la sessionNon, il faut attendre l'expiration ou tenir une liste de révocation
Plusieurs serveurs ou APIIl faut partager le stockage des sessionsChaque API vérifie la signature seule
Taille transmiseUn identifiant courtUn jeton de plusieurs centaines d'octets
Usage typiqueSite 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 : ClientAliveInterval et ClientAliveCountMax dans /etc/ssh/sshd_config ferment 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 utilise decode au lieu de verify ; 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 :

json
{"sub":"14","nom":"Paul Martin","role":"comptable","iat":1793712600,"exp":1793716200}
  1. Quelle est la durée de validité du jeton ?
  2. 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 ?
  3. Paul, qui a reçu ce jeton, peut-il le modifier pour devenir admin ? Explique.
  4. 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
  1. exp − iat = 1 793 716 200 − 1 793 712 600 = 3 600 secondes, soit 1 heure.
  2. 13 h 30 + 1 h = 14 h 30 UTC (15 h 30 à Paris en novembre).
  3. 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 verify et l'algorithme imposé, le jeton modifié est rejeté (401).
  4. 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 :

js
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
  1. jwt.decode() ne vérifie ni la signature ni l'expiration : un jeton fabriqué à la main est accepté.
  2. Le rôle est lu dans req.body.role, que le client choisit librement : il suffit d'envoyer {"role":"comptable"}. La variable charge n'est même pas utilisée.

Correction, avec le middleware proteger du chapitre (qui appelle jwt.verify et pose req.utilisateur) :

js
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.

  1. Pourquoi peut-il encore utiliser l'API ?
  2. Propose une architecture qui corrige le problème.
Voir la réponseMasquer la réponse de l'exercice 4
  1. 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.
  2. 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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : OAuth 2.0, OpenID Connect, SAML : l'authentification unique (SSO)Revoir le sommaire du cours