A propos de l'auteur

Darshan Somashekar est un entrepreneur technologique qui a construit et vendu deux startups. Son dernier projet amusant est un site solitaire appelé Solitaired. Auparavant,…
Plus à propos
Darshan

La fonctionnalité de réinitialisation du mot de passe est un enjeu de table pour toute application conviviale. Cela peut aussi être un cauchemar pour la sécurité. En utilisant NodeJS et MySQL, Darshan montre comment créer avec succès un flux de mot de passe de réinitialisation sécurisé afin que vous puissiez éviter ces pièges.

Si vous êtes comme moi, vous avez oublié votre mot de passe plusieurs fois, en particulier sur les sites que vous n'avez pas visités depuis longtemps. Vous avez probablement également vu et / ou été mortifié par la réinitialisation des e-mails de mot de passe contenant votre mot de passe en texte brut.

Malheureusement, le flux de travail de réinitialisation du mot de passe est écourté et fait l'objet d'une attention limitée lors du développement de l'application. Cela peut non seulement conduire à une expérience utilisateur frustrante, mais peut également laisser votre application avec des trous de sécurité béants.

Nous allons voir comment créer un flux de travail de réinitialisation de mot de passe sécurisé. Nous utiliserons NodeJS et MySQL comme composants de base. Si vous écrivez en utilisant un langage, un framework ou une base de données différents, vous pouvez toujours bénéficier des "Conseils de sécurité" généraux décrits dans chaque section.

Un flux de réinitialisation de mot de passe comprend les composants suivants:

  • Un lien pour envoyer l'utilisateur au début du workflow.
  • Un formulaire qui permet à l'utilisateur de soumettre son e-mail.
  • Une recherche qui valide l'e-mail et envoie un e-mail à l'adresse.
  • Un e-mail contenant le jeton de réinitialisation avec une expiration qui permet à l'utilisateur de réinitialiser son mot de passe.
  • Un formulaire qui permet à l'utilisateur de générer un nouveau mot de passe.
  • Enregistrer le nouveau mot de passe et laisser l'utilisateur se reconnecter avec le nouveau mot de passe.

Outre Node, Express & MySQL, nous utiliserons les bibliothèques suivantes:

Sequelize est un ORM de base de données NodeJS qui facilite l'exécution de migrations de base de données ainsi que la création de requêtes de sécurité. Nodemailer est une bibliothèque de messagerie NodeJS populaire que nous utiliserons pour envoyer des e-mails de réinitialisation de mot de passe.

Conseil de sécurité n ° 1

Certains articles suggèrent que les flux de mots de passe sécurisés peuvent être conçus à l'aide de jetons Web JSON (JWT), ce qui élimine le besoin de stockage de base de données (et est donc plus facile à implémenter). Nous n'utilisons pas cette approche sur notre site, car les secrets des jetons JWT sont généralement stockés directement dans le code. Nous voulons éviter d'avoir "un secret" pour les gouverner tous (pour la même raison, vous ne salez pas les mots de passe avec la même valeur), et nous devons donc déplacer ces informations dans une base de données.

Installation

Tout d'abord, installez Sequelize, Nodemailer et d'autres bibliothèques associées:

$ npm install --save sequelize sequelize-cli mysql crypto nodemailer

Dans l'itinéraire où vous souhaitez inclure vos workflows de réinitialisation, ajoutez les modules requis. Si vous avez besoin d'un rappel sur Express et les itinéraires, consultez leur guide.

const nodemailer = require ('nodemailer');

Et configurez-le avec vos identifiants SMTP de messagerie.

const transport = nodemailer.createTransport ({
    hôte: process.env.EMAIL_HOST,
    port: process.env.EMAIL_PORT,
    sécurisé: vrai,
    auth: {
       utilisateur: process.env.EMAIL_USER,
       pass: process.env.EMAIL_PASS
    }
});

La solution de messagerie que j'utilise est le service de messagerie simple d'AWS, mais vous pouvez utiliser n'importe quoi (Mailgun, etc.).

Si c'est la première fois que vous configurez votre service d'envoi d'e-mails, vous devrez passer un peu de temps à configurer les clés de domaine appropriées et à configurer les autorisations. Si vous utilisez Route 53 avec SES, c'est super simple et fait presque automatiquement, c'est pourquoi je l'ai choisi. AWS propose des didacticiels sur le fonctionnement de SES avec Route53.

Conseil de sécurité # 2

Pour stocker les informations d'identification loin de mon code, j'utilise dotenv, qui me permet de créer un fichier .env local avec mes variables d'environnement. De cette façon, lorsque je déploie en production, je peux utiliser différentes clés de production qui ne sont pas visibles dans le code, et me permet donc de restreindre les autorisations de ma configuration à certains membres de mon équipe uniquement.

Configuration de la base de données

Puisque nous allons envoyer des jetons de réinitialisation aux utilisateurs, nous devons stocker ces jetons dans une base de données.

Je suppose que vous avez une table d'utilisateurs fonctionnelle dans votre base de données. Si vous utilisez déjà Sequelize, tant mieux! Sinon, vous voudrez peut-être rafraîchir Sequelize et la CLI Sequelize.

Si vous n'avez pas encore utilisé Sequelize dans votre application, vous pouvez le configurer en exécutant la commande ci-dessous dans le dossier racine de votre application:

$ sequelize init

Cela créera un certain nombre de nouveaux dossiers dans votre configuration, y compris les migrations et les modèles.

Cela créera également un fichier de configuration. Dans votre fichier de configuration, mettez à jour le développement bloquer avec les informations d'identification sur votre serveur de base de données mysql local.

Utilisons l'outil CLI de Sequelize pour générer la table de base de données pour nous.

$ sequelize model: create --name ResetToken --attributes email: string, token: string, expiration: date, used: integer
$ sequelize db: migrer

Ce tableau comprend les colonnes suivantes:

  • Adresse e-mail de l'utilisateur,
  • Jeton qui a été généré,
  • Expiration de ce jeton,
  • Si le jeton a été utilisé ou non.

En arrière-plan, sequelize-cli exécute la requête SQL suivante:

CRÉER LA TABLE `ResetTokens` (
  `id` int (11) NOT NULL AUTO_INCREMENT,
  `email` varchar (255) DEFAULT NULL,
  `token` varchar (255) DEFAULT NULL,
  `expiration` datetime DEFAULT NULL,
  `createdAt` datetime NOT NULL,
  `updatedAt` datetime NOT NULL,
  `used` int (11) NOT NULL DEFAULT '0',
  CLÉ PRIMAIRE (`id`)
) MOTEUR = InnoDB AUTO_INCREMENT = 21 CHARGES PAR DEFAUT = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;

Vérifiez que cela fonctionne correctement à l'aide de votre client SQL ou de la ligne de commande:

mysql> décrire ResetTokens;
+ ------------ + -------------- + ------ + ----- + -------- - + ---------------- +
| Champ | Type | Null | Clé | Par défaut | Extra |
+ ------------ + -------------- + ------ + ----- + -------- - + ---------------- +
| id | int (11) | NON | PRI | NULL | auto_increment |
| email | varchar (255) | OUI | | NULL | |
| jeton | varchar (255) | OUI | | NULL | |
| expiration | datetime | OUI | | NULL | |
| createdAt | datetime | NON | | NULL | |
| updatedAt | datetime | NON | | NULL | |
| utilisé | int (11) | NON | | 0 | |
+ ------------ + -------------- + ------ + ----- + -------- - + ---------------- +
7 lignes en jeu (0,00 sec)

Conseil de sécurité n ° 3

Si vous n'utilisez pas actuellement un ORM, vous devriez envisager de le faire. Un ORM automatise l'écriture et l'échappement correct des requêtes SQL, rendant votre code plus lisible et plus sécurisé par défaut. Ils vous aideront à éviter les attaques par injection SQL en échappant correctement à vos requêtes SQL.

Configurer la réinitialisation de la route du mot de passe

Créez l'itinéraire get in user.js:

router.get ('/ mot de passe oublié', fonction (req, res, next) {
  res.render ('utilisateur / mot de passe oublié', {});
});

Créez ensuite l'itinéraire POST, qui est l'itinéraire qui est atteint lorsque le formulaire de réinitialisation du mot de passe est publié. Dans le code ci-dessous, j'ai inclus quelques fonctionnalités de sécurité importantes.

Conseils de sécurité # 4-6

  1. Même si nous ne trouvons pas d'adresse e-mail, nous renvoyons «ok» comme statut. Nous ne voulons pas que des robots fâcheux découvrent quels e-mails sont réels ou non réels dans notre base de données.
  2. Plus vous utilisez d'octets aléatoires dans un jeton, moins il peut être piraté. Nous utilisons 64 octets aléatoires dans notre générateur de jetons (n'utilisez pas moins de 8).
  3. Expirez le jeton en 1 heure. Cela limite la fenêtre de temps pendant laquelle le jeton de réinitialisation fonctionne.
router.post ('/ mot de passe oublié', fonction asynchrone (req, res, next) {
  // assurez-vous d'avoir un utilisateur avec cet e-mail
  var email = attendent User.findOne ({où: {email: req.body.email}});
  if (email == null) {
  / **
   * nous ne voulons pas dire aux attaquants qu'un
   * l'email n'existe pas, car cela permettra
   * les utiliser ce formulaire pour trouver ceux qui le font
   * existent.
   ** /
    return res.json ({status: 'ok'});
  }
  / **
   * Expire tous les jetons qui étaient auparavant
   * défini pour cet utilisateur. Cela empêche les anciens jetons
   * d'être utilisé.
   ** /
  attendre ResetToken.update ({
      utilisé: 1
    },
    {
      où: {
        e-mail: req.body.email
      }
  });
 
  // Créer un jeton de réinitialisation aléatoire
  var fpSalt = crypto.randomBytes (64) .toString ('base64');
 
  // le jeton expire après une heure
  var expireDate = new Date ();
  expireDate.setDate (expireDate.getDate () + 1/24);
 
  // insérer des données de jeton dans la base de données
  attendre ResetToken.create ({
    e-mail: req.body.email,
    expiration: expireDate,
    jeton: jeton,
    utilisé: 0
  });
 
  // créer un e-mail
  message const = {
      de: process.env.SENDER_ADDRESS,
      à: req.body.email,
      replyTo: process.env.REPLYTO_ADDRESS,
      subject: process.env.FORGOT_PASS_SUBJECT_LINE,
      text: 'Pour réinitialiser votre mot de passe, veuillez cliquer sur le lien ci-dessous.  n  nhttps: //'+process.env.DOMAIN+'/user/reset-password? token =' + encodeURIComponent (token) + '& email =' + req.body.email
  };
 
  // envoyer un e-mail
  transport.sendMail (message, fonction (err, info) {
     if (err) {console.log (err)}
     else {console.log (info); }
  });
 
  return res.json ({status: 'ok'});
});

Vous verrez une variable utilisateur référencée ci-dessus – qu'est-ce que c'est? Aux fins de ce didacticiel, nous supposons que vous disposez d'un modèle utilisateur qui se connecte à votre base de données pour récupérer des valeurs. Le code ci-dessus est basé sur Sequelize, mais vous pouvez le modifier si nécessaire si vous interrogez directement la base de données (mais je recommande Sequelize!).

Nous devons maintenant générer la vue. En utilisant Bootstrap CSS, jQuery et le framework pug intégré au framework Node Express, la vue ressemble à ceci:

étend ../layout
 
bloquer le contenu
  div.container
    div.row
      div.col
        h1 Mot de passe oublié
        p Saisissez votre adresse e-mail ci-dessous. Si nous l'avons dans le dossier, nous vous enverrons un e-mail de réinitialisation.
        div.forgot-message.alert.alert-success (style = "display: none;") Adresse e-mail reçue. Si vous avez un e-mail dans le dossier, nous vous enverrons un e-mail de réinitialisation. Veuillez patienter quelques minutes et vérifiez votre dossier spam si vous ne le voyez pas.
        form # ForgotPasswordForm.form-inline (onsubmit = "return false;")
          div.form-group
            label.sr-only (for = "email") Adresse e-mail:
            input.form-control.mr-2 # emailFp (type = 'email', nom = 'email', placeholder = "Adresse email")
          div.form-group.mt-1.text-center
            bouton # fpButton.btn.btn-success.mb-2 (type = 'soumettre') Envoyer un e-mail
 
  scénario.
    $ ('# fpButton'). on ('click', function () {
      $ .post ('/ utilisateur / mot de passe oublié', {
        email: $ ('# emailFp'). val (),
      }, fonction (resp) {
        $ ('. oublié-message'). show ();
        $ ('# ForgotPasswordForm'). remove ();
      });
    });

Voici le formulaire sur la page:

champ de réinitialisation du mot de passe pour votre flux de travail de réinitialisation sécurisée du mot de passe
Votre formulaire de réinitialisation de mot de passe. (Grand aperçu)

À ce stade, vous devriez pouvoir remplir le formulaire avec une adresse e-mail qui se trouve dans votre base de données, puis recevoir un e-mail de réinitialisation du mot de passe à cette adresse. Cliquer sur le lien de réinitialisation ne fera rien pour le moment.

Configurer la route «Réinitialiser le mot de passe»

Maintenant, allons de l'avant et configurons le reste du flux de travail.

Ajoutez le module Sequelize.Op à votre itinéraire:

const Sequelize = require ('sequelize');
const Op = Sequelize.Op;

Maintenant, construisons l'itinéraire GET pour les utilisateurs qui ont cliqué sur ce lien de réinitialisation de mot de passe. Comme vous le verrez ci-dessous, nous voulons nous assurer que nous validons le jeton de réinitialisation de manière appropriée.

Conseil de sécurité n ° 7:

Assurez-vous que vous recherchez uniquement des jetons de réinitialisation qui n'ont pas expiré et n'ont pas été utilisés.

À des fins de démonstration, j'efface également tous les jetons expirés en charge ici pour garder la table petite. Si vous avez un grand site Web, déplacez-le vers un cronjob.

router.get ('/ reset-password', fonction asynchrone (req, res, next) {
  / **
   * Ce code efface tous les jetons expirés. Vous
   * devrait déplacer ceci vers un cronjob si vous avez un
   * grand site. Nous l'incluons ici comme
   * manifestation.
   ** /
  attendre ResetToken.destroy ({
    où: {
      expiration: { [Op.lt]: Sequelize.fn ('CURDATE')},
    }
  });
 
  // trouve le jeton
  var record = wait ResetToken.findOne ({
    où: {
      e-mail: req.query.email,
      expiration: { [Op.gt]: Sequelize.fn ('CURDATE')},
      jeton: req.query.token,
      utilisé: 0
    }
  });
 
  if (record == null) {
    return res.render ('user / reset-password', {
      message: 'Le jeton a expiré. Veuillez réessayer la réinitialisation du mot de passe. ',
      showForm: false
    });
  }
 
  res.render ('user / reset-password', {
    showForm: true,
    record: record
  });
});

Créons maintenant la route POST qui est ce qui est frappé une fois que l'utilisateur a rempli ses nouveaux détails de mot de passe.

Conseil de sécurité n ° 8 à 11:

  • Assurez-vous que les mots de passe correspondent et répondent à vos exigences minimales.
  • Vérifiez à nouveau le jeton de réinitialisation pour vous assurer qu'il n'a pas été utilisé et qu'il n'a pas expiré. Nous devons le vérifier à nouveau car le jeton est envoyé par un utilisateur via le formulaire.
  • Avant de réinitialiser le mot de passe, marquez le jeton comme utilisé. De cette façon, si quelque chose d'imprévu se produit (panne du serveur, par exemple), le mot de passe ne sera pas réinitialisé tant que le jeton est toujours valide.
  • Utilisez un sel aléatoire cryptographiquement sécurisé (dans ce cas, nous utilisons 64 octets aléatoires).
router.post ('/ reset-password', fonction asynchrone (req, res, next) {
  // comparer les mots de passe
  if (req.body.password1! == req.body.password2) {
    return res.json ({status: 'error', message: 'Les mots de passe ne correspondent pas. Veuillez réessayer.'});
  }
 
  / **
  * Assurez-vous que le mot de passe est valide (isValidPassword
  * la fonction vérifie si le mot de passe est> = 8 caractères, alphanumérique,
  * a des caractères spéciaux, etc.)
  ** /
  if (! isValidPassword (req.body.password1)) {
    return res.json ({status: 'error', message: 'Le mot de passe ne répond pas aux exigences minimales. Veuillez réessayer.'});
  }
 
  var record = wait ResetToken.findOne ({
    où: {
      e-mail: req.body.email,
      expiration: { [Op.gt]: Sequelize.fn ('CURDATE')},
      jeton: req.body.token,
      utilisé: 0
    }
  });
 
  if (record == null) {
    return res.json ({status: 'error', message: 'Token not found. Veuillez réessayer le processus de réinitialisation du mot de passe.'});
  }
 
  var upd = attendre ResetToken.update ({
      utilisé: 1
    },
    {
      où: {
        e-mail: req.body.email
      }
  });
 
  var newSalt = crypto.randomBytes (64) .toString ('hex');
  var newPassword = crypto.pbkdf2Sync (req.body.password1, newSalt, 10000, 64, 'sha512'). toString ('base64');
 
  attendre User.update ({
    mot de passe: newPassword,
    sel: newSalt
  },
  {
    où: {
      e-mail: req.body.email
    }
  });
 
  return res.json ({status: 'ok', message: 'Password reset. Veuillez vous connecter avec votre nouveau mot de passe.'});
});

Et encore une fois, la vue:

étend ../layout
 
bloquer le contenu
  div.container
    div.row
      div.col
        h1 Réinitialiser le mot de passe
        p Saisissez votre nouveau mot de passe ci-dessous.
        si message
          div.reset-message.alert.alert-warning # {message}
        autre
          div.reset-message.alert (style = 'display: none;')
        si showForm
          form # resetPasswordForm (onsubmit = "return false;")
            div.form-group
              label (for = "password1") Nouveau mot de passe:
              input.form-control # password1 (type = 'mot de passe', nom = 'mot de passe1')
              small.form-text.text-muted Le mot de passe doit comporter 8 caractères ou plus.
            div.form-group
              label (for = "password2") Confirmer le nouveau mot de passe
              input.form-control # password2 (type = 'mot de passe', nom = 'mot de passe2')
              small.form-text.text-muted Les deux mots de passe doivent correspondre.
            entrée # emailRp (type = 'caché', nom = 'email', valeur = record.email)
            input # tokenRp (type = 'caché', name = 'token', value = record.token)
            div.form-group
              button # rpButton.btn.btn-success (type = 'submit') Réinitialiser le mot de passe
 
  scénario.
    $ ('# rpButton'). on ('click', function () {
      $ .post ('/ user / reset-password', {
        password1: $ ('# password1'). val (),
        password2: $ ('# password2'). val (),
        email: $ ('# emailRp'). val (),
        jeton: $ ('# tokenRp'). val ()
      }, fonction (resp) {
        if (resp.status == 'ok') {
          $ ('. reset-message'). removeClass ('alert-danger'). addClass ('alert-success'). show (). text (resp.message);
          $ ('# resetPasswordForm'). remove ();
        } autre {
          $ ('. reset-message'). removeClass ('alert-success'). addClass ('alert-danger'). show (). text (resp.message);
        }
      });
    });

Voici à quoi cela devrait ressembler:

formulaire de réinitialisation de mot de passe pour votre flux de travail de réinitialisation de mot de passe sécurisé
Votre formulaire de réinitialisation de mot de passe. (Grand aperçu)

Ajoutez le lien à votre page de connexion

Enfin, n'oubliez pas d'ajouter un lien vers ce flux depuis votre page de connexion! Une fois que vous faites cela, vous devriez avoir un flux de mot de passe de réinitialisation de travail. Assurez-vous de tester soigneusement à chaque étape du processus pour confirmer que tout fonctionne et que vos jetons ont une courte expiration et sont marqués avec le bon état à mesure que le flux de travail progresse.

Prochaines étapes

J'espère que cela vous a aidé à coder une fonction de réinitialisation du mot de passe sécurisée et conviviale.

  • Si vous souhaitez en savoir plus sur la sécurité cryptographique, je recommande le résumé de Wikipedia (attention, c'est dense!).
  • Si vous souhaitez ajouter encore plus de sécurité à l'authentification de votre application, consultez 2FA. Il existe de nombreuses options différentes.
  • Si je vous ai fait peur de créer votre propre flux de réinitialisation de mot de passe, vous pouvez compter sur des systèmes de connexion tiers tels que Google et Facebook. PassportJS est un middleware que vous pouvez utiliser pour NodeJS qui implémente ces stratégies.
Smashing Editorial(dm, yk, il)