c) Authentification & utilisation d'opérations protégées par JWT au sein d'une IHM

YoutubeImage

Authentification d'un utilisateur via une IHM & JWT

Pour authentifier un utilisateur via une IHM, il suffit de faire une requête à une RESTful API.

Généralement, l'utilisateur devra d'abord créer son compte. Il utilisera un formulaire demandant au minimum un identifiant (username, adresse e-mail ou autres) et un password.
Dans le cadre d'une SPA, l'IHM fera appel à une opération de type register lorsque l'utilisateur soumet le formulaire.

Par la suite, lorsque le compte de l'utilisateur existe, l'IHM fera appel à une opération de type login lorsque l'utilisateur tentera de se connecter à l'aide d'un formulaire.

Dans les deux cas, register ou login, le développeur devra connaître les opérations mises à disposition par l'API.

Dans le cadre du site de la pizzeria, nous savons que l'API met à disposition ces deux opérations :

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

Pour ce nouveau tutoriel, nous allons continuer le développement de l'IHM async-await-hmi (/web2/tutorials/pizzeria/hmi/async-await) pour que le formulaire de register et de login fassent appelle à l'API et sauve temporairement le token renvoyé.

Au sein de votre repo web2, veuillez créer le projet nommé /web2/tutorials/pizzeria/hmi/jwt-fetch sur base d'un copié/collé de /web2/tutorials/pizzeria/hmi/async-await (ou async-await-hmi).

Veuillez démarrer la version /web2/tutorials/pizzeria/api/safe de la RESTful API de la pizzeria. En cas de souci, vous pouvez utiliser ce code-ci : api-safe.

Pour la suite du tutoriel, nous considérons que tous les chemins absolus démarrent du répertoire /web2/tutorials/pizzeria/hmi/jwt-fetch.

Veuillez mettre à jour le code de la RegisterPage afin de faire appel à la méthode POST /auths/register et, si tout est OK, rediriger l'utilisateur vers la HomePage via la fonction Navigate :

js
1
import { clearPage, renderPageTitle } from '../../utils/render';
2
import Navigate from '../Router/Navigate';
3
4
const RegisterPage = () => {
5
clearPage();
6
renderPageTitle('Register');
7
renderRegisterForm();
8
};
9
10
function renderRegisterForm() {
11
const main = document.querySelector('main');
12
const form = document.createElement('form');
13
form.className = 'p-5';
14
const username = document.createElement('input');
15
username.type = 'text';
16
username.id = 'username';
17
username.placeholder = 'username';
18
username.required = true;
19
username.className = 'form-control mb-3';
20
const password = document.createElement('input');
21
password.type = 'password';
22
password.id = 'password';
23
password.required = true;
24
password.placeholder = 'password';
25
password.className = 'form-control mb-3';
26
const submit = document.createElement('input');
27
submit.value = 'Register';
28
submit.type = 'submit';
29
submit.className = 'btn btn-danger';
30
form.appendChild(username);
31
form.appendChild(password);
32
form.appendChild(submit);
33
main.appendChild(form);
34
form.addEventListener('submit', onRegister);
35
}
36
37
async function onRegister(e) {
38
e.preventDefault();
39
40
const username = document.querySelector('#username').value;
41
const password = document.querySelector('#password').value;
42
43
const options = {
44
method: 'POST',
45
body: JSON.stringify({
46
username,
47
password,
48
}),
49
headers: {
50
'Content-Type': 'application/json',
51
},
52
};
53
54
const response = await fetch('/api/auths/register', options);
55
56
if (!response.ok) throw new Error(`fetch error : ${response.status} : ${response.statusText}`);
57
58
const authenticatedUser = await response.json();
59
60
console.log('Newly registered & authenticated user : ', authenticatedUser);
61
62
Navigate('/');
63
}
64
65
export default RegisterPage;

Veuillez exécuter le frontend et vous assurer que l'utilisateur que vous tentez de créer est bien créé par votre API. Si tout fonctionne, vous aurez une confirmation dans la console de votre browser.

Bien, nous souhaitons pour l'instant sauvegarder authenticatedUser de manière temporaire, car cet objet contient comme info le username et le token de l'utilisateur.

💭 Comment faire pour que cet objet soit disponible dans d'autres modules ? Cette question nous permet d'explorer un propriété importante des export d'objets via ECMAScript : ce sont des objets immuables, c'est-à-dire dont vous ne pouvez pas directement changer leurs valeurs. Comment faire alors pour offrir authenticatedUser ?
Nous allons offrir quatre nouvelles fonctions :

  • une fonction qui renverra l'état de la variable authenticatedUser ;
  • une fonction qui permettra de modifier la variable authenticatedUser ;
  • une fonction qui indiquera si l'utilisateur est authentifié ou pas ;
  • une fonction qui permettra de faire un reset de l'utilisateur en cours (on utilisera cette fonction lors d'un logout).

Veuillez créer le fichier /src/utils/auths.js et y ajouter le code suivant :

js
1
let currentUser;
2
3
const getAuthenticatedUser = () => currentUser;
4
5
const setAuthenticatedUser = (authenticatedUser) => {
6
currentUser = authenticatedUser;
7
};
8
9
const isAuthenticated = () => currentUser !== undefined;
10
11
const clearAuthenticatedUser = () => {
12
currentUser = undefined;
13
};
14
15
// eslint-disable-next-line object-curly-newline
16
export { getAuthenticatedUser, setAuthenticatedUser, isAuthenticated, clearAuthenticatedUser };

Veuillez mettre à jour le code de la RegisterPage afin de sauver en mémoire vive l'utilisateur authentifié. Nous allons aussi préparer la suite afin d'avoir une Navbar qui s'adaptera lorsqu'un utilisateur est authentifié, c'est pourquoi nous allons appeler le composant Navbar :

js
1
import { setAuthenticatedUser } from '../../utils/auths';
2
import { clearPage, renderPageTitle } from '../../utils/render';
3
import Navbar from '../Navbar/Navbar';
4
import Navigate from '../Router/Navigate';
5
6
const RegisterPage = () => {
7
clearPage();
8
renderPageTitle('Register');
9
renderRegisterForm();
10
};
11
12
function renderRegisterForm() {
13
const main = document.querySelector('main');
14
const form = document.createElement('form');
15
form.className = 'p-5';
16
const username = document.createElement('input');
17
username.type = 'text';
18
username.id = 'username';
19
username.placeholder = 'username';
20
username.required = true;
21
username.className = 'form-control mb-3';
22
const password = document.createElement('input');
23
password.type = 'password';
24
password.id = 'password';
25
password.required = true;
26
password.placeholder = 'password';
27
password.className = 'form-control mb-3';
28
const submit = document.createElement('input');
29
submit.value = 'Register';
30
submit.type = 'submit';
31
submit.className = 'btn btn-danger';
32
form.appendChild(username);
33
form.appendChild(password);
34
form.appendChild(submit);
35
main.appendChild(form);
36
form.addEventListener('submit', onRegister);
37
}
38
39
async function onRegister(e) {
40
e.preventDefault();
41
42
const username = document.querySelector('#username').value;
43
const password = document.querySelector('#password').value;
44
45
const options = {
46
method: 'POST',
47
body: JSON.stringify({
48
username,
49
password,
50
}),
51
headers: {
52
'Content-Type': 'application/json',
53
},
54
};
55
56
const response = await fetch('/api/auths/register', options);
57
58
if (!response.ok) throw new Error(`fetch error : ${response.status} : ${response.statusText}`);
59
60
const authenticatedUser = await response.json();
61
62
console.log('Newly registered & authenticated user : ', authenticatedUser);
63
64
setAuthenticatedUser(authenticatedUser);
65
66
Navbar();
67
68
Navigate('/');
69
}
70
71
export default RegisterPage;

Pour la LoginPage, les modifications à faire sont les mêmes :

js
1
import { setAuthenticatedUser } from '../../utils/auths';
2
import { clearPage, renderPageTitle } from '../../utils/render';
3
import Navbar from '../Navbar/Navbar';
4
import Navigate from '../Router/Navigate';
5
6
const LoginPage = () => {
7
clearPage();
8
renderPageTitle('Login');
9
renderRegisterForm();
10
};
11
12
function renderRegisterForm() {
13
const main = document.querySelector('main');
14
const form = document.createElement('form');
15
form.className = 'p-5';
16
const username = document.createElement('input');
17
username.type = 'text';
18
username.id = 'username';
19
username.placeholder = 'username';
20
username.required = true;
21
username.className = 'form-control mb-3';
22
const password = document.createElement('input');
23
password.type = 'password';
24
password.id = 'password';
25
password.required = true;
26
password.placeholder = 'password';
27
password.className = 'form-control mb-3';
28
const submit = document.createElement('input');
29
submit.value = 'Login';
30
submit.type = 'submit';
31
submit.className = 'btn btn-danger';
32
form.appendChild(username);
33
form.appendChild(password);
34
form.appendChild(submit);
35
main.appendChild(form);
36
form.addEventListener('submit', onLogin);
37
}
38
39
async function onLogin(e) {
40
e.preventDefault();
41
42
const username = document.querySelector('#username').value;
43
const password = document.querySelector('#password').value;
44
45
const options = {
46
method: 'POST',
47
body: JSON.stringify({
48
username,
49
password,
50
}),
51
headers: {
52
'Content-Type': 'application/json',
53
},
54
};
55
56
const response = await fetch('/api/auths/login', options);
57
58
if (!response.ok) throw new Error(`fetch error : ${response.status} : ${response.statusText}`);
59
60
const authenticatedUser = await response.json();
61
62
console.log('Authenticated user : ', authenticatedUser);
63
64
setAuthenticatedUser(authenticatedUser);
65
66
Navbar();
67
68
Navigate('/');
69
}
70
71
export default LoginPage;

Nous souhaitons maintenant faire en sorte que la Navbar affiche des éléments différents si l'utilisateur est authentifié ou pas :

  • s'il est authentifié : on affiche Home, Login, Register et le username de l'utilisateur connecté.
  • s'il est anonyme : on affiche Home, Add a pizza, Logout.

Nous allons mettre à jour la Navbar pour s'adapter à l'authentification d'un utilisateur :

js
// eslint-disable-next-line no-unused-vars
import { Navbar as BootstrapNavbar } from 'bootstrap';
import { getAuthenticatedUser, isAuthenticated } from '../../utils/auths';
const Navbar = () => {
renderNavbar();
};
function renderNavbar() {
const authenticatedUser = getAuthenticatedUser();
const anonymousUserNavbar = `
<nav class="navbar navbar-expand-lg navbar-light bg-danger">
<div class="container-fluid">
<a class="navbar-brand" href="#">e-Pizzeria</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#" data-uri="/">Home</a>
</li>
<li id="loginItem" class="nav-item">
<a class="nav-link" href="#" data-uri="/login">Login</a>
</li>
<li id="registerItem" class="nav-item">
<a class="nav-link" href="#" data-uri="/register">Register</a>
</li>
</ul>
</div>
</div>
</nav>
`;
const authenticatedUserNavbar = `
<nav class="navbar navbar-expand-lg navbar-light bg-danger">
<div class="container-fluid">
<a class="navbar-brand" href="#">e-Pizzeria</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#" data-uri="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-uri="/add-pizza">Add a pizza</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-uri="/logout">Logout</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">${authenticatedUser?.username}</a>
</li>
</ul>
</div>
</div>
</nav>
`;
const navbar = document.querySelector('#navbarWrapper');
navbar.innerHTML = isAuthenticated() ? authenticatedUserNavbar : anonymousUserNavbar;
}
export default Navbar;

Ce code est quasi entièrement neuf. On a rajouté l'élément qui permettra de réaliser le logout d'un utilisateur. Avant d'aller plus loin, veuillez tester l'application.
Loguez-vous avec l'utilisateur admin (et le password admin) et vérifiez que vous êtes bien redirigé vers la HomePage une fois authentifié, que la Navbar contient bien les éléments attendus, dont le username.
Veuillez maintenant tester un clic sur l'élément Add a pizza de la Navbar. Il ne se passe rien...

💭 Pourquoi les clics ne sont plus pris en compte ?
En fait, regardons le code de notre Router, pour la gestion des clics sur la Navbar :

js
function onNavBarClick() {
const navItems = document.querySelectorAll('.nav-link');
navItems.forEach((item) => {
item.addEventListener('click', (e) => {
e.preventDefault();
const uri = e.target?.dataset?.uri;
const componentToRender = routes[uri];
if (!componentToRender) throw Error(`The ${uri} ressource does not exist.`);
componentToRender();
window.history.pushState({}, '', uri);
});
});
}

Tant dans LoginPage que dans RegisterPage, nous faisons appel à la fonction Navbar qui fait un "rerender" (réaffichage) de la Navbar. Ainsi, tous les éléments de la barre de navigation sont "rerender", et donc comme le Router n'est pas réappelé, la fonction onNavBarClick n'est pas réexécutée. Ainsi, même si les nouveaux éléments de la Navbar ont la classe CSS nav-link, leurs écouteurs d'événements de type click n'existent plus.

💭 Comment corriger cela ?
On pourrait se dire qu'il suffit de faire appel à la fonction onNavBarClick dans la Navbar. Cette solution ne fonctionnerait pas car nous aurions des dépendances cycliques, la Navbar devrait faire appel aux routes, qui elles font appel aux pages, les pages faisant appel à la Navbar...
Dès lors, le mieux serait de mettre un écouteur d'événements au niveau du wrapper de la Navbar.
Comme le wrapper n'est jamais réinitialisé, tout sera en ordre.
Veuillez donc mettre à jour la fonction onNavBarClick de /src/Components/Router/Router.js :

js
function onNavBarClick() {
const navbarWrapper = document.querySelector('#navbarWrapper');
navbarWrapper.addEventListener('click', (e) => {
e.preventDefault();
const navBarItemClicked = e.target;
const uri = navBarItemClicked?.dataset?.uri;
if (uri) {
const componentToRender = routes[uri];
if (!componentToRender) throw Error(`The ${uri} ressource does not exist.`);
componentToRender();
window.history.pushState({}, '', uri);
}
});
}

Veuillez noter que le navbarWrapper est initialisé au sein du composant Header.
Dans la nouvelle version du code du Router, le gestionnaire de clics est ajouté au niveau de ce wrapper. Grâce à l'event objet e, on accède à l'élément sur lequel on a cliqué grâce à la propriété target. On retrouve donc l'élément de la navbar sur lequel on a cliqué très facilement.

Veuillez tester votre IHM et vérifier qu'une fois logué, vous puissiez bien voyager entre les pages.

Il reste maintenant à créer un composant permettant de faire un logout. On souhaite que ce composant de logout supprime l'utilisateur authentifié, réaffiche la Navbar pour un utilisateur anonyme et redirige l'utilisateur vers la page de login.

Pour ce faire, veuillez créer le dossier et le fichier /src/Components/Logout/Logout.js et y ajouter ce code :

js
import { clearAuthenticatedUser } from '../../utils/auths';
import Navbar from '../Navbar/Navbar';
import Navigate from '../Router/Navigate';
const Logout = () => {
clearAuthenticatedUser();
Navbar();
Navigate('/login');
};
export default Logout;

Attention, même si nous avons mis à jour la Navbar et créé le composant Logout, le boilerplate du frontend impose de rajouter une route au sein du Router pour qu'un clic sur l'élément Logout de la Navbar amène à appeler le composant Logout. Veuillez donc mettre à jour routes au sein de /src/Components/Router/Router.js :

js
import Logout from '../Logout/Logout';
import AddPizzaPage from '../Pages/AddPizzaPage';
import HomePage from '../Pages/HomePage';
import LoginPage from '../Pages/LoginPage';
import RegisterPage from '../Pages/RegisterPage';
const routes = {
'/': HomePage,
'/login': LoginPage,
'/register': RegisterPage,
'/add-pizza': AddPizzaPage,
'/logout': Logout,
};

Veuillez maintenant tester le login et le logout. Tout devrait être fonctionnel !
Il nous reste à faire en sorte que l'on puisse autoriser l'opération de création de pizza.

Autorisation de l'appel à une opération protégée

Nous allons maintenant voir comment, à partir d'une IHM, nous pouvons utiliser un token pour accéder à une opération d'une RESTful API.

Veuillez vous assurer que la version /web2/tutorials/pizzeria/api/safe de la RESTful API de la pizzeria est bien démarrée. En cas de souci, vous pouvez utiliser ce code-ci : api-safe.

Veuillez vous connecter à l'IHM du tutoriel en cours (/web2/tutorials/pizzeria/hmi/jwt-fetch) à l'aide du compte manager et tentez d'ajouter une pizza. Cela ne devrait pas fonctionner.

Veuillez regarder dans la console : il devrait y avoir une erreur qui s'affiche avec le "status code" 401 : Unauthorized.
En effet, l'API attend un token pour autoriser l'opération de création d'une pizza.

Nous allons donc mettre à jour /src/Components/Pages/AddPizzaPage.js pour ajouter le token de l'utilisateur authentifié au sein du header de la requête (il n'y a que trois lignes à rajouter) :

js
1
import { getAuthenticatedUser } from '../../utils/auths';
2
import { clearPage, renderPageTitle } from '../../utils/render';
3
import Navigate from '../Router/Navigate';
4
5
const AddPizzaPage = () => {
6
clearPage();
7
renderPageTitle('Add a pizza to the menu');
8
renderAddPizzaForm();
9
};
10
11
function renderAddPizzaForm() {
12
const main = document.querySelector('main');
13
const form = document.createElement('form');
14
form.className = 'p-5';
15
const title = document.createElement('input');
16
title.type = 'text';
17
title.id = 'title';
18
title.placeholder = 'title of your pizza';
19
title.required = true;
20
title.className = 'form-control mb-3';
21
const content = document.createElement('input');
22
content.type = 'text';
23
content.id = 'content';
24
content.required = true;
25
content.placeholder = 'Content of your pizza';
26
content.className = 'form-control mb-3';
27
const submit = document.createElement('input');
28
submit.value = 'Add pizza to the menu';
29
submit.type = 'submit';
30
submit.className = 'btn btn-danger';
31
form.appendChild(title);
32
form.appendChild(content);
33
form.appendChild(submit);
34
main.appendChild(form);
35
form.addEventListener('submit', onAddPizza);
36
}
37
38
async function onAddPizza(e) {
39
e.preventDefault();
40
41
const title = document.querySelector('#title').value;
42
const content = document.querySelector('#content').value;
43
44
const authenticatedUser = getAuthenticatedUser();
45
46
const options = {
47
method: 'POST',
48
body: JSON.stringify({
49
title,
50
content,
51
}),
52
headers: {
53
'Content-Type': 'application/json',
54
Authorization: authenticatedUser.token,
55
},
56
};
57
58
const response = await fetch('/api/pizzas', options);
59
60
if (!response.ok) throw new Error(`fetch error : ${response.status} : ${response.statusText}`);
61
62
const newPizza = await response.json();
63
64
console.log('New pizza added : ', newPizza);
65
66
Navigate('/');
67
}
68
69
export default AddPizzaPage;

Veuillez vous connecter à l'IHM à l'aide du compte manager et tenter d'ajouter une pizza. Cela ne devrait toujours pas fonctionner. Veuillez regarder dans la console : il devrait y avoir une erreur qui s'affiche avec le "status code" 403 : Forbidden.
En effet, l'API attend un token pour autoriser l'opération de création d'une pizza, mais seulement admin a le privilège d'ajouter une pizza au menu !

Déconnectez-vous (logout), reconnectez-vous à l'aide du compte admin, et ajoutez une pizza. Voila ! Le site devrait être entièrement fonctionnel !

Si tout fonctionne bien, faites un commit de votre repo (web2) avec comme message : jwt-fetch-hmi tutorial.

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

💭 Est-ce que c'est "safe" que notre IHM affiche le menu "Add a pizza" pour un utilisateur qui n'est pas l'admin ?
En fait oui, c'est "safe", vous l'avez testé. L'API ne doit jamais faire confiance aux applications clientes pour appliquer la sécurité. Ainsi, même si le frontend autorise l'accès à des opérations qui ne devraient pas être permises, au regard des autorisations appliquées par l'API, ça n'a pas d'importance point de vue sécurité.
De la même façon, c'est pour ça qu'une API doit aussi toujours valider les paramètres qu'elle reçoit. Elle ne peut pas faire confiance aux applications clientes, comme par exemple à une application web tournant dans un browser, pour valider tous les champs d'un formulaire.
La raison est simple, l'API est développée indépendamment des applications clientes, elle ne peut pas supposer que les requêtes seront toujours bien construites.

💭 OK, tout est "safe" si l'API fait toutes les vérifications nécessaires. Néanmoins, n'y a-t-il pas des règles de bonnes pratiques au niveau des IHM, pour ne pas permettre de faire n'importe quelles requêtes vers des API ?
Et bien oui, au niveau des IHM, pour des questions d'ergonomie, d'expérience utilisateur, on va faire en sorte :

  • de ne pas offrir des opérations qui ne seront pas autorisées. Par exemple, dans le cadre de ce tutoriel sur un site permettant de gérer une pizzeria, il ne faut pas "frustrer" les utilisateurs en leur faisant croire qu'ils ont accès à l'opération de créer une pizza ! Imaginez-vous, vous créez une nouvelle pizza de 32 ingrédients, et lors de la soumission, vous recevez un message comme quoi vous n'êtes pas l'admin du site et que vous n'avez donc pas le droit de créer une pizza 😲!
  • de ne pas demander du travail à une API quand l'IHM peut détecter que ce n'est pas utile. Ainsi, quand une IHM offre des formulaires, qui amèneront à des requêtes vers des API, on évitera d'autoriser la soumission des données tant que les champs n'ont pas été validés. Tout ce que l'IHM peut valider côté client, elle doit le faire. Le feedback sera plus rapide pour l'utilisateur, et les ressources de l'API seront économisées (pas d'appel inutile).

N'hésitez donc pas à mettre à jour ce tutoriel pour faire en sorte de n'afficher "Add a pizza" que si l'utilisateur est admin.

💭 Ca n'est pas un peu "cheap" que seul l'utilisateur admin puisse avoir le privilège d'administrateur du site ?
Hé bien oui, c'est "cheap". Généralement, dans le cadre d'applications plus robustes, nous allons ajouter un ou plusieurs rôle(s) aux utilisateurs. Par exemple, dans le cadre d'applications où les rôles sont simples, qu'il n'y a jamais qu'un seul rôle associé à un utilisateur, il suffirait d'ajouter au niveau de l'API la propriété role aux utilisateurs. La majorité des utilisateurs pourrait avoir un rôle dont la valeur serait default, et une minorité d'utilisateur auraient le rôle d'admin...

On n'affiche actuellement pas de message d'erreur à l'utilisateur lorsque la réponse d'une API renvoie une erreur. Pour améliorer l'expérience de l'utilisateur, ce serait une amélioration à faire.

Finalement, lorsqu'on ferme le browser et revient sur l'application par la suite, on n'est plus authentifié. Nous allons donc prochainement voir comment nous pourrions sauvegarder des données de session (le token et le username) côté-client, dans le browser.

Mise en place du localStorage pour sauvegarder les données de session

Dans le cadre du site nous permettant de gérer une pizzeria, nous allons faire en sorte de sauvegarder les données de session au sein du localStorage, et plus juste en mémoire vive. Nous allons appliquer ce que nous avons appris dans la partie sur Les sessions côté client.

Veuillez démarrer la version /web2/tutorials/pizzeria/api/safe de la RESTful API de la pizzeria. En cas de souci, vous pouvez utiliser ce code-ci : api-safe.

Pour ce nouveau tutoriel, au sein de votre repo web2, veuillez créer le projet nommé /web2/tutorials/pizzeria/hmi/web-storage sur base d'un copier/coller de /web2/tutorials/pizzeria/hmi/jwt-fetch (ou jwt-fetch-hmi).

Pour la suite du tutoriel, nous considérons que tous les chemins absolus démarrent du répertoire /web2/tutorials/pizzeria/hmi/web-storage.

Afin de sauvegarder les données de session, c'est à dire l'objet authenticatedUser contenant un token et un username, nous devons juste mettre à jour le fichier /usr/utils/auths.js :

js
1
const STORE_NAME = 'user';
2
let currentUser;
3
4
const getAuthenticatedUser = () => {
5
if (currentUser !== undefined) return currentUser;
6
7
const serializedUser = localStorage.getItem(STORE_NAME);
8
if (!serializedUser) return undefined;
9
10
currentUser = JSON.parse(serializedUser);
11
return currentUser;
12
};
13
14
const setAuthenticatedUser = (authenticatedUser) => {
15
const serializedUser = JSON.stringify(authenticatedUser);
16
localStorage.setItem(STORE_NAME, serializedUser);
17
18
currentUser = authenticatedUser;
19
};
20
21
const isAuthenticated = () => currentUser !== undefined;
22
23
const clearAuthenticatedUser = () => {
24
localStorage.removeItem(STORE_NAME);
25
currentUser = undefined;
26
};
27
28
// eslint-disable-next-line object-curly-newline
29
export { getAuthenticatedUser, setAuthenticatedUser, isAuthenticated, clearAuthenticatedUser };

Au sein de getAuthenticatedUser :

  • on fait un premier check afin d'éviter d'aller lire dans le localStorage si la variable currentUser est déjà initialisée.
  • on parse l'utilisateur authentifié et sérialisé qui est retrouvé dans le localStorage via la clé STORE_NAME.

Au sein de setAuthenticatedUser, on sérialise authenticatedUser avant d'ajouter une paire clé/valeur au localStorage. Ces données restent dans le browser, peu importe le nombre de fois que l'on redémarre son browser.

Dans clearAuthenticatedUser, on efface la paire clé/valeur associée à l'utilisateur authentifié (via la clé STORE_NAME).

Veuillez bien mettre à jour votre code et tester l'application.
Connectez-vous à l'aide de l'utilisateur manager. Veuillez fermer votre browser.
Veuillez le réouvrir.
Vous devriez automatiquement être authentifié : veuillez observer l'état de la Navbar pour vous en assurer.

💭 Où puis-je observer l'état des données sauvegardées dans le web storage de mon browser ?
Tout en ayant la fenêtre de votre application ouverte, via Chrome, allez dans vos outils de développeurs : F12.
Puis, dans l'onglet Application, vous trouverez dans Storage : Local Storage et Session Storage.
Ici, nous utilisons le Local Storage, donc cliquez dessus, vous verrez apparaître http://localhost:8080. Cliquez sur cette URL, et vous verrez vos données de session, quelque chose du style {"username":"manager","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1hbmFnZXIiLCJpYXQiOjE2NjE3NjM3MDksImV4cCI6MTc0ODE2MzcwOX0.jAxH0WsOgiK5vf4QduDZ8JgTR-SKC42G9aPieV_OTOo"}.
N'hésitez pas à faire un clic droit sur l'URL http://localhost:8080, puis Clear.
Si vous faites ensuite un refresh de votre page, comme votre session aura été effacée, votre utilisateur ne sera plus connecté. Votre Navbar affichera le menu pour un utilisateur anonyme !
Faites ce test, c'est intéressant 😉.

Si tout fonctionne bien, faites un commit de votre repo (web2) avec comme message : web-storage-hmi tutorial.

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

Projet 4.3 : Authentification et appel d'opérations protégées par JWT

Mise en place du projet

Vous devez mettre à jour votre frontend afin qu'il consomme les nouvelles opérations de votre API qui ont été développées pour Projet 4.2.

Le code de votre frontend doit se trouver dans votre repository local et votre web repository (normalement appelé web2) dans le répertoire nommé /project/4.3/hmi sur base d'un copier/coller du code de Projet 3.1.

Authentification et appel d'opérations nécessitant une autorisation JWT

Même si votre projet ne nécessite pas d'authentification, afin d'apprendre les concepts associés à "JWT auths", veuillez créer un prototype de frontend introduisant ces cas d'utilisation :

  • register : les utilisateurs doivent pouvoir créer un compte.
  • login : les utilisateur doivent pouvoir se loguer.
  • logout : les utilisateurs doivent pouvoir se déconnecter.

Pensez à bien mettre à jour votre Navbar pour afficher les bons menus en fonction que l'utilisateur est authentifié ou pas.

Faites attention, il n'est pas autorisé, pour des raisons d'ergonomie, que le frontend offre les fonctionnalités d'écriture de ressources pour les utilisateur non authentifiés. Vous devez donc rendre invisible les opérations non autorisées aux utilisateurs.

Dans un premier temps, veuillez faire en sorte de rajouter dans le web storage ces nouvelles données de session lors du login ou du register : le token et le username.
NB : précédemment vous aviez des données de session à gérer pour le thème et pour un message concernant le respect de la vie privée.

Quand votre prototype de frontend offre l'authentification (register & login) et utilise toutes les opérations sécurisées de votre API, que les sessions sont bien sauvegardées, veuillez faire un commit de votre code avec comme message : 4.3.1 : hmi jwt auths.

🤝 Tips

  • N'hésitez pas à copier/coller les fichiers utiles trouvés dans le tutoriel associé à cette partie : RegisterPage, LoginPage, Logout, auths, Navbar (différent affichage pour un utilisateur anonyme que pour un utilisateur connecté)....
  • Attention à ne pas oublier de mettre à jour votre Router : nouvelles pages pour le register, le login et le logout, la Navbar...
  • Ajoutez les tokens dans le header de vos requêtes fetch...
  • Concernant le Router, il faut mettre à jour onNavBarClick pour que les gestionnaires d'evénéments restent attachés même quand la Navbar est réaffichée.
  • 💭 Comment rendre invisible les opérations d'écriture de certaines ressources au sein du frontend ?
    Par exemple, vous pourriez afficher des boutons de type Delete ou Save que si l'utilisateur est authentifié.
    De même, si l'utilisateur est anonyme, les ressources ne devraient pas être éditables.

🍬 Gestion de session & remember me

Vous pourriez ajouter une fonction "remember me" à votre formulaire de "login" et de "register" et faire en sorte que vos données de session soient sauvegardées :

  • dans le localStorage si l'on clique sur une checkbox "Remember me" ;
  • dans le sessionStorage si l'on ne clique pas sur la checkbox "Remember me" lors du login ou du register.

Quand votre prototype de frontend est finalisé, veuillez faire un commit de votre code avec comme message : 4.3.2 : remember me.

🤝 Tips

  • Vous allez pouvoir refaire un peu de gestion d'événements pour détecter les clics sur une checkbox. N'hésitez pas à voir ce que propose Bootstrap 5 pour les checkboxes.
  • Prenez un moment pour voir comment gérer la persistance de l'info 'Remember me'...
    Est-ce que ce n'est pas une donnée de session qui doit persister lorsque l'utilisateur ferme son browser ?
    En effet, point de vue ergonomie, il est intéressant que le dernier choix de l'utilisateur soit toujours présenté. La checkbox devrait donc rester checked ou pas, tant que l'utilisateur ne change pas son état, via un clic, ou via un Logout ; et cet état doit subsister aux travers des connexions (ouvertures / fermetures du browser).

🍬 Persistance de données de session via des cookies

Authentification & autorisation JWT à l'aide de cookies

Dans la partie optionnelle sur l'Authentification & autorisation JWT à l'aide de cookies, nous avons vu comment mettre à jour l'API afin d'intégrer les tokens JWT aux cookies.

Veuillez démarrer la version /web2/tutorials/pizzeria/api/cookies de la RESTful API de la pizzeria. En cas de souci, vous pouvez utiliser ce code-ci : api-cookies.

Nous allons voir maintenant comment le frontend peut utiliser ces cookies.

Gestion de session côté client via une IHM et des cookies

Pour ce nouveau tutoriel, nous allons repartir de la dernière version de notre frontend.

Au sein de votre repo web2, veuillez créer le projet nommé /web2/tutorials/pizzeria/hmi/cookies sur base d'un copié/collé de /web2/tutorials/pizzeria/hmi/web-storage (ou web-storage-hmi).

Pour la suite du tutoriel, nous considérons que tous les chemins absolus démarrent du répertoire /web2/tutorials/pizzeria/hmi/cookies.

Afin de sauvegarder les données de session, c'est à dire l'objet authenticatedUser contenant juste un username, nous ne devons même pas mettre à jour le fichier /usr/utils/auths.js. En effet, l'API cookies renvoie un objet du genre {username: "manager"}. Au niveau de l'IHM, le code est donc toujours fonctionnel pour sauvegarder le username grâce à authenticatedUser.

Il ne reste donc qu'à changer le code où nous avons besoin d'une autorisation. Pour l'application de gestion de la pizzeria, il s'agit de la création de pizza.
Veuillez donc mettre à jour /src/Components/AddPizzaPage.js en enlevant ces deux lignes :

  • l'authenticatedUser : const authenticatedUser = getAuthenticatedUser(); et l'import associé (import { getAuthenticatedUser } from '../../utils/auths';),
  • la ligne s'occupant de l'authorization header : Authorization: authenticatedUser.token.

Veuillez tester votre dernière version du frontend. Loguez-vous avec l'utilisateur admin (et le password admin).
Ajoutez une pizza et vérifiez qu'elle s'affiche bien.

💭 Comment vérifier le cookie ?
Tout en ayant la fenêtre de votre application ouverte, via Chrome, allez dans vos outils de développeurs : F12.
Puis, dans l'onglet Application, cliquez sur Cookies, vous verrez apparaître http://localhost:8080. Cliquez sur cette URL, et vous verrez vos 2 cookies de session, user.sig et user.
N'hésitez pas à aller décoder la valeur du cookie user sur base64decode en faisant un copier / coller de Value. Vous devriez voir quelque chose apparaître du style {"username":"manager","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1hbmFnZXIiLCJpYXQiOjE2NjE3NzUxMDgsImV4cCI6MTc0ODE3NTEwOH0.sAZqq6vbrjCCZZoLH-n8hJKBoXJJJ8jEoupk8xKu5WI"} !

Toujours dans l'onglet Application des outils de développeurs de Chrome, faites un clear des cookies : clic droit sur http://localhost:8080, Clear.
Tentez maintenant d'ajouter une pizza... Ca ne fonctionne plus, et c'est bien normal, car il n'y a plus de token qui est envoyé à l'API !

Suite à ces tests, si tout fonctionne bien, faites un commit de votre repo (web2) avec comme message : cookies-hmi tutorial.

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

💭 Notons que cette version de notre frontend pourrait être améliorée. Actuellement, lorsqu'on fait un logout, on n'efface pas le cookie du browser.
Comment feriez vous ?
Vous pourriez par exemple appeler la méthode GET /auths/logout 😉.