Académique Documents
Professionnel Documents
Culture Documents
front-end
ReactJS/NodeJS : authentification et sécurité
3. Authentification
1. Les différentes méthodes d’authentification
2. Se connecter avec NextJS
3. Exemple
4. Récapitulatif
5. Présentation du projet
Nous avons des notes dans notre base de données, créées lors de nos tests avec Postman. Essayons de les afficher en premier lieu. Dans
notre composant List.js, nous allons supprimer les données en dur et rajouter l’import suivant :
import { useState, useEffect } from 'react'
useEffect(() => {
fetch('http://localhost:3001/notes', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then((res) => res.json())
.then((data) => {
setNotes(data)
})
}, [])
La méthode useEffect() va appeler la route à chaque fois que l’on va accéder au composant, et récupérer avec fetch() les
éléments depuis l’API.
La date n’apparaît plus : c’est normal, le nom du champ est différent dans la base de données. Dans le fichier Note.js nous allons
remplacer la balise time existante par celle-ci :
Maintenant procédons à la création d’une note. Cette fois-ci nous n’utiliserons pas useEffect(). Pourquoi ? Car nous voulons juste déclencher la
route lors du click sur le bouton. Dans AddForm.js, Nous allons ajouter ces imports :
import { useState } from ‘react'
import { useRouter } from "next/navigation";
Et une nouvelle fois, ajouter les états qui nous intéressent : le but ici est de sauvegarder les inputs de l’utilisateur, tant qu’il n’a pas soumis son
formulaire, et de gérer plus facilement les changements. Nous allons également initialiser le routeur qui nous servira pour la suite.
Maintenant nous allons modifier notre JSX et mettre à jour les lignes suivantes :
<form onSubmit={submitForm}>
Ainsi que pour tous les inputs définis, les mettre à jour avec une valeur (value) et une fonction onChange() (voir slides suivantes).
Input title :
<input
type="text"
name="title"
id="title"
className="XXX"
onChange={(e) => setTitle(e.target.value)}
value={title}
/>
Input tag :
<input
type="text"
name="tag"
id="tag"
className="XXX"
onChange={(e) => setTag(e.target.value)}
value={tag}
/>
Textarea note :
<textarea
id="note"
name="note"
rows={3}
className="XXX"
onChange={(e) => setNote(e.target.value)}
value={note}
/>
Maintenant, nous allons créer la fonction submitForm() qui fera l’appel vers l’API une fois que le bouton aura été appuyé :
Nous allons maintenant rajouter de quoi modifier et supprimer une note. En dessous de notre note, nous allons rajouter des actions qui
vont nous permettre de le faire. Cela devrait ressembler à quelque chose comme ça :
Pour ceux qui utilisent Tailwind, rajoutez le code juste en dessous du paragraphe:
<div className="mt-2">
<span className="text-sm cursor-pointer">Modifier</span>
<span className="text-sm ml-3 text-red-500 cursor-pointer">Supprimer</span>
</div>
Nous allons commencer par implémenter la suppression. Toujours dans Note.js, nous allons ajouter les imports suivants :
import { useRouter } from « next/navigation";
Sur notre span « Supprimer » nous allons ajouter une fonction onClick() :
<span onClick={() => deleteNote(note.id)} className="XXX">Supprimer</span>
Pour la modification, nous allons procéder autrement. La solution facile serait de créer un deuxième panel EditForm.js et de l’appeler lorsque l’on clique
sur le bouton. La solution optimale serait de réutiliser AddForm.js (et potentiellement le renommer Form.js) et de gérer les états de façon plus granulaire
depuis index.js. C’est ce que nous allons faire.
Dans index.js nous allons rajouter deux états qui nous permettront de traquer si le formulaire est appelé pour ajouter/modifier et aussi centraliser la note à
ajouter/modifier :
Et nous allons mettre à jour l’appel de nos composant Panel, List et ListHeader :
<Panel
open={open}
setOpen={setOpen}
isUpdate={isUpdate}
currentNote={currentNote}
setCurrentNote={setCurrentNote}
/>
Alexandre TELLE // Master MIAGE SITN // Novembre 2023 13
1. Correction de l’exercice Notes app
Connecter l’API à son frontend
onClick={() => {
setOpen((open) => !open);
setIsUpdate(false);
setCurrentNote({
id: '',
title: '',
note: '',
tag: '',
});
}}
<Note
key={note.id}
note={note}
open={open}
setOpen={setOpen}
setIsUpdate={setIsUpdate}
setCurrentNote={setCurrentNote}
/>
<span
className="text-sm cursor-pointer"
onClick={() => {
setOpen((open) => !open);
setIsUpdate(true);
setCurrentNote({
id: note.id,
title: note.title,
note: note.note,
tag: note.tag,
});
}}
>
Modifier
</span>
Maintenant dernière étape : AddForm.js. On y supprime les états définis auparavant et nous adaptons la définition du composant :
if(isUpdate){
fetchMethod = 'PUT';
url = 'http://localhost:3001/notes/' + currentNote.id;
}
Nous adaptons les onChange() et value de nos inputs. Ci-dessous l’exemple avec title.
onChange={(e) => {
setCurrentNote({
...currentNote,
title: e.target.value,
});
}}
value={currentNote.title}
La sécurité des applications web désigne l’ensemble des pratiques consistant à protéger les sites web, les applications et les API contre les
attaques. Il s'agit d'une vaste discipline, mais ses objectifs ultimes sont de maintenir le bon fonctionnement des applications web et de
protéger les entreprises du cyber vandalisme, du vol de données, de la concurrence déloyale et d'autres conséquences négatives.
Les applications web peuvent être confrontées à plusieurs types d'attaques en fonction des objectifs de l'attaquant, de la nature du travail
de l'organisation ciblée et des lacunes de sécurité particulières de l'application. Les types d'attaques les plus courants sont les suivants :
• Vulnérabilités de type "zero day" : Il s'agit de vulnérabilités inconnues des fabricants d'une application, et qui n'ont donc pas de
correctif disponible. Les hackers cherchent à les exploiter le plus rapidement possible.
• Injection XSS
• Injection SQL
• Attaque DDoS
• Attaque CSRF
• Page scraping : Il s’agit d’utiliser des robots pour "aspirer" le contenu de pages web à grande échelle et ensuite utiliser ce contenu
pour obtenir un avantage tarifaire sur un concurrent, imiter le site à des fins malveillantes, ou pour d'autres raisons.
• Abus d’API : Les attaquants cherchent à envoyer un code malveillant dans l'une des applications ou d'intercepter des données sensibles
lorsqu'elles passent d'une application à l’autre via l’API.
• Credential stuf ng : Les attaquants peuvent utiliser des robots pour saisir rapidement un grand nombre de combinaisons de noms
d'utilisateur et de mots de passe volés dans le portail de connexion d'une application Web.
Alexandre TELLE // Master MIAGE SITN // Novembre 2023 20
fi
2. Sécurité
Attaque CSRF
CSRF ou Cross-Site Request Forgery (falsification de requêtes entre sites). Cette attaque a pour objectif de piéger une victime a n de
l'amener à effectuer une requête utilisant ses identi ants ou ses autorisations. L'exploitation des privilèges de compte d'un utilisateur
permet à un acteur malveillant d'envoyer une requête en se faisant passer pour ce dernier. Une fois le compte d'un utilisateur compromis,
le pirate peut exfiltrer, détruire ou modifier les informations importantes qu'il recèle. Les comptes aux privilèges les plus élevés, comme
ceux des administrateurs ou des cadres, sont fréquemment pris pour cible.
Afin de se prémunir au mieux de ce genre d’attaques, voici quelques mesures qui peuvent être prises :
• Utiliser des tokens CSRF : Un jeton CSRF est une valeur unique, secrète et imprévisible générée par l'application côté serveur et
partagée avec le client. Lorsqu'il tente d'effectuer une action sensible, telle que l'envoi d'un formulaire, le client doit inclure le jeton
CSRF correct dans sa demande. Il est ainsi très difficile pour un pirate de construire une requête valide au nom de la victime.
• Utilisation de cookies SameSite : SameSite est un mécanisme de sécurité du navigateur qui détermine quand les cookies d'un site
web sont inclus dans des requêtes provenant d'autres sites web. Comme les demandes d'exécution d'actions sensibles nécessitent
généralement un cookie de session authentifié, les restrictions SameSite appropriées peuvent empêcher un attaquant de déclencher
ces actions intersites. Depuis 2021, Chrome applique les restrictions SameSite par défaut.
• Validation basée sur le header Referer : Certaines applications utilisent l'en-tête HTTP Referer pour tenter de se défendre contre les
attaques CSRF, normalement en vérifiant que la demande provient du propre domaine de l'application. Cette méthode est
généralement moins efficace que la validation du jeton CSRF. Pour rappel, l'en-tête de requête Referer contient l'adresse de la page
web précédente à partir de laquelle un lien a été suivi pour demander la page courante. L’en-tête Referer permet aux serveurs
d'identifier la provenance des visiteurs d'une page et cette information peut être utilisée à des fins d'analyse, de journalisation ou pour
améliorer la politique de cache par exemple.
XSS ou Cross-Site Scripting : cette faille de sécurité permet à un pirate d'injecter des scripts au sein d'une page web, côté client, dans
le but d'accéder directement à des informations importantes, d'usurper l'identité des utilisateurs ou de piéger ces derniers afin de les
amener à révéler des informations importantes.
Les attaques de script intersites en JavaScript sont populaires parce que JavaScript a accès à certaines données sensibles qui peuvent
être utilisées pour usurper l'identité et à d'autres fins malveillantes. Par exemple, JavaScript a accès à des cookies, et un pirate pourrait
utiliser une attaque XSS pour voler les cookies d'un utilisateur et se faire passer pour lui en ligne. JavaScript peut également créer des
requêtes HTTP, qui peuvent être utilisées pour renvoyer des données (telles que des cookies volés) au pirate. En outre, le JavaScript côté
client peut également aider un pirate à accéder à des API qui contiennent des coordonnées de géolocalisation, des données de webcam
et d'autres informations sensibles.
Pour éviter le Cross-Site Scripting, il n’y a pas de stratégie unique, mais un ensemble de mesures de protection :
• Éviter le HTML en input : empêcher les utilisateurs de poster du HTML dans les données des formulaires.
• Validation des inputs : La validation consiste à mettre en œuvre des règles qui empêchent un utilisateur de poster des données dans
une forme qui ne répond pas à certains critères.
• Assainissement des données : examiner et supprimer les données indésirables, telles que les balises HTML jugées dangereuses.
Conservez les données sûres et supprimez les caractères dangereux. L'assainissement des données est similaire à la validation, mais il a
lieu après que les données ont déjà été publiées sur le serveur web, mais avant qu'elles ne soient affichées à un autre utilisateur.
• Sécurité des cookies : Les applications web peuvent également définir des règles spéciales pour la gestion des cookies, ce qui peut
limiter le vol de cookies par des attaques de Cross-Site Scripting.
La sécurité passe aussi par le fait de pouvoir reconnaître les personnes qui utilisent les services que l’on fournit. Pour pouvoir le faire, il y a
deux concepts essentiels :
• L’authenti cation (authentication) : L'authentification est le processus de confirmation de l'identité des utilisateurs. Ce processus
empêche l'accès illégal/non autorisé à un fichier sécurisé et prévient également le vol de données.
• L’autorisation (authorization) : L'autorisation est une procédure de sécurité qui contrôle/limite l'accès d'un utilisateur à une entité
sécurisée particulière. Ce concept de sécurité concerne le nombre de privilèges d'accès dont dispose un utilisateur pour pouvoir
accéder à un fichier sécurisé.
Lorsque l’on parle d’authentification, on peut penser à plusieurs types de méthodes, qui varient en fonction des systèmes utilisés :
• Token authentication
• Password authentication
• Session authentication
• Biometric authentication
• Multi-factor authentication
• Certificate-based authentication
• Identification Authentication methods
• API authentication methods
• User authentication methods
• Vault authentication methods
• Wireless authentication methods
• Email authentication methods
• Database authentication methods
• Et bien d’autres…
Les sessions sont généralement utilisés pour gérer l'authentification des utilisateurs dans les applications web. Voici un diagramme qui
illustre ce fonctionnement :
L’utilisation de cookies peut représenter quelques inconvénients. Par défaut, l'authentification basée sur les cookies ne dispose pas d'une
protection solide contre les attaques, et elle est principalement vulnérable aux attaques de type cross-site scripting (XSS) et cross-site
request forgery (CSRF).
Mais il est possible de modifier explicitement les en-têtes des cookies pour les protéger contre ces attaques. Par exemple, les cookies
peuvent être facilement protégés contre les attaques XSS en utilisant l'attribut HttpOnly lors de la définition des en-têtes de cookies.
De plus, les cookies ne sont pas forcément la meilleure façon pour gérer l’authentification pour une API (sauf si le client est un web
browser, du coup une application React/Vue/Angular/etc.) ou une application mobile.
Par ailleurs, vu que le cookie est émis par le serveur, si le nombre d’utilisateurs augmente, il faudra sauvegarder les sessions pour chacun
de ces utilisateurs. Il y aura une sujet en terme de scalabilité à gérer.
L'authentification par jeton a été introduite pour remédier à plusieurs lacunes de l'approche basée sur les cookies. Contrairement aux
cookies, l'approche basée sur les jetons nécessite une mise en œuvre manuelle et les jetons sont sauvegardés du côté du client.
Lorsque vous vous connectez à une application web, le serveur vérifie vos informations d'identification et envoie un jeton crypté au
navigateur. Le navigateur stocke ensuite ce jeton, qui peut être ajouté à l'en-tête d'autorisation des futures demandes.
En termes d’authentification basée sur les jetons, les JSON Web Token (JWT) sont les plus utilisés. Un jeton JWT est composé de 3
parties : l’en-tête (qui contient le type de chiffrement par exemple), le payload (les informations à transmettre) et la signature (en base64
qui aide à valider le token et le décrypter)
Bien que les jetons tentent de résoudre les problèmes de sécurité liés aux cookies, ils ne sont pas totalement à l'épreuve des balles.
Les jetons enregistrés dans le navigateur peuvent être vulnérables aux attaques XSS si votre application permet à des scripts JavaScript
externes d'être intégrés dans votre application.
En outre, comme le jeton est sans état (stateless), s'il est exposé à l’extérieur de votre application, il n'y a aucun moyen de le révoquer
avant qu'il n'expire. Il est donc essentiel que le jeton soit le plus petit possible. Pas mal de services d'identité utiliser 5 minutes par défaut
pour les jetons JWT, ou des jetons de refresh.
OAuth est un protocole ouvert qui permet des méthodes d'authentification sécurisées à partir d'applications web, mobiles et de bureau.
Ce protocole permet de s'authentifier auprès du serveur en tant qu’utilisateur.
Typiquement, le Social Login se repose sur OAuth. Lorsque vous vous connectez sur un site avec votre compte Facebook, Google,
LinkedIn, Reddit ou Apple, vous utilisez OAuth sous le capot.
Nous avons vu la théorie autour de l’authentification et la sécurité d’une application web. L’authenti cation est quelque chose de
compliqué à mettre en place : il y a pas mal de paramètres et de comportements à prendre en compte, et même si on peut la mettre en
place de façon simple en comparant juste un nom d’utilisateur et un mot de passe depuis une base de données, c’est le moyen le plus
facile de rendre son application vulnérable.
Heureusement pour nous, il existe des librairies/middlewares qui nous facilitent la vie et implémentent pas mai de comportements
nécessaires à un bon processus d’authentification :
• Avec NextJS nous avons NextAuth
• Il y a également Passport.js qui est une librairie très connue (certainement la plus populaire dans le monde du JS)
• Il y en a d’autres encore, notamment des entreprises privées telles que Auth0, Clerk, Firebase etc.
La majorité de ses solutions permettent de gérer les authentification via Token, Cookie ou encore Social Media. Pour ce cours, nous allons
utiliser Passport.js, ce qui vous permettra de pouvoir le réutiliser avec d’autres frameworks.
fi
3. Authentification
Se connecter avec NextJS
Passport.js offre plusieurs stratégies. Si nous nous rendons sur le site officiel, nous pouvons voir une liste exhaustive de 539 stratégies
d’authentification :
Dans le dossier routes, nous allons créer un nouveau fichier nommé auth.route.js. Dans celui-ci nous allons ajouter ce code :
// Routes
routerAuth.post('/login', login);
routerAuth.post(‘/signup', signup);
// Functions
function login(req, res) {
res.json({ "message" : "Route en construction" });
}
Dans notre fichier index.js, nous allons rajouter les deux lignes suivantes afin de faire appel à notre nouveau routeur :
et
app.use('/auth', routerAuth);
Rajoutez les à côté des lignes déjà créées pour le routeur des notes.
Ici nous installons le package et également la stratégie JWT qui est la stratégie permettant de s’authentifier avec un token.
Maintenant créons notre modèle User. Dans le dossier models, ajoutons un fichier user.model.js :
Maintenant il est heure de créer notre stratégie d’authentification avec Passport. Dans notre fichier index.js, nous allons ajouter les
imports suivants :
On en profite pour retirer l’option { force : true } du code de synchronisation des modèles :
Cela permettra de ne plus supprimer le contenu de la base à chaque fois que l’API est lancée.
Ensuite, au niveau des appels middlewares (là où se trouvent les app.use() ), nous allons en rajouter un nouveau (juste après
app.use(cors()); par exemple) pour notre session/authentification :
app.use(passport.initialize());
Alexandre TELLE // Master MIAGE SITN // Novembre 2023 40
3. Authentification
Exemple
Globalement, le bout de code précédent indique qu’à chaque fois que notre API recevra un JWT, on vérifiera le token afin de voir si un
utilisateur y existe vraiment.
Dans un premier temps on définit nos options (la méthode d’extraction du JWT et le secret pour la signature).
Ensuite on analyse le payload afin d’y extraire l’user s’il existe, et renvoyer les erreurs adéquates dans le cas contraire.
La fonction done() est un callback. Si on regarde sa définition, on peut comprendre un peu mieux comment elle fonctionne :
Parfait ! Maintenant il est l’heure d’intégrer notre stratégie dans nos routes. Dans notre fichier auth.route.js, nous allons rajouter les
imports suivants :
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
Bcrypt est une librairie de cryptographie qui va nous permettre de hasher nos mots de passe. Installez-la avec la commande suivante :
npm install bcryptjs
Nous pouvons essayer nos nouvelles routes sur Postman. D’abord, enregistrons un nouvel utilisateur :
Maintenant que l’authentification fonctionne… Comment s’assurer que nos routes soient protégées par un JWT ?
Rien de plus simple. De retour dans index.js, nous allons modifier l’appel du routeur routerNote :
Maintenant à chaque fois que nous allons appeler une route /notes, il faudra y renseigner un token pour accéder au résultat. Dans
Postman, on peut le faire depuis l’onglet Authorization :
Maintenant nous allons repasser du côté de notre application NextJS, afin d’y rajouter une page de login. Dans le dossier pages, créer
un nouveau fichier nommé login.js. Nous allons y mettre le code suivant :
Nous allons installer un package afin de stocker notre token dans un cookie côté client :
npm install cookies-next
Nous allons maintenant lier notre formulaire à notre API. Toujours dans notre fichier login.js, nous allons rajouter les imports suivants :
Ensuite juste avant notre fonction return(), nous allons initialiser les variables que l’on utilisera. Nous allons stocker l’username et le
mot de passe dans un state :
Ensuite, sur le même principe que la soumission du formulaire pour la création d’une note, nous allons soumettre un formulaire pour la connexion :
return (
<>
<h1>Se connecter</h1>
<form onSubmit={submitForm}>
<section>
<label for="username">Username</label>
<input
id="username"
name="username"
type="text"
required
onChange={(e) => {
setUsername(e.target.value);
}}
value={username}
/>
</section>
<section>
<label for="password">Password</label>
<input
id="password"
name="password"
type="password"
required
onChange={(e) => {
setPassword(e.target.value);
}}
value={password}
/>
</section>
<button type="submit">Se connecter</button>
</form>
</>
)
Alexandre TELLE // Master MIAGE SITN // Novembre 2023 52
3. Authentification
Exemple
Nous avions créé précédemment les routes permettant de modifier et supprimer des notes… Mais maintenant elles sont protégées par
un JWT. Comment fait-on maintenant pour les appeler ?
Nous allons réutiliser le cookie qui a été défini lors du login. Ainsi, à chaque fois que l’ont appelle fetch(), nous allons rajouter dans le
header notre token :
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getCookie('authToken')}`
}
Lors de ce cours nous avons vu plusieurs notions, exemples et technologies. Ce qu’il faut retenir, pour maintenant et pour la suite :
• Il y a plusieurs frameworks que ce soit pour le frontend ou le backend : essayez les le plus possible afin de vous faire un avis
• Faire du style c’est compliqué, heureusement il y a des frameworks comme Tailwind et Bootstrap qui facilitent la vie. Revoyez les
bases du CSS, ça rendra leur utilisation plus facile
• Les frameworks frontend tournent autour du concept de composants
• Les nouveautés du JavaScript après ES6 sont essentielles dans la compréhension et l’utilisation des nouvelles technologies web
• Le développement d’une API est intimement lié à son modèle de données : de préférence réfléchir à ce dernier avant de se lancer
dans la conception de l’API
• Utiliser un ORM pour gérer sa base de données est une pratique répandue et permet d’accélérer le développement de son application
• La sécurité est un volet important : essayer de tout faire tout seul peut-être dangereux et inutile. Utilisez les outils/méthodologies/
approches existants
• Les promises sont très utilisées pour le développement web : elles permettent de faire les appels asynchrones de votre API depuis
votre frontend
• Apprenez Typescript
• Et enfin… Lisez les documentations of cielles. Constamment (et de préférence en anglais). Il y a énormément de notions à voir,
d’exemples à explorer. Prendre le temps de bien lire les docs c’est faire plus de la moitié du travail. Il n’y a JAMAIS une seule façon de
faire.
Il s’agit d’un des projets les plus communs à réaliser en développement web ! Nous allons réaliser une application qui permet de créer et
gérer des todo-lists.
Niveau template, quartier libre également. Tailwind, Bootstrap, pur CSS, le choix et libre.
Il y aura une soutenance de projet. Votre présentation devra contenir vos choix des techniques et les explications associées. Aussi,
détaillez-y le rôle de chaque membre du projet, et rajoutez-y tout élément que vous estimez important : comme toujours, mieux vous
expliquez, plus vous serez récompensés !
A vous de jouer !