diff --git a/server/tp3/package.json b/server/tp3/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..cdc4b3f17ce1ecf4d38eb6437809bb38a1b44f0c
--- /dev/null
+++ b/server/tp3/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "user-server",
+  "version": "1.0.0",
+  "description": "",
+  "main": "server.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "start": "node server.js"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "express": "^4.17.1",
+    "express-jwt": "^6.0.0",
+    "jsonwebtoken": "^8.5.1"
+  }
+}
diff --git a/server/tp3/server.js b/server/tp3/server.js
new file mode 100644
index 0000000000000000000000000000000000000000..d6c69509bcc34110302cfb8ebbc593bd391e4095
--- /dev/null
+++ b/server/tp3/server.js
@@ -0,0 +1,97 @@
+const express = require("express");
+const body_parser = require("body-parser");
+const jsonwebtoken = require("jsonwebtoken");
+const expressjwt = require("express-jwt");
+const path = require("path");
+
+const secret = "fze5EVvs:,;hsegFZEQGhtrh,;$:^fz";
+
+const app = express();
+
+// app.use(express.static("../build"));
+app.use(body_parser.json());
+
+app.all("*", (req, res, next) => {
+    res.set("Access-Control-Allow-Origin", "*");
+    res.set("Access-Control-Allow-Credentials", true);
+    res.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, DELETE, PUT");
+    res.set(
+        "Access-Control-Allow-Headers",
+        "X-Requested-With, Content-Type, Authorization"
+    );
+    if (req.method === "OPTIONS") {
+        return res.status(200).end();
+    } else {
+        next();
+    }
+});
+
+const users = [
+    {
+        id: 1,
+        username: "pierre",
+        password: "kraemer"
+    },
+    {
+        id: 2,
+        username: "toto",
+        password: "tutu"
+    }
+];
+let nextid = 3;
+
+app.post("/signin", (req, res, next) => {
+    const user = users.find(u => u.username === req.body.username);
+    if (user?.password === req.body.password) {
+        const token = jsonwebtoken.sign({ id: user.id }, secret, {
+            algorithm: 'HS256',
+            expiresIn: 60 * 60 * 12
+        });
+        const { password, ...user_without_pw } = user;
+        return res.json({
+            user: user_without_pw,
+            token
+        });
+    }
+    next({ status: 401, message: "Bad username or password" });
+});
+
+app.get("/whoami", expressjwt({ secret, algorithms: ['HS256'] }), (req, res, next) => {
+    const user = users.find(u => u.id === req.user.id);
+    if (user) {
+        const { password, ...user_without_pw } = user;
+        return res.json(user_without_pw);
+    }
+    next({ status: 404, message: "User not found" });
+});
+
+app.post("/signup", (req, res, next) => {
+    const available = !users.some(u => u.username === req.body.username);
+    if (available) {
+        users.push({
+            id: nextid++,
+            username: req.body.username,
+            password: req.body.password
+        });
+        return res.status(200).end();
+    }
+    next(new Error("Username not available"));
+});
+
+// app.all("/*", (req, res) => {
+//     res.sendFile(path.resolve("../build/index.html"));
+// });
+
+app.use((err, req, res, next) => {
+    if (err.status) {
+        return res.status(err.status).send(err.message);
+    } else {
+        return res.status(500).send(err.message);
+    }
+});
+
+const server = app.listen(4200, "localhost", () => {
+    const host = server.address().address;
+    const port = server.address().port;
+    console.log("App listening at http://%s:%s", host, port);
+});
diff --git a/sujets/sujet2.md b/sujets/sujet2.md
index 9621c6b0e7d219da3949640fab7d86093c593585..be684c87cb39ed5e73892a7b505be65d342a6767 100644
--- a/sujets/sujet2.md
+++ b/sujets/sujet2.md
@@ -37,7 +37,7 @@ Comme pour la liste de personnages, en attendant la réponse du serveur, le comp
 
 S'assurer que la liste de films est bien mise à jour lors de la sélection d'un nouveau personnage.
 
-V3
+<!-- V3
 ---
 
 Ecrire un custom hook `useDataFromUrl`.
@@ -47,13 +47,13 @@ En interne, cette fonction déclare les éléments de `state` nécessaire, et d
 
 Utiliser ce custom hook dans l'ensemble des composants qui font des requêtes.
 
-Comment faire pour que l'on puisse passer un tableau d'URL à la fonction `useDataFromUrl`, et que le champ `data` obtenu soit un tableau contenant les données obtenue depuis chaque URL ?
+Comment faire pour que l'on puisse passer un tableau d'URL à la fonction `useDataFromUrl`, et que le champ `data` obtenu soit un tableau contenant les données obtenue depuis chaque URL ? -->
 
-V4
+V3
 ---
 
 Utiliser la bibliothèque [react-query](https://react-query.tanstack.com/) pour gérer les requêtes à l'API (à la place de notre custom hook).
-Bien lire la documentation sur les requêtes (https://react-query.tanstack.com/docs/guides/queries) et s'assurer d'avoir compris le fonctionnement des `keys` et le passage de paramètres à la fonction asynchrone de récupération des données.
+Bien lire la documentation sur les requêtes (https://react-query.tanstack.com/docs/guides/queries) et s'assurer d'avoir compris le fonctionnement des `keys`.
 
 > ___Indication___ : pour récupérer un tableau d'identifiants de films à partir d'un tableau d'URL du type `https://swapi.dev/api/films/3` -> `const filmsId = filmsUrl.map(u => u.split('/').filter(Boolean).pop());`.
 
diff --git a/sujets/sujet3.md b/sujets/sujet3.md
new file mode 100644
index 0000000000000000000000000000000000000000..c38b5afd11953e0cc4c8166149c5b25427758521
--- /dev/null
+++ b/sujets/sujet3.md
@@ -0,0 +1,53 @@
+Authentification, routage et contextes
+===
+
+Ecrire une application qui gère la connexion d'un utilisateur en lien avec une API HTTP distante.
+
+Lire la [doc de `react-router`](https://reacttraining.com/react-router/web/guides/quick-start).
+
+V1
+---
+
+L'application affiche un menu (donné par un composant `Menu`) contenant les entrées suivantes :
+ - Home (qui mène à la route "/")
+ - si l'utilisateur n'est pas connecté :
+   - Signin (qui mène à la route "/signin")
+   - Signup (qui mène à la route "/signout")
+ - si l'utilisateur est connecté :
+    - le nom de l'utilisateur connecté ("Connected as ...")
+    - Signout (qui appelle la logique de déconnexion)
+
+A chaque route ("/", "/signin", "/signup") correspond le rendu d'un composant particulier (`Home`, `Signin`, `Signup`).
+
+Les informations relatives à l'utilisateur connecté sont maintenues dans le composant principal (seul à contenir la logique de connexion ainsi que la connaissance de l'API) :
+ - Un objet `user` dans le state (initialisé à `null`) contenant l'utilisateur actuellement connecté
+ - Une fonction `signin` qui permet de soumettre un couple (identifiant, mot de passe) à l'API distante qui, en cas de succès, renvoie un JSON Web Token (JWT) ainsi qu'un objet correspondant au `user` authentifié. Le token doit alors être stocké en `localStorage` et l'utilisateur peut être emmené vers la route "/".
+ - Une fonction `signup` qui permet de soumettre un couple (identifiant, mot de passe) pour inscription. En cas de succès, on emmène l'utilisateur vers la route "/signin".
+
+Les composants `Signin` et `Signup` proposent un formulaire adéquat et font appel, lors de la soumission du formulaire, à une fonction `onSubmit` reçue en prop qui fait à son tour appel respectivement aux fonctions `signin` et `signup` du composant principal.
+Faire en sorte qu'en cas d'échec des requêtes, le message d'erreur reçu s'affiche sous le formulaire.
+
+Au lancement de l'application, faire en sorte de restaurer l'utilisateur connecté en exploitant le JSON Web Token présent en `localStorage` si il existe (voir la route `/whoami` de l'API).
+En attendant la réponse du serveur, ne rendre qu'un message d'attente.
+
+API
+---
+
+L'API HTTP fournie comprend les routes suivantes :
+ - `POST /signin` : reçoit un objet de la forme `{ username: '...', password: '...' }`. En cas de succès, renvoie une réponse HTTP code 200 avec un objet de la forme `{ token: '...', user: { id: 42, username: '...' } }`. En cas d'échec, renvoie une réponse HTTP code 401 avec un message d'erreur.
+ - `POST /signup` : reçoit un objet de la forme `{ username: '...', password: '...' }`. En cas de succès, renvoie une réponse HTTP code 200. En cas d'échec, renvoie une réponse HTTP code 500 avec un message d'erreur.
+ - `GET /whoami` : lit l'en-tête `Authorization` de la requête HTTP au format `"Bearer [JSON Web Token]"`. En cas de succès du décodage du token, renvoie une réponse HTTP code 200 avec un objet de la forme `{ id: 42, username: '...' }`. En cas d'échec, renvoie une réponse HTTP code 404 avec un message d'erreur.
+
+V2
+---
+
+Les données concernant l'utilisateur connecté sont potentiellement utiles à différents niveaux de l'application.
+Pour éviter d'avoir à passer ces informations manuellement au travers de nombreuses couches de composants ("props drilling"), on peut mettre en place un contexte, qui donnera accès aux données souhaitées dans tout le sous-arbre de l'application.
+
+Dans un module séparé (`auth.js` par exemple), créer un contexte `AuthContext`, et exporter 2 éléments :
+ - un composant `AuthProvider` dans lequel on va déplacer les données et la logique liées à la gestion de l'utilisateur. Ce composant rend le `Provider` du contexte `AuthContext` en lui passant comme valeur un objet contenant les champs `user`, `signup`, `signin` et `signout`. Le contenu du `Provider` est les éléments fils reçus par le composant `AuthProvider` (`children`).
+ - un custom hook `useAuth` qui retourne simplement le résultat de `useContext(AuthContext)`
+
+Nettoyer le composant principal de l'application qui ne devrait plus contenir que la déclaration du `AuthProvider`, le menu et les routes.
+
+Dans les composants `Menu`, `Signin` et `Signup`, récupérer les données nécessaires du contexte d'authentification grâce à la fonction `useAuth`.