b) Sécurisation d'une RESTful API

YoutubeImage

C'est quoi l'authentification et l'autorisation ?

Authentification, c'est quoi ?

L'authentification, c'est le processus de vérifier qui est l'utilisateur d'une application.

Pour authentifier un utilisateur, on va généralement passer via un formulaire de connexion, lui demandant un username et un password.

Autorisation, c'est quoi ?

L'autorisation, c'est le processus de vérifier ce à quoi un utilisateur à accès.

Une API va autoriser les accès à certaines opérations en fonction des privilèges associés aux utilisateurs. Il y aura des opérations qui seront autorisées :

  • pour tous les utilisateurs, tant anonymes qu'authentifiés ;
    imaginez par exemple les opérations de lecture de produits sur un site "vitrine". Il ne faut pas devoir créer de compte utilisateur pour pouvoir afficher les produits du site.
  • pour tous les utilisateurs authentifiés, peu importe leurs privilèges ;
    imaginez le fait de pouvoir poster des messages dans un forum ; il faut avoir un compte pour pouvoir le faire, mais il ne faut pas de privilèges spécifiques (pas besoin d'être admin).
  • pour seulement un ou plusieurs utilisateur(s) authentifié(s) ayant les privilèges requis ;
    imaginez une opération de lecture de tous les profils des utilisateurs d'une banque ; il faut avoir un compte admin de la banque pour pouvoir le faire. Ca serait catastrophique si n'importe quel utilisateur authentifié pourrait accéder aux profils de tous les utilisateurs !

Différents moyens d'authentification

Authentification stateful

Traditionnellement, ou anciennement, nous avons l'authentification qui est supportée à l'aide de cookies. L'utilisateur envoie via un formulaire son username et password, le serveur vérifie ceux-ci et crée un id de session et le renvoie à l'utilisateur via un cookie.
Après l'authentification, à chaque requête du client sur ce serveur, le cookie est envoyé, et le serveur, qui a sauvegardé la session, va la retrouver sur base de l'id de session présent dans le cookie et va autoriser ou non l'accès à l'opération demandée par le client.
C'est ce qu'on appelle une authentification stateful.

Un mécanisme d'authentification stateful indique que le serveur est responsable de sauvegarder les données de session des utilisateurs.

On parle de session d'un utilisateur comme étant toute la durée où le client s'authentifie à l'application web jusqu'à ce qu'il quitte cette application, lorsqu'il ferme son browser.

Authentification stateless

De manière plus moderne, nous avons des mécanismes d'authentification qui sont dits stateless. Dans ce genre de mécanisme, c'est le client qui doit sauvegarder les données de session, et donc le browser. L'authentification à l'aide de tokens devient très populaire.
Il existe différents moyen de l'implémenter. Dans le cadre de ce cours, nous focaliserons sur la façon la plus habituelle, les JSON Web Tokens, ou JWT.

Il existe d'autres mécanismes très modernes qui utilisent des tokens, et qui sont mis en place par des tiers : OAuth, OpenId... Ces mécanismes sont offerts par Microsoft, Google, Facebook... Nous ne les verrons pas dans le cadre de cours. Néanmoins, à la fin de ce cours, vous devriez être apte à pouvoir les utiliser, sous réserve de bien lire la documentation 😉.

Stateful VS stateless authentication

Quels sont les avantages & inconvénients d'une authentification stateful ? et d'une authentification stateless ?

Authentification stateful

Inconvénients :

  • La session utilise de la mémoire pour chaque utilisateur.
  • Le backend n'a aucune manière de déterminer si le frontend s'est déconnecté du site ou non : gestion de l'expiration d'une session plus compliquée, notamment si le frontend revient après une longue durée.
  • Toutes les sessions sont perdues en cas de redémarrage du serveur (ré-authentification).
  • Load balancing compliqué ; en effet, si un client fait une requête, pendant toute la durée de la session, s'il y a plusieurs serveurs qui peuvent y répondre, comment est gérée la session de ce client ? Par quel(s) serveur(s) ? Comment se partagent-ils les données de session ? Est-ce que ça doit toujours être le même serveur qui réponde au même client ?

Avantages :

  • Peu gourmand en ressource point de vue processing et très rapide ; en effet, une fois qu'une session est en place, la vérification que le cookie contient le bon id de session se fait vite.

Authentification stateless

Inconvénients : Comme le serveur ne retient plus l'utilisateur en mémoire, il doit utiliser de la cryptographie pour créer et valider les token ; c'est donc consommateur en ressource point de vue processing.

Avantages :

  • Pas de session à gérer, même après redémarrage du serveur, il n'y a pas de ré-authentification nécessaire.
  • Évolutif, utilisation facile de plusieurs serveurs si du load balancing est nécessaire ; en effet, comme chaque requête du client contient toutes les infos pour se faire autoriser, il n'est pas nécessaire de savoir quel serveur va prendre en charge la requête.

Notons que la "scalability" horizontale, la possibilité de permettre à une application d'augmenter sa capacité de répondre à une charge grandissante simplement en ajoutant des machines, est quelque chose de très important à notre époque.
Certaines applications web ont des centaines de millions d'utilisateurs ; pour celles-ci, on ne peut pas compter sur la "scalability" verticale, c'est-à-dire le fait d'augmenter les ressources d'une machine, en augmentant sa RAM, son processeur, son espace de stockage...

Dès lors, dans le cadre de ce cours, nous allons préférer les applications web qui peuvent tourner sur des serveurs stateless.

C'est quoi les tokens JWT ?

Les JSON Web token, ou JWT, appartiennent à un standard internet permettant l'échange sécurisé de tokens entre plusieurs parties.

Un JWT contient trois parties encodées en base64 et ressemble à qqch du style :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Voici les 3 parties d'un JWT et leurs caractéristiques :

  • l'entête (ou header) : un objet JSON identifiant le type de token (JWT) et l'algorithme utilisé pour générer la signature du token (HS256), un algorithme de hachage.
  • le payload : un objet JSON permettant de spécifier le claim ; ce sont des paramètres optionnels précisant les affirmations associées au token, comme la date d'expiration du token, son créateur, le fait que l'utilisateur est admin... généralement, on y met pas trop d'info, principalement l'identifiant de l'utilisateur ; si l'API a besoin de plus d'info concernant l'utilisateur, elle ira généralement les chercher dans ses données.
  • la signature : c'est une signature numérique construite à partir d'un secret privé ou d'une clé publique/privée, via l'algorithme précisé dans la signature.

💭 C'est bien joli tout ça, mais comment ça permet de sécuriser la session d'un utilisateur ?
Généralement, l'API, et elle uniquement, connaît le secret privé. Elle va donc créer le token d'un utilisateur authentifié, en y ajoutant le claim (ou les affirmations) qu'elle juge utile.
Lorsque le client reçoit son token, il va le sauvegarder dans son browser.
Pour chaque requête nécessitant une autorisation de l'API, le client enverra son token au sein du header de la requête.
L'API utilisera le secret privé, connu d'elle-même uniquement, pour vérifier la signature du token. Si tout est OK, c'est que l'utilisateur est bien authentifié et que son claim est OK.

Imaginez maintenant qu'un hacker tente de modifier le token, pour modifier le claim, notamment en changeant l'id de l'utilisateur présent dans le payload du token. Et bien la signature ne correspondra plus à ce token là ! L'API le détectera.
Pour que le hacker réussisse son acte malicieux, il est obligé de connaître le secret privé qui ne doit être connu que de l'API. Ca ne serait que sur base du secret privé que les hackers pourraient générer un nouveau token qui serait validé par l'API. Pas évident à faire...

Ainsi, on peut imaginer un token comme une enveloppe sécurisé par un cadenas très spécial : ce cadenas, qui est mis sur l'enveloppe contenant, par exemple, l'identifiant d'un utilisateur, est fermé par la clé 🔑 que seulement connaît l'API. Si quelqu'un touche à l'enveloppe, le cadenas ne s'ouvrira plus avec la 🔑 de l'API : la tentative de hack sera détectée ! Si quelqu'un tente de créer une enveloppe sécurisé, il le fera avec une autre clé, 🗝 par exemple, car il ne connaît pas 🔑. L'API, tentant d'ouvrir l'enveloppe sécurisée à l'aide de 🔑, elle ne s'ouvrira pas : la tentative de hacking sera détectée !

💭 Est-ce que cette métaphore d'enveloppe sécurisée vous parle ? Attention que dans la réalité, le payload classique d'un token sera decodable, que l'on connaisse ou pas le secret privé. Il est encodé en base64 ! Ne mettez donc jamais de secrets dans le payload d'un token !
Si vraiment un jour vous deviez mettre un secret dans un payload, bien que déconseillé, vous pourriez toujours le faire en cryptant le payload.

👍 Le mécanisme de token assure que l'on aie une très haute probabilité de détecter les altérations faites sur les tokens, les tentatives de forgeage, grâce à la signature de ceux-ci.

⚡ Par contre, si nous laissions traîner des informations dans le payload d'un token, comme un password d'un utilisateur et son username, alors là, c'est juste un beau cadeau que l'on offre aux hackers ; ils n'auraient plus qu'à trouver un moyen de voler à distance nos données de session ; ils pourraient ensuite utiliser le formulaire de connexion et prendre possession de notre identité 😨.

Il existe un site fort intéressant qui montre la structure d'un token et qui permet de les décoder : jwt.io [R.63]. Nous l'utiliserons plus tard pour décoder les tokens générés par nos RESTful API.

Authentification et création de token

Introduction

Via un exemple concret associé à notre RESTful API qui gère les ressources de type "pizzas" et de type "boisson", nous allons mettre en place un processus d'authentification et d'autorisation JWT.

La librairie que nous allons utiliser dans le cadre de ce cours pour gérer des tokens JWT est jsonwebtoken.

Dans ce nouveau tutoriel, nous allons continuer le développement de l'API services pour ajouter des opérations permettant l'authentification et l'autorisation d'utilisateurs.

Veuillez créer un nouveau projet nommé auths (dans votre repo git /web2/tutorials/pizzeria/api/auths) sur base d'un copié/collé du tutoriel services.

Veuillez installer la librairie jsonwebtoken au sein de votre nouveau projet auths :

bash
npm i jsonwebtoken

Comme nous utilisons TS, nous devons aussi installer les types de cette librairie :

bash
npm i -D @types/jsonwebtoken

Mécanisme d'authentification et création du token

Toujours à l'aide d'Express, nous allons créer un router auths qui mettra à disposition les opérations de login et de register. Voici le contrat associé à ces nouvelles opérations :

Opérations sur les ressources de type "auths"
URIMéthode HTTPOpération
auths/loginPOSTVérifier les credentials d'une ressource de type "users" et renvoyer le username et un token JWT si les credentials sont OK
auths/registerPOSTCréer une ressource de type "users" et renvoyer le username et un token JWT

Le service users s'occupera de créer les utilisateur, de vérifier leurs credentials ainsi que de créer des tokens.

Voici le workflow attendu pour une opération de login ou de register :

GatsbyImage
Appel à une RESTful API pour s'authentifier et obtenir un JWT

Ce que l'on voit dans l'image ci-dessus :

  • si le service considère que l'utilisateur est authentifiable, que ses credentials sont OK, alors il va utiliser la méthode sign de l'objet jwt pour créer un token.
  • le token est envoyé dans le body de la réponse à l'utilisateur.

Ce que l'on ne voit pas dans l'image ci-dessus :

  • c'est le job du client de sauvegarder le token. Si l'application cliente est un browser, alors celui-ci pourra être sauvegardé dans le web storage du browser.
  • l'application cliente peut être faite avec n'importe quelle technologie. Par exemple, nous pourrons utiliser REST Client pour faire une requête à l'API...

Dans notre RESTful API, nous avons décidé que lorsqu'un client s'enregistre, quand il fait appel à l'opération register pour créer un compte, l'API considère automatiquement que cet utilisateur est authentifié. Tout comme la méthode login, la méthode register créera et renverra un token JWT à l'utilisateur.

NB : Il est possible d'envisager un workflow différent : après le register, l'application demanderait à l'utilisateur un login, register ne renvoyant jamais de token à l'utilisateur.

Implémentation de login & register

Nous allons à présent mettre en place le code permettant d'implémenter le workflow que l'on vient de découvrir.

Veuillez créer le service users en créant le fichier /services/users.ts et y inclure ce code :

ts
1
import jwt from "jsonwebtoken";
2
import path from "node:path";
3
import { parse, serialize } from "../utils/json";
4
import { AuthenticatedUser, User } from "../types";
5
6
const jwtSecret = "ilovemypizza!";
7
const lifetimeJwt = 24 * 60 * 60 * 1000; // in ms : 24 * 60 * 60 * 1000 = 24h
8
9
const jsonDbPath = path.join(__dirname, "/../data/users.json");
10
11
const defaultUsers: User[] = [
12
{
13
id: 1,
14
username: "admin",
15
password: "admin",
16
},
17
];
18
19
function login(
20
username: string,
21
password: string
22
): AuthenticatedUser | undefined {
23
const userFound = readOneUserFromUsername(username);
24
if (!userFound) return undefined;
25
if (userFound.password !== password) return undefined;
26
27
const token = jwt.sign(
28
{ username }, // session data added to the payload (payload : part 2 of a JWT)
29
jwtSecret, // secret used for the signature (signature part 3 of a JWT)
30
{ expiresIn: lifetimeJwt } // lifetime of the JWT (added to the JWT payload)
31
);
32
33
const authenticatedUser: AuthenticatedUser = {
34
username,
35
token,
36
};
37
38
return authenticatedUser;
39
}
40
41
function register(
42
username: string,
43
password: string
44
): AuthenticatedUser | undefined {
45
const userFound = readOneUserFromUsername(username);
46
if (userFound) return undefined;
47
48
createOneUser(username, password);
49
50
const token = jwt.sign(
51
{ username }, // session data added to the payload (payload : part 2 of a JWT)
52
jwtSecret, // secret used for the signature (signature part 3 of a JWT)
53
{ expiresIn: lifetimeJwt } // lifetime of the JWT (added to the JWT payload)
54
);
55
56
const authenticatedUser: AuthenticatedUser = {
57
username,
58
token,
59
};
60
61
return authenticatedUser;
62
}
63
64
function readOneUserFromUsername(username: string) {
65
const users = parse(jsonDbPath, defaultUsers);
66
const userFound = users.find((user) => user.username === username);
67
if (!userFound) return undefined;
68
69
return userFound;
70
}
71
72
function createOneUser(username: string, password: string) {
73
const users = parse(jsonDbPath, defaultUsers);
74
75
const nextId =
76
users.reduce((acc, user) => (user.id > acc ? user.id : acc), 0) + 1;
77
78
const createdUser: User = {
79
id: nextId,
80
username,
81
password,
82
};
83
84
users.push(createdUser);
85
86
serialize(jsonDbPath, users);
87
88
return createdUser;
89
}
90
91
export { login, register, readOneUserFromUsername };

Dans le code ci-dessus, jwtSecret est le secret privé connu uniquement du serveur. C'est la même secret qu'il faudra utiliser pour vérifier un token.

jwt.sign() permet de créer le token et ses 3 parties :

  • le payload du token, la 2ème partie du token, est complété principalement via le permier argument de sign() ; ici, le token affirme que l'utilisateur possédant le username donné est authentifié.
  • le payload du token sera aussi modifié sur base de la durée d'expiration du token, selon l'argument expiresIn.
  • jwtSecret est utilisé pour créer la 3ème partie du token, sa signature.

Pour le service users, nous avons du créer deux nouveaux types : AuthenticatedUser et User. Veuillez mettre à jour le fichier /types.ts et y inclure ce code :

ts
1
interface AuthenticatedUser {
2
username: string;
3
token: string;
4
}
5
6
interface User {
7
id: number;
8
username: string;
9
password: string;
10
}
11
12
export type {
13
Pizza,
14
NewPizza,
15
PizzaToUpdate,
16
Drink,
17
NewDrink,
18
AuthenticatedUser,
19
User,
20
};

Il faut maintenant que nous créions le router auths offrant les opérations de login et de register, en faisant appel au service users.
Veuillez créer le fichier /routes/auths.ts et y inclure le code suivant :

ts
import { Router } from "express";
import { PotentialUser } from "../types";
import { login, register } from "../services/users";
const router = Router();
/* Register a user */
router.post("/register", (req, res) => {
const body: unknown = req.body;
if (
!body ||
typeof body !== "object" ||
!("username" in body) ||
!("password" in body) ||
typeof body.username !== "string" ||
typeof body.password !== "string" ||
!body.username.trim() ||
!body.password.trim()
) {
return res.sendStatus(400);
}
const { username, password } = body as PotentialUser;
const authenticatedUser = register(username, password);
if (!authenticatedUser) {
return res.sendStatus(409);
}
return res.json(authenticatedUser);
});
/* Login a user */
router.post("/login", (req, res) => {
const body: unknown = req.body;
if (
!body ||
typeof body !== "object" ||
!("username" in body) ||
!("password" in body) ||
typeof body.username !== "string" ||
typeof body.password !== "string" ||
!body.username.trim() ||
!body.password.trim()
) {
return res.sendStatus(400);
}
const { username, password } = body as PotentialUser;
const authenticatedUser = login(username, password);
if (!authenticatedUser) {
return res.sendStatus(401);
}
return res.json(authenticatedUser);
});
export default router;

Il n'y a rien de bien spécial à ce code. On fait simplement appel aux opérations du service users.
Nous avons néanmoins du créer un nouveau type PotentialUser. Veuillez mettre à jour le fichier /types.ts et y inclure ce code :

ts
type PotentialUser = Omit<User, "id">;
export type {
Pizza,
NewPizza,
PizzaToUpdate,
Drink,
NewDrink,
AuthenticatedUser,
User,
PotentialUser,
};

Attention, il faut rajouter le nouveau router au sein de app.js pour que notre API puisse offrir les nouvelles opérations ; veuillez donc ajouter ce code dans /app.ts :

ts
1
import express, { ErrorRequestHandler } from "express";
2
3
import usersRouter from "./routes/users";
4
import pizzaRouter from "./routes/pizzas";
5
import drinkRouter from "./routes/drinks";
6
import authsRouter from "./routes/auths";
7
8
const app = express();
9
10
app.use((_req, _res, next) => {
11
console.log(
12
"Time:",
13
new Date().toLocaleString("fr-FR", { timeZone: "Europe/Brussels" })
14
);
15
next();
16
});
17
18
app.use(express.json());
19
app.use(express.urlencoded({ extended: false }));
20
21
app.use("/users", usersRouter);
22
app.use("/pizzas", pizzaRouter);
23
app.use("/drinks", drinkRouter);
24
app.use("/auths", authsRouter);
25
26
const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
27
console.error(err.stack);
28
return res.status(500).send("Something broke!");
29
};
30
31
app.use(errorHandler);
32
export default app;

Utilisation de Rest Client pour tester les nouvelles opérations

Veuillez démarrer votre API auths.

On va utiliser REST Client pour tester ces nouvelles opérations.

Veuillez créer le fichier /REST Client/auths.http et y ajouter le code suivant :

http
@baseUrl = http://localhost:3000
### Try to login an unknown user
POST {{baseUrl}}/auths/login
Content-Type: application/json
{
"username":"unknown",
"password":"admin"
}
### Login the default admin
POST {{baseUrl}}/auths/login
Content-Type: application/json
{
"username":"admin",
"password":"admin"
}
### Create the manager user
POST {{baseUrl}}/auths/register
Content-Type: application/json
{
"username":"manager",
"password":"manager"
}
### Login the manager user
POST {{baseUrl}}/auths/login
Content-Type: application/json
{
"username":"manager",
"password":"manager"
}

Veuillez exécuter les différentes requêtes. Tout devrait fonctionner, vous devriez récupérer le username et le token d'un utilisateur authentifié.

Pour le fun, nous allons décoder un token :

  • Veuillez copier le token de ce que renvoie votre API pour la requête de login de l'utilisateur manager (qqch qui doit ressembler à une string du genre : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1hbmFnZXIiLCJpYXQiOjE2NjEyNDg1MjksImV4cCI6MTc0NzY0ODUyOX0.JvYzM0gtmVkqFr9R3f1Bf6ow_QuyHJY-vedJ39N7JKw).
  • Veuillez vous rendre sur le site JWT et coller votre token dans le champs Encoded. Dans la colonne Decoded, vous devriez voir l'algorithme utilisé par la méthode sign, ainsi que les données présentes dans le payload, dont "username": "manager" !

Vous comprenez maintenant pourquoi on n'ajoute pas des secrets dans un token 😉.

Autorisation et vérification de token

Mécanisme d'autorisation d'utilisateurs à des opérations d'une API

Intro

Dans le workflow que nous avons choisi, les utilisateurs reçoivent un token lors du register ou du login. C'est le job de l'application cliente de trouver un moyen de sauvegarder ce token.

Lorsqu'une application client souhaite créer une pizza, on souhaite autoriser cette opération qu'à l'administrateur du site gérant la pizzeria.
Nous décidons donc pour l'opération de création de pizza que l'utilisateur doit non seulement être authentifié, mais qu'en plus, il doit être l'admin du site.

Mécanisme d'autorisation quand le token JWT est valide

Pour que l'application client qui fait la requête à l'API puisse s'autoriser, elle doit ajouter un authorization header à sa requête contenant comme valeur le token reçu lors du login ou du register :

GatsbyImage
Appel à une opération d'une RESTful API nécessitant une autorisation JWT : autorisation valide

Le router de "pizzas", avant même de passer la requête à la route POST /pizzas, va lancer la fonction middleware d'autorisation nommée authorize. La fonction middleware authorize va s'occuper de vérifier le token envoyé par le client dans un header à l'aide de la méthode verifiy de l'objet jwt (de la librairie jsonwebtoken).

💭 Si le token est valide, est-ce que ça signifie que l'utilisateur est bien authentifié ?
Hé bien non, car imaginez qu'entre le moment où l'utilisateur se soit logué, il ait été supprimé de l'application par un admin... dans ce cas-ci, on ne veut pas l'autoriser à ajouter une pizza au menu de la pizzeria ! Il n'est p-e même plus un employé, il souhaite p-e écrire du contenu malicieux 😨

Donc si le token est valide, authorize fera appel au service users pour vérifier que l'utilisateur existe bien dans le support de données (fichiers JSON, base de données ou autres) et pour obtenir toutes les infos sur cet utilisateur. Si l'utilisateur existe, alors cela signifie que l'utilisateur est bien authentifié. La fonction middleware authorize va passer la main à une autre fonction middleware pour vérifier que l'utilisateur est bien l'admin.

💭 Comment est-ce que authorize peut faire appel au service pour vérifier que l'utilisateur existe bien ? Est-ce que cela signifie que le token doit contenir tout un tas de données sur l'utilisateur ?

👍 En règle générale, on va juste mettre un identifiant de l'utilisateur dans ce qu'on appelle le payload du token. C'est ensuite l'API, suite au décodage de l'identifiant de l'utilisateur, qui fera appel aux données pour retrouver tout ce qui concerne l'utilisateur.

La fonction middleware isAdmin va vérifier que l'utilisateur est bien l'admin. Si c'est OK, elle passera la main à la fonction middleware qui gère la route POST /pizzas au sein du router de pizzas en faisant l'appel à la fonction next.
🍬 NB : on pourrait aussi laisser la fonction creatPizza s'occuper de vérifier que l'utilisateur demandant la création de la pizza soit bien l'admin. Ici, comme il s'agit de vérifier que le contrat de l'API soit respecté, c'est-à-dire que l'utilisateur soit bien authentifié & admin, alors on préfère réaliser cette action en dehors du service. Dans nos choix architecturaux pour nos RESTful API, nous avons décidé que ce n'est pas le service qui s'occupe de présenter les données aux clients, c'est le rôle des routers (et les fonctions middleware associées).

Maintenant que le client a les bons privilèges, l'opération de création de la pizza peut donc être autorisée. L'opération de createPizza du service renverra la nouvelle pizza au router qui s'occupera de présenter la nouvelle pizza au client, au format JSON.

Mécanisme d'autorisation quand le token JWT est invalide

Voici le workflow d'autorisation si le token n'est pas valide :

GatsbyImage
Appel à une opération d'une RESTful API nécessitant une autorisation JWT : autorisation invalide

Dans ce cas là, authorize ne passe pas la main à isAdmin ni même à la fonction middleware qui gère la route POST /pizzas.
La fonction middleware authorize renvoie directement un code d'erreur 401 Unauthorized au client.

Mécanisme d'autorisation quand l'utilisateur n'est pas admin

Vous pourriez aussi imaginer le workflow où le client envoie un token valide, mais l'utilisateur associé n'est pas admin. Dans ce cas-ci, authorize ferait appel à isAdmin, mais isAdmin renverrait directement un code d'erreur 403 Forbidden au client.
En effet, l'API indiquerait ainsi qu'elle aurait vérifié que l'utilisateur est bien authentifié, mais que celui ne possède pas les privilèges suffisant pour accéder à l'opération demandée.

Implémentation du mécanisme d'autorisation

😨 Wow, le workflow d'autorisation pour la création d'une pizza est assez long.
Nous allons maintenant l'implémenter dans notre tutoriel en cours.
Nous vous inquiétez pas, ce qui importe dans le code qui va suivre, ce n'est pas de savoir écrire tout le code, mais de bien comprendre les mécanismes associés. En effet, en règle générale, vous allez utiliser des librairies vous permettant d'autoriser les accès aux opérations de vos API. Vous écrirez donc rarement les mécanismes d'autorisation, par contre, vous devrez pouvoir les utiliser.

Veuillez créer les nouvelles fonctions middleware authorize et isAdmin au sein d'un nouveau fichier /utils/auths.ts :

ts
1
import jwt from "jsonwebtoken";
2
import { readOneUserFromUsername } from "../services/users";
3
import { NextFunction, RequestHandler, Response } from "express";
4
import { AuthenticatedRequest, JwtPayload, User } from "../types";
5
6
const jwtSecret = "ilovemypizza!";
7
8
const authorize = (
9
req: AuthenticatedRequest,
10
res: Response,
11
next: NextFunction
12
) => {
13
const token = req.get("authorization");
14
if (!token) {
15
return res.sendStatus(401);
16
}
17
18
try {
19
const decoded = jwt.verify(token, jwtSecret) as JwtPayload;
20
const { username } = decoded;
21
22
const existingUser = readOneUserFromUsername(username);
23
24
if (!existingUser) {
25
return res.sendStatus(401);
26
}
27
28
req.user = existingUser; // request.user object is available in all other middleware functions
29
return next();
30
} catch (err) {
31
console.error("authorize: ", err);
32
return res.sendStatus(401);
33
}
34
};
35
36
const isAdmin: RequestHandler = (req: AuthenticatedRequest, res, next) => {
37
const { username } = req.user as User;
38
39
if (username !== "admin") {
40
return res.sendStatus(403);
41
}
42
return next();
43
};
44
45
export { authorize, isAdmin };

Voici quelques explications sur le code de la fonction middleware authorize :

  • Grâce à req.get('authorization') (ligne 13), on récupère le token qui a été envoyé par le client au sein de l'authorization header de la requête sous forme de string.
  • La méthode jwt.verify(token, jwtSecret) (ligne 19) vérifie tant la signature du token que son éventuelle expiration. Elle utilise le secret jwtSecret qui doit être le même que celui pris en compte lors de la création du token.
  • Elle charge toutes les données de l'utilisateur authentifié au sein de l'objet req (ligne 28), dans user. Cela est une bonne pratique, cela permet, pour toute la durée du traitement de cette requête, de mettre à disposition ces données à toutes les fonctions middleware.
    💭 Mais pourquoi faire cela ? Imaginez que vous faites un appel à une base de données externes à chaque fois que vous souhaitez obtenir les informations d'un utilisateur... Cela est très consommateur en temps... Lorsque vous allez utiliser d'autres fonctions middleware comme isAdmin, vous n'avez plus besoin de faire appel à la base de données.

La fonction middleware isAdmin récupère les données de l'utilisateur authentifié via l'objet req.user.
Si l'utilisateur n'est pas admin, c'est le code 403 Forbidden qui est renvoyé à l'application cliente, signifiant que l'utilisateur est bien authentifié, mais il n'a pas les privilèges pour accéder à l'opération demandée (création de pizza).
Si tout est OK, isAdmin fait appel à next(), ce qui consiste à exécuter la prochaine fonction middleware qui est présente après l'appel de isAdmin.

Pour créer ces deux fonctions middleware, nous avons du créer deux nouveaux types : AuthenticatedRequest et JwtPayload. Veuillez mettre à jour le fichier /types.ts et y inclure ce code :

ts
1
import { Request } from "express";
2
3
// Reste du code
4
5
interface AuthenticatedRequest extends Request {
6
user?: User;
7
}
8
9
interface JwtPayload {
10
username: string;
11
exp: number; // Expiration time (in seconds since the epoch)
12
iat: number; // Issued at time (in seconds since the epoch)
13
}
14
15
export type {
16
Pizza,
17
NewPizza,
18
PizzaToUpdate,
19
Drink,
20
NewDrink,
21
AuthenticatedUser,
22
User,
23
PotentialUser,
24
AuthenticatedRequest,
25
JwtPayload,
26
};

Ici nous avons ajouté un nouveau type AuthenticatedRequest qui étend le type Request d'Express, ceci afin de pouvoir ajouter un objet de type User à req.

Nous avons aussi ajouté un nouveau type JwtPayload qui représente le payload d'un token JWT. Ce type est utilisé pour typer l'objet decoded retourné par la méthode jwt.verify.

Où allons-nous utiliser les nouvelles fonctions middleware authorize et isAdmin ?

Nous pouvons le faire au niveau que nous souhaitons, soit au niveau :

  • de l'application, pour toutes les routes, via app.use(authorize).
  • d'un router, pour toutes les routes associées ; par exemple, on pourrait dire que toutes les routes du router de pizzas sont protégées par une autorisation JWT. On écrirait : app.use("/pizzas", authorize, pizzaRouter);.
    Cela signifierait que toutes les opérations sur des ressources de type "pizzas" ne seraient autorisées que si l'utilisateur était authentifié. Cela serait problématique pour deux raisons :
    • On veut pouvoir afficher le menu des pizzas pour tous les utilisateurs, même s'ils sont anonymes.
    • On ne veut pas simplement vérifier qu'un utilisateur est authentifié pour créer une pizza, on veut aussi vérifier qu'il est admin.
  • d'une route, pour une opération de notre RESTful API. C'est ce que nous souhaitons faire ici. Veuillez mettre à jour le code du router de "pizzas" au sein de /routes/pizzas.ts :
js
// Code existant...
import { authorize, isAdmin } from "../utils/auths";
// Code existant...
// Create a pizza to be added to the menu.
router.post("/", authorize, isAdmin, (req, res) => {
const body: unknown = req.body;
if (
!body ||
typeof body !== "object" ||
!("title" in body) ||
!("content" in body) ||
typeof body.title !== "string" ||
typeof body.content !== "string" ||
!body.title.trim() ||
!body.content.trim()
) {
return res.sendStatus(400);
}
const { title, content } = body as NewPizza;
const addedPizza = createPizza({ title, content });
return res.json(addedPizza);
});

Ainsi, nous avons juste fait l'appel de deux fonctions middleware pour vérifier :

  • que l'utilisateur est authentifié via authorize ; si tout est OK au niveau du token fournit par l'application cliente, authorize fait appel via next() à la prochaine fonction middleware. Dans ce cas-ci, c'est isAdmin. S'il y a un problème, authorize termine le traitement de la requête en envoyant un code d'erreur au client et les prochaines fonctions middleware (isAdmin, puis la fonction arrow) ne sont pas exécutées.
  • que l'utilisateur est admin via isAdmin ; si tout est OK, que l'utilisateur authentifié est l'admin, isAdmin fait appel via next() à la prochaine fonction middleware. Dans ce cas-ci, c'est la fonction arrow qui appelle l'opération demandée pour créer la pizza : createPizza. Si l'utilisateur authentifié n'est pas l'admin, alors isAdmin termine le traitement en envoyant un code d'erreur au client et la fonction traitant de l'opération de création n'est pas exécutée.

Comment pouvons-nous tester l'opération de création d'une pizza ? Via REST Client. Tentons le coup à l'aide de cette requête (elle est déjà présente dans /REST Client/pizzas.http) :

http
POST {{baseUrl}}/pizzas
Content-Type: application/json
{
"title":"Magic Green",
"content":"Epinards, Brocolis, Olives vertes, Basilic"
}

Après avoir exécuté cette requête, vous devriez avoir reçu un status code 401 Unauthorized. C'est normal, comme nous n'avons pas envoyé de token, nous ne pouvons donc pas être autorisé.
Au prochain point nous allons voir comment utiliser REST Client pour sauvegarder de l'information, comme un token, suite à une requête vers une API.

Client REST avec JWT

Précédemment, nous avons appris à utiliser REST Client, l'extension de VS Code, pour faire des requêtes vers des API.

Voici quelques notions supplémentaire pour utiliser REST Client avec des JWT :

  • Il est possible de créer des Request Variables afin de récupérer la réponse associée à une requête au sein d'une variable.
  • On va donc pouvoir récupérer le token, suite à une requête d'authentification, au sein d'une Request Variable, pour ensuite fournir ce token dans le Authorization header de toutes les requêtes demandant une autorisation JWT.

Si vous souhaitez plus d'infos sur les Request Variables, vous pouvez consulter la documentation de REST Client [R.55].

Voici comment mettre à jour le script /REST Client/pizzas.http pour créer une pizza en passant le token de l'utilisateur admin :

http
### Create a pizza by using the admin account
#### First login as the admin
##### Define a request variable named admin :
# @name admin
POST {{baseUrl}}/auths/login
Content-Type: application/json
{
"username":"admin",
"password":"admin"
}
#### Create a pizza with the admin token
POST {{baseUrl}}/pizzas
Content-Type: application/json
Authorization: {{admin.response.body.token}}
{
"title":"Magic Green",
"content":"Epinards, Brocolis, Olives vertes, Basilic"
}

Pour tester l'opération de création de pizza, veuillez d'abord exécuter la première requête, puis la seconde donnée ci-dessus.

A ce stade-ci, il serait aussi intéressant de tester certains cas d'erreurs associés aux tokens. Veuillez compléter le script /REST Client/pizzas.http avec :

http
### 1. Create a pizza without a token
POST {{baseUrl}}/pizzas
Content-Type: application/json
{
"title":"Magic Green",
"content":"Epinards, Brocolis, Olives vertes, Basilic"
}
### 2. Create a pizza without being the admin, use manager account
#### 2.1 First login as the manager
##### 2.1.1 Define a request variable nammed manager
# @name manager
POST {{baseUrl}}/auths/login
Content-Type: application/json
{
"username":"manager",
"password":"manager"
}
##### 2.1.2 Define a file variable to simplify the access to the token of manager
@managerToken = {{manager.response.body.token}}
#### 2.2 Try to create a pizza with the manager token
POST {{baseUrl}}/pizzas
Content-Type: application/json
Authorization: {{managerToken}}
{
"title":"Magic Green",
"content":"Epinards, Brocolis, Olives vertes, Basilic"
}

Nous voyons qu'à l'aide de REST Client, nous pouvons utiliser une File Variable pour allouer une partie de la réponse faite à une requête.
Dans ce cas, on peut faire appel à l'API en passant le token via la File Variable nommé managerToken (code associé : Authorization: {{managerToken}}) au lieu d'utiliser une partie seulement de la Request Variable nommée manager (code possible : Authorization: {{manager.response.body.token}}).
Cela permet de créer des requêtes plus concises.

Protection des opérations d'écriture d'une API

Veuillez mettre à jour votre RESTful API gérant les pizzas afin que toutes les opérations d'écriture soient protégées par une autorisation JWT, n'autorisant que l'utilisateur admin.

Voici le code à mettre à jour dans /routes/pizzas pour correctement autoriser les opérations de suppression et de modification sur des ressources de type "pizzas":

js
// Delete a pizza from the menu based on its id
router.delete("/:id", authorize, isAdmin, (req, res) => {
const id = Number(req.params.id);
const deletedPizza = deletePizza(id);
if (!deletedPizza) return res.sendStatus(404);
return res.json(deletedPizza);
});
// Update a pizza based on its id and new values for its parameters
router.patch("/:id", authorize, isAdmin, (req, res) => {
const body: unknown = req.body;
if (
!body ||
typeof body !== "object" ||
("title" in body &&
(typeof body.title !== "string" || !body.title.trim())) ||
("content" in body &&
(typeof body.content !== "string" || !body.content.trim()))
) {
return res.sendStatus(400);
}
const pizzaToUpdate: PizzaToUpdate = body;
const id = Number(req.params.id);
const updatedPizza = updatePizza(id, pizzaToUpdate);
if (!updatedPizza) return res.sendStatus(404);
return res.json(updatedPizza);
});

Pour vous assurer que les opérations de suppression et de modification sont bien fonctionnelles, veuillez mettre à jour les requêtes associées afin d'utiliser un token. Veuillez mettre à jour /REST Client/pizzas.http en ajoutant ces lignes :

http
1
### Delete pizza identified by 2 with the admin token
2
DELETE {{baseUrl}}/pizzas/2
3
Authorization: {{admin.response.body.token}}
4
5
### Update the pizza identified by 6 with the admin token
6
PATCH {{baseUrl}}/pizzas/6
7
Content-Type: application/json
8
Authorization: {{admin.response.body.token}}
9
10
{
11
"title":"Magic Green 2"
12
}

Veuillez exécutez les requêtes de type DELETE et de type PATCH afin de vous assurer que l'API est en ordre.

En cas de souci, vous pouvez utiliser le code du tutoriel auths.

Si vous le souhaitez, vous pouvez aussi faire les modifications pour les ressources de type "drinks".

Veuillez mettre à jour /routes/drinks pour ajouter l'appel aux fonctions middleware authorize et isAdmin pour les opérations de création, suppression et de modification :

ts
1
router.post("/", authorize, isAdmin, (req, res) => {
2
const body: unknown = req.body;
3
if (
4
!body ||
5
typeof body !== "object" ||
6
!("title" in body) ||
7
!("image" in body) ||
8
!("volume" in body) ||
9
!("price" in body) ||
10
typeof body.title !== "string" ||
11
typeof body.image !== "string" ||
12
typeof body.volume !== "number" ||
13
typeof body.price !== "number" ||
14
!body.title.trim() ||
15
!body.image.trim() ||
16
body.volume <= 0 ||
17
body.price <= 0
18
) {
19
return res.sendStatus(400);
20
}
21
22
const { title, image, volume, price } = body as NewDrink;
23
24
const newDrink = createOneDrink({ title, image, volume, price });
25
return res.json(newDrink);
26
});
27
28
router.delete("/:id", authorize, isAdmin, (req, res) => {
29
const id = Number(req.params.id);
30
const deletedDrink = deleteOneDrink(id);
31
if (!deletedDrink) {
32
return res.sendStatus(404);
33
}
34
return res.json(deletedDrink);
35
});
36
37
router.patch("/:id", authorize, isAdmin, (req, res) => {
38
const id = Number(req.params.id);
39
40
const body: unknown = req.body;
41
42
if (
43
!body ||
44
typeof body !== "object" ||
45
("title" in body &&
46
(typeof body.title !== "string" || !body.title.trim())) ||
47
("image" in body &&
48
(typeof body.image !== "string" || !body.image.trim())) ||
49
("volume" in body &&
50
(typeof body.volume !== "number" || body.volume <= 0)) ||
51
("price" in body && (typeof body.price !== "number" || body.price <= 0))
52
) {
53
return res.sendStatus(400);
54
}
55
56
const { title, image, volume, price }: Partial<NewDrink> = body;
57
58
const updatedDrink = updateOneDrink(id, { title, image, volume, price });
59
60
if (!updatedDrink) {
61
return res.sendStatus(404);
62
}
63
64
return res.json(updatedDrink);
65
});

N'oubliez pas de faire l'import de authorize et isAdmin au début du fichier /routes/drinks.ts.

Pour tester les opérations de création, de suppression et de modification sur des ressources de type "drinks", veuillez mettre à jour le script /REST Client/drinks.http :

http
1
### Read all drinks with File variable
2
@baseUrl = http://localhost:3000
3
GET {{baseUrl}}/drinks
4
5
### Read a single drink
6
GET {{baseUrl}}/drinks/3
7
8
### Read all drinks cheaper or equal to 3 €
9
GET {{baseUrl}}/drinks/?budget-max=3
10
11
### Create a drink
12
##### Define a request variable named admin :
13
# @name admin
14
POST {{baseUrl}}/auths/login
15
Content-Type: application/json
16
17
{
18
"username":"admin",
19
"password":"admin"
20
}
21
22
### Create a drink with the admin token
23
POST {{baseUrl}}/drinks
24
Content-Type: application/json
25
Authorization: {{admin.response.body.token}}
26
27
{
28
"title":"Virgin Tonic",
29
"image":"https://plus.unsplash.com/premium_photo-1668771899398-1cdd763f745e?q=80&w=1374&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
30
"volume":0.25,
31
"price":4.5
32
}
33
34
35
### Try to create a drink with incomplete data
36
POST {{baseUrl}}/drinks
37
Content-Type: application/json
38
Authorization: {{admin.response.body.token}}
39
40
41
{
42
"title":"Missing Data Drink",
43
"volume":0.25,
44
"price":4.5
45
}
46
47
### Delete a drink
48
DELETE {{baseUrl}}/drinks/5
49
Authorization: {{admin.response.body.token}}
50
51
52
### Update a drink
53
PATCH {{baseUrl}}/drinks/3
54
Content-Type: application/json
55
Authorization: {{admin.response.body.token}}
56
57
{
58
"title":"Citronnade"
59
}

Sécurisation des passwords

Nous souhaitons maintenant assurer que les passwords enregistrés dans un support de données (un fichier ou une base de données) ne puissent pas être récupérés.

👍 Pour ce faire, il est recommandé d'hacher les passwords avant de les enregistrer au niveau d'une API.

Afin de se protéger contre les "hash attacks", on utilise du salt :

  • un salt : c'est une donnée aléatoire qui est utilisé en entrée d'une fonction qui hache des données. Ainsi, si un hacker utilise une base de données de passwords hachés, il devra en plus trouver le bon salt pour que son attaque puisse fonctionner.
  • salt round : nombre de fois que l'opération de hachage est faite, rendant les attaques de force brute plus lente et donc difficile ; une valeur de 10 semble être généralement raisonnable et recommandée.

Pour hacher sous Node.js, nous utiliserons la librairie bcrypt.

Veuillez installer la librairie bcrypt et les types associés à bcrypt au sein de votre API auths :

bash
npm i bcrypt
npm i @types/bcrypt -D

En résumé, nous allons utiliser :

  • la fonction asynchrone hash de bcrypt pour hacher un password ;
  • la fonction asynchrone compare de bcrypt pour comparer un password en clair à un password haché ; si le résultat est positif, cela signifie que le password fournit pour un utilisateur correspond au password initial.

Nous préférons utiliser les librairies asynchrone afin que l'API reste disponible à gérer des requêtes clientes et ne bloque pas celles-ci jusqu'à la fin d'une opération de bcrypt !

Veuillez effacer le fichier /data/users.json contenant les credentials d'utilisateurs où les passwords sont donnés en clair.

Nous allons maintenant mettre à jour le service "users" pour utiliser bcrypt. Veuillez modifier /services/users.ts :

ts
1
import jwt from "jsonwebtoken";
2
import bcrypt from "bcrypt";
3
import path from "node:path";
4
import { parse, serialize } from "../utils/json";
5
import { AuthenticatedUser, User } from "../types";
6
7
const jwtSecret = "ilovemypizza!";
8
const lifetimeJwt = 24 * 60 * 60 * 1000; // in ms : 24 * 60 * 60 * 1000 = 24h
9
10
const saltRounds = 10;
11
12
const jsonDbPath = path.join(__dirname, "/../data/users.json");
13
14
const defaultUsers: User[] = [
15
{
16
id: 1,
17
username: "admin",
18
password: bcrypt.hashSync("admin", saltRounds),
19
},
20
];
21
22
async function login(
23
username: string,
24
password: string
25
): Promise<AuthenticatedUser | undefined> {
26
const userFound = readOneUserFromUsername(username);
27
if (!userFound) return undefined;
28
const passwordMatch = await bcrypt.compare(password, userFound.password);
29
if (!passwordMatch) return undefined;
30
31
const token = jwt.sign(
32
{ username }, // session data added to the payload (payload : part 2 of a JWT)
33
jwtSecret, // secret used for the signature (signature part 3 of a JWT)
34
{ expiresIn: lifetimeJwt } // lifetime of the JWT (added to the JWT payload)
35
);
36
37
const authenticatedUser: AuthenticatedUser = {
38
username,
39
token,
40
};
41
42
return authenticatedUser;
43
}
44
45
async function register(
46
username: string,
47
password: string
48
): Promise<AuthenticatedUser | undefined> {
49
const userFound = readOneUserFromUsername(username);
50
if (userFound) return undefined;
51
52
await createOneUser(username, password);
53
54
const token = jwt.sign(
55
{ username }, // session data added to the payload (payload : part 2 of a JWT)
56
jwtSecret, // secret used for the signature (signature part 3 of a JWT)
57
{ expiresIn: lifetimeJwt } // lifetime of the JWT (added to the JWT payload)
58
);
59
60
const authenticatedUser: AuthenticatedUser = {
61
username,
62
token,
63
};
64
65
return authenticatedUser;
66
}
67
68
function readOneUserFromUsername(username: string) {
69
const users = parse(jsonDbPath, defaultUsers);
70
const userFound = users.find((user) => user.username === username);
71
if (!userFound) return undefined;
72
73
return userFound;
74
}
75
76
async function createOneUser(username: string, password: string) {
77
const users = parse(jsonDbPath, defaultUsers);
78
79
const hashedPassword = await bcrypt.hash(password, saltRounds);
80
81
const nextId =
82
users.reduce((acc, user) => (user.id > acc ? user.id : acc), 0) + 1;
83
84
const createdUser: User = {
85
id: nextId,
86
username,
87
password: hashedPassword,
88
};
89
90
users.push(createdUser);
91
92
serialize(jsonDbPath, users);
93
94
return createdUser;
95
}
96
97
export { login, register, readOneUserFromUsername };

Voici les modifications apportées :

  • pour créer un password haché : on utilise la fonction asynchrone hash pour créer le password haché. Dès lors, createOneUser devient asynchrone. De plus, comme createOneUser est devenue asynchrone, dans register, pour chaîner le traitement de création du token, on met un await à l'appel de createOneUser. Et comme un await est ajouté au sein de register, il faut mettre un async à la méthode register. Attention, du coup, comme register est devenue asynchrone, il faudra aussi bien chaîner les traitements où register est appelé, dans le router auths. Notez aussi que nous avons changé le type de retour des fonctions asynchrones qui renvoient maintenant des Promise.
  • pour vérifier qu'un password en clair "match" à un password haché : on utilise la fonction asynchrone compare que l'on chaîne à la création du token à l'aide de await. Dès lors, la fonction login doit elle aussi être déclarée async. Attention, du coup, comme login est devenue asynchrone, il faudra bien chaîner les traitements où login est appelé, dans le router auths.
  • pour créer l'utilisateur admin se trouvant dans les defaultUsers avec des credentials par défaut : on souhaite indiquer comme password le password haché correspondant au password "admin". Pour se simplifier la vie, on appelle la fonction synchrone hashSync (voir ligne 18). Bien évidemment, dans une application robuste et sécurisée, il est interdit de coder en dur ce genre de secrets dans les sources de notre application 😉 !

Afin de traiter des deux fonctions du service users qui sont devenues asynchrones, login et register, nous allons modifier le router /routes/auths :

ts
1
/* eslint-disable @typescript-eslint/no-misused-promises */
2
/*
3
In Express V4, asynchronous functions are not fully supported in TypeScript
4
(only void return type for RequestHandler is allowed).
5
In Express V5, this issue has been addressed, but V5 is still in beta.
6
Consequently, the ESLint rule "no-misused-promises" is disabled. */
7
import { Router } from "express";
8
import { PotentialUser } from "../types";
9
import { login, register } from "../services/users";
10
const router = Router();
11
12
/* Register a user */
13
router.post("/register", async (req, res) => {
14
const body: unknown = req.body;
15
if (
16
!body ||
17
typeof body !== "object" ||
18
!("username" in body) ||
19
!("password" in body) ||
20
typeof body.username !== "string" ||
21
typeof body.password !== "string" ||
22
!body.username.trim() ||
23
!body.password.trim()
24
) {
25
return res.sendStatus(400);
26
}
27
28
const { username, password } = body as PotentialUser;
29
30
const authenticatedUser = await register(username, password);
31
32
if (!authenticatedUser) {
33
return res.sendStatus(409);
34
}
35
36
return res.json(authenticatedUser);
37
});
38
39
/* Login a user */
40
router.post("/login", async (req, res) => {
41
const body: unknown = req.body;
42
if (
43
!body ||
44
typeof body !== "object" ||
45
!("username" in body) ||
46
!("password" in body) ||
47
typeof body.username !== "string" ||
48
typeof body.password !== "string" ||
49
!body.username.trim() ||
50
!body.password.trim()
51
) {
52
return res.sendStatus(400);
53
}
54
55
const { username, password } = body as PotentialUser;
56
57
const authenticatedUser = await login(username, password);
58
59
if (!authenticatedUser) {
60
return res.sendStatus(401);
61
}
62
63
return res.json(authenticatedUser);
64
});
65
66
export default router;

Nous avons simplement chaîné les réponses à faire au client seulement une fois les opérations register et login terminées. Pour ce faire, nous avons précédé le nom de ces méthodes par await. Nous avons donc du ajouter async aux fonctions middleware s'occupant des routes POST /auths/register et POST /auths/login.

Notons que nous avons ajouté un commentaire pour désactiver la règle @typescript-eslint/no-misused-promises. En effet, dans Express V4, les fonctions asynchrones ne sont pas pleinement supportées en TypeScript (seul le type de retour void pour RequestHandler est autorisé). Dans Express V5, ce problème a été résolu, mais V5 est toujours en version bêta. Par conséquent, la règle ESLint no-misused-promises est désactivée.

Veuillez vérifier que votre application fonctionne correctement.
Via l'IHM, veuillez faire un register d'un nouvel utilisateur.
Au niveau de l'API, allez voir le contenu du nouveau fichier /data/users.json. Les passwords devraient maintenant être hachés, comme par exemple :

json
[
{
"id": 1,
"username": "admin",
"password": "$2b$10$RioLKlPFyYFEhv/46gR7dufDkke07eDpWH9tBt/A4Z9tJh0oJnnf2"
},
{
"id": 2,
"username": "manager",
"password": "$2b$10$NZZ1zxOPdby6gl4Dw8K0Q.v4ZRWTbh1Ta7qcYzH5G4SrO5z71H0kO"
}
]

En cas de souci, vous pouvez utiliser le code du tutoriel auths.

Exercice 3.1 : Authentification & autorisation d'opérations

Vous allez sécuriser la RESTful API de myMovies.

Mise en place du projet

Veuillez créer un nouveau projet ans votre repo git dans /exercises/3.1 en clonant le boilerplate du cours offrant l'authentification et l'autorisation JWT : jwt-ts-api-boilerplate. Dans ce boilerplate, vous trouverez déjà un service users qui gère les utilisateurs, ainsi que des routes pour les opérations de login et de register.

⚡️ Attention, n'oubliez pas les soucis d'avoir un git dans le git... Pensez à supprimer le dossier .git de votre nouveau projet.

Identification des opérations & création de la documentation de l'API

Nous allons maintenant passer à une phase d'analyse de votre API. Nous allons documenter les opérations de votre API, ainsi que les opérations qui seront protégées par JWT.

Pour les opérations existantes sur des films, vous devez planifier de protéger les opérations d'écriture (création, modification, suppression) de vos films par JWT. Les opérations de lecture de films ne doivent pas être protégées par JWT.

Pour la protection des opérations, vous ne devrez pas gérer de rôles, mais simplement vérifier que l'utilisateur est authentifié.

Vous devez planifier des nouvelles opérations :

  • lire tous les commentaires et pouvoir les filtrer par film ;
  • ajouter des commentaires sur un film, en y associant l'utilisateur ou l'utilisatrice authentifiée ;
  • effacer un commentaire sur un film sur base de l'utilisateur ou de l'utilisatrice authentifiée.

Veuillez identifier toutes les opérations mises à disposition par votre API (y compris l'authentification), ainsi que si celles-ci sont protégées par JWT, au sein du fichier README.md (fichier Markdown) de votre projet. Voici un exemple de comment nous vous recommandons de documenter votre API, sous forme de tableau :

URIMéthode HTTPAuths?Opération
filmsGETNonREAD ALL : Lire toutes les ressources de la collection
films/:idGETNonREAD ALL : Lire une ressource de la collection
filmsPOSTOuiCREATE ONE : Créer une ressource de la collection basée sur un body au format {title: string; director: string; duration: number; budget?: number; description?: string; imageUrl?: string; }
............
commentsGETJWTREAD ALL FILTERED : Lire toutes les ressources de la collection
commentsPOSTJWTCREATE ONE : Créer une ressource basée sur un body au format {...}
............

Comment créer un tableau dans un fichier Markdown (pour README.md) ?
Voici deux options :

  • Soit vous utiliser des | pour délimiter les cellules et des |---| pour séparer les headers du corps du tableau. Voici le Markdown de l'exemple donné ci-dessus :
text
| URI | Méthode HTTP | Auths? | Opération |
|---|---|---|---|
| **`films`** | GET | Non | READ ALL : Lire toutes les ressources de la collection |
| **`films/:id`** | GET | Non | READ ALL : Lire une ressource de la collection |
| **`films`** | POST | Oui | CREATE ONE : Créer une ressource de la collection basée sur un body au format `{title: string; director: string; duration: number; budget?: number; description?: string; imageUrl?: string; }` |
| ... | ... | ... | ... |
| **`comments`** | GET | JWT | READ ALL FILTERED : Lire toutes les ressources de la collection |
| **`comments`** | POST | JWT | CREATE ONE : Créer une ressource basée sur un body au format `{...}` |
| ... | ... | ... | ... |
  • Soit c'est simplement un tableau HTML (<table>).

Une fois la documentation de votre API prête, veuillez faire un commit avec le message suivant : new:ex3.1-doc.

Sécuriser les opérations d'écriture pour les films

Vous allez maintenant travailler sur le code de votre nouveau projet.

Dans votre nouveau projet, dans un premier temps, sans utiliser JWT, veuillez intégrer le code utile de vos opérations (router & service de films, requêtes HTTP pour tester les opérations sur les films) de votre /exercises/1.8.

Veuillez protéger les opérations d'écriture (création, modification, suppression) de vos films par JWT (en intégrant l'authentification) et vérifier que les opérations protégées fonctionnent correctement via REST Client.
Vous devez assurer qu'un utilisateur non authentifié ne puisse pas créer, modifier ou supprimer un film.

Une fois tout fonctionnel, veuillez faire un commit avec le message suivant : new:ex3.1.

Implémenter les opérations associées aux commentaires

Vous pouvez implémenter les nouvelles opérations de gestion de commentaires et les protéger par JWT.

Veuillez vérifier que vos opérations sont bien protégées et fonctionnelles en les testant avec Rest Client.

Veuillez bien nettoyer votre code et ne pas laisser de code mort.

Quand votre api est finalisée et testée, veuillez faire un commit de votre code avec comme message : new: ex3.1-comments.

Sécurisation de vos opérations

💭 Avez-vous bien protégé vos opérations ?
Est-ce que pour l'opération de création d'un commentaire, vous avez bien associé l'utilisateur ou l'utilisatrice authentifiée à ce commentaire sur base du token fourni par l'application cliente ? En effet, si vous utilisez le body pour valider le username sans checker le token, il est facilement possible de modifier le body et de créer un commentaire au nom d'un autre utilisateur...
De même pour l'opération de suppression d'un commentaire, avez-vous avez bien vérifié l'utilisateur ou l'utilisatrice authentifiée via le token ? Ou est-ce que vous avez simplement vérifié le username à partir de l'URL ?

💭 Concernant la gestion du contrat associé à vos opérations, les status codes, avez-vous pensé à :

  • vérifier lors de la création d'un commentaire que le film existe bien ? que l'utilisateur ou l'utilisatrice n'a pas déjà commenté ce film ?
  • vérifier lors de la suppression d'un commentaire que le commentaire existe bien pour l'utilisateur ou l'utilisatrice authentifiée et pour le film indiqué e ?

Si nécessaire, veuillez mettre à jour votre API, sa documentation et vos tests pour corriger les éventuelles soucis.

Quand votre api est finalisée et testée, veuillez faire un commit de votre code avec comme message : new: ex3.1-sécurité.

🍬 Exercice 3.2 : une autre API sécurisée

Cet exercice est optionnel, n'hésitez pas à le faire si vous avez du temps.

Peut-être avez-vous déjà entendu parler de cette mode montante dans le Web qu'est la JAMstack ?

Nous souhaitons développer un Headless CMS, c'est-à-dire un outil permettant de créer du contenu, très souvent utilisé dans le cadre de sites JAMstack.

Un Headless CMS fournira une interface web pour créer du contenu (ou des données), ainsi qu'une RESTful API mettant à disposition ce contenu.

L'Headless CMS que nous souhaitons développer doit permettre de créer le contenu associé à un blog.

Un blog sera structuré en pages. Une page aura un id, un titre, un URI, un contenu, un auteur ainsi qu'un statut de publication.

Le statut de publication d'une page peut actuellement prendre comme valeur :

  • « hidden »
  • « published »

Seul un utilisateur authentifié pourra lire, créer, modifier ou supprimer des pages.

La modification d'une page, tout comme la suppression d'une page ne pourra se faire que par son auteur.

L'ajout ou la modification d'une page ne sera possible que si le statut de publication donné correspond à un des deux statuts actuels (« hidden ou « published »). Sinon un message d'erreur devra être renvoyé.

La lecture d'une page, ou de toutes les pages, pourra être réalisée par n'importe quel utilisateur authentifié pour les pages dont le statut vaut « published ».

Pour les pages dont le statut vaut « hidden », seul leur auteur authentifié pourra les lire.

Veuillez créer un nouveau projet dans votre repo git dans /exercises/3.2 sur base du boilerplate : jwt-ts-api-boilerplate.

⚡ Si vous avez fait un clone du boilerplate, attention au Git dans le Git, n'oubliez pas de supprimer le dossier .git présent dans votre nouveau projet.

Puis, veuillez formaliser les opérations associées à vos RESTful APIs sous forme d'un tableau dans un fichier README.md.

Veuillez tester toutes les méthodes offertes par votre application à l'aide du client HTTP de REST Client.

Veuillez faire un commit de votre code avec le message suivant : new: ex3.2.

🍬 Bonus

  • N°1 : vous souhaitez rapidement découvrir les principes d'une architecture Web associée à la JAMstack ? https://jamstack.wtf/
  • N°2 : si vous souhaitez découvrir le format Markdown, notamment très souvent utilisé dans certains sites JAMstack (source des données pour certains Headless CMS) : https://www.markdownguide.org/basic-syntax/

🍬 Challenges

  • N°1 : Modifiez les autorisations au niveau des opérations de lecture : la lecture d'une page, ou de toutes les pages, pourra être réalisée par n'importe quel utilisateur anonyme ou authentifié pour les pages dont le statut vaut « published ». Pour les pages dont le statut vaut « hidden », seul leur auteur authentifié pourra les lire.
  • N°2 : lorsqu'un utilisateur tente d'effacer ou modifier une page dont il n'est pas l'auteur, renvoyez un code d'erreur approprié au client.
  • N°3 : Si ça n'est pas déjà fait, pensez à ne pas renvoyer les passwords des utilisateurs dans les réponses de votre API.

🍬 Authentification & autorisation JWT à l'aide de cookies

Pourquoi utiliser l'autorisation JWT à l'aide de cookies ?

Nous avons vu une façon de gérer des token JWT, sans gérer de cookies.

Cela impose aux client de sauvegarder les tokens et de les envoyer dans un authorization header lorsqu'ils souhaitent accéder à une opération protégée.

Il est aussi possible d'intégrer les tokens au sein de cookies. Dans ce cas-là, les clients qui ont reçu un cookie de l'API renverront automatiquement ce cookie (mécanisme des browsers). Les tokens JWT voyageront automatiquement grâce au mécanisme de cookies.

💭 Faut-il mieux intégrer les token JWT dans des cookies ou pas ?
Il est difficile de donner une réponse à cette question. Chaque approche a des avantages et des inconvénients. Ce qui devrait être le point d'attention, c'est la sécurité. Et dans les deux cas, on atteint un niveau de sécurité raisonnable. Pour ce cours, nous avons choisi la façon la plus moderne, en laissant au client le choix de sauvegarder les token dans le web storage (nous allons voir ça tout prochainement pour l'aspect frontend).
Notons que le cas le plus sûr est probablement d'avoir deux types de token, ce qui est d'une complexité qui dépasse les objectifs de ce cours. Néanmoins, pour votre info, les mécanismes d'authentification comme OAuth (authorization de MS Azure) & OpenID Connect (authentification de MS Azure) gère deux types de token :

  • un token à durée de vie courte qui sera enregistré en mémoire vive au niveau du client (access token) ou dans le web storage.
  • un token à durée de vie longue qui sera enregistré par le client dans un cookie (refresh token, notamment utilisé par l'API pour créer un nouvel access token).

Un cookie, c'est quoi ?

Un cookie représente des données qu'un serveur envoie à un browser.
Le browser peut sauver ce cookie. Pour chaque requête faite au serveur sur la même origine (que l'origine où le cookie a été reçu), le cookie sera automatiquement envoyé au serveur.

Il fut un temps où les cookies étaient utilisés comme un mécanisme général de stockage de données côté client.

👍 Actuellement, si les cookies sont utilisés pour sauvegarder les données de session, il faut se protéger contre les attaques XSS et rendre les cookies inaccessibles au JavaScript : on utilise donc les cookies HttpOnly qui sont inaccessibles à la Document.cookie API.

Pour la suite, on va donc voir :

  • comment utiliser Express pour créer des cookies au niveau d'une API ;
  • comment rendre ces cookies inaccessibles aux attaques XSS en configurant HttpOnly.

Implémentation de l'authentification & l'autorisation JWT au sein de cookies

Intro

Nous allons donc créer une nouvelle version de l'API sauvegardant le token d'un utilisateur au sein d'un cookie, ainsi que son username, sans que ces infos soient accessible au JS côté client.

Pour ce faire nous allons utiliser la librairie cookie-session qui permet d'enregistrer des données de session dans des cookies.

Veuillez créer un nouveau projet nommé /back/api/cookies sur base d'un copié/collé du projet auths).

Veuillez installer la librairie cookie-session au sein de votre nouveau projet cookies et les types associés (pour TypeScript):

bash
npm i cookie-session
npm install @types/cookie-session -D

Utilisation de la fonction middleware cookieSession

Veuillez mettre à jour /app.ts pour mettre en place la gestion de cookies :

ts
1
import express, { ErrorRequestHandler } from "express";
2
import cookieSession from "cookie-session";
3
4
import usersRouter from "./routes/users";
5
import pizzaRouter from "./routes/pizzas";
6
import drinkRouter from "./routes/drinks";
7
import authsRouter from "./routes/auths";
8
9
const app = express();
10
11
const expiryDateIn3Months = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30 * 3);
12
const cookieSecreteKey = 'YouWouldnot!not!like!mypizza';
13
app.use(
14
cookieSession({
15
name: 'user',
16
keys: [cookieSecreteKey],
17
httpOnly: true,
18
expires: expiryDateIn3Months,
19
}),
20
);
21
22
app.use((_req, _res, next) => {
23
console.log(
24
"Time:",
25
new Date().toLocaleString("fr-FR", { timeZone: "Europe/Brussels" })
26
);
27
next();
28
});
29
30
app.use(express.json());
31
app.use(express.urlencoded({ extended: false }));
32
33
app.use("/users", usersRouter);
34
app.use("/pizzas", pizzaRouter);
35
app.use("/drinks", drinkRouter);
36
app.use("/auths", authsRouter);
37
38
const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
39
console.error(err.stack);
40
return res.status(500).send("Something broke!");
41
};
42
43
app.use(errorHandler);
44
export default app;

Nous avons donc bien indiqué que le cookie est inaccessible au JS via : httpOnly: true.

Nous avons fait en sorte que le cookie soit signé via la clé cookieSecreteKey.
Le mécanisme de signature correspond à ce qui a été vu dans le cadre des tokens JWT.
Ainsi, si un cookie venait être modifié par un utilisateur, lors de la vérification du cookie, cela sera automatiquement détecté par la fonction middleware cookieSession et la session ne sera pas créée.

Pour info, la fonction middleware cookieSession va créer deux cookies :

  • un cookie portant comme nom la valeur de name ; il est encodé en base64. N'hésitez pas à vous amuser à décoder un cookie généré par cookieSession sur base64decode.
  • un cookie portant comme nom la valeur de name + .sig : c'est la signature qui prévient contre le "tempering" (acte intentionnel mais non autorisé qui amène à la modification d'un système ou de données).

Lecture et ajout de données de session via req.session

Pour créer des données de session, il suffit de simplement les ajouter à l'objet req.session.

Dans le cadre de la RESTful API gérant les pizzas, cela est fait lors d'une opération de type register ou login.

Veuillez mettre à jour le router /routes/auths.ts :

ts
1
/* eslint-disable @typescript-eslint/no-misused-promises */
2
/*
3
In Express V4, asynchronous functions are not fully supported in TypeScript
4
(only void return type for RequestHandler is allowed).
5
In Express V5, this issue has been addressed, but V5 is still in beta.
6
Consequently, the ESLint rule "no-misused-promises" is disabled. */
7
import { Request, Router } from "express";
8
import { AuthenticatedUser, PotentialUser } from "../types";
9
import { login, register } from "../services/users";
10
const router = Router();
11
12
/* Register a user */
13
router.post("/register", async (req, res) => {
14
const body: unknown = req.body;
15
if (
16
!body ||
17
typeof body !== "object" ||
18
!("username" in body) ||
19
!("password" in body) ||
20
typeof body.username !== "string" ||
21
typeof body.password !== "string" ||
22
!body.username.trim() ||
23
!body.password.trim()
24
) {
25
return res.sendStatus(400);
26
}
27
28
const { username, password } = body as PotentialUser;
29
30
const authenticatedUser = await register(username, password);
31
32
if (!authenticatedUser) {
33
return res.sendStatus(409);
34
}
35
36
createCookieSessionData(req, authenticatedUser);
37
38
return res.json({ username: authenticatedUser.username });
39
});
40
41
/* Login a user */
42
router.post("/login", async (req, res) => {
43
const body: unknown = req.body;
44
if (
45
!body ||
46
typeof body !== "object" ||
47
!("username" in body) ||
48
!("password" in body) ||
49
typeof body.username !== "string" ||
50
typeof body.password !== "string" ||
51
!body.username.trim() ||
52
!body.password.trim()
53
) {
54
return res.sendStatus(400);
55
}
56
57
const { username, password } = body as PotentialUser;
58
59
const authenticatedUser = await login(username, password);
60
61
if (!authenticatedUser) {
62
return res.sendStatus(401);
63
}
64
65
createCookieSessionData(req, authenticatedUser);
66
67
return res.json({ username: authenticatedUser.username });
68
});
69
70
/* Logout a user */
71
router.get("/logout", (req, res) => {
72
req.session = null;
73
return res.sendStatus(200);
74
});
75
76
function createCookieSessionData(
77
req: Request,
78
authenticatedUser: AuthenticatedUser
79
) {
80
if (!req.session) {
81
return;
82
}
83
req.session.username = authenticatedUser.username;
84
req.session.token = authenticatedUser.token;
85
}
86
87
export default router;

Dans le code ci-dessus, nous préparons les données de session qui seront écrites dans le cookie à l'aide de l'objet req.session.
Lorsque nous renvoyons du JSON aux clients, nous ne renvoyons plus le token, mais juste le username de l'utilisateur. L'application cliente, le browser, pourra utiliser cette info pour afficher le nom de l'utilisateur. Pour rappel, le browser n'a pas accès, via le JS, à l'info se trouvant dans le cookie.

Quand nous gérons une session via des cookies, il n'est pas évident de bien clore une session. Nous avons créé une nouvelle opération de type GET /auths/logout qui permet d'effacer les données de session d'un utilisateur.

Il nous reste à changer le mécanisme d'autorisation. Les tokens ne seront plus reçus via un authorization header, mais via un cookie.
Nous allons donc mettre à jour le middleware /utils/authorize :

ts
1
import jwt from "jsonwebtoken";
2
import { readOneUserFromUsername } from "../services/users";
3
import { NextFunction, RequestHandler, Response } from "express";
4
import { AuthenticatedRequest, JwtPayload, User } from "../types";
5
6
const jwtSecret = "ilovemypizza!";
7
8
const authorize = (
9
req: AuthenticatedRequest,
10
res: Response,
11
next: NextFunction
12
) => {
13
if (!req.session) {
14
return res.sendStatus(401);
15
}
16
const { token } = req.session;
17
if (!token) {
18
return res.sendStatus(401);
19
}
20
21
try {
22
const decoded = jwt.verify(token, jwtSecret) as JwtPayload;
23
const { username } = decoded;
24
25
const existingUser = readOneUserFromUsername(username);
26
27
if (!existingUser) {
28
return res.sendStatus(401);
29
}
30
31
req.user = existingUser; // request.user object is available in all other middleware functions
32
return next();
33
} catch (err) {
34
console.error("authorize: ", err);
35
return res.sendStatus(401);
36
}
37
};
38
39
const isAdmin: RequestHandler = (req: AuthenticatedRequest, res, next) => {
40
const { username } = req.user as User;
41
42
if (username !== "admin") {
43
return res.sendStatus(403);
44
}
45
return next();
46
};
47
48
export { authorize, isAdmin };

🍬 Test via REST Client d'une RESTful API attendant des cookies

Il nous reste à tester nos requêtes via REST Client.

Il n'y a pas de nouvelles notions à apprendre pour utiliser REST Client avec des cookies : le comportement par défaut de REST Client, lorsqu'un cookie est renvoyé dans une réponse, est d'inclure ce cookie dans chaque requête vers la même origine.

Dès lors, pour tester l'API, il suffit d'enlever tous les authorization headers et de rajouter une requête pour tester l'effacement d'une session.

Veuillez tester les requêtes à l'aide de /REST Client/pizzas.http :

http
######### NORMAL OPERATION ###########
### Read all pizzas
GET http://localhost:3000/pizzas
### Read all pizzas with File variable
@baseUrl = http://localhost:3000
GET {{baseUrl}}/pizzas
### Read all pizzas sorted by title (ascending)
GET {{baseUrl}}/pizzas/?order=+title
### Read all pizzas sorted by title (descending)
GET {{baseUrl}}/pizzas/?order=-title
### Read pizza identified by 2
GET {{baseUrl}}/pizzas/2
### Create a pizza by using the admin account
#### First login as the admin
POST {{baseUrl}}/auths/login
Content-Type: application/json
{
"username":"admin",
"password":"admin"
}
#### Create a pizza with the admin token
POST {{baseUrl}}/pizzas
Content-Type: application/json
{
"title":"Magic Green",
"content":"Epinards, Brocolis, Olives vertes, Basilic"
}
### Delete pizza identified by 2 with the admin token
DELETE {{baseUrl}}/pizzas/2
### Update the pizza identified by 6 with the admin token
PATCH {{baseUrl}}/pizzas/6
Content-Type: application/json
{
"title":"Magic Green 2"
}
######### ERROR OPERATION ###########
### 1. Create a pizza without a token
POST {{baseUrl}}/pizzas
Content-Type: application/json
{
"title":"Magic Green",
"content":"Epinards, Brocolis, Olives vertes, Basilic"
}
### 2. Create a pizza without being the admin, use manager account
#### 2.1 First login as the manager
POST {{baseUrl}}/auths/login
Content-Type: application/json
{
"username":"manager",
"password":"manager"
}
#### 2.2 Try to create a pizza with the manager token
POST {{baseUrl}}/pizzas
Content-Type: application/json
{
"title":"Magic Green",
"content":"Epinards, Brocolis, Olives vertes, Basilic"
}
### Read pizza which does not exists
GET {{baseUrl}}/pizzas/100
### Create a pizza which lacks a property
POST {{baseUrl}}/pizzas
Content-Type: application/json
{
"content":"Epinards, Brocolis, Olives vertes, Basilic"
}
### Create a pizza without info for a property
POST {{baseUrl}}/pizzas
Content-Type: application/json
{
"title":"",
"content":"Epinards, Brocolis, Olives vertes, Basilic"
}
### Update for a pizza which does not exist
PATCH {{baseUrl}}/pizzas/200
Content-Type: application/json
{
"title":"Magic Green 2"
}
### Update for a pizza which does not provide any info for a property
PATCH {{baseUrl}}/pizzas/1
Content-Type: application/json
{
"title":"Magic Green 2",
"content":""
}

Veuillez mettre à jour /REST Client/auths.http pour tester GET /auths/logout :

http
### Logout any user
GET {{baseUrl}}/auths/logout

Pour ajouter une pizza, il suffit juste :

  1. De loguer l'admin.
  2. De créer une nouvelle pizza ; le cookie est automatiquement envoyé.

Admettons que vous souhaitez tester l'ajout d'une pizza sans envoyer de token :

  1. Lancez l'opération de logout (GET /auths/logout) ; le cookie renvoyé ne contient pas de données de session ;
  2. Tentez la création d'une pizza qui renverra un code 401 Unauthorized.

En cas de souci, vous pouvez utiliser le code du tutoriel cookies.