Search code examples
node.jsexpressjwtbcrypt

Why is my plain text and salt not coinciding?


I am developing an api, an I am using nodejs, express, sequelize, mysql.

I am implementing my authentication routes and methods using bcrypt and jwt. My user registration works fine, the users are being saved with their encrypted password in my db.

The issue I am struggling with, is that when I want to authenticate a user the plain text password and the hashed password don´t match.

This is my route inside users.js

router.post('/', [
    
    check('email', 'Plese include a valid email').isEmail(),
    check('password', 'Password is required').exists()
], 
async (req, res)=> {
    const errors = validationResult(req);
    if(!errors.isEmpty()){
        return res.status(400).json({ errors:errors.array()}); //400 is for bad requests
    }


    const { email, password } = req.body;

    try{

        // Validate emil & password
        if (!email || !password) {
            return next(new ErrorResponse('Please provide an email and password', 400));
        }

        //See if user exists
        let user = await User.findOne({ where: { email: email }, attributes: ['email', 'password'] });

        if(!user){
            return res.status(400).json({ errors: [{ msg:'Invalid credentials' }] });
        }

        //Compare the input password, plane text, to the encrypted password.
        const isMatch = await user.matchPassword(password);
        
        if(!isMatch){
            return res.status(400).json({ errors: [{ msg:'Invalid credentials' }] });
        }
    
        //Return jsonwebtoken -> this for users to be logged in right after registration
        sendTokenResponse(user, 200, res);

    }catch(err){
        console.error(err.message);
        res.status(500).send('Server Error');
    }
    

});

And this is my User model in user.model.js

const { Sequelize, DataTypes } = require('sequelize');
const sequelize = require('../config/db');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

const User = sequelize.define('User', {
    id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true
    },
    name: {
        type: DataTypes.STRING,
        allowNull: false
    },
    lastname: {
        type: DataTypes.STRING,
        allowNull: false
    },
    email: {
        type: DataTypes.STRING,
        allowNull: false,
        unique: true
    },
    password: {
        type: DataTypes.STRING,
        allowNull: false
    }
});


User.addHook('beforeCreate', async (user, options) => {
    if (user.password) {
       const salt = await bcrypt.genSalt(10);
       user.password = await bcrypt.hash(user.password, salt);
    }
});
   
User.prototype.getSignedJwtToken = function() {
    return jwt.sign({ id: this.id }, process.env.JWT_SECRET, {
       expiresIn: process.env.JWT_EXPIRE
    });
};
   
User.prototype.matchPassword = async function(enteredPassword) {
    console.log(this);
    console.log(enteredPassword, this.password);
   console.log(await bcrypt.compare(enteredPassword, this.password));

   return await bcrypt.compare(enteredPassword, this.password);
};

module.exports = User;

As you can see I have some methods inside my model to encrypt my password and to compare the inserted plain text password to the hashed password in the db.

For some reason when I enter correctly the credentials, my method matchPassword always returns false.

I´ll also leave my registering route, just in case I am encrypting differently the password with a different salt that the one being used in matchPassword

router.post('/register', [
    check('name', 'Name is required')
        .not()
        .isEmpty(),
    check('lastname', 'Lastname is required')
        .not()
        .isEmpty(),
    check('email', 'Plese include a valid email').isEmail(),
    check('password', 'Please enter a password with 6 or more characters').isLength({min:6})
 ], 
 async (req, res) => {
    const errors = validationResult(req);
    if(!errors.isEmpty()){
        return res.status(400).json({ errors:errors.array()}); //400 is for bad requests
    }
 
    const { name, lastname, email, password } = req.body;
 
    try {
        
        //Check if user exists
        let user = await User.findOne({ where: { email } });
 
        if(user){
            return res.status(400).json({ errors: [{ msg: 'User already exists'}]});
        }
 
        //Encrypt password
        const salt = await bcrypt.genSalt(10);
        hashedPassword = await bcrypt.hash(password, salt);


        //Create user
        user = await User.create({ name, lastname, email, password:hashedPassword });

        //Return jsonwebtoken -> this for users to be logged in right after registration

        sendTokenResponse(user, 200, res);
       
 
    } catch (error) {
        console.error('Error:', error);
        res.status(500).send('Server error');
    }
 
 });

Solution

  • You used the hash function twice.

    One of those is in your model definition. You have bcrypt.hash in beforeCreate hook, and the other is in the register controller.

    If you use bcrypt.hash to encrypt the password, you don't need to use it in the register controller. You have to pass the password as plain text. Because you have already that in beforeCreate hook.