Search code examples
javascriptnode.jsmongodbmongoosebcrypt

Why bcrypt fails when MongoDB versionKey "_v" is changed?


I'm trying to implement an authentication flow in Nodejs. I'm using MongoDB as database and there is a problem with 'bcrypt password hashing' and 'mongoose document versioning'.

When I create a new account and login with this account, there is no problem and everything is working. But when I do changes on subdocuments, versionKey "_v" is changing and I can no longer access the account. It throws me the 'Invalid password' error which comes from passport middleware. I don't figure out why it's happening.

Here is the structure:

Mongoose User Model

const bcrypt = require("bcrypt");
const userSchema = new mongoose.Schema(
    {
        name: { type: String, required: true },
        surname: { type: String, required: true },
        email: { type: String, required: true, unique: true },
        password: { type: String, required: true },
        username: { type: String },
        bio: { type: String },
        title: { type: String },
        area: { type: String },
        image: {
            type: String,
            default:
                "https://icon-library.com/images/no-profile-pic-icon/no-profile-pic-icon-24.jpg",
        },
        experiences: [
            { type: mongoose.Schema.Types.ObjectId, ref: "Experience" },
        ],
        friends: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }],
        friendRequests: [
            {
                type: mongoose.Schema.Types.ObjectId,
                ref: "User",
            },
        ],
    },
    { timestamp: true }
);

/**
 * Enyrcyp user password before saving DB
 */
userSchema.pre("save", async function (next) {
    try {
        // const user = this;
        // if (!user.isModified("password")) return next();
        const salt = await bcrypt.genSalt(10);
        this.password = await bcrypt.hash(this.password, salt);
    } catch (error) {
        console.log("Bcryp hash error: ", error);
        next(error);
    }
});

/**
 * Checks entered password and hashed password in DB
 * returns boolean
 * @param {String} enteredPassword
 */
userSchema.methods.isValidPassword = async function (enteredPassword) {
    try {
        return await bcrypt.compare(enteredPassword, this.password);
    } catch (error) {
        console.log("Bcrypt password check error: ", error);
        next(error);
    }
};

const User = mongoose.model("User", userSchema);
module.exports = User;

Passport middleware to handling user login process

passport.use(
    new LocalStrategy(
        {
            usernameField: "email",
        },
        async (email, password, done) => {
            try {
                const foundUser = await db.User.findOne({ email });
                if (!foundUser) throw new ApiError(400, "Invalid email ");

                const isPasswordsMatched = await foundUser.isValidPassword(
                    password
                );

                if (!isPasswordsMatched)
                    throw new ApiError(400, "Invalid password");

                //Send user if everything  is ok
                done(null, foundUser);
            } catch (error) {
                console.log("Passport local strategy error: ", error);
                done(error, false);
            }
        }
    )
);

Solution

  • I discovered that if I change the logic of hashing password it works. I deleted the mongoose pre save hook which was adding hash to password right before saving database.

    Here is the working structure:

    • I just converted mongoose hook to method
    /**
     * Enyrcyp user password before saving DB
     */
    userSchema.methods.hashPassword = async function () {
        try {
            const salt = await bcrypt.genSalt(10);
            this.password = await bcrypt.hash(this.password, salt);
        } catch (error) {
            console.log("Bcryp hash error: ", error);
            next(error);
        }
    };
    
    
    • And I'm calling the hashPassword function right before saving new user to database.

    So with this structure I'm just using bcrypt once when the user is signed up. Probably document version changes was effecting the bcrypt hash. So it works now.