Les JSON Web Tokens (JWT)

En PHP, l’authentification se faisait via des sessions PHP et leurs cookies. Ici, on va utiliser le même principe : des cookies de session. Cela nous amène à un mécanisme appelé JSON Web Token (JWT). Mais, en Node, ces cookies sont gérés un peu différemment. En effet, Apache/PHP conserve les informations de session sur le serveur, ce qui peut poser des problèmes de « passage à l’échelle » si on a trop de clients. En Node, les informations sont conservées par le client, ce qui permet de gérer beaucoup plus de clients. L’inconvénient, c’est que le client (Angular/votre navigateur) doit retransmettre ces informations à chaque requête.

Exercice 1 : Backend - Installations   

Afin d’exploiter les JWT, dans le répertoire backend_node, installez via npm les packages suivants jsonwebtoken et cookie-parser. Afin que l’instance app de votre serveur Express utilise cookie-parser, qui permet d’exploiter aisément les cookies dans les applications Node, il vous suffit de rajouter les deux lignes suivantes dans le fichier serveur.js :

serveur.js
const cookieParser = require('cookie-parser');
app.use(cookieParser());

Anatomie d'un JWT

Étant donné que le JWT est stocké chez le client (le navigateur), ce dernier ne doit pas pouvoir lire et modifier son contenu (sinon, on a une grosse faille de sécurité). L’idée des JWT consiste donc à ce que le backend chiffre les données contenues dans le JWT. Pour cela, on va utiliser ci-dessous un couple clef publique/clef privée. Le backend va chiffrer le JWT en utilisant la méthode sign de jsonwebtoken et la clef privée. Il décodera celui transmis par le client via la méthode verify de jsonwebtoken et la clef publique.

Le prototype de la fonction sign est sign(payload, secretOrPrivateKey, [options]). Le payload représente l’information stockée dans le JWT. En principe, c’est un objet Javascript. Le second argument est la clef privée de chiffrement. Enfin, dans les options, on peut passer l’algorithme de chiffrement et, surtout, le temps de validité du JWT. En effet, il n’est pas souhaitable que celui-ci soit valide indéfiniment car un hacker pourrait alors avoir le temps de le déchiffrer. De plus, si un utilisateur est supprimé/révoqué de votre application, on ne souhaite pas que son JWT reste encore valide très longtemps.

Le prototype de la fonction verify est verify(token, secretOrPublicKey, options). On lui passe donc en premier argument un JWT transmis par le navigateur. Le 2ème paramètre est la clef de déchiffrement. Enfin, dans les options, on peut passer l’algorithme de chiffrement/déchiffrement à utiliser.

Voici un code illustrant le principe :

sessionJWT.js
const JWT = require('jsonwebtoken');
const fs = require('fs');

// ici, on récupère nos clefs publique/privée, qui nous serviront
// pour la signature de nos JWT. On ne s'embête pas à faire de
// l'asynchrone car ces deux instructions seront, ici, uniquement
// exécutées au démarrage du serveur.
const RSA_PRIVATE_KEY = fs.readFileSync('./keys/jwtRS256.key');
const RSA_PUBLIC_KEY = fs.readFileSync('./keys/jwtRS256.key.pub');


// crée et renvoie un nouveau token JWT
function createJWT(userId) {
  // ci-dessous, on met en place le JWT : on signe un token JWT.
  // Ici, le payload est l'identifiant de l'utilisateur (mais on pourrait
  // envisager de stocker plus d'informations) plus un champ refreshTime dont
  // verra l'intérêt dans la fonction sendSessionCookie.
  // Quand on décodera le JWT, on aura accès à l'ID de l'utilisateur.
  // Dans les options du token, le champ expiresIn indique pendant combien
  // de temps le token sera valide (pas besoin de se relogguer tant que le
  // token n'a pas expiré).
  const jwtToken = JWT.sign(
    {
      userId: userId,
      refreshTime: Math.floor(Date.now() / 1000) + 2700 // validité: 45mn
    },
    RSA_PRIVATE_KEY,
    {
      algorithm: 'RS256',
      expiresIn: '1h' // champ exp: validité 1h
    });

  return jwtToken;
}
// ici, on n'exporte pas la fonction, elle reste interne


// Décode un cookie de session transmis dans une requête (on suppose qu'il
// contient le JWT) et renvoie les informations contenues dans ce cookie,
// ici le userId. Si le cookie n'existe pas ou bien si le token a expiré,
// la fonction renvoie juste un objet avec un userId égal à -1.
function decodeSessionCookie(req) {
  // on suppose que le cookie de session contenant le JWT s'appelle SESSIONID.
  // S'il n'existe pas, on renvoie une session vide avec juste un userId à -1
  if (typeof req.cookies.SESSIONID === 'undefined') {
    return {userId: -1};
  }

  // ici, le cookie existe. On récupère ses données
  const sessionid = req.cookies.SESSIONID;
  try {
    const token = JWT.verify(
      sessionid,        // le token
      RSA_PUBLIC_KEY,   // la clef de déchiffrement
      {algorithms: ['RS256']});

    return token;  // contient le payload du token
  } catch (err) {  // exception TokenExpiredError si JWT expiré
    return {userId: -1};
  }
}
module.exports.decodeSessionCookie = decodeSessionCookie;


// sendSessionCookie envoie le cookie de session JWT au navigateur.
// Le paramètre payload correspond à ce qui a été retourné par la fonction
// decodeSessionCookie. Autrement dit, si celui-ci contient une propriété
// idUser et une propriété refreshTime, cela correspond au contenu d'un
// JWT transmis par le navigateur. S'il n'est pas trop vieux (encore loin de
// sa date d'invalidité), on le renvoie tel quel. Dans les autres cas, on
// renvoie un cookie avec un nouveau JWT.
function sendSessionCookie(req, res, payload) {
  // On regarde si le payload provient bien d'un JWT transmis par le navigateur.
  // Si c'est le cas, il a une date de validité (cf. expiresIn). On pourrait
  // renvoyer le JWT du navigateur tant qu'on n'a pas atteint cette limite. Mais
  // si l'on est à 1s de cette date et qu'on renvoie le JWT du navigateur, la
  // prochaine fois que l'utilisateur tentera d'accéder au backend, le JWT aura
  // expiré et cela obligera l'utilisateur à se relogguer. Pour pallier cela,
  // refreshTime correspond à 15mn avant l'expiration du JWT. Si on a dépassé
  // refreshTime, on reconstruit un nouveau JWT. Notez que cela a un coût (de
  // chiffrement) et, donc, on veut éviter de créer un nouveau JWT à chaque
  // requête.
  let jwtToken = '';
  if ((typeof payload.userId !== 'undefined') &&
    (typeof payload.refreshTime !== 'undefined') &&
    (Math.floor(Date.now() / 1000) <= payload.refreshTime)) {
    // ici, on peut renvoyer le JWT que le navigateur avait transmis : il reste
    // à l'utilisateur au moins 15 minutes pour effectuer sa prochaine requête
    jwtToken = req.cookies.SESSIONID;
  } else {
    // on crée un nouveau JWT
    jwtToken = createJWT(payload.userId);
  }

  // on renvoie le cookie au client
  // on met le secure à false afin de pouvoir utiliser http plutôt que https
  // pour nos tests
  res.cookie('SESSIONID', jwtToken, {
    httpOnly: true,
    secure: false,
    sameSite: 'Lax'
  });
}
module.exports.sendSessionCookie = sendSessionCookie;

Utilisation des JWT :

L’utilisation du code ci-dessus dans les scripts tels que getCourses.js est relativement simple. Il suffit de décoder le cookie de session et de vérifier que l’id de l’utilisateur est différent de -1 pour déterminer qu’on est bien authentifié. Le cas échéant, on renvoie un cookie de session et on enchaîne avec le code qui doit retourner la liste des cours :

getCourses.js
const {sendError, sendMessage} = require ("./message");
const sessionJWT = require('./sessionJWT')
const Courses = require("./database/sql_courses");

async function getCourses(req, res) {
   // On récupère la variable de session et, dans celle-ci, on va récupérer
   // l'ID du user. C'est équivalent en PHP à :
   // session_start();
   // $userId = $_SESSION['userId'];
   const session = sessionJWT.decodeSessionCookie(req);
   const userId = session.userId;
   if (userId === -1)
     return sendError (res, 'not authenticated');
   sessionJWT.sendSessionCookie (req, res, session);

   // ici, on récupère la liste des cours et on la renvoie via un
   // return sendMessage(res, ....)
   ..............
}
module.exports = getCourses;

Évidemment, il faut initier le processus d’envoi de cookies avec le script checkLogin.js. Celui-ci n’a pas besoin d’exécuter toutes les lignes ci-dessus. Il lui suffit simplement, lorsque l’utilisateur est bien authentifié, d’appeler la méthode sendSessionCookie en lui passant l’id de l’utilisateur :

checkLogin.js
......
sessionJWT.sendSessionCookie(req, res, { userId: userId});
return sendMessage(res, 'message');

Exercice 2 : Backend - La clef des champs   

Téléchargez l’archive ci-dessous (au choix au format zip ou tgz), qui contient deux clefs (publique/privée) et désarchivez la dans votre répertoire backend_node. Cela créera un répertoire keys incluant deux fichiers contenant respectivement une clef publique et une clef privée.

keys.zip

keys.tgz

Note

Les JWT exploitent un mécanisme de cryptographie par clef publique/privée. Vous pouvez générer ces clefs via les commandes suivantes(cf. https://gist.github.com/ygotthilf/baa58da5c3dd1f69fae9).

ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
# ne pas ajouter de passphrase
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
cat jwtRS256.key
cat jwtRS256.key.pub

Exercice 3 : Backend - Finalisation du login et des cours   

Mettez à jour votre fichier checkLogin.js afin qu’il crée et envoie le cookie de session en cas d’authentification réussie.

Mettez à jour votre fichier getCourses.js afin qu’il récupère le cookie de session transmis par l’utilisateur et qu’il vérifie que ce dernier est bien authentifié. Le cas échéant, il renvoie un cookie de session ainsi que la liste des cours de l’utilisateur.

Note

Dans cet exercice, on doit explicitement rajouter des instructions dans getCourses.js afin de vérifier que l’utilisateur est bien authentifié, ce que l’on n’avait pas à faire en PHP grâce à helper.php. En Express, on peut parvenir au même résultat en utilisant un middleware (cf. https://expressjs.com/en/guide/using-middleware.html) et les propriétés de l’objet request (https://expressjs.com/en/4x/api.html#req) mais ceci dépasse le cadre de ce module.

Exercice 4 : Backend - Finalisation du backend   

Traduisez tous les fichiers de votre backend PHP en fichiers Javascript (en n’oubliant pas de rajouter dans serveur.js les routes correspondantes).

Exercice 5 : Optionnel : pour ceux/celles qui souhaitent comprendre les JWT   

L’url https://hackernoon.com/why-do-we-need-the-json-web-token-jwt-in-the-modern-web-k29l3sfd décrit très clairement comment fonctionnent les JWT et les raisons pour lesquelles les informations de session ne doivent pas être stockées sur le serveur.

La ressource suivante peut également vous être utile pour comprendre le fonctionnement d’une authentification par JWT :

https://blog.angular-university.io/angular-jwt-authentication

 
© C.G. 2007 - 2025