117

Suppose I have two collections/schemas. One is the Users Schema with username and password fields, then, I have a Blogs Schema that has a reference to the Users Schema in the author field. If I use Mongoose to do something like

Blogs.findOne({...}).populate("user").exec()

I will have the Blog document and the user populated too, but how do I prevent Mongoose/MongoDB from returning the password field? The password field is hashed but it shouldn't be returned.

I know I can omit the password field and return the rest of the fields in a simple query, but how do I do that with populate. Also, is there any elegant way to do this?

Also, in some situations I do need to get the password field, like when the user wants to login or change the password.

Luis Elizondo
  • 1,979
  • 2
  • 14
  • 15

17 Answers17

385

You can change the default behavior at the schema definition level using the select attribute of the field:

password: { type: String, select: false }

Then you can pull it in as needed in find and populate calls via field selection as '+password'. For example:

Users.findOne({_id: id}).select('+password').exec(...);
JohnnyHK
  • 305,182
  • 66
  • 621
  • 471
  • 3
    Great. Can you provide an example on who to add it in find? Assuming I have: Users.find({id: _id}) where should I add the "+password+? – Luis Elizondo Aug 23 '12 at 18:13
  • Found the example at the link you provided. http://mongoosejs.com/docs/api.html#schematype_SchemaType-select Thanks – Luis Elizondo Aug 23 '12 at 18:19
  • 14
    Is there a way to apply this to the object passed to save() callback? Such that when I save a user profile the password isn't included in the callback parameter. – Matt Jun 29 '14 at 04:15
  • 4
    This is by far the best answer in my opinion. Add this once, and it is exclude. Much better than adding a select or exclude option to every query. – AndyH Aug 18 '16 at 08:29
  • 3
    This should be the ultimate answer. Added to schema, and don't have to forget excluding during queries. – KhoPhi Nov 08 '19 at 21:25
  • 2
    This is the most flexible solution! – Chunlong Li Dec 10 '20 at 07:17
  • This approach works really fine when using populate, but it'll not work when using aggregate with $lookup – BaDr Amer Apr 17 '21 at 21:09
  • For those wondering how to achieve this using `save()`, I've researched a bit and it's not possible, so the way I solve it by using `const { _id } = await resourceToCreate.save(); return this.findOne(_id);` (note that I use populate in findOne function) Not a sofisticated solution but it works – José Manuel Blasco May 01 '23 at 15:26
77
.populate('user' , '-password')

http://mongoosejs.com/docs/populate.html

JohnnyHKs answer using Schema options is probably the way to go here.

Also note that query.exclude() only exists in the 2.x branch.

aaronheckmann
  • 10,625
  • 2
  • 40
  • 30
  • this will also work .populate('user': 1, 'password':0) – Sudhanshu Gaur Jul 24 '15 at 21:08
  • 1
    I didn't understand why adding " - " works and was curious so found a decent explanation on the docs: When using string syntax, prefixing a path with - will flag that path as excluded. When a path does not have the - prefix, it is included. Lastly, if a path is prefixed with +, it forces inclusion of the path, which is useful for paths excluded at the schema level. Here's the link for the full thing https://mongoosejs.com/docs/api.html#query_Query-select – Connor Sep 30 '20 at 14:32
39

Edit:

After trying both approaches, I found that the exclude always approach wasn't working for me for some reason using passport-local strategy, don't really know why.

So, this is what I ended up using:

Blogs.findOne({_id: id})
    .populate("user", "-password -someOtherField -AnotherField")
    .populate("comments.items.user")
    .exec(function(error, result) {
        if(error) handleError(error);
        callback(error, result);
    });

There's nothing wrong with the exclude always approach, it just didn't work with passport for some reason, my tests told me that in fact the password was being excluded / included when I wanted. The only problem with the include always approach is that I basically need to go through every call I do to the database and exclude the password which is a lot of work.


After a couple of great answers I found out there are two ways of doing this, the "always include and exclude sometimes" and the "always exclude and include sometimes"?

An example of both:

The include always but exclude sometimes example:

Users.find().select("-password")

or

Users.find().exclude("password")

The exclude always but include sometimes example:

Users.find().select("+password")

but you must define in the schema:

password: { type: String, select: false }
Joundill
  • 6,828
  • 12
  • 36
  • 50
Luis Elizondo
  • 1,979
  • 2
  • 14
  • 15
  • I would go for the last option. Never select the password, except in the logIn / passwordUpdate functions you really need it in. – rdrey Aug 24 '12 at 13:54
  • For some reason that option didn't work with Passport.js local strategy, don't know why. – Luis Elizondo Aug 24 '12 at 17:49
  • Good answer, thanks!!! Don't know why but when I do `.select("+field")` it brings only the `__id`, even though `.select("-field")` excludes nicely the field I want – Renato Gama Feb 13 '13 at 02:19
  • Sorry, it works perfect, didn't notice that `select: false` is mandatory – Renato Gama Feb 13 '13 at 02:24
  • 1
    This is working for my local strategy: await User.findOne({ email: username }, { password: 1 }, async (err, user) => { ... }); – TomoMiha Mar 13 '19 at 11:13
  • If you want to populate it either then write `.populate("field")` no need to select anymore. Or `popluate({ path: "field", select: "+sub_field" })` – Hayyaun Aug 21 '22 at 22:08
19

You can achieve that using the schema, for example:

const UserSchema = new Schema({/* */})

UserSchema.set('toJSON', {
    transform: function(doc, ret, opt) {
        delete ret['password']
        return ret
    }
})

const User = mongoose.model('User', UserSchema)
User.findOne() // This should return an object excluding the password field
Ikbel
  • 7,721
  • 3
  • 34
  • 45
14

User.find().select('-password') is the right answer. You can not add select: false on the Schema since it will not work, if you want to login.

Ylli Gashi
  • 645
  • 6
  • 13
  • Can you not override the behavior in the login endpoint? If so, this seems like the safest option. – erfling Jun 06 '18 at 16:45
  • @erfling, it won't. – jrran90 Apr 22 '19 at 17:05
  • 3
    It will, you can use `const query = model.findOne({ username }).select("+password");` and use that on login and password change/resetting routes and otherwise ensure it never comes out. This is by far way safer to have it not return by default as people are proven to mistakes imo – Cacoon May 12 '20 at 04:01
10

I'm using for hiding password field in my REST JSON response

UserSchema.methods.toJSON = function() {
 var obj = this.toObject(); //or var obj = this;
 delete obj.password;
 return obj;
}

module.exports = mongoose.model('User', UserSchema);
Ratan Uday Kumar
  • 5,738
  • 6
  • 35
  • 54
Gere
  • 2,114
  • 24
  • 24
7

const userSchema = new mongoose.Schema(
  {
    email: {
      type: String,
      required: true,
    },
    password: {
      type: String,
      required: true,
    },
  },
  {
    toJSON: {
      transform(doc, ret) {
        delete ret.password;
        delete ret.__v;
      },
    },
  }
);
  • 1
    Could you please explain why this solves the issue? – AliAvci Feb 24 '21 at 22:20
  • i confirm this response is correct way for return the value, how you can use this way : const exemple = async (req, res) => { // your code const user = await UserModel.create(yourUser) user.toJson() } – Canonne Gregory Feb 18 '23 at 22:01
6

I found another way of doing this, by adding some settings to schema configuration.

const userSchema = new Schema({
    name: {type: String, required: false, minlength: 5},
    email: {type: String, required: true, minlength: 5},
    phone: String,
    password: String,
    password_reset: String,
}, { toJSON: { 
              virtuals: true,
              transform: function (doc, ret) {
                delete ret._id;
                delete ret.password;
                delete ret.password_reset;
                return ret;
              }

            }, timestamps: true });

By adding transform function to toJSON object with field name to exclude. as In docs stated:

We may need to perform a transformation of the resulting object based on some criteria, say to remove some sensitive information or return a custom object. In this case we set the optional transform function.

Nerius Jok
  • 3,059
  • 6
  • 21
  • 27
4

Blogs.findOne({ _id: id }, { "password": 0 }).populate("user").exec()

Ricky Sahu
  • 23,455
  • 4
  • 42
  • 32
4

While using password: { type: String, select: false } you should keep in mind that it will also exclude password when we need it for authentication. So be prepared to handle it however you want.

Anand Kapdi
  • 491
  • 5
  • 4
3

Assuming your password field is "password" you can just do:

.exclude('password')

There is a more extensive example here

That is focused on comments, but it's the same principle in play.

This is the same as using a projection in the query in MongoDB and passing {"password" : 0} in the projection field. See here

onkar
  • 4,427
  • 10
  • 52
  • 89
Adam Comerford
  • 21,336
  • 4
  • 65
  • 85
3
router.get('/users',auth,(req,res)=>{
   User.findById(req.user.id)
    //skip password
    .select('-password')
    .then(user => {
        res.json(user)
    })
})
prosoitos
  • 6,679
  • 5
  • 27
  • 41
Desai Ramesh
  • 131
  • 1
  • 3
2

This is more a corollary to the original question, but this was the question I came across trying to solve my problem...

Namely, how to send the user back to the client in the user.save() callback without the password field.

Use case: application user updates their profile information/settings from the client (password, contact info, whatevs). You want to send the updated user information back to the client in the response, once it has successfully saved to mongoDB.

User.findById(userId, function (err, user) {
    // err handling

    user.propToUpdate = updateValue;

    user.save(function(err) {
         // err handling

         /**
          * convert the user document to a JavaScript object with the 
          * mongoose Document's toObject() method,
          * then create a new object without the password property...
          * easiest way is lodash's _.omit function if you're using lodash 
          */

         var sanitizedUser = _.omit(user.toObject(), 'password');
         return res.status(201).send(sanitizedUser);
    });
});
Bennett Adams
  • 1,808
  • 14
  • 17
  • Thanks man! To add to this, maybe it helps someone - after removing the field using `loadash`, the resulting `object` is not a `mongoose object` anymore. To convert it back, use `hydrate` in this way: `MongooseObject.hydrate(yourObject)` – Ezeeroc Mar 10 '23 at 15:24
2

You can pass a DocumentToObjectOptions object to schema.toJSON() or schema.toObject().

See TypeScript definition from @types/mongoose

 /**
 * The return value of this method is used in calls to JSON.stringify(doc).
 * This method accepts the same options as Document#toObject. To apply the
 * options to every document of your schema by default, set your schemas
 * toJSON option to the same argument.
 */
toJSON(options?: DocumentToObjectOptions): any;

/**
 * Converts this document into a plain javascript object, ready for storage in MongoDB.
 * Buffers are converted to instances of mongodb.Binary for proper storage.
 */
toObject(options?: DocumentToObjectOptions): any;

DocumentToObjectOptions has a transform option that runs a custom function after converting the document to a javascript object. Here you can hide or modify properties to fill your needs.

So, let's say you are using schema.toObject() and you want to hide the password path from your User schema. You should configure a general transform function that will be executed after every toObject() call.

UserSchema.set('toObject', {
  transform: (doc, ret, opt) => {
   delete ret.password;
   return ret;
  }
});
netishix
  • 183
  • 4
  • 10
1

The solution is to never store plaintext passwords. You should use a package like bcrypt or password-hash.

Example usage to hash the password:

 var passwordHash = require('password-hash');

    var hashedPassword = passwordHash.generate('password123');

    console.log(hashedPassword); // sha1$3I7HRwy7$cbfdac6008f9cab4083784cbd1874f76618d2a97

Example usage to verify the password:

var passwordHash = require('./lib/password-hash');

var hashedPassword = 'sha1$3I7HRwy7$cbfdac6008f9cab4083784cbd1874f76618d2a97';

console.log(passwordHash.verify('password123', hashedPassword)); // true
console.log(passwordHash.verify('Password0', hashedPassword)); // false
Sanket Berde
  • 6,555
  • 4
  • 35
  • 39
Cameron Hudson
  • 3,190
  • 1
  • 26
  • 38
  • 10
    Regardless of password being hashed or not, the password/the hash should never shown to the user. An attacker can get some important information of the hashed password (which algorithm was used) and so. – Marco Jan 15 '19 at 21:03
0
const { password,  ...others } = user._doc;

and send it like this

res.status(200).json(others);
Tyler2P
  • 2,324
  • 26
  • 22
  • 31
0

*** I have two solutions for this:

// OPT: 1

/** If you print these params, the doc and ret are the same objects
 * and opt is another object with special params (only details): */

userSchema.set('toJSON', {
    transform: function(doc, ret, opt) {
        console.log("DOC-RET-OPT", {
            doc,
            ret,
            opt
        });
        // You can remove the specific params with this structure
        delete ret['password'];
        delete ret['__v'];
        return ret;
    }
});

// REMEMBER: You cannot apply destructuring for the objects doc or ret...

// OPT: 2

/* HERE: You can apply destructuring object 'cause the result
* is toObject instance and not document from Mongoose... */

userSchema.methods.toJSON = function() {
    const {
        __v,
        password,
        ...user
    } = this.toObject();
    return user;
};

// NOTE: The opt param has this structure:
opt: {
    _calledWithOptions: {},
    flattenDecimals: true,
    transform: [Function: transform],
    _isNested: true,
    json: true,
    minimize: true
}
Parterdev
  • 881
  • 1
  • 2
  • 6