Vous êtes sur la page 1sur 8

Formation : NodeJS

Période : Les 22, 23 et 24 Aout 2022 (Formation en ligne)


Formateur : Anis ASSAS

Exemple d’application web FullStack MERN : Notions avancées

Partie 1 : Optimisation de la structure back-end


En se référant toujours au projet développé au niveau des ateliers précédents, on se
propose maintenant de réorganiser la structure de notre back-end pour en faciliter la
compréhension et la gestion.

Il est techniquement possible de conserver toute notre logique dans le fichier server.js, mais
ce fichier peut vite devenir bien trop volumineux, ce qui compliquerait la gestion. Par
conséquent, rendons les choses un peu plus modulaires.

- Configuration de routage
o Sous le dossier 'server', créer un dossier 'routes', puis dans ce nouveau
dossier créer le fichier 'departRoutes.js' incluant le code suivant :

const express = require('express');

const router = express.Router();

module.exports = router;

o Couper toutes les routes de server.js et les coller dans ce nouveau fichier.
o Veiller alors à remplacer toutes les occurrences de app par router.
o Importer ce nouveau module au niveau de server.js pour pouvoir utiliser les routes
o Re-exécuter le programme pour vérifier le bon fonctionnement.

- Configuration de modèle de données


o Sous le dossier ‘server’, créer un dossier ‘models’, puis dans ce nouveau
dossier créer le fichier ‘departModel.js’ incluant le code relatif à la création
du modèle de données.

- Configuration des contrôleurs


o Sous le dossier ‘server’, créer un dossier ‘controllers’, puis dans ce nouveau
dossier le fichier ‘departControllers.js’ incluant le code relatif à la couche
métier (à savoir les fonctions : getDeparts, getDepartById, addDepart,
updateDepart et deleteDepart).
 Allure de la définition de ces fonctions :

exports.getDeparts = (req, res) => {


// traitement
}
Application web FullStack (A. ASSAS) Page 1|8
o Remplacer alors au niveau du fichier 'departRoutes.js', le code relatif à la
couche métiers par l’appel à ces fonctions.
o Revérifier toujours le bon fonctionnement de l’application.

- Configuration de la connexion
o Sous le dossier ‘server’, créer un dossier ‘configuration’, puis dans ce
nouveau dossier le fichier ‘connexionMongo.js’ incluant le code permettant
de se connecter à l’url de la base de données mongoDB.
o Importer ce fichier au niveau de server.js et vérifier le bon fonctionnement de
l’application.

Partie 2 : Sécurité de l’api


On se propose dans cette partie de sécuriser l’application en termes d’accès aux API déjà
définies.
Pour ce faire, on implémentera des nouvelles fonctionnalités afin de créer des utilisateurs
qui seront authentifiés par tokens JWT (JSON Web Token).

JWT, c’est quoi ?

JWT pour JSON Web Token est une méthode sécurisée d’échange d’informations, décrite par
la RFC 7519. L’information est échangée sous la forme d’un jeton signé afin de pouvoir en
vérifier la légitimité. Ce jeton est compact et peut être inclus dans une URL sans poser de
problème.
JWT est couramment utilisé pour implémenter des mécanismes d’authentification stateless
pour des SPA (Single Page Application) ou pour des application mobiles.

Un JWT est composé de trois parties, chacune contenant des informations différentes :
 un header : identifie quel algorithme a été utilisé pour générer la signature
 un payload : la partie du token qui contient les informations que l’on souhaite transmettre
 la signature : créée à partir du header et du payload générés et d’un secret

Application web FullStack (A. ASSAS) Page 2|8


Une fois ces 3 éléments générés, on peut assembler le JWT comme suit :

token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' +


encodeBase64(signature)

Exemple (https://jwt.io/) :

Header: { "alg": "HS256", "typ": "JWT" }


Payload: { "subject": "John Doe", "iat": " 1516239022" }
Token généré :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik
pvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.he0ErCNloe4J7Id0Ry2SEDg09lKkZkfsRiGsdX
_vgEg

- Création d’un modèle de données

La première étape de l'implémentation de l'authentification est de créer un modèle de base


de données pour les informations des utilisateurs.
o Au niveau de la même base de données mongoDB, créer une nouvelle
collection appelée 'users'.
o Sous le dossier models, créer un nouveau fichier userModel.js définissant le
schéma de données composé par les deux attributs : email et password (de
type String).

Pour s'assurer que deux utilisateurs ne puissent pas utiliser la même adresse e-
mail, nous utiliserons le mot clé unique pour l'attribut email du schéma
d'utilisateur.

Les erreurs générées par défaut par MongoDB pouvant être difficiles à résoudre, il est alors
préférable d’installer un package de validation pour prévalider les informations avant de les
enregistrer :

> npm install mongoose-unique-validator

Dans le schéma de données qu’on vient de définir, la valeur unique


avec l'élément mongoose-unique-validator passé comme plug-in, s'assurera que deux
utilisateurs ne puissent partager la même adresse e-mail.

const uniqueValidator = require('mongoose-unique-validator');


. . .

userSchema.plugin(uniqueValidator);

- Configuration des routes d’authentification

On se propose tout d’abord de créer l'infrastructure nécessaire à nos routes


d'authentification. Il nous faudra un contrôleur et un routeur, puis nous devrons enregistrer
ce routeur dans notre application Express.

Application web FullStack (A. ASSAS) Page 3|8


o Créer le fichier 'userControllers.js' au niveau du dossier 'controllers' incluant
les deux fonctions suivantes :

exports.signup = (req, res, next) => {

};

exports.login = (req, res, next) => {

};

On implémentera dans la suite ces deux fonctions qui serviront


respectivement pour l’inscription et la connexion.

o Afin d’implémenter les routes, créer un autre fichier 'userRoutes.js', cette fois
dans le dossier 'routes' :

const express = require('express');


const router = express.Router();

const userCtrl = require('../controllers/userControllers') ;

router.post('/users/signup', userCtrl.signup);
router.post('/users/login', userCtrl.login);

module.exports = router;

- Création des utilisateurs

Il nous faudra le package de chiffrement bcrypt pour la fonction signup. Installons-le donc
dans le projet :
> npm install bcrypt

Nous ferons recours par la suite à la fonction de hachage de bcrypt : bcrypt.hash permettant
de générer le hash appliqué sur le mot de passe.

Nous pouvons l'importer dans le contrôleur afin d’implémenter la fonction signup qui
permettra de créer un utilisateur et l'enregistrer dans la base de données, en renvoyant une
réponse de réussite en cas de succès, et des erreurs avec le code d'erreur en cas d'échec :

exports.signup = (req, res, next) => {


bcrypt.hash(req.body.password, 10)
.then(hash => {
const user = new User({
email: req.body.email,
password: hash
});

Application web FullStack (A. ASSAS) Page 4|8


user.save()
.then(() => res.status(201).json({ message: 'Utilisateur créé !' }))
.catch(error => res.status(400).json({ error }));
})
.catch(error => res.status(500).json({ error }));
};

- Implémentation de la fonction de connexion : login

Maintenant que nous pouvons créer des utilisateurs dans la base de données, il nous faut
une méthode permettant de vérifier si un utilisateur qui tente de se connecter dispose
d'identifiants valides. Au niveau de cette fonction :

 En se référant au modèle Mongoose, vérifier que l'e-mail entré par l'utilisateur


correspond à un utilisateur existant de la base de données :
o dans le cas contraire, nous renvoyons une erreur 401 Unauthorized et un
message « Utilisateur non trouvé »,
o si l'e-mail correspond à un utilisateur existant, nous utilisons la fonction
compare de bcrypt pour comparer le mot de passe entré par l'utilisateur avec
le hash enregistré dans la base de données :
 s'ils ne correspondent pas, nous renvoyons une erreur 401 Unauthorized et un
message « Mot de passe incorrect ! »,
 s'ils correspondent, les informations d'identification de l’utilisateur sont valides. Dans
ce cas, nous renvoyons une réponse 200 contenant l'ID utilisateur et un token. Ce
token est une chaîne générique pour l'instant ‘TOKEN’, mais nous allons le modifier
et le crypter par la suite.

exports.login = (req, res, next) => {


User.findOne({ email: req.body.email })
.then(user => {
if (!user) {
return res.status(401).json({ error: 'Utilisateur non trouvé !' });
}
bcrypt.compare(req.body.password, user.password)
.then(valid => {
if (!valid) {
return res.status(401).json({ error: 'Mot de passe incorrect !' });
}
res.status(200).json({
userId: user._id,
token: 'TOKEN'
});
})
.catch(error => res.status(500).json({ error }));
})
.catch(error => res.status(500).json({ error }));
};

Application web FullStack (A. ASSAS) Page 5|8


- Création des tokens d’authentification

Les tokens d'authentification permettent aux utilisateurs de ne se connecter qu'une seule


fois à leur compte. Au moment de se connecter, ils recevront leur token et le renverront
automatiquement à chaque requête par la suite. Ceci permettra au back-end de vérifier que
la requête est authentifiée.

Pour pouvoir créer et vérifier les tokens d'authentification, il nous faudra un nouveau
package :

> npm install jsonwebtoken

Nous l'importerons ensuite dans le contrôleur utilisateur afin de pouvoir l’utiliser dans la
fonction login qu’on va mettre à jour de sorte à :

 utiliser la fonction sign de jsonwebtoken pour encoder un nouveau token qui va


contenir l'ID de l'utilisateur en tant que payload (les données encodées dans
le token) ;
 utiliser une chaîne secrète de développement
temporaire ‘RANDOM_SECRET_KEY’ pour encoder le token (à remplacer par une
chaîne aléatoire beaucoup plus longue pour la production) ;
 définir la durée de validité du token à 24 heures. L'utilisateur devra donc se
reconnecter au bout de 24 heures ;
 renvoyer le token au front-end avec la réponse.

exports.login = (req, res, next) => {


. . .
bcrypt.compare(req.body.password, user.password)
.then(valid => {
if (!valid) {
return res.status(401).json({ error: 'Mot de passe incorrect !'});
}
res.status(200).json({
userId: user._id,
token: jwt.sign(
{ userId: user._id },
'RANDOM_TOKEN_SECRET',
{ expiresIn: '24h' }
)
});
})
. . .

Utiliser l'onglet « Réseau » de Chrome DevTools pour vérifier qu’une fois connecté,
chaque requête provenant du front-end contient bien le token !

Application web FullStack (A. ASSAS) Page 6|8


- Configuration de middleware d’authentification

On se propose maintenant de créer le middleware qui protégera les routes sélectionnées et


vérifiera que l'utilisateur est authentifié avant d'autoriser l'envoi de ses requêtes.

o Créez un dossier 'middleware' et un fichier 'auth.js' à l'intérieur :

Dans ce middleware :
- récupérer le token du header Authorization de la requête entrante.
- utiliser ensuite la fonction verify pour décoder le token. Si celui-ci n'est pas valide,
une erreur sera générée
- récupérer l'ID utilisateur du token ;
o si la demande contient un ID utilisateur, nous le comparons à celui extrait
du token. S'ils sont différents, nous générons une erreur ;
o dans le cas contraire, tout fonctionne et notre utilisateur est authentifié. nous
passons l'exécution du middleware suivant à l'aide de la fonction next() .

const jwt = require('jsonwebtoken');

module.exports = (req, res, next) => {


try {
const token = req.headers.authorization.split(' ')[1];
const decodedToken = jwt.verify(token, 'RANDOM_TOKEN_SECRET');
const userId = decodedToken.userId;
if (req.body.userId && req.body.userId !== userId) {
throw 'Invalid user ID';
} else {
next();
}
} catch {
res.status(401).json({
error: new Error('Invalid request!')
});
}
};

Maintenant, nous devons appliquer ce middleware à toutes les routes relatives à notre
module département.
Au niveau du routeur departRoutes.js, nous importons alors notre middleware et le passons
comme argument aux routes à protéger :

const auth = require('../middleware/auth')

// méthode GET - affichage des départements


router.get("/departements",auth, getDeparts);

// méthode GET - affichage d'un département connaissant son id


router.get("/departements/:id",auth, getDepartById);
Application web FullStack (A. ASSAS) Page 7|8
// méthode POST - ajout département
router.post("/add",auth, addDepart);

// méthode put - mise à jour département


router.put("/update/:id", auth, updateDepart);

// méthode delete - suppression d'un département


router.delete("/delete/:id",auth, deleteDepart);

- Utiliser une application (telle que postman) afin de vérifier que les requêtes non
autorisées ne fonctionnent pas.

Vous pouvez passer, à titre d’exemple une demande sans en-tête Authorization. L'API
refusera l'accès et renverra une réponse 401.

Application web FullStack (A. ASSAS) Page 8|8

Vous aimerez peut-être aussi