can't hash a password and set it to field in mongodb schema

Problem

I'm experimenting with a demo blog platform by madhusudhan srinivasa for node.js, express, and mongodb.. I've created a blog heavily based on his implementation. But, I'm having trouble with hashing a password and creating a user. I've attached the model and controller for creating a user since I don't know exactly where the problem is.

My user creation form has user and password fields which are passed into the controller with bodyParser.

My problem is that when i submit the form to create a user, I get an undefined error from the hash validation function, "cannot get length of undefined."

If i comment this hash validation function out, a user is created. But when i look at the created user in mongodb command line, it doesn't have a hash field at all, but the name and salt fields are set correctly. However, when I console.log the hash inside encryptPassword(), it seems to output a correctly hashed password.

I have been working on this for hours and am completely at a loss to what the problem could be.

model:

var mongoose = require('mongoose')
    , Schema = mongoose.Schema
    , crypto = require('crypto')
    , _ = require('underscore')

// user schema

var UserSchema = new Schema ({
    name: { type: String, default: '' },
    hash: { type: String, default: '' },         
    salt: { type: String, default: '' }
})

UserSchema
    .virtual('password')
    .set(function(password) {
        this._password = password
        this.salt = this.makeSalt()
        this.hash = this.encryptPassword(password)
    })
    .get(function() { return this._password })


var validatePresenceOf = function (value) {   
    return value && value.length
}

UserSchema.path('name').validate(function(name) {
    return name.length
}, 'you need a name..')

UserSchema.path('name').validate(function(name, cb) {
    var User = mongoose.model('User')

    if (this.isNew || this.isModified('name')) {
    User.find({ name : name }).exec(function(err, users) {
        cb(!err && users.length === 0)
    })
    } else cb(true)
    }, 'name already exists..')

// i get an undefined error at the below function:

UserSchema.path('hash').validate(function(hash) {
    return hash.length
}, 'you need a password...')

UserSchema.pre('save', function(next) {
    if (!this.isNew) return next()

    if (!validatePresenceOf(this.password)) {
    next(new Error('invalid password.'))
    } else {
        next()
    }
})

UserSchema.methods = {

    // auth
    authenticate: function(plaintext) {
        return this.encryptPassword(plaintext) === this.hash 
    },

    // salt 
    makeSalt: function() {
        return crypto.randomBytes(128)
    },

    encryptPassword: function (password) {
        if (!password) return ''      
        crypto.pbkdf2(password, this.salt, 2000, 128, function(err, derivedKey) {
            if (err) throw err
            var myhash = derivedKey.toString()
            console.log('hash: ' + myhash)
            return myhash
        })
    } 
}

mongoose.model('User', UserSchema)

controller:

exports.create = function(req, res) {
    var user = new User(req.body)
    user.save(function (err) {
        if (err) {
            return res.render('signup', {
                errors: err.errors,
                user: user,
                title: 'SIGN UP'
            })
        }
        req.logIn(user, function(err) {
            if (err) return next(err)
            return res.redirect('backend')
        })
    })
}
Problem courtesy of: amagumori

Solution

Your encryption procedure uses crypto.pbkdf2, which is an asynchronous function. This means encryptPassword() will not return your hash when your virtual setter is called. Your hash only gets returned inside of the callback passed to crypto.pbkdf2 - which is why console.log works in your example.

One way to solve this problem is to change encryptPassword() to make use of pbkdf2's synchronous sibling - crypto.pbkdf2Sync. Link to docs

Example below (with some error handling):

encryptPassword: function (password) {
    if (!password) return ''      
    var encrypted
    try {
        encrypted = crypto.pbkdf2Sync(password, this.salt, 2000, 128).toString();    
        return encrypted
    } catch (err) {
        // Handle error
    }  
} 
Solution courtesy of: C Blanchard

Discussion

  encryptPassword: function (password) {
        if (!password || !this.salt)
            return '';
        var salt = new Buffer(this.salt, 'base64');
        return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64');
    }
Discussion courtesy of: Kapil Gandhi

This recipe can be found in it's original form on Stack Over Flow.