Archives Wild Code School
Express.js avancé

Express 07 - 🔐 Enregistrer des utilisateurs de maniĂšre sĂ©curisĂ©e

QuĂȘte 1 sur 2

⚠ Avant de commencer cette quĂȘte, on te recommande de jeter un coup d'Ɠil Ă  celle-ci :
QuĂȘtes liĂ©es

Dans le monde réel, les APIs ont besoin d'un moyen d'authentifier les utilisateurs afin d'autoriser ou de restreindre certaines fonctionnalités (favoris, édition de profil...).

Afin d'authentifier les utilisateurs de notre service Web, ils doivent d'abord pouvoir s'enregistrer en saisissant des informations d'identification (e-mail/mot de passe) qu'ils pourront utiliser pour se connecter ultérieurement à l'application.

đŸ€“ Dans cette quĂȘte, tu vas apprendre :

✅ Comment hasher un mot de passe ✅ Ajouter un middleware sur une route

⌚ Mise en place

Si tu ne l'as pas déjà sur ta machine, clone ce dépÎt et switch sur la branche express07.

Ouvre express_quests.sql. À ce stade, notre table users est dĂ©clarĂ©e ainsi :

sql
// ...

CREATE TABLE users (
    id int primary key NOT NULL AUTO_INCREMENT,
    firstname varchar(255) NOT NULL,
    lastname varchar(255) NOT NULL,
    email varchar(255) UNIQUE NOT NULL,
    city varchar(255) DEFAULT NULL,
    language varchar(255) DEFAULT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

// ...

Nous avons déjà un champ email que nous pouvons utiliser pour identifier une personne de façon unique plus tard lors de sa connexion. Mais nous n'avons pas encore de champ password...

Avant d'ajouter un champ password, lis cette ressource :

↗
Recommandations de l'OWASP concernant le stockage des mots de passe
cheatsheetseries.owasp.org

Comme tu l'as lu, le stockage de mots de passe en clair dans une base de données pose un énorme problÚme de sécurité.

Nous devrions plutÎt hacher le mot de passe (transformer le mot de passe simple en charabia) avant de le stocker, afin qu'il soit inutilisable par les pirates si la base de données est compromise.

Ajoute la colonne hashedPassword dans ta table users et ajoute des exemples de mots de passe pour nos 6 membres :

sql
// ...

CREATE TABLE users (
    id int primary key NOT NULL AUTO_INCREMENT,
    firstname varchar(255) NOT NULL,
    lastname varchar(255) NOT NULL,
    email varchar(255) UNIQUE NOT NULL,
    city varchar(255) DEFAULT NULL,
    language varchar(255) DEFAULT NULL,
    hashedPassword varchar(255) NOT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

INSERT INTO
  users (firstname, lastname, email, city, language, hashedPassword)
VALUES
  (
    'John',
    'Doe',
    'john.doe@example.com',
    'Paris',
    'English',
    "$argon2id$v=19$m=16,t=2,p=1$emVmZXpmemZlemVmZWR6ZXplZg$rqZkhxu5YbqCGHPNrjJZpQ"
  ),(
    'Valeriy',
    'Appius',
    'valeriy.ppius@example.com',
    'Moscow',
    'Russian',
    '$argon2id$v=19$m=16,t=2,p=1$emVmemVmemZlemZ6ZnpmZQ$eSetR6KPUNAGW+q+wDadcw'
  ),(
    'Ralf',
    'Geronimo',
    'ralf.geronimo@example.com',
    'New York',
    'Italian',
    '$argon2id$v=19$m=16,t=2,p=1$emVmemVmemZlemZ6ZnpmZXphZGF6ZGQ$a0bg5DZB6H6v3jjQC81DXg'
  ),(
    'Maria',
    'Iskandar',
    'maria.iskandar@example.com',
    'New York',
    'German',
    '$argon2id$v=19$m=16,t=2,p=1$emVmemVmemZlenplZHpkZnpmemZlemFkYXpkZA$V1qAnJDyMuuWG7g9yoGYXA'
  ),(
    'Jane',
    'Doe',
    'jane.doe@example.com',
    'London',
    'English',
    '$argon2id$v=19$m=16,t=2,p=1$emVmemVmemZlenplZHpkZGZ6ZnpmZXphZGF6ZGQ$VCzq45PL9t8khtc44Kk5iw'
  ),(
    'Johanna',
    'Martino',
    'johanna.martino@example.com',
    'Milan',
    'Spanish',
    '$argon2id$v=19$m=16,t=2,p=1$emVmemVmemVmemZlenplZHpkZGZ6ZnpmZXphZGF6ZGQ$UKaGZ9hGFn/S5SBQDMe/Uw'
  );

// ...

Tu peux maintenant réinitialiser ta base de données à l'aide du script SQL. Connecte-toi à MySQL dans le dossier du projet, utilise ta base de données puis source le fichier comme ceci :

sql
USE express_quests;
source express_quests.sql;
Info

Si cela ne fonctionne pas, essaie de copier-coller tout le contenu du fichier express_quests.sql dans le terminal MySQL et appuie sur Entrée.

Utilisation d'un algorithme de hash dans Node.js

Il existe de nombreuses bibliothĂšques qui peuvent t'aider Ă  hash un mot de passe, par exemple Scrypt ou Bcrypt. Mais puisque l'OWASP recommande l'algorithme Argon2, c'est celui que nous allons utiliser :

↗
Argon2 NPM Package

Une implémentation de l'algorithme Argon2, primé pour sa sécurité, en Node.js

npmjs.com

Commençons par installer le package :

sh
npm i argon2

Nous voulons hacher le mot de passe avant d'insĂ©rer l'utilisateur dans la base de donnĂ©es. Une bonne idĂ©e ici est de s'appuyer sur l'architecture basĂ©e middleware d'Express : une route peut prendre plusieurs middlewares en paramĂštres. Tu as peut-ĂȘtre dĂ©jĂ  vu un exemple de ce genre :

javascript
const step1 = (req, res, next) => {
  req.message = "I went through step1";
  next();
};

const step2 = (req, res, next) => {
  req.message += " and step2";
  next();
};

const lastStep = (req, res) => {
  res.send(req.message);
};

app.get("/justToTest", step1, step2, lastStep);

Ici step1, step2 et lastStep sont des middlewares, et seront exĂ©cutĂ©s l'un aprĂšs l'autre au dĂ©clenchement de la route. La fonction next aprĂšs les paramĂštres req et res est utilisĂ©e pour dire "J'ai fini ma partie, vous pouvez passer Ă  la suivante". C'est une pratique courante d'utiliser req comme outil pour transmettre des donnĂ©es au middleware suivant. Dans cet exemple, step1 ajoute une propriĂ©tĂ© message Ă  l'objet req avec une valeur initiale "I went through step1". Ensuite, step2 complĂšte le message qui est finalement envoyĂ© en rĂ©ponse. Si tu essayais une requĂȘte sur cette route, elle rĂ©pondrait "I went through step1 and step2".

Revenons Ă  notre quĂȘte de hachage. Nous pouvons crĂ©er un middleware hashPassword (dans un fichier dĂ©diĂ© auth.js pour garder la base de code bien organisĂ©e) et l'ajouter aux routes concernĂ©es par l'envoi de mot de passe Ă  la base de donnĂ©e dans app.js :

  • La route POST /api/users
  • La route PUT /api/users/:id

Comme ceci :

javascript
// in auth.js

const argon2 = require("argon2");

const hashPassword = (req, res, next) => {
  // hash the password using argon2 then call next()
};

module.exports = {
  hashPassword,
};

// in app.js

const { hashPassword } = require("./auth.js");

app.post("/api/users", hashPassword, userHandlers.postUser);
app.put("/api/users/:id", hashPassword, userHandlers.updateUser);

Le middleware hashPassword ici sera exécuté en premier et préparera les données pour userHandlers.postUser et userHandlers.updateUser.

La documentation de argon2 (cette page en particulier) montre des exemples de code utilisant async/await :

javascript
const hashedPassword = await argon2.hash(password, hashingOptions);

Juste pour aider, un code équivalent en utilisant then/catch :

javascript
argon2
  .hash(password, hashingOptions)
  .then((hashedPassword) => {
    // do something with hashedPassword
  })
  .catch((err) => {
    // do something with err
  });

C'est à toi maintenant ! Essaie de compléter hashPassword à partir des instructions suivantes :

  • OWASP recommande la configuration suivante pour Argon2 : "Utiliser Argon2id avec une configuration minimale de 15 Mio de mĂ©moire, un nombre d'itĂ©rations de 2 et 1 degrĂ© de parallĂ©lisme".
  • Tu dois rĂ©cupĂ©rer le mot de passe Ă  hacher Ă  partir de req.body.password.
  • Une fois que tu as obtenu la version hachĂ©e, utiliser console.log() pour l'afficher dans le terminal.
  • Tu dois stocker le mot de passe hachĂ© dans req.body.hashedPassword.
  • Pour t'assurer que le mot de passe en clair ne pourra plus ĂȘtre utilisĂ© aprĂšs ton middleware, supprime-le.

N'oublie pas : étape par étape ;)

hidden
Afficher la solution|||javascript|||Tu as des doutes sur comment faire ?|||0|||Masquer
const argon2 = require("argon2");

const hashingOptions = {
  type: argon2.argon2id,
  memoryCost: 2 ** 16,
  timeCost: 5,
  parallelism: 1,
};

const hashPassword = (req, res, next) => {
  argon2
    .hash(req.body.password, hashingOptions)
    .then((hashedPassword) => {
      console.log(hashedPassword);

      req.body.hashedPassword = hashedPassword;
      delete req.body.password;

      next();
    })
    .catch((err) => {
      console.error(err);
      res.sendStatus(500);
    });
};

module.exports = {
  hashPassword,
};

Essaie de POST un nouvel utilisateur (avec un mot de passe). Tu devrais voir quelque chose comme ça dans ta console :

shell
$argon2id$v=19$m=65536,t=5,p=1$6F4WFjpSx9bSq9k4lp2fiQ$cjVgCHF/voka5bZI9YAainiaT+LkaQxfNN638b/h4fQ

Tu n'as probablement pas exactement la mĂȘme chose et c'est parfaitement normal. Si tu dĂ©clenches la route Ă  nouveau avec le mĂȘme mot de passe, tu verras que le hachage produit est toujours diffĂ©rent, et c'est le but.

Tu peux supprimer le console.log() dans hashPassword() : ton middleware est prĂȘt !

đŸ’Ș Challenge

Tu as vu comment hacher un mot de passe via un middleware. Maintenant, les fonctions postUser et updateUser dans userHandlers.js devraient intégrer le mot de passe haché lors de leur exécution :

  • Adapte les pour que le mot de passe hachĂ© soit envoyĂ© dans la base de donnĂ©es.
  • VĂ©rifie (et corrige si nĂ©cessaire) les routes liĂ©es aux utilisateurs afin qu'elles n'envoient jamais le mot de passe (mĂȘme hachĂ©) en dehors du serveur.

Héberge ton application améliorée sur GitHub et partage l'URL en solution.

🧐 Critùres de validation

  • [ ] L'application permet de poster et de mettre Ă  jour un utilisateur avec un mot de passe
  • [ ] Le mot de passe donnĂ© n'est pas stockĂ© dans la base de donnĂ©es, mais un hachage du mot de passe est stockĂ© Ă  la place
  • [ ] Les routes liĂ©es aux utilisateurs ne doivent renvoyer aucune information sensible (attention aux SELECT * ... !)

==$==

Ce que tu devrais avoir dans :

javascript
// ...

const userHandlers = require("./userHandlers");
const { hashPassword } = require("./auth");

app.get("/api/users", userHandlers.getUsers);
app.get("/api/users/:id", userHandlers.getUserById);
app.post("/api/users", hashPassword, userHandlers.postUser);
app.put("/api/users/:id", hashPassword, userHandlers.updateUser);
app.delete("/api/users/:id", userHandlers.deleteUser);

// ...
javascript
const argon2 = require("argon2");

const hashingOptions = {
  type: argon2.argon2id,
  memoryCost: 2 ** 16,
  timeCost: 5,
  parallelism: 1,
};

const hashPassword = (req, res, next) => {
  argon2
    .hash(req.body.password, hashingOptions)
    .then((hashedPassword) => {
      req.body.hashedPassword = hashedPassword;
      delete req.body.password;

      next();
    })
    .catch((err) => {
      console.error(err);
      res.sendStatus(500);
    });
};

module.exports = {
  hashPassword,
};
javascript
// ...

const getUsers = (req, res) => {
  const initialSql = "select id, firstname, lastname, email, city, language from users";
  const where = [];

  // ...

  database
    .query(
      where.reduce(
        (sql, { column, operator }, index) =>
          `${sql} ${index === 0 ? "where" : "and"} ${column} ${operator} ?`,
        initialSql
      ),
      where.map(({ value }) => value)
    )
    .then(([users]) => {
      res.json(users);
    })
    .catch((err) => {
      console.error(err);
      res.status(500).send("Error retrieving data from database");
    });
};

const getUserById = (req, res) => {
  const id = parseInt(req.params.id);

  database
    .query("select id, firstname, lastname, email, city, language from users where id = ?", [id])
    .then(([users]) => {
      if (users[0] != null) {
        res.json(users[0]);
      } else {
        res.status(404).send("Not Found");
      }
    })
    .catch((err) => {
      console.error(err);
      res.status(500).send("Error retrieving data from database");
    });
};

const postUser = (req, res) => {
  const { firstname, lastname, email, city, language, hashedPassword } = req.body;

  database
    .query(
      "INSERT INTO users(firstname, lastname, email, city, language, hashedPassword) VALUES (?, ?, ?, ?, ?, ?)",
      [firstname, lastname, email, city, language, hashedPassword]
    )
    .then(([result]) => {
      res.location(`/api/users/${result.insertId}`).sendStatus(201);
    })
    .catch((err) => {
      console.error(err);
      res.status(500).send("Error saving the user");
    });
};

const updateUser = (req, res) => {
  const id = parseInt(req.params.id);
  const { firstname, lastname, email, city, language, hashedPassword } = req.body;

  database
    .query(
      "update users set firstname = ?, lastname = ?, email = ?, city = ?, language = ?, hashedPassword = ? where id = ?",
      [firstname, lastname, email, city, language, hashedPassword, id]
    )
    .then(([result]) => {
      if (result.affectedRows === 0) {
        res.status(404).send("Not Found");
      } else {
        res.sendStatus(204);
      }
    })
    .catch((err) => {
      console.error(err);
      res.status(500).send("Error editing the user");
    });
};

// ...

Ressources partagĂ©es sur cette quĂȘte