Mongoose: Besoin de vérifier si le champ de mot de passe a été modifié dans pre findOneAndUpdate

Créé le 28 sept. 2016  ·  24Commentaires  ·  Source: Automattic/mongoose

J'ai rencontré un problème hier soir et je n'ai toujours pas trouvé de solution. J'espère que quelqu'un pourra me conseiller. J'autorise les utilisateurs à mettre à jour leur nom d'utilisateur et leur mot de passe, et chaque fois que le champ du mot de passe a été modifié, je dois le hacher avant de le stocker dans mongo.

Jusqu'à présent, j'avais utilisé ce code pour hacher le mot de passe de l'utilisateur :

UserSchema.pre('save', function(next) {
    if (this.isModified('password')) // If the pw has been modified, then encrypt it again
    this.password = this.encryptPassword(this.password);

    next();
});

La fonction encryptPassword est une fonction personnalisée que j'ai ajoutée à mon User Schema :

// Add custom methods to our User schema
UserSchema.methods = {
    // Hash the password
    encryptPassword: function(plainTextPassword) {
        if (!plainTextPassword) {
            return ''
        } else {
            var salt = bcrypt.genSaltSync(10);
            return bcrypt.hashSync(plainTextPassword, salt);
        }
    }
};

Cela ne fonctionne que lorsque l'utilisateur s'inscrit pour la première fois et que je crée une nouvelle instance utilisateur, puis l'enregistre. J'ai également besoin de la même fonctionnalité lors de la mise à

Je faisais des recherches dans des numéros antérieurs ici et je suis tombé sur celui-ci : les pré, post middleware ne sont pas exécutés sur findByIdAndUpdate , et je suis tombé sur un commentaire dans lequel un autre développeur essayait de faire quelque chose de très similaire à ce dont je parle : https:// github.com/Automattic/mongoose/issues/964#issuecomment -154332797

@vkarpov15 a répondu à sa question avec la suggestion de code suivante :

TodoSchema.pre('findOneAndUpdate', function() {
  this.findOneAndUpdate({}, { password: hashPassword(this.getUpdate().$set.password) });
});

Le seul problème est que cela nous obligera à ressasser le mot de passe chaque fois que vous mettez à jour un utilisateur, ce qui est faux. Vous ne devez hacher le mot de passe que lorsque l'utilisateur s'inscrit pour la première fois et chaque fois qu'il envoie une demande de modification de son mot de passe. Si vous exécutez le code ci-dessus à chaque fois que l'utilisateur est mis à jour, vous finirez par ressasser l'ancien hachage. Ainsi, lorsque l'utilisateur saisira son mot de passe la prochaine fois sur le client, il ne correspondra plus au hachage stocké dans mongo. .

Existe-t-il un moyen d'utiliser quelque chose comme this.isModified('password') intérieur de .pre('findOneAndUpdate' , de sorte que je ne hache le mot de passe que lorsque l'utilisateur l'a mis à jour ?

A ce stade, je suis ouvert à toutes les suggestions merci.

Commentaire le plus utile

J'utilise actuellement cette solution et elle fonctionne. Vous n'avez pas besoin d'exécuter une autre requête.

schema.pre("update", function(next) {
            const password = this.getUpdate().$set.password;
            if (!password) {
                return next();
            }
            try {
                const salt = Bcrypt.genSaltSync();
                const hash = Bcrypt.hashSync(password, salt);
                this.getUpdate().$set.password = hash;
                next();
            } catch (error) {
                return next(error);
            }
        });

Tous les 24 commentaires

J'ai testé cela un peu plus et j'ai pensé ajouter quelques notes supplémentaires. À ce stade, je ne peux pas utiliser user.save ou User.update .

Si j'essaie de mettre à jour l'utilisateur à l'aide de user.save , je rencontre une erreur de validation à cause d'un hook de validation personnalisé que j'ai configuré :

//// Custom error for unique username
UserSchema.path('username').validate(function(value, done) {
    mongoose.model('User', UserSchema).count({ username: value }, function(err, count) {
        if (err) {
            return done(err);
        } 
        // If `count` is greater than zero, "invalidate"
        done(!count);
    });
}, 'Sorry but this username is already taken');

Ce qui précède est nécessaire pour que je puisse renvoyer un message d'erreur de validation personnalisé au client si un nouvel utilisateur essaie de s'inscrire et choisit un nom d'utilisateur avec lequel quelqu'un d'autre s'est déjà inscrit. Si j'essaie de mettre à jour les informations d'un utilisateur en faisant user.save , ce validateur personnalisé s'exécute et renvoie une erreur car le nom d'utilisateur est déjà pris.

Si j'essaie d'utiliser User.update place... Je ne peux pas appeler this.isModified('password') car cela fait référence au modèle et non à un document.

Encore une fois, je suis bloqué à ce stade et je ne sais pas quoi faire. J'espère avoir fourni suffisamment d'informations. C'est le premier problème que j'ai rencontré dans la mangouste que je n'ai pas pu résoudre par moi-même, donc toute aide est grandement appréciée, merci.

D'accord, j'ai mentionné dans mon commentaire d'origine que @vkarpov15 a publié une solution potentielle dans un autre problème :

TodoSchema.pre('findOneAndUpdate', function() {
  this.findOneAndUpdate({}, { password: hashPassword(this.getUpdate().$set.password) });
});

J'ai essayé d'implémenter ceci dans un pré-crochet pour findOneAndUpdate et update :

UserSchema.pre('findOneAndUpdate', function(next) {
    this.findOneAndUpdate({}, { password: encryptPassword(this.getUpdate().$set.password) });
    next();
});

Cela fonctionne et je peux enregistrer le mot de passe haché, mais maintenant je ne peux pas le faire après la validation du champ de mon mot de passe. J'ai une validation de champ de mot de passe qui indique que le mot de passe ne peut contenir que 4 caractères. Je ne suis pas ici pour débattre de la longueur ou de la longueur d'un mot de passe... le fait est que le crochet ci-dessus ne fonctionnera pas car il ne peut pas dépasser la validation, alors que lorsque je faisais le hachage dans le pré-crochet save , il s'exécutait après la validation.

Je ne sais toujours pas quoi faire à ce stade.

J'ai donc finalement trouvé une solution pour cela. J'ai arrêté d'utiliser les méthodes de mise à jour pour ce cas d'utilisation spécifique et je suis revenu à l'utilisation de save . J'ai supprimé le code UserSchema.path('username') que j'avais partagé précédemment et voici à quoi ressemble mon hook de pré-sauvegarde maintenant :

UserSchema.pre('save', function(next, done) {
    var self = this;
    mongoose.models["User"].findOne({username: self.username}, function(err, user) {
        if(err) {
            done(err);
        } else if(user) {            
            if (user._id.equals(self._id)) return next(); // If id's are equal, then we don't care about false dupe username error because its the same user!

            self.invalidate('username', 'Sorry but this username is already taken');
            done(new Error('Sorry but this username is already taken'));
        } else {
            next();
        }
    });
});

Dans le code ci-dessus, je vérifie si nous avons déjà un utilisateur dans mongo dont le nom d'utilisateur essaie d'être enregistré. Si le nom d'utilisateur existe déjà, je compare le _id's du document utilisateur trouvé et le document utilisateur essayant d'être enregistré. Si leurs _id's sont égaux, alors nous ne nous soucions pas de l'erreur de validation du nom d'utilisateur unique car ils sont le même utilisateur, nous appelons donc simplement next() et passons à autre chose.

Cela permet à mon crochet de pré-sauvegarde pour le hachage du mot de passe de s'exécuter également :

UserSchema.pre('save', function(next) {
    console.log('pre save password: ' + this.password);
    if (this.isModified('password')) // If the pw has been modified, then encrypt it again
        this.password = this.encryptPassword(this.password);
    next();
});

Je peux donc toujours hacher le mot de passe s'il est mis à jour, et je continue à exécuter toutes mes règles de validation que j'ai configurées pour mon UserSchema . J'ai testé cela dans un projet de démonstration et je n'arrive pas à trouver de défauts à cela.

Je vais fermer ce problème, mais je reviendrai après l'avoir intégré dans mon application de production et laisser une mise à jour avec mes progrès. J'ai du mal à croire que je suis le seul à avoir déjà eu ce problème, alors j'aimerais toujours entendre des conseils ou des commentaires sur la solution que j'ai trouvée, ou sur la façon dont vous le feriez vous-même.

Merci.

J'ai donc testé la solution dans mon commentaire précédent dans une application de production et j'ai réalisé qu'elle est imparfaite et ne fonctionnera pas. Malheureusement, si j'appelle save sur le document utilisateur et que le champ de mot de passe n'a pas été mis à jour, il ne s'agit donc que d'un hachage... il ne passera pas ma validation pour mon champ de mot de passe.

Il semble donc que je ne pourrai pas utiliser save sans plusieurs solutions de contournement, ce qui me ramène simplement à la gestion des fonctions update et de leurs problèmes que j'ai déjà mentionnés ci-dessus.

Si quelqu'un a une idée de comment je peux résoudre ce problème, j'aimerais avoir de vos nouvelles.

Il s'avère qu'il n'y a aucun moyen de le faire. C'est juste un cas limite étrange que vous rencontrez lors de l'utilisation de la validation. J'ai fini par supprimer les validations de mongoose pour mon champ de mot de passe et je l'ai simplement géré manuellement à l'aide d'une expression régulière personnalisée. Cela m'a donné un contrôle total et m'a permis de prendre en charge tous les scénarios de sauvegarde/mise à jour pour mon modèle User .

Eh bien, il existe une solution, vous utilisez update ou findOneAndUpdate et ne transmettez le champ de mot de passe du client que lorsque vous voulez que le mot de passe soit mis à jour. IMO stocker le hachage du mot de passe dans le document de l'utilisateur n'est pas une bonne pratique de toute façon, car vous devez alors être très discipliné pour que votre API ne divulgue pas le hachage du mot de passe

IMO stocker le hachage du mot de passe dans le document de l'utilisateur n'est pas une bonne pratique de toute façon, car vous devez alors être très discipliné pour que votre API ne divulgue pas le hachage du mot de passe

Intéressant. À quoi ressemblerait une alternative à celle-ci @vkarpov15 ?

Collection séparée avec 2 champs, identifiant et mot de passe. Pour vérifier le mot de passe des utilisateurs, vous interrogez la collection de hachages pour leur hachage et le vérifiez normalement. Facilite l'intégration des modifications de mot de passe dans un paradigme reposant et permet de s'assurer que vous ne divulguez pas le mot de passe des utilisateurs lors de l'affichage d'une liste d'utilisateurs, car l'obtention du hachage du mot de passe des utilisateurs est facultative

@mitchellporter salut...

Je suis tombé sur votre message en faisant face au même problème. J'ai essayé ça tout à l'heure et je pense que ça va couvrir mes besoins. Je voulais le partager.

const bcrypt = require('bcrypt')
const debug = require('debug')('db')
const mongoose = require('mongoose')
mongoose.Promise = global.Promise;
const { Schema } = mongoose
const { Types } = Schema
const { ObjectId } = Types
const db = mongoose.connection

const UserSchema = new Schema({
  email: { type: String, index: { unique: true, dropDups: true } },
  hash: { type: String, required: true }
})

// This is the important bit
// Using a virtual lets me pass `{ password: 'xyz' }` 
// without actually having it save.
// Instead it is caught by this setter 
// which performs the hashing and 
// saves the hash to the document's hash property.
UserSchema.virtual('password').set(function(value) {
  const salt = bcrypt.genSaltSync(10)
  this.hash = bcrypt.hashSync(value, salt)
})

// A method for checking the password
UserSchema.methods.comparePassword = function(password) {
  return bcrypt.compareSync(password, this.hash)
}

// Here I make sure I never return the
// password in the JSON representation
// Note that I don't do the same to
// `toObject` so i can still see the hash
// in, say, the console.
UserSchema.set('toJSON', {
  getters: true,
  transform: (doc, ret, options) => {
    delete ret.hash;
    return ret;
  }
})

const User = mongoose.model('Store', UserSchema)

module.exports = { db }

Voici comment cela apparaît lors de la sortie de l'objet vers la console :

{
  _id: '595317e5f3afec2e0453bc64',
  email: '[email protected]',
  hash: '$2a$10$Bovjefiwfdwu/q9liKpdyluQTFIFT/VcrpzUWYPc.bGUYUIFIG',
  __v: 2,
}

Et voici à quoi cela ressemble lorsqu'il est renvoyé en tant que JSON :

{
  email: '[email protected]',
}

Cela ne sépare pas les hachages dans une collection distincte comme @ vkarpov15 suggéré que j'aimerais faire plus tôt que tard, mais peut-être que cela aide votre cas.

Eh bien... C'était une perte de temps... Désolé.

Le test unitaire que j'utilisais pour vérifier que c'était faux. Il s'avère que les seuls ensembles virtuels qui hachent en mémoire et qu'ils ne sont pas persistants.

Revenons donc à la question de savoir comment hacher les mises à jour.

@lgomez Depuis, j'ai abandonné l'utilisation de MongoDB dans mes projets, donc je n'utilise plus mongoose non plus... bonne chance avec ça !

Peut-être que cela pourrait aider ?

schema.pre('update', function(next) {
  this.findOne({"_id":this.getUpdate().$set._id},function(err, doc){
    if(doc.password != this.getUpdate().$set.password){
      this.getUpdate().$set.password = bcrypt.hashSync(this.getUpdate().$set.password, 10);
    }
    next();
  })
});

@NicolasBlois ça marche. Il y a une condition de course potentielle là-bas, mais cela fonctionnera dans la plupart des cas.

J'utilise actuellement cette solution et elle fonctionne. Vous n'avez pas besoin d'exécuter une autre requête.

schema.pre("update", function(next) {
            const password = this.getUpdate().$set.password;
            if (!password) {
                return next();
            }
            try {
                const salt = Bcrypt.genSaltSync();
                const hash = Bcrypt.hashSync(password, salt);
                this.getUpdate().$set.password = hash;
                next();
            } catch (error) {
                return next(error);
            }
        });

Quelle est la différence entre la solution ci-dessus et quelque chose comme ça pour remplacer les quatre premières lignes du middleware ?
if (!this.isModified('password')) return next()

isModified serait mieux si votre mot de passe était un objet complexe car il traverse tous ses champs en profondeur, mais coûterait probablement un peu plus cher. Bien que je suppose que cela ne ferait pas beaucoup de différence dans ce cas d'utilisation.

Traverse-t-il tous les champs lorsqu'un chemin est donné ?

Aucune idée. Peut-être regarder à l'intérieur du code source pour le comprendre ?

Je l'ai fait mais merci pour la suggestion.

@ezamelczyk merci pour votre réponse. ça marche super !

Je vais partager ici comment je fais la demande de mise à jour du mot de passe ; au cas où les gens le rechercheraient.

exports.updateUserPassword = ({_id}, passObj) => {
    const userId = _id;

    if(passObj.newPassword !== passObj.passwordConfirmation){
        throw new Error('New password and confirmation do not match!');
    }

    return new Promise(async(resolve, reject) => {
        try{
            await User.update({ _id: userId }, {
                $set: { password: passObj.newPassword }
            })            
            resolve({})
        }catch(error){
            reject({ message: error.message, error: error })
        }
    })
}

Bien que cette mise à jour soit déclenchée à partir de la page des paramètres, pas du "mot de passe oublié" et dans la page des paramètres, l'utilisateur doit saisir le mot de passe actuel pour pouvoir mettre à jour le mot de passe. Donc, je dois vérifier si le mot de passe actuel correspond. J'ai la méthode comparePassword disponible mais je ne sais pas comment appeler ? passObj contient currentPassword, newPassword, passwordConfirmation.

userSchema.methods.comparePassword = function(candidatePassword, callback){
    bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
        if(err){ return callback(err); }
        callback(null, isMatch);
    });
}

@aaronfulkerson comment pouvez-vous accéder à this.isModified dans le hook de pré-mise à jour ?

Autant que je sache, isModified n'est applicable que dans l'API Document , tandis que dans le crochet this pré-mise à jour, Query place.

Donc je ne crois pas qu'il y ait d'autre moyen que celui suggéré par @ezamelczyk

@aminnaggar je ne me souviens pas. Je l'ai fait fonctionner, mais j'ai ensuite remplacé Mongo par Postgres.

définir le hashedpassword sur getUpdate().$set.password ou getUpdate.password, stockera toujours le mot de passe non haché. Au lieu de this.getUpdate, utilisez this._update. Cela a fonctionné pour moi.

J'ai résolu ce problème avec le code suivant, j'espère que cela vous aidera

userSchema.pre('findOneAndUpdate', async function() {
  const docToUpdate = await this.model.findOne(this.getQuery())

  if (docToUpdate.password !== this._update.password) {
    const newPassword = await hash(this._update.password, 10)
    this._update.password = newPassword
  }
})
Cette page vous a été utile?
0 / 5 - 0 notes