node.jsmongodbexpressmongooseexpress-jwt

express-jwt setting user object to req.user._doc instead of just req.user?


I've used the npm package express-jwt in the past for easy JWT signing, decoding, etc. Usually (and according to the docs) it intercepts a request, decodes the token with the user object payload and sets req.user to that payload. However, this time around it's showing that req.user looks like this:

{ '$__': 
   { strictMode: true,
     getters: {},
     wasPopulated: false,
     activePaths: { paths: [Object], states: [Object], stateNames: [Object] },
     emitter: { domain: null, _events: {}, _maxListeners: 0 } },
  isNew: false,
  _doc: 
   { __v: 0,
     password: '$2a$10$ypbCbWsEA7W17IQjdox5Oe..MhhCco/0yIw.J1Y6m6vJDllCB0LLS',
     username: 'b',
     lastname: 'Last',
     firstname: 'First',
     _id: '56969210e3f8bf2ab9aee66d' },
  _pres: { '$__original_save': [ null, null, null ] },
  _posts: { '$__original_save': [] },
  iat: 1452709101 
}

Instead of just:

{
    __v: 0,
    password: '$2a$10$ypbCbWsEA7W17IQjdox5Oe..MhhCco/0yIw.J1Y6m6vJDllCB0LLS',
    username: 'b',
    lastname: 'Last',
    firstname: 'First',
    _id: '56969210e3f8bf2ab9aee66d'
}

I'm trying to figure out why this time around I need to specify req.user._doc to get the specific info about the user. I've compared my code at every source and it's practically identical to a past project, and I just tested them side by side - one project sets req.user to the user object, the other sets req.user to the object shown above, where the user object can only be found in the _doc property.

I checked what is actually getting returned from the mongoose .findOne() method in both projects and it is the larger object shown above in both projects, but express-jwt seems to be weeding out the extra stuff automatically in one project but not in the other.

I've also noticed that the JWT with all the additional info I don't want is much longer than the JWT on the project with the correct req.user object. So perhaps that means something weird is happening when the JWT is getting signed with the payload...

Here's the relevant code from the one that is requiring req.user._doc:

server.js

...
var expressJwt = require('express-jwt');
var authRoutes = require('./routes/authRoutes');
var nationRoutes = require('./routes/nationRoutes');
...
app.use('/api', expressJwt({ secret: config.secret }));
app.use('/api/nation', nationRoutes);
app.use('/auth', authRoutes);
...

authRoutes.js

...
authRouter.post('/login', function (req, res) {
    User.findOne({ username: req.body.username }, function (err, user) {
        if (err) res.status(500).send(err);
        if (!user) res.status(401).send('The username you entered does not exist');
        else if (user) {
            bcrypt.compare(req.body.password, user.password, function (err, match) {
                if (err) throw (err);
                if (!match) res.status(401).send("Incorrect Password");
                else {
                    var token = jwt.sign(user, config.secret);
                    res.send({
                        user: user,
                        token: token
                    });
                }
            });
        }
    });
});
...

nationRoutes.js (where the problem is manifesting itself)

By this point, express-jwt has decoded the token and (supposedly) set the token's payload (which is supposed to be just the user's info, not the additional $__, etc. stuff) to req.user, but I'm having to do req.user._doc as shown in the code below to get it to work.

...
nationRouter.route('/')
    .get(function (req, res) {
        console.log(req.user);  // Shows the larger object with added stuff from above
        Nation.find({ user: req.user._doc._id }, function (err, nations) {
            if (err) {
                res.status(500).send(err);
            }
            res.send(nations);
        });
    })
...

In case it's helpful, or possibly part of the problem, here is the user model and the nation model:

user.js

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var bcrypt = require("bcrypt");

var userSchema = new Schema({
    firstname: {
        type: String,
        required: true
    },
    lastname: {
        type: String,
        required: true
    },
    username: {
        type: String,
        required: true,
        unique: true,
        lowercase: true
    },
    password: {
        type: String,
        required: true
    },
    email: String
});

userSchema.pre("save", function (next) {
    var user = this;

    if (!user.isModified("password")) {
        next();
    }

    bcrypt.hash(user.password, 10, function (err, hash) {
        if (err) return next(err);

        user.password = hash;
        next();
    }); 
});

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

nation.js

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var Nation = new Schema({
    name: {
        type: String,
        required: true
    },
    creationDate: {
        type: Date,
        default: Date.now
    },
    user: {
        type: Schema.Types.ObjectId,
        ref: 'User'
    }
});

module.exports = mongoose.model('Nation', Nation);

Solution

  • I played around with past version numbers of the jsonewebtoken package in my code and the problem goes away with version 5.5.1. (5.5.2 seems to have introduced the problem). I looked into the source code for jsonwebtoken, and had a couple conversations with some of the developers on their github issues page (here and here). It seems like they introduced a package called xtend to the payload in the jwt.sign() method, which seems to be adding all the additional meta properties onto the payload before encoding.

    I'm using Mongoose, and according to one of the jsonwebtoken developers, I will need to begin using the Document.toObject() method from Mongoose. So from now on:

    ...
    jwt.sign(user.toObject(), ...)
    ...
    

    will be the way to do this in Mongoose.