← Blog

Readable NodeJS authorization

Today I’m going to post a nifty piece of code for NodeJS authorization.

Wow… It’s been a while since I wrote something here.

Let’s get dirty, first of all there are 2 reasonable assumptions:

  • You are using Express (or just Connect).
  • You are using Passport (for authentication). Passport is used to get a current user and his role from Request object.

For those who don’t know: “authentication” is about finding out who the person is, classic example is email + password. Authorization is about what this person is allowed to do.

In this example, for clarity, I’m going to use CoffeeScript, but it can be easily translated into JS (by using for example js2coffee.org).

What I wanted to do is to use rules in middleware style that works well with NodeJS async model.

The goal for authorization is to look something like that:

# Everyone is allowed to see a list of all products
app.get "/api/products", products.index

# Only authenticated users should see product details and create new ones (so guests can't)
app.get "/api/products/:id", auth(), products.show
app.post "/api/products", auth(), products.create

# Only admins and product owners can delete and change product details
app.del "/api/products/:id", auth(admin: true, user: auth.checkOwner), products.delete
app.put "/api/products/:id", auth(admin: true, user: auth.checkOwner), products.update

# Only admins can delete users
app.del "/api/users/:id", auth(admin: true), users.delete

# "checkOwner" is some custom function written in middleware style.
auth.checkOwner = ()-> (req, res, next)->
  getResource(req).findOne {_id: req.params.id}, (err, resource)->
    # toString() is used because mongoose key doesn't equal to otherkey just like that
    if resource._creator.toString() is req.user._id.toString()
      next(true)
    else
      res.send(403) # or next(false)
# It can allow action by next(true), it can forbid it by res.send(403) or it can let next middleware decide by next()

This is the cleanest API I could come up with.

Custom function can do 3 things:

  1. Allow action by returning next(true)
  2. Forbid action by returning next(false)
  3. Let the next middleware decide by returning next(), if it was the last function in a chain, then the action is forbidden

So here’s code that achieves it:

auth = (role_middleware_hash)->
  return (req, res, next)->
    # transform to [{role: function}, {another_role: true}] format:
    role_middleware_array = for key, value of role_middleware_hash then obj={}; obj[key]=value; obj
    if not req.user? then res.send(403); return; # no user == no access
    nextFn = (allowed)->
      if allowed is true # previous call allowed access
        next()
      else if role_middleware_array.length is 0 or allowed is false # all middleware behind, still no explicit access
        res.send(403)
      else
        [[role,fn]]=for key, value of role_middleware_array.splice(0,1)[0] then [key, value];
        if req.user.role is role
          if typeof(fn) is "function" # User with this role can do stuff only if provided function allows it
            fn.call()(req, res, nextFn)
          else  # User with this role is allowed without any conditions
            nextFn(fn)
        else
          nextFn() # this middleware says nothing, try next
    nextFn() # start recursion

module.exports = auth

The beauty is that it works well with asynchronous calls just like good Node code should.

Thank you for reading my post! Have a few interesting things going on, hope some day I find time to write about them. :-)