Sequelize: don't return password
Asked Answered
F

8

53

I'm using Sequelize to do a DB find for a user record, and I want the default behavior of the model to not return the password field for that record. The password field is a hash but I still don't want to return it.

I have several options that will work, but none seems particularly good:

  1. Create a custom class method findWithoutPassword for the User model and within that method do a User.find with the attributes set as shown in the Sequelize docs

  2. Do a normal User.find and filter the results in the controller (not preferred)

  3. Use some other library to strip off unwanted attributes

Is there a better way? Best of all would be if there is a way to specify in the Sequelize model definition to never return the password field, but I haven't found a way to do that.

Febri answered 15/1, 2015 at 20:19 Comment(0)
T
78

I would suggest overriding the toJSON function:

sequelize.define('user', attributes, {
  instanceMethods: {
    toJSON: function () {
      var values = Object.assign({}, this.get());

      delete values.password;
      return values;
    }
  }
});

Or in sequelize v4

const User = sequelize.define('user', attributes, {});

User.prototype.toJSON =  function () {
  var values = Object.assign({}, this.get());

  delete values.password;
  return values;
}

toJSON is called when the data is returned to the user, so end users won't see the password field, but it will still be available in your code.

Object.assign clones the returned object - Otherwise you will completely delete the property from the instance.

Tripartition answered 16/1, 2015 at 8:15 Comment(15)
This is an interesting approach. Would this preclude ever getting the password (or other blacklisted attributes) out of the model?Febri
EDIT: this is not filtering out the password for me. Looks like the instanceMethod toJSON isn't getting called for the return of either user.get() or user.dataValues. Should I be using another method to return attributes?Febri
You should either call toJSON, or just return the user object. For example res.send(200, user) will internally call JSON.stringify on the user object, which in turn calls toJSONTripartition
IMPORTANT! If you follow the above to the letter, delete values.password will actually remove the password attribute from the user instance - not just JSON output. Use var values = Object.assign({}, this.get()) or appropriate polyfill to avoid mutating the actual user's properties.Rakes
got error while using above code Unhandled rejection Error: TypeError: Cannot read property 'get' of undefinedRicoricochet
@AkshayPratapSingh Are you using an arrow function?Tripartition
@JanAagaardMeier yeahRicoricochet
You can't do that in this case - we use .bind to set the context to the instance - But you can't do that with arrow functionsTripartition
This will work if you're just creating a JSON representation of a user. BUT! If you include a user from another model, the toJSON that gets called is the other models! This will cause you to leak your hashed password out when user's are eagerly loaded. See this: github.com/sequelize/sequelize/issues/3891Bolme
this does not work anymore with sequelize 4, instanceMethods are deprecated, replace with this Model.prototype.someMethod = function () {..}, according to this docs.sequelizejs.com/manual/tutorial/…Longley
I want to exclude certain attributes after a create - is that possible? The toJSON approach is doing it all the time, that is not working for me.Wintery
Instead of Object.assign({}, this.get()) you can use this.get({ clone: true })Turncoat
ES syntax makes this nice and tidy: { ...this.get(), password: undefined }Knossos
This method will only work when directly fetching this model. It will not exclude the attribute through associations. So, if you fetch UserTransactions and include Users, the user password will show up in the response. The defaultScope answer below solves this problem.Quarterstaff
Is there any why to override the toJSON function for all Models at once and not per each Model?Shepperd
R
124

Another way is to add a default scope to the User model.

Add this in the model's options object

defaultScope: {
  attributes: { exclude: ['password'] },
}

Or you can create a separate scope to use it only in certain queries.

Add this in the model's options object

scopes: {
  withoutPassword: {
    attributes: { exclude: ['password'] },
  }
}

Then you can use it in queries

User.scope('withoutPassword').findAll();
Rebeckarebeka answered 20/1, 2018 at 15:45 Comment(11)
IMPORTANT! This is the only answer that worked for me. It is important to know that accepted anwser will work ONLY if you directly fetch user model. If you include user model throug another model toJSON functio in user model will not get called and you will leak your passwords to the client!!Mercaptide
that's the best answer. I don't know why that is not on the top.Leaden
NOTE: This answer works fine but the excluded field will still be exposed for create. Overriding toJSON protects the field from being exposed during create.Lamond
@DeanKoštomaj .Using toJSON worked fine for me when using include in findAll. The deleted field wasn't included.Lamond
@DeanKoštomaj. Noticed the problem when I tried including Parent in the child.Lamond
This still works in Sequelize 5. Also, it's a great solution!Algebra
@Lamond Wouldn't you want to expose the password field for create? Its needed to set the user's password when their account gets created.Rebeckarebeka
@pawan samdani Yes, It is needed, but if you will send the created user as JSON after creation, you will also be sending the password too.Lamond
This comment brought exactly what I needed but I still didn't know. Very good thank you!! Below is a link to the scope definitions sequelize.org/master/manual/scopes.htmlBasildon
@Lamond Thanks for highlighting that security flaw with this solution! The best fix I found is to add await user.reload(); in an afterCreate hook for the model.Marion
For people who want to use the scope way inside an include for relational tables: include: { model: models.users.scope('withoutPassword'), as: "developer" },Kerriekerrigan
T
78

I would suggest overriding the toJSON function:

sequelize.define('user', attributes, {
  instanceMethods: {
    toJSON: function () {
      var values = Object.assign({}, this.get());

      delete values.password;
      return values;
    }
  }
});

Or in sequelize v4

const User = sequelize.define('user', attributes, {});

User.prototype.toJSON =  function () {
  var values = Object.assign({}, this.get());

  delete values.password;
  return values;
}

toJSON is called when the data is returned to the user, so end users won't see the password field, but it will still be available in your code.

Object.assign clones the returned object - Otherwise you will completely delete the property from the instance.

Tripartition answered 16/1, 2015 at 8:15 Comment(15)
This is an interesting approach. Would this preclude ever getting the password (or other blacklisted attributes) out of the model?Febri
EDIT: this is not filtering out the password for me. Looks like the instanceMethod toJSON isn't getting called for the return of either user.get() or user.dataValues. Should I be using another method to return attributes?Febri
You should either call toJSON, or just return the user object. For example res.send(200, user) will internally call JSON.stringify on the user object, which in turn calls toJSONTripartition
IMPORTANT! If you follow the above to the letter, delete values.password will actually remove the password attribute from the user instance - not just JSON output. Use var values = Object.assign({}, this.get()) or appropriate polyfill to avoid mutating the actual user's properties.Rakes
got error while using above code Unhandled rejection Error: TypeError: Cannot read property 'get' of undefinedRicoricochet
@AkshayPratapSingh Are you using an arrow function?Tripartition
@JanAagaardMeier yeahRicoricochet
You can't do that in this case - we use .bind to set the context to the instance - But you can't do that with arrow functionsTripartition
This will work if you're just creating a JSON representation of a user. BUT! If you include a user from another model, the toJSON that gets called is the other models! This will cause you to leak your hashed password out when user's are eagerly loaded. See this: github.com/sequelize/sequelize/issues/3891Bolme
this does not work anymore with sequelize 4, instanceMethods are deprecated, replace with this Model.prototype.someMethod = function () {..}, according to this docs.sequelizejs.com/manual/tutorial/…Longley
I want to exclude certain attributes after a create - is that possible? The toJSON approach is doing it all the time, that is not working for me.Wintery
Instead of Object.assign({}, this.get()) you can use this.get({ clone: true })Turncoat
ES syntax makes this nice and tidy: { ...this.get(), password: undefined }Knossos
This method will only work when directly fetching this model. It will not exclude the attribute through associations. So, if you fetch UserTransactions and include Users, the user password will show up in the response. The defaultScope answer below solves this problem.Quarterstaff
Is there any why to override the toJSON function for all Models at once and not per each Model?Shepperd
C
40

I like to use a combination of both of Pawan's answers and declare the following:

defaultScope: {
    attributes: { exclude: ['password'] },
},
scopes: {
    withPassword: {
        attributes: { },
    }
}

This allows me to exclude the password by default and use the withPassword scope to explicitly return the password when needed, such as when running a login method.

userModel.scope('withPassword').findAll()

This ensure that the password is not returned when including the user via a referenced field, e.g.

accountModel.findAll({
    include: [{
        model: userModel,
        as: 'user'
    }]
})
Collyrium answered 18/8, 2018 at 0:12 Comment(1)
This should be the best practice!Lanza
P
32

Maybe you can just add exclude at your attribute when you find, look like this:

var User = sequelize.define('user', attributes);

User.findAll({
    attributes: {
        exclude: ['password']
    }
});

Read the docs for more details

Penitence answered 18/9, 2016 at 14:51 Comment(1)
adding the block of attributes to each query is not so good. a way to define exclude attributes on the model level to apply all queries is needed!Tartaglia
M
5

The code below worked for me. We wanted access to the instance attributes at runtime but remove them before sending the data to the client.

const Sequelize = require('sequelize')

const sequelize = new Sequelize('postgres://user:[email protected]:5432/dbname')

const PROTECTED_ATTRIBUTES = ['password', 'token']

const Model = Sequelize.Model

class User extends Model {
  toJSON () {
    // hide protected fields
    let attributes = Object.assign({}, this.get())
    for (let a of PROTECTED_ATTRIBUTES) {
      delete attributes[a]
    }
    return attributes
  }
}

User.init({
  email: {
    type: Sequelize.STRING,
    unique: true,
    allowNull: false,
    validate: {
      isEmail: true
    }
  },
  password: {
    type: Sequelize.STRING,
    allowNull: false
  },
  token: {
    type: Sequelize.STRING(16),
    unique: true,
    allowNull: false
  },
},
{
  sequelize,
  modelName: 'user'
})

module.exports = User

Github Gist

Mendelson answered 19/5, 2019 at 8:25 Comment(0)
M
5

I was able to get this working by adding a getter to the field which returns undefined

firstName: {
      type: DataTypes.STRING,
      get() {
        return undefined;
      }
    }

The accepted answer does not work when the model is included from another model.

I had a virtual field, say fullName that depends on the fields that I wanted to hide, say firstName and lastName. And the solution based on defaultScope does not work in this case.

Muzhik answered 14/12, 2019 at 17:20 Comment(1)
Thanks for the hint. I struggled for an hour to find the solution where I needed to hide the column but show in some virtual field.Classics
S
0

there's a plugin for scoping attributes as discussed here.

i went with the override as mentioned in the accepted answer, except i called the original toJSON rather than get with this.constructor.super_.prototype.toJSON.apply(this, arguments) as described in the api docs

Stokehold answered 5/12, 2015 at 22:21 Comment(0)
V
0

You can do this in a simple way by excluding attributes. See the below code (comment line)

    Patient.findByPk(id, {
            attributes: {
              exclude: ['UserId', 'DiseaseId'] // Removing UserId and DiseaseId from Patient response data
            },
            include: [
              { 
                model: models.Disease
              },
              {
                model: models.User,
                attributes: {
                  exclude: ['password'] // Removing password from User response data
                }
              }
           ]
    })
    .then(data => {
        res.status(200).send(data);
    })
    .catch(err => {
        res.status(500).send({
            message: `Error retrieving Patient with id ${id} : ${err}`
        });
    });
Virgulate answered 21/5, 2020 at 6:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.