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.
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
:
const cookieParser = require('cookie-parser');
app.use(cookieParser());
É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 :
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;
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 :
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 :
......
sessionJWT.sendSessionCookie(req, res, { userId: userId});
return sendMessage(res, 'message');
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.
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
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.
Traduisez tous les fichiers de votre backend PHP en fichiers Javascript (en
n’oubliant pas de rajouter dans serveur.js
les routes correspondantes).
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