cookie-parser
Les cookies sont des informations qui sont enregistrées dans le client web, et qui sont
automatiquement renvoyées par le client au serveur à chaque requête.
De fait, même si le mécanisme de cookie n'est pas directement lié au mécanisme d'authentification par token,
son utilisation est bien appropriée pour stocker les tokens sur les clients, et pour les renvoyer
automatiquement au serveur à chaque requête.
Pour le mettre en oeuvre, nous utiliserons le module express
cookie-parser.
Installation : npm install cookie-parser
.
L'activation de cookie-parser
dans l'application peut être faite dans app.js
:
const cookieParser = require('cookie-parser') ;
app.use(cookieParser()) ;
La création d'un cookie sur le client est réalisée grâce à l'objet réponse res
.
Exemple de création d'un cookie :
const cookieName = 'nom_du_cookie' ;
const data = 'les données du cookie' ; // peut aussi être un objet
const options = {
httpOnly: true, // rend le cookie inaccessible au Javascript client
// maxAge: 500000, // un nombre en ms à partir de Date.now() pour l'expiration
// signed: true, // pour la création de cookie signé
// ... il existe d'autres optionss
} ;
res.cookie(cookieName, data, options) ; // création du cookie sur le client
La lecture d'un cookie reçu du client est réalisée grâce à l'objet requête req
.
Exemple de lecture d'un cookie :
const cookieName = 'nom_du_cookie' ;
const data = req.cookies[cookieName];// récupération des données du cookie envoyé par le client
Vous trouverez de plus amples informations sur les autres méthodes de cookie-parser
ICI.
jsonwebtoken
Le module jsonwebtoken fournit une implémentation qui facilite la génération et la
vérification de tokens d'authentification.
Installation :
npm install jsonwebtoken
Pour l'utiliser, il suffit de l'importer :
const jwt = require('jsonwebtoken') ;
Un token JWT contient 3 parties (que vous pouvez tester sur
https://jwt.io/) :
-
header
: une méta-information qui indique le type de token et l'algorithme de chiffrement
utilisé.
-
payload
: toute information que vous souhaitez inscrire dans le token pour la récupérer
lorsqu'il est transmis avec une requête
(ex. l'_id
de l'utilisateur à l'origine de la requête).
-
signature
: une chaîne générée cryptée contenant la concaténation du header
,
du payload
et d'un secret
en utilisant l'algorithme de chiffrement indiqué
dans le header
.
Le secret
est une chaîne aléatoire complexe que seul le serveur doit posséder !
Génération d'un token
La génération d'un token est réalisée avec la méthode jwt.sign(payload, secret, options)
:
const payload = {
userId: 'id_de_l_utilisateur'
// on pourrait ajouter d'autres informations non 'volatiles'
}
const SECRET_KEY = '!!! une chaine aleatoire complexe qui ne doit pas etre partagee !!!' ;
const options = {
expiresIn: '30s' // on peut utiliser 's', 'h', 'd', ou pas d'unité pour les ms
// algorithm : 'HS256', // HS256 est la valeur par défaut
// ... il existe d'autre options possibles (cf. lien ci-dessous)
}
const token = jwt.sign(payload, SECRET_KEY, options) ; // création du token
Les options JWT possibles sont listée ICI.
Vérification d'un token
La vérification d'un token est réalisée avec la méthode jwt.verify(token, secret)
:
try {
// Vérification du token et récupération du payload
// (si la vérification échoue, une exception est levée)
const payload = jwt.verify(token, SECRET_KEY);
} catch {
// code à exécuter en cas de token invalide
}
Exemple d'implémentation
Dans cet exemple très simplifié, nous allons créer 4 routes :
-
/
: une route de "home page" d'un site qui sera accessible
à tous.
-
/admin
: une route protégée qui ne sera accessible qu'aux
utilisateurs ayant un JWT valide.
-
/login
: une route qui renvoie un JWT.
-
/logout
: une route qui "deconnecte" le client.
Pour simplifier, nous n'allons ici créer qu'un seul et unique router mainRouter
et un seul et unique controller nommé mainController
.
Home Page : /
(accessible à tous)
La route /
est une route très classique avec les déclarations suivantes :
Dans mainRouter.js
router.get('/', mainController.home);
Dans mainController.js
module.exports.home = (req, res)=>{
res.render('pages/home') ;
}
Page Login : /login
Dans un exemple réel, la page de login enverrait un formulaire d'authentification avec une
méthode GET, puis soumettrait les informations remplies par l'utilisateur dans une méthode POST.
Le JWT ne serait alors créé et envoyé au client que si les informations d'authentification
ont été jugées correctes par le serveur
(par exemple en vérifiant la paire login/password stockée dans une DB).
Dans notre exemple simpliste, la route /login
va toujours automatiquement
créer et envoyer un JWT au client (donc sans aucune vérification de quoi que ce soit).
Bien entendu, on ne procéderait pas ainsi dans une application réelle !
Dans mainRouter.js
router.get('/login', mainController.login);
Pour regrouper toutes les méthodes dont nous aurons besoin autour de JWT, nous allons créer un
controller "utilitaire" nommé authorization
.
Sa première méthode nommée createToken(res, userId)
recevra un objet réponse
(le fameux res
) et un identifiant utilisateur qui sera mis dans le payload
du token créé.
autorization.js
const jwt = require('jsonwebtoken') ;
// Le nom du cookie dans lequel le JWT sera stocké
const COOKIE_NAME = 'token' ;
// Le secret est sensible et a donc été mis dans le fichier .env (utilisation du module 'dotenv')
// (NB : il aura pu être généré dans un script externe avec le module 'crypto')
const SECRET_KEY = process.env.SECRET_KEY ;
// Pour les tests, le JWT ne sera valide que 30s
const EXPIRATION = '30s'
module.exports.createToken = (res, userId)=>{
// Création du token
const token = jwt.sign(
{ userId },
SECRET_KEY,
{ expiresIn: EXPIRATION });
// Enregistrement du token dans un cookie
res.cookie(COOKIE_NAME, token, { httpOnly: true })
}
On peut maintenant écrire le handler login(req, res)
de notre mainController
:
Dans mainController.js
const authorization = require("./authorization");
module.exports.login = (req, res)=>{
let userId = 666 ; // cette information devrait provenir d'une DB
authorization.createToken(res, userId) ;
res.redirect('/admin') ;
}
Page Admin : /admin
(protégée)
L'affichage de la page Admin doit être conditionné à la présence d'un JWT valide
dans le cookie qui a été crée lors de l'accès à /login
.
Pour cette vérification, nous allons créer un handler autorize(req, res, next)
dans le controller authorization
:
Dans authorization.js
module.exports.authorize = (req, res, next)=>{
// Récupération du cookie
const token = req.cookies[COOKIE_NAME];
// On vérifie si le cookie existe
if (!token) {
return res.sendStatus(403); // Stoppe le traitement en renvoyant une erreur 'forbidden'
}
// -> puisque le cookie existe, on va vérifier le token qu'il contient
try {
// Vérification et récupération du payload :
const payload = jwt.verify(token, SECRET_KEY);
// Place le payload dans req pour l'envoyer au middleware suivant :
req.userId = payload.userId ;
// Tout est ok : on exécute le handler/middleware suivant :
next() ;
} catch {
// Le token n'était pas/plus valide :
res.clearCookie(COOKIE_NAME) ; // On efface le cookie
return res.sendStatus(403); // Stoppe le traitement en renvoyant une erreur 'forbidden'
}
}
Lorsque tout ira bien, le traitement de la page Admin sera réalisé par le handler
admin(req, res)
dans mainController
:
Dans mainController.js
module.exports.admin = (req, res)=>{
// Si le token a été jugé valide par authorize, req contiendra le champ userId
let userId = req.userId ;
res.render('pages/admin', { userId }) ;
}
Il ne reste plus qu'à enchainer ces handlers dans la route /admin
de
mainRouter
:
Dans mainRouter.js
const authorization = require('../controllers/authorization');
// Route protégée par le middleware authorization.authorize :
router.get('/admin', authorization.authorize, mainController.admin);
Du fait de l'insertion d'authorize
, un accès à /admin
sans être passé par /login
ou après l'expiration du JWT aura pour effet :
Par contre, si je JWT existe et est valide, on aura :
Déconnexion : /logout
Pour terminer, on peut ajouter la route /logout
qui a pour tâche de déconnecter l'utilisateur.
Dans notre exemple simple, nous allons simplement retirer le cookie contenant le JWT : de ce fait,
les prochaines requêtes du client ne contiendront plus de JWT et l'accès à la route protégée sera invalidé.
Dans authorization.js
module.exports.clearToken = (res)=>{
// Suppression du cookie contenant le token
res.clearCookie(COOKIE_NAME) ;
}
Dans mainRouter.js
router.get('/logout', mainController.logout);
Dans mainController.js
module.exports.logout = (req, res)=>{
authorization.clearToken(res) ;
res.redirect('/') ;
}
NB : dans une application réelle, on pourrait en plus conserver dans le serveur une liste de tokens "bannis"
pour éviter qu'un utilisateur ne trouve le moyen de renvoyer un token dont la date d'expiration n'est pas
dépassée, mais qu'on ne souhaite plus autoriser.