Fermer

mars 11, 2020

Création de flux de mots de passe sécurisés avec NodeJS et MySQL


À propos de l'auteur

Darshan Somashekar est un entrepreneur technologique qui a construit et vendu deux startups. Son dernier projet amusant est un site de solitaire appelé Solitaired . Auparavant,…
En savoir plus
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 réinitialisation de mot de passe sécurisé afin que vous puissiez éviter ces pièges.

Si vous êtes comme moi, vous avez oublié votre mot de passe plus d'une fois, en particulier sur les sites que vous n'avez pas " t visité dans un certain temps. Vous avez probablement également vu, et / ou été mortifié par, la réinitialisation des e-mails de mot de passe qui contiennent votre mot de passe en texte brut.

Malheureusement, le flux de travail de réinitialisation du mot de passe est court et peu d'attention 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 flux de travail.
  • Formulaire permettant à l'utilisateur d'envoyer son e-mail.
  • Une recherche qui valide l'e-mail et envoie un e-mail à l'adresse.
  • Un e-mail contenant la réinitialisation jeton 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.
  • Enregistrement du nouveau mot de passe et permettre à l'utilisateur de se reconnecter avec le nouveau mot de passe.

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.

Astuce 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 à mettre en œuvre). 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 devons donc déplacer ces informations dans une base de données.

Installation

Premièrement, installez Sequelize, Nodemailer et les 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'une mise à jour sur Express et sur les itinéraires, consultez leur guide .

 const nodemailer = require ('nodemailer'); 

Et configurez-le avec vos informations d'identification 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 AWS's Simple Email Service mais vous pouvez utiliser n'importe quoi (Mailgun, etc.).

Si c'est la première fois que vous configurez l'envoi de vos e-mails service, 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 la façon dont SES fonctionne avec Route53 .

Conseil de sécurité n ° 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 'va 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 revoir 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 votre application. dossier racine:

 $ 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 bloc de développement 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 - attribue le courrier électronique: chaîne, jeton: chaîne, expiration: date, utilisé: entier
$ sequelize db: migrate 

Ce tableau contient les colonnes suivantes:

  • Adresse e-mail de l'utilisateur,
  • Jeton généré,
  • Expiration de ce jeton,
  • Indique si le jeton a été utilisé ou

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

 CREATE 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`)
) ENGINE = InnoDB AUTO_INCREMENT = 21 DEFAULT CHARSET = 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écrivez 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 | |
+ ------------ + -------------- + ------ + ----- + -------- - + ---------------- +
Ensemble de 7 lignes (0,00 s) 

Astuce de sécurité n ° 3

Si vous n'utilisez pas actuellement d'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 la route get dans user.js :

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

Créez ensuite la route POST, qui est la route qui est atteinte 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.

Astuces 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 est probable qu'il puisse ê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 ('/ Forgot-Password', fonction async (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 cadre pug intégré au cadre 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 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 )

À 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 route:

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

Maintenant, construisons la route 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 les 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 async (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
  });
}); 

Maintenant, créons 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é # 8 à 11:

  • Assurez-vous que les mots de passe correspondent et respectent votre minimum
  • Vérifiez à nouveau le jeton de réinitialisation pour vous assurer qu'il n'a pas été utilisé et 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 sera 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 vers 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 au fur et à mesure que le flux de travail progresse.

Prochaines étapes

J'espère que cela vous a aidé sur votre chemin vers

  • Si vous souhaitez en savoir plus sur la sécurité cryptographique, je recommande le résumé de Wikipedia (attention, c'est dense!).
  • Si vous voulez 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 comme Google et Facebook. PassportJS est un middleware que vous pouvez utiliser pour NodeJS qui implémente ces stratégies.
 Éditorial Smashing (dm, yk, il)




Source link