Check resource ownership on Node.js rest API

Viewed 1470

I'm trying to develop a Node.js backend using express and mongoose.

Over the network there's plenty of examples of how to implement a proper authentication layer, but I couldn't find any example of how to correctly implement an authorization layer.

In my specific case, I'm creating the backend of a multi-user application and I want that every user can only see the data inserted by himself/herself.

I have three models:

  1. User
  2. Category
  3. Document

User owns one or many Categories, Categories contain zero or more Documents.

The CRUD operations are implemented on the following endpoints:

/user/:userid
/user/:userid/category
/user/:userid/category/:categoryid
/user/:userid/category/:categoryid/document
/user/:userid/category/:categoryid/document/:documentid

In the authentication part I set to each request the current logged in user id, so I can check easily that

jsonwebtoken.userId == req.params.userid

And return a 403 error otherwise.

Checking the ownership of categories is quite easy, because each category contains a reference to the user who created them.

var CategorySchema = mongoose.Schema({
    name: {
        type: String,
        required: true,
        trim: true
    },
    user_id: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        index: true
    }
});

In the Document model, however, I only have a reference to the category it belongs, but I didn't add a reference to the user.

I'm wondering therefore how to proceed with "nested" relationships. Do I need to add a user_id reference to all of them at any depth level? Is there any best practice?

Moreover, is this the right way to do what I need or is there any official/mature library that already does the same?

2 Answers

Well no-sql database gives you the power of embedding your sub documents(or equivalent table in relational db) in to the single document. So you may consider redesigning your schema to something like

{
  userId:"",
  categories": [
    {
      "categoryId": "",
      "name": "",
      "documents": [
        {
          "documentId": "",

        },
        {
          "documentId": "",

        },

      ]
    },
    {
      "categoryId": "",
      "name": "",
      "documents": [
        {
          "documentId": "",

        },
        {
          "documentId": "",

        },

      ]
    }
  ]
}

This may help you optimize the number of db query but the important thing to note here is that if the number of categories and documents per user and per category repectively could grow very large then this approach would not be good.

Always remember 6 important thumb rules for mongo db schema design

  1. Favor embedding unless there is a compelling reason not to

  2. Needing to access an object on its own is a compelling reason not to embed it

  3. Arrays should not grow without bound. If there are more than a couple of hundred documents on the “many” side, don’t embed them; if there are more than a few thousand documents on the “many” side, don’t use an array of ObjectID references. High-cardinality arrays are a compelling reason not to embed.

  4. Don’t be afraid of application-level joins

  5. Consider the write/read ratio when denormalizing. A field that will mostly be read and only seldom updated is a good candidate for denormalization.

  6. You want to structure your data to match the ways that your application queries and updates it.

Taken from here

After some tinkering, I ended up with the following middleware.

It basically checks for route parameters in the expected order and checks for coherent memberships.

Not sure if it's the best way of achieving this, but it works:

var Category = require('../category/Category'),
    Document = require('../document/Document'),
    unauthorizedMessage = 'You are not authorized to perform this operation.',
    errorAuthorizationMessage = 'Something went wrong while validating authorizations.',
    notFoundMessage = ' not found.';

var isValidMongoId = function (id) {
    if (id.match(/^[0-9a-fA-F]{24}$/)) {
        return true;
    }
    return false;
}

var verifyPermissions = function (req, res, next) {
    if (req.userId) {
        if (req.params.userid && isValidMongoId(req.params.userid)) {
            if (req.userId != req.params.userid) {
                return res.status(403).send({error: 403, message: unauthorizedMessage});
            }

            if (req.params.categoryid && isValidMongoId(req.params.userid)) {
                Category.findOne({_id: req.params.categoryid, user_id: req.params.userid}, function(err, category){
                    if (err) {
                        return res.status(500).send({error: 500, message: errorAuthorizationMessage})
                    }
                    if (!category) {
                        return res.status(404).send({error: 404, message: 'Category' + notFoundMessage});
                    }

                    if (req.params.documentid && isValidMongoId(req.params.documentid)) {
                        Document.findOne({_id: req.params.documentid, category_id: req.params.categoryid}, function(err, document){
                            if (err) {
                                return res.status(500).send({error: 500, message: errorAuthorizationMessage})
                            }
                            if (!document) {
                                return res.status(404).send({error: 404, message: 'Document' + notFoundMessage});
                            }
                        });
                    }
                });
            }
        }

        next();
    } else {
        return res.status(403).send({error: 403, message: unauthorizedMessage});
    }
};

module.exports = verifyPermissions;
Related